diff --git a/libs/apprise-1.9.8.dist-info/INSTALLER b/libs/apprise-1.11.0.dist-info/INSTALLER similarity index 100% rename from libs/apprise-1.9.8.dist-info/INSTALLER rename to libs/apprise-1.11.0.dist-info/INSTALLER diff --git a/libs/apprise-1.9.8.dist-info/METADATA b/libs/apprise-1.11.0.dist-info/METADATA similarity index 90% rename from libs/apprise-1.9.8.dist-info/METADATA rename to libs/apprise-1.11.0.dist-info/METADATA index f0197b00f..308c89904 100644 --- a/libs/apprise-1.9.8.dist-info/METADATA +++ b/libs/apprise-1.11.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: apprise -Version: 1.9.8 +Version: 1.11.0 Summary: Push Notifications that work with just about every platform! Author-email: Chris Caron License: BSD-2-Clause @@ -8,7 +8,7 @@ Project-URL: Homepage, https://appriseit.com Project-URL: Source, https://github.com/caronc/apprise Project-URL: Tracker, https://github.com/caronc/apprise/issues Project-URL: Documentation, https://appriseit.com -Keywords: 46elks,Africas Talking,Alerts,Apprise API,Automated Packet Reporting System,AWS,Bark,BlueSky,Brevo,BulkSMS,BulkVS,Burst SMS,Chanify,Chat,CLI,Clickatell,ClickSend,D7Networks,Dapnet,DBus,DingTalk,Discord,Dot,Email,Emby,Enigma2,FCM,Feishu,Flock,Fluxer,Form,Free Mobile,Gnome,Google Chat,Gotify,Growl,Guilded,Home Assistant,httpSMS,IFTTT,IRC,Jellyfin,Join,JSON,Kavenegar,KODI,Kumulos,LaMetric,Lark,Line,MacOSX,Mailgun,Mastodon,Matrix,Mattermost,MessageBird,Microsoft,Misskey,MQTT,MSG91,MSTeams,Nextcloud,NextcloudTalk,Notica,NotificationAPI,Notifiarr,Notifico,Ntfy,Office365,OneSignal,Opsgenie,PagerDuty,PagerTree,ParsePlatform,Plivo,PopcornNotify,Power Automate,Prowl,Push Notifications,PushBullet,PushDeer,Pushed,Pushjet,PushMe,Pushover,Pushplus,PushSafer,Pushy,QQ Push,Quote/0,Reddit,Resend,Revolt,Rocket.Chat,RSyslog,Ryver,SendGrid,SendPulse,ServerChan,SES,Seven,SFR,Signal,SIGNL4,SimplePush,Sinch,Slack,SMPP,SMS Manager,SMSEagle,SMTP2Go,SNS,SparkPost,Spike,Splunk,SpugPush,Streamlabs,Stride,Synology Chat,Syslog,Techulus,Telegram,Threema Gateway,Twilio,Twist,Twitter,Vapid,Viber,VictorOps,Voipms,Vonage,Webex,Webpush,WeCom Bot,WhatsApp,Windows,Workflows,WxPusher,XBMC,XML,XMPP,Zulip +Keywords: 46elks,Africas Talking,Alerts,Apprise API,Automated Packet Reporting System,Amazon Chime,AWS,Bark,Blink(1),BlueSky,Brevo,BulkSMS,BulkVS,Burst SMS,Chanify,Chat,CLI,Clickatell,ClickSend,D7Networks,Dapnet,DBus,DingTalk,Discord,Dot,Email,Emby,Enigma2,Evolution API,Exotel,FCM,Feishu,Flock,Fluxer,Form,Free Mobile,Gnome,Google Chat,Gotify,GroupMe,Growl,Guilded,Home Assistant,httpSMS,HumHub,IFTTT,IRC,Jellyfin,Jira,Join,JSON,Kavenegar,KODI,Kumulos,LaMetric,Lark,Line,MacOSX,Mailgun,Mastodon,Matrix,Mattermost,MessageBird,Microsoft,Misskey,MQTT,MSG91,MSTeams,Nextcloud,NextcloudTalk,Notica,NotificationAPI,Notifiarr,Notifico,Ntfy,Office365,OneSignal,Opsgenie,PagerDuty,PagerTree,ParsePlatform,Plivo,PopcornNotify,Postmark,Power Automate,Prowl,Push Notifications,PushBullet,PushDeer,Pushed,Pushjet,PushMe,Pushover,Pushplus,PushSafer,Pushy,QQ Push,Quote/0,Reddit,Resend,Revolt,RingCentral,Rocket.Chat,RSyslog,Ryver,SendGrid,SendPulse,ServerChan,Session Open Group Server,SES,Seven,SFR,Signal,SIGNL4,SimplePush,Sinch,Slack,SMPP,SMS Manager,SMSEagle,SMTP2Go,SNS,SparkPost,Spike,Splunk,SpugPush,Streamlabs,Stride,Synology Chat,Syslog,Techulus,Telegram,Threema Gateway,Twilio,Twist,Twitter,Vapid,Viber,VictorOps,Voipms,Vonage,Webex,Webpush,WeChat (WeCom),WeCom Bot,WhatsApp,Windows,Workflows,WxPusher,XBMC,XML,XMPP,Zoom,Zulip Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators @@ -40,14 +40,15 @@ Requires-Dist: tox; extra == "dev" Requires-Dist: pytest; extra == "dev" Requires-Dist: pytest-cov; extra == "dev" Requires-Dist: pytest-mock; extra == "dev" -Requires-Dist: ruff; extra == "dev" +Requires-Dist: ruff==0.15.8; extra == "dev" Requires-Dist: babel; extra == "dev" Requires-Dist: validate-pyproject; extra == "dev" Provides-Extra: all-plugins Requires-Dist: cryptography; extra == "all-plugins" Requires-Dist: gntp; extra == "all-plugins" -Requires-Dist: paho-mqtt!=2.0.*; extra == "all-plugins" +Requires-Dist: paho-mqtt>=2.1.0; extra == "all-plugins" Requires-Dist: PGPy; extra == "all-plugins" +Requires-Dist: hidapi; extra == "all-plugins" Requires-Dist: smpplib; extra == "all-plugins" Requires-Dist: slixmpp>=1.10.0; extra == "all-plugins" Provides-Extra: windows @@ -114,31 +115,37 @@ The section identifies all of the services supported by this library. [Check out ## Productivity Based Notifications -The table below identifies the services this tool supports and some example service urls you need to use in order to take advantage of it. Click on any of the services listed below to get more details on how you can configure Apprise to access them. +The table below identifies the services this tool supports and some example service urls you need to use in order to take advantage of it. Click on any of the services listed below to get more details on how you can configure Apprise to access them. If you're having trouble constructing your own URL; try our [Apprise URL Builder](https://appriseit.com/tools/url-builder/) out. | Notification Service | Service ID | Default Port | Example Syntax | | -------------------- | ---------- | ------------ | -------------- | | [Apprise API](https://appriseit.com/services/apprise_api/) | apprise:// or apprises:// | (TCP) 80 or 443 | apprise://hostname/Token | [AWS SES](https://appriseit.com/services/ses/) | ses:// | (TCP) 443 | ses://user@domain/AccessKeyID/AccessSecretKey/RegionName
ses://user@domain/AccessKeyID/AccessSecretKey/RegionName/email1/email2/emailN | [Bark](https://appriseit.com/services/bark/) | bark:// | (TCP) 80 or 443 | bark://hostname
bark://hostname/device_key
bark://hostname/device_key1/device_key2/device_keyN
barks://hostname
barks://hostname/device_key
barks://hostname/device_key1/device_key2/device_keyN +| [Blink(1)](https://appriseit.com/services/blink1/) | blink1:// | USB | blink1://
blink1://serial/ | [BlueSky](https://appriseit.com/services/bluesky/) | bluesky:// | (TCP) 443 | bluesky://Handle:AppPw
bluesky://Handle:AppPw/TargetHandle
bluesky://Handle:AppPw/TargetHandle1/TargetHandle2/TargetHandleN | [Brevo](https://appriseit.com/services/brevo/) | brevo:// | (TCP) 443 | brevo://APIToken:FromEmail/
brevo://APIToken:FromEmail/ToEmail
brevo://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/ | [Chanify](https://appriseit.com/services/chanify/) | chantify:// | (TCP) 443 | chantify://token +| [Amazon Chime](https://appriseit.com/services/chime/) | chime:// | (TCP) 443 | chime://WebhookID/Token | [Discord](https://appriseit.com/services/discord/) | discord:// | (TCP) 443 | discord://webhook_id/webhook_token
discord://avatar@webhook_id/webhook_token | [Dot.](https://appriseit.com/services/dot/) | dot:// | (TCP) 443 | dot://apikey@device_id/text/
dot://apikey@device_id/image/
**Note**: `device_id` is the Quote/0 hardware serial | [Emby](https://appriseit.com/services/emby/) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/
emby://user:password@hostname | [Enigma2](https://appriseit.com/services/enigma2/) | enigma2:// or enigma2s:// | (TCP) 80 or 443 | enigma2://hostname +| [Evolution API](https://appriseit.com/services/evolution/) | evolution:// or evolutions:// | (TCP) 80 or 443 | evolution://apikey@hostname/instance/ToPhoneNo
evolution://apikey@hostname:port/instance/ToPhoneNo
evolution://apikey@hostname/instance/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [FCM](https://appriseit.com/services/fcm/) | fcm:// | (TCP) 443 | fcm://project@apikey/DEVICE_ID
fcm://project@apikey/#TOPIC
fcm://project@apikey/DEVICE_ID1/#topic1/#topic2/DEVICE_ID2/ | [Feishu](https://appriseit.com/services/feishu/) | feishu:// | (TCP) 443 | feishu://token | [Flock](https://appriseit.com/services/flock/) | flock:// | (TCP) 443 | flock://token
flock://botname@token
flock://app_token/u:userid
flock://app_token/g:channel_id
flock://app_token/u:userid/g:channel_id | [Google Chat](https://appriseit.com/services/googlechat/) | gchat:// | (TCP) 443 | gchat://workspace/key/token | [Gotify](https://appriseit.com/services/gotify/) | gotify:// or gotifys:// | (TCP) 80 or 443 | gotify://hostname/token
gotifys://hostname/token?priority=high +| [GroupMe](https://appriseit.com/services/groupme/) | groupme:// | (TCP) 443 | groupme://bot_id
groupme://bot_id/access_token | [Growl](https://appriseit.com/services/growl/) | growl:// | (UDP) 23053 | growl://hostname
growl://hostname:portno
growl://password@hostname
growl://password@hostname:port
**Note**: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1 | [Guilded](https://appriseit.com/services/guilded/) | guilded:// | (TCP) 443 | guilded://webhook_id/webhook_token
guilded://avatar@webhook_id/webhook_token | [Home Assistant](https://appriseit.com/services/homeassistant/) | hassio:// or hassios:// | (TCP) 8123 or 443 | hassio://hostname/accesstoken
hassio://user@hostname/accesstoken
hassio://user:password@hostname:port/accesstoken
hassio://hostname/optional/path/accesstoken +| [HumHub](https://appriseit.com/services/humhub/) | humhub:// or humhubs:// | (TCP) 80 or 443 | humhubs://token@hostname/container_id
humhubs://user:password@hostname/container_id
humhubs://token@hostname/id1/id2/id3 | [IFTTT](https://appriseit.com/services/ifttt/) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event
ifttt://webhooksID/Event1/Event2/EventN
ifttt://webhooksID/Event1/?+Key=Value
ifttt://webhooksID/Event1/?-Key=value1 | [IRC](https://appriseit.com/services/irc/) | irc:// or ircs:// | (TCP) 6667 or 6697 | ircs://user:pass@irc.server/@user
ircs://user:pass@irc.server/#channel?join=true&mode=nickserv
ircs://user:pass@znc.server/@user1/@user2/@user3/#channel1 | [Jellyfin](https://appriseit.com/services/jellyfin/) | jellyfin:// or jellyfins:// | (TCP) 8096 | jellyfin://user@hostname/
jellyfins://user:password@hostname +| [Jira](https://appriseit.com/services/jira/) | jira:// | (TCP) 443 | jira://APIKey
jira://APIKey/@UserID
jira://APIKey/#Team
jira://APIKey/\*Schedule
jira://APIKey/^Escalation | [Join](https://appriseit.com/services/join/) | join:// | (TCP) 443 | join://apikey/device
join://apikey/device1/device2/deviceN/
join://apikey/group
join://apikey/groupA/groupB/groupN
join://apikey/DeviceA/groupA/groupN/DeviceN/ | [KODI](https://appriseit.com/services/kodi/) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname
kodi://user@hostname
kodi://user:password@hostname:port | [Kumulos](https://appriseit.com/services/kumulos/) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey @@ -150,7 +157,6 @@ The table below identifies the services this tool supports and some example serv | [Matrix](https://appriseit.com/services/matrix/) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname
matrix://user@hostname
matrixs://user:pass@hostname:port/#room_alias
matrixs://user:pass@hostname:port/!room_id
matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2
matrixs://token@hostname:port/?webhook=matrix
matrix://user:token@hostname/?webhook=slack&format=markdown | [Mattermost](https://appriseit.com/services/mattermost/) | mmost:// or mmosts:// | (TCP) 8065 | mmost://hostname/authkey
mmost://hostname:80/authkey
mmost://user@hostname:80/authkey
mmost://hostname/authkey?channel=channel
mmosts://hostname/authkey
mmosts://user@hostname/authkey
| [Microsoft Power Automate / Workflows (MSTeams)](https://appriseit.com/services/workflows/) | workflows:// | (TCP) 443 | workflows://WorkflowID/Signature/ -| [Microsoft Teams](https://appriseit.com/services/msteams/) | msteams:// | (TCP) 443 | msteams://TokenA/TokenB/TokenC/ | [Misskey](https://appriseit.com/services/misskey/) | misskey:// or misskeys://| (TCP) 80 or 443 | misskey://access_token@hostname | [MQTT](https://appriseit.com/services/mqtt/) | mqtt:// or mqtts:// | (TCP) 1883 or 8883 | mqtt://hostname/topic
mqtt://user@hostname/topic
mqtts://user:pass@hostname:9883/topic | [Nextcloud](https://appriseit.com/services/nextcloud/) | ncloud:// or nclouds:// | (TCP) 80 or 443 | ncloud://adminuser:pass@host/User
nclouds://adminuser:pass@host/User1/User2/UserN @@ -158,8 +164,9 @@ The table below identifies the services this tool supports and some example serv | [Notica](https://appriseit.com/services/notica/) | notica:// | (TCP) 443 | notica://Token/ | [NotificationAPI](https://appriseit.com/services/notificationapi/) | napi:// | (TCP) 443 | napi://ClientID/ClientSecret/Target
napi://ClientID/ClientSecret/Target1/Target2/TargetN
napi://MessageType@ClientID/ClientSecret/Target | [Notifiarr](https://appriseit.com/services/notifiarr/) | notifiarr:// | (TCP) 443 | notifiarr://apikey/#channel
notifiarr://apikey/#channel1/#channel2/#channeln -| [Notifico](https://appriseit.com/services/notifico/) | notifico:// | (TCP) 443 | notifico://ProjectID/MessageHook/ +| [Notifico](https://appriseit.com/services/notifico/) | notifico:// or notificos:// | (TCP) 80 or 443 | notifico://ProjectID/MessageHook/
notifico://host/ProjectID/MessageHook/
notificos://host/ProjectID/MessageHook/ | [ntfy](https://appriseit.com/services/ntfy/) | ntfy:// | (TCP) 80 or 443 | ntfy://topic/
ntfys://topic/ +| [Octopush](https://appriseit.com/services/octopush/) | octopush:// | (TCP) 443 | octopush://APILogin/APIKey/TargetPhoneNo
octopush://Sender:APILogin/APIKey/TargetPhoneNo
octopush://Sender:APILogin/APIKey/TargetPhoneNo1/TargetPhoneNo2/TargetPhoneNoN | [Office 365](https://appriseit.com/services/office365/) | o365:// | (TCP) 443 | o365://TenantID:AccountEmail/ClientID/ClientSecret
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail1/TargetEmail2/TargetEmailN | [OneSignal](https://appriseit.com/services/onesignal/) | onesignal:// | (TCP) 443 | onesignal://AppID@APIKey/PlayerID
onesignal://TemplateID:AppID@APIKey/UserID
onesignal://AppID@APIKey/#IncludeSegment
onesignal://AppID@APIKey/Email | [Opsgenie](https://appriseit.com/services/opsgenie/) | opsgenie:// | (TCP) 443 | opsgenie://APIKey
opsgenie://APIKey/UserID
opsgenie://APIKey/#Team
opsgenie://APIKey/\*Schedule
opsgenie://APIKey/^Escalation @@ -167,6 +174,7 @@ The table below identifies the services this tool supports and some example serv | [PagerTree](https://appriseit.com/services/pagertree/) | pagertree:// | (TCP) 443 | pagertree://integration_id | [ParsePlatform](https://appriseit.com/services/parseplatform/) | parsep:// or parseps:// | (TCP) 80 or 443 | parsep://AppID:MasterKey@Hostname
parseps://AppID:MasterKey@Hostname | [PopcornNotify](https://appriseit.com/services/popcornnotify/) | popcorn:// | (TCP) 443 | popcorn://ApiKey/ToPhoneNo
popcorn://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
popcorn://ApiKey/ToEmail
popcorn://ApiKey/ToEmail1/ToEmail2/ToEmailN/
popcorn://ApiKey/ToPhoneNo1/ToEmail1/ToPhoneNoN/ToEmailN +| [Postmark](https://appriseit.com/services/postmark/) | postmark:// | (TCP) 443 | postmark://APIToken:FromEmail/
postmark://APIToken:FromEmail/ToEmail
postmark://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/ | [Prowl](https://appriseit.com/services/prowl/) | prowl:// | (TCP) 443 | prowl://apikey
prowl://apikey/providerkey | [PushBullet](https://appriseit.com/services/pushbullet/) | pbul:// | (TCP) 443 | pbul://accesstoken
pbul://accesstoken/#channel
pbul://accesstoken/A_DEVICE_ID
pbul://accesstoken/email@address.com
pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE | [Pushjet](https://appriseit.com/services/pushjet/) | pjet:// or pjets:// | (TCP) 80 or 443 | pjet://hostname/secret
pjet://hostname:port/secret
pjets://secret@hostname/secret
pjets://hostname:port/secret @@ -188,6 +196,7 @@ The table below identifies the services this tool supports and some example serv | [SendGrid](https://appriseit.com/services/sendgrid/) | sendgrid:// | (TCP) 443 | sendgrid://APIToken:FromEmail/
sendgrid://APIToken:FromEmail/ToEmail
sendgrid://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/ | [SendPulse](https://appriseit.com/services/sendpulse/) | sendpulse:// | (TCP) 443 | sendpulse://user@host/ClientId/ClientSecret
sendpulse://user@host/ClientId/clientSecret/ToEmail
sendpulse://user@host/ClientId/ClientSecret/ToEmail1/ToEmail2/ToEmailN/ | [ServerChan](https://appriseit.com/services/serverchan/) | schan:// | (TCP) 443 | schan://sendkey/ +| [Session Open Group Server](https://appriseit.com/services/sogs/) | session:// or sessions:// | (TCP) 80 or 443 | sessions://public_key:seed@hostname/room
sessions://public_key:seed@hostname/room1/room2
sessions://public_key:seed@hostname:port/room
session://public_key:seed@hostname/room | [Signal API](https://appriseit.com/services/signal/) | signal:// or signals:// | (TCP) 80 or 443 | signal://hostname:port/FromPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [SIGNL4](https://appriseit.com/services/signl4/) | signl4:// | (TCP) 80 or 443 | signl4://hostname | [SimplePush](https://appriseit.com/services/simplepush/) | spush:// | (TCP) 443 | spush://apikey
spush://salt:password@apikey
spush://apikey?event=Apprise @@ -206,11 +215,13 @@ The table below identifies the services this tool supports and some example serv | [Vapid (WebPush)](https://appriseit.com/services/vapid/) | vapid:// | (TCP) 443 | vapid://subscriber/target
vapid://subscriber/target?subfile=path&keyfile=path | [Viber](https://appriseit.com/services/viber/) | viber:// | (TCP) 443 | viber://token/target | [Webex Teams (Cisco)](https://appriseit.com/services/wxteams/) | wxteams:// | (TCP) 443 | wxteams://Token +| [WeChat (WeCom)](https://appriseit.com/services/wechat/) | wechat:// | (TCP) 443 | wechat://CorpID:AppSecret@AgentID/@all
wechat://CorpID:AppSecret@AgentID/UserID | [WeCom Bot](https://appriseit.com/services/wecombot/) | wecombot:// | (TCP) 443 | wecombot://BotKey | [WhatsApp](https://appriseit.com/services/whatsapp/) | whatsapp:// | (TCP) 443 | whatsapp://AccessToken@FromPhoneID/ToPhoneNo
whatsapp://Template:AccessToken@FromPhoneID/ToPhoneNo | [WxPusher](https://appriseit.com/services/wxpusher/) | wxpusher:// | (TCP) 443 | wxpusher://AppToken@UserID1/UserID2/UserIDN
wxpusher://AppToken@Topic1/Topic2/Topic3
wxpusher://AppToken@UserID1/Topic1/ | [XBMC](https://appriseit.com/services/xbmc/) | xbmc:// or xbmcs:// | (TCP) 8080 or 443 | xbmc://hostname
xbmc://user@hostname
xbmc://user:password@hostname:port | [XMPP](https://appriseit.com/services/xmpp/) | xmpp:// or xmpps:// | (TCP) 5222 or 5223 | xmpp://user:pass@hostname
xmpps://user:pass@hostname/jid
xmpps://user:pass@hostname/jid1/jid2@example.ca +| [Zoom](https://appriseit.com/services/zoom/) | zoom:// | (TCP) 443 | zoom://WebhookID/Token | [Zulip Chat](https://appriseit.com/services/zulip/) | zulip:// | (TCP) 443 | zulip://botname@Organization/Token
zulip://botname@Organization/Token/Stream
zulip://botname@Organization/Token/Email ## SMS Notifications @@ -231,12 +242,14 @@ SMS Notifications for the most part do not have a both a `title` and `body`. Th | [DAPNET](https://appriseit.com/services/dapnet/) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign
dapnet://user:pass@callsign1/callsign2/callsignN | [D7 Networks](https://appriseit.com/services/d7networks/) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo
d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [DingTalk](https://appriseit.com/services/dingtalk/) | dingtalk:// | (TCP) 443 | dingtalk://token/
dingtalk://token/ToPhoneNo
dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/ +| [Exotel](https://appriseit.com/services/exotel/) | exotel:// | (TCP) 443 | exotel://sid:token@FromPhoneNo
exotel://sid:token@FromPhoneNo/ToPhoneNo
exotel://sid:token@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [Free-Mobile](https://appriseit.com/services/freemobile/) | freemobile:// | (TCP) 443 | freemobile://user@password/ | [httpSMS](https://appriseit.com/services/httpsms/) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo
httpsms://ApiKey@FromPhoneNo/ToPhoneNo
httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Kavenegar](https://appriseit.com/services/kavenegar/) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo
kavenegar://FromPhoneNo@ApiKey/ToPhoneNo
kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [MessageBird](https://appriseit.com/services/messagebird/) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo
msgbird://ApiKey/FromPhoneNo/ToPhoneNo
msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [MSG91](https://appriseit.com/services/msg91/) | msg91:// | (TCP) 443 | msg91://TemplateID@AuthKey/ToPhoneNo
msg91://TemplateID@AuthKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Plivo](https://appriseit.com/services/plivo/) | plivo:// | (TCP) 443 | plivo://AuthID@Token@FromPhoneNo
plivo://AuthID@Token/FromPhoneNo/ToPhoneNo
plivo://AuthID@Token/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [RingCentral](https://appriseit.com/services/ringcentral/) | ringc:// | (TCP) 443 | ringc://SourcePhoneNo:Password@ClientID/ClientSecret
ringc://SourcePhoneNo:JWTToken@ClientID/ClientSecret/ToPhoneNo
ringc://SourcePhoneNo:JWTToken@ClientID/ClientSecret/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Seven](https://appriseit.com/services/seven/) | seven:// | (TCP) 443 | seven://ApiKey/FromPhoneNo
seven://ApiKey/FromPhoneNo/ToPhoneNo
seven://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Société Française du Radiotéléphone (SFR)](https://appriseit.com/services/sfr/) | sfr:// | (TCP) 443 | sfr://user:password>@spaceId/ToPhoneNo
sfr://user:password>@spaceId/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Signal API](https://appriseit.com/services/signal/) | signal:// or signals:// | (TCP) 80 or 443 | signal://hostname:port/FromPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo
signal://hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ @@ -387,6 +400,24 @@ apprise -vv -t "Union Test" \ apprise -vv -t "Intersection Test" \ --config=~/apprise.yml \ -g devops,critical +``` + +Tags also support an optional **priority prefix** and **retry suffix**. In your configuration file you assign a priority to a tag like `1:alerts` or `5:alerts`. A lower number means higher urgency. + +* **Escalation (no prefix)**: `-g alerts` dispatches priority-1 entries first. If they all succeed, Apprise returns early and never runs the priority-5 fallbacks. A failure in the lower-priority group triggers the next group as an escalation chain. +* **Exclusive (with prefix)**: `-g "2:alerts"` notifies *only* services whose `alerts` tag has priority 2. No other priority levels are triggered. +* **Per-call retry**: `-g "alerts:3"` retries each matched service up to 3 times on failure (overrides the service's own retry setting for this call only). +* **Combined**: `-g "2:alerts:3"` -- exclusive priority-2 filter with up to 3 retries. + +```bash +# Escalation: priority-1 first; skip priority-5 if all succeed +apprise -vv -t "Alert" --config=~/apprise.yml -g alerts + +# Exclusive: only priority-2 alert services +apprise -vv -t "Alert" --config=~/apprise.yml -g "2:alerts" + +# With retry override: retry each matched service up to 3 times +apprise -vv -t "Alert" --config=~/apprise.yml -g "alerts:3" ## CLI File Attachments diff --git a/libs/apprise-1.11.0.dist-info/RECORD b/libs/apprise-1.11.0.dist-info/RECORD new file mode 100644 index 000000000..2ec52b662 --- /dev/null +++ b/libs/apprise-1.11.0.dist-info/RECORD @@ -0,0 +1,254 @@ +../../bin/apprise,sha256=41ocDdUKbn8QbOtncNxcwagiZ6d2DL1ARZyEgip9OYA,186 +apprise-1.11.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +apprise-1.11.0.dist-info/METADATA,sha256=0Mtor318wezpQZiQZK7qgtmhipQOxIqI301lFtU8Zm0,60979 +apprise-1.11.0.dist-info/RECORD,, +apprise-1.11.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise-1.11.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +apprise-1.11.0.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 +apprise-1.11.0.dist-info/licenses/LICENSE,sha256=msAH6Aa9cZnPMVit_4OhY1GuhcRjc8NTOpi5YZdRA_I,1343 +apprise-1.11.0.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 +apprise/__init__.py,sha256=ywvSlfaydFuLcYHLx1TN_6vhJqlimlLWEk6xtVaWTno,3900 +apprise/apprise.py,sha256=dcaebcBwAeXNyjPgwDoLZDTQTst7snMKL89Iz3rbaCg,66621 +apprise/apprise_attachment.py,sha256=I7H83OHlI0C4b2JxYvomk2zU3Z3ySZzer7lG017xVOo,13671 +apprise/apprise_config.py,sha256=KlCjql3qQNAMUYFHLFbhr2OB6SlUpNqDMLmgV6VZXZA,18537 +apprise/asset.py,sha256=juhfgB6TQEIKSbYR3tkgvEHW6fQrAfNqKd4EgnSYvyA,19000 +apprise/assets/NotifyXML-1.0.xsd,sha256=292qQ_IUl5EWDhPyzm9UTT0C2rVvJkyGar8jiODkJs8,986 +apprise/assets/NotifyXML-1.1.xsd,sha256=bjR3CGG4AEXoJjYkGCbDttKHSkPP1FlIWO02E7G59g4,1758 +apprise/assets/themes/default/apprise-failure-128x128.ico,sha256=Mt0ptfHJaN3Wsv5UCNDn9_3lyEDHxVDv1JdaDEI_xCA,67646 +apprise/assets/themes/default/apprise-failure-128x128.png,sha256=66ps8TDPxVH3g9PlObJqF-0x952CjnqQyN3zvpRcOT8,16135 +apprise/assets/themes/default/apprise-failure-256x256.png,sha256=bQBsKKCsKfR9EqgYOZrcVcVa5y8qG58PN2mEqO5eNRI,41931 +apprise/assets/themes/default/apprise-failure-32x32.png,sha256=vH0pZffIDCvkejpr3fJHGXW__8Yc3R_p0bacX6t6l18,2437 +apprise/assets/themes/default/apprise-failure-72x72.png,sha256=EP5A8DHRDr9srgupFSwOoyQ308bNJ8aL192J_L4K-ec,7600 +apprise/assets/themes/default/apprise-info-128x128.ico,sha256=F5_CirmXueRCRI5Z_Crf6TS6jVIXTJlRD83zw1oJ66g,67646 +apprise/assets/themes/default/apprise-info-128x128.png,sha256=bBqRZAgQey-gkmJrnFhPbzjILSrljE59mRkgj3raMQo,16671 +apprise/assets/themes/default/apprise-info-256x256.png,sha256=B5r_O4d9MHCmSWZwfbqQgZSp-ZetTdiBSwKcMTF1aFA,43331 +apprise/assets/themes/default/apprise-info-32x32.png,sha256=lt3NZ95TzkiCNVNlurrB2fE2nriMa1wftl7nrNXmb6c,2485 +apprise/assets/themes/default/apprise-info-72x72.png,sha256=kDnsZpqNUZGqs9t1ECUup7FOfXUIL-rupnQCYJp9So4,7875 +apprise/assets/themes/default/apprise-logo.png,sha256=85ttALudKkLmiqilJT7mUQLUXRFmM1AK89rnwLm313s,160907 +apprise/assets/themes/default/apprise-success-128x128.ico,sha256=uCopPwdQjxgfohKazHaDzYs9y4oiaOpL048PYC6WRlg,67646 +apprise/assets/themes/default/apprise-success-128x128.png,sha256=nvDuU_QqhGlw6cMtdj7Mv-gPgqCEx-0DaaXn1KBLVYg,17446 +apprise/assets/themes/default/apprise-success-256x256.png,sha256=vXfKuxY3n0eeXHKdb9hTxICxOEn7HjAQ4IZpX0HSLzc,48729 +apprise/assets/themes/default/apprise-success-32x32.png,sha256=Jg9pFJh3YPI-LiPBebyJ7Z4Vt7BRecaE8AsRjQVIkME,2471 +apprise/assets/themes/default/apprise-success-72x72.png,sha256=FQbgvIhqKOhEK0yvrhaSpai0R7hrkTt_-GaC2KUgCCk,7858 +apprise/assets/themes/default/apprise-warning-128x128.ico,sha256=6XaQPOx0oWK_xbhr4Yhb7qNazCWwSs9lk2SYR2MHTrQ,67646 +apprise/assets/themes/default/apprise-warning-128x128.png,sha256=pf5c4Ph7jWH7gf39dJoieSj8TzAsY3TXI-sGISGVIW4,16784 +apprise/assets/themes/default/apprise-warning-256x256.png,sha256=SY-xlaiXaj420iEYKC2_fJxU-yj2SuaQg6xfPNi83bw,43708 +apprise/assets/themes/default/apprise-warning-32x32.png,sha256=97R2ywNvcwczhBoWEIgajVtWjgT8fLs4FCCz4wu0dwc,2472 +apprise/assets/themes/default/apprise-warning-72x72.png,sha256=L8moEInkO_OLxoOcuvN7rmrGZo64iJeH20o-24MQghE,7913 +apprise/attachment/__init__.py,sha256=HVj63cGWbHlQrUy8WedaDFtvSRMPKsATb8lvKXfR-6A,1654 +apprise/attachment/base.py,sha256=_6PK9tIWy_PwtcbT168i_yPYR_d1F0LUv33GYrtLTbI,16446 +apprise/attachment/file.py,sha256=MGfXyCZDVxbvK3_s_2-eWVY33RCwOmsWXB2qJ32DTd0,4952 +apprise/attachment/http.py,sha256=SSvY5shFLbwxuzoMBdlXWpgvMWg9HVU0Yc3OJiqkFHk,14600 +apprise/attachment/memory.py,sha256=V8VR8YV3UZd26cS3lc1jNwInKZJfF9K9TEfiyiTXPFY,6911 +apprise/cli.py,sha256=rdU-TNE7Lep139L9ISZ4ec117UDI3GcOS5l1hmsMzyc,42145 +apprise/common.py,sha256=AEBMjg7_q70LnG0ij9hIzhbRoufHuQ4dMHJS6YPHnec,7486 +apprise/compat.py,sha256=hjaMSzwCcdodK79bZ54_vxhhO_KhcidIen-tJwE0NQQ,2043 +apprise/config/__init__.py,sha256=BrkAKU9NZ_dN8KuiUwBjYB9U7gU1wVW95uksS9rAbmI,1655 +apprise/config/base.py,sha256=LX7CcsNrNpEMHUICNAQ5DK_J991lvOdnXf5flbTOAl4,58001 +apprise/config/file.py,sha256=7dQFjecSuVPHhXwoaRiPSaKr8cY3Ec0a7xVWV2PSIN0,6117 +apprise/config/http.py,sha256=6-9c8ZaPkYB-vZl24Oqsbl0lLzLeqdyHmjDIYUlXGXg,10183 +apprise/config/memory.py,sha256=J49cB4MFDvJADgnZVCX1GmNSwrQm-wnFsi6o1nY22vg,2733 +apprise/conversion.py,sha256=KnlhqtSAElUH_gNUJye8VVf5gpUXRYh8sie3BRfOerk,6392 +apprise/decorators/__init__.py,sha256=UJvg7JUgzj50yr_uQqZd1Z2y6My-T9oeR2Luc1nUDJw,1456 +apprise/decorators/base.py,sha256=EIQtJ6he7av7sxo2YgyXAsHJ_m4n4ECBC5ayRTH7qA8,7899 +apprise/decorators/notify.py,sha256=yNtbZ3zID6cYnR6b-UTuZ0ILmZETwPO0HK2ZzjpvOUI,5066 +apprise/emojis.py,sha256=NW3L0g3cEJt9LqRrEajjiC6JtSCi_uJgajwfIHfl1ak,88358 +apprise/exception.py,sha256=9mOUrGXxLzwdpMo7XEk4iMUaED5FJU6QiSf9q5-NQdk,2512 +apprise/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise/i18n/apprise.pot,sha256=DuZXxP_KzuSndO5uUORhhI2PBv2kYaeBd3o4W7PczSY,50042 +apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=Wafexa8rhH5HIta_BLgU0qG3GZG40af3Y3nRv9leeHo,3656 +apprise/i18n/en/LC_MESSAGES/apprise.po,sha256=S8JVYUIQW9xv3k_FIciFpu9d1BOucJZQnfIhNvparew,54010 +apprise/i18n/es/LC_MESSAGES/apprise.mo,sha256=HFRjB7DE_sDRSRaLSBD-KrRXu6j9vO6o24jFrDCa6L8,11771 +apprise/i18n/es/LC_MESSAGES/apprise.po,sha256=TSoHNvU9vlj-oVQN3TBUb-8_azMHqgQCK1sF-eR94EA,58071 +apprise/i18n/pt_BR/LC_MESSAGES/apprise.mo,sha256=cw4VhUSJ00Awnhu16fOhS4GNN1B_I3c99c6XrzqG51o,11662 +apprise/i18n/pt_BR/LC_MESSAGES/apprise.po,sha256=1ARdfjdBMeb87qKlyVDpJCtBjIq0p0Cm_ay5zo96qis,57850 +apprise/locale.py,sha256=Qg0Jr49cPQQctNrzCSuzrWd27YMGgoG7shrAt6kepJU,8983 +apprise/logger.py,sha256=01qEr8aL2RrGnl7DNaawEbBLnGZr0izn9vb6e4e974U,6715 +apprise/manager.py,sha256=UuhUHpRZS3XlAksCp7rdrZ7_HUBSBDSLGQIFWfDfYac,34863 +apprise/manager_attachment.py,sha256=Zqqq6X-m1qvIKbbqdXvCuuwfbzE7kXSBmDVjKD_Vkvg,2134 +apprise/manager_config.py,sha256=G1jqINnmbRkzWYusEDi5kOrOK09vfKaISq81P1lwIMk,2146 +apprise/manager_plugins.py,sha256=xoXNG-UUxqAmCxhoyQ-TCybsmJeHMCKrWrIBnyVUUEA,2160 +apprise/persistent_store.py,sha256=F99gVerv2v3lKJezlwhvXeQWgp136byHOfYAsjia_VM,60970 +apprise/plugins/__init__.py,sha256=RZqi1nZCgQOmC6y6axLb0zqebMFkFdpmygsWUXFCHkQ,18662 +apprise/plugins/africas_talking.py,sha256=8-FB2e4J7LdHQ1P3TZVfRQIVbzCpc-9Mw2EwjButqJQ,16761 +apprise/plugins/apprise_api.py,sha256=rrdXLTQdoiBRc97YGFINdCCoCEdXGRKnrTr4tsW3i30,18416 +apprise/plugins/aprs.py,sha256=ZCWAZm0ny8pYgF8YWjrJ62Z_9y5Snjy59PVuR-cZ-G0,25100 +apprise/plugins/bark.py,sha256=mLguWutI5ZTb1aTXpT2PthZ5KVIRC4V4yqYgQWX2s04,19054 +apprise/plugins/base.py,sha256=FKRyHMmftPVbRGB7L3fRN7ORMl1DZBl4qnRp2O3Xjz0,51984 +apprise/plugins/blink1.py,sha256=BwLu77Jd3zeFFF_C8avnj_7Li-lhlL4hjHF8cesSYQE,11913 +apprise/plugins/bluesky.py,sha256=uNSSIagSLHZsuj_L9FfIUnO2QmVB_xkSaX44q1olXEk,23425 +apprise/plugins/brevo.py,sha256=_yCesLYXTKlwKBH1bnsGnTXA928hK0BovODPfnDNE_E,19821 +apprise/plugins/bulksms.py,sha256=T9Carw0ZufcpI2DEZ0YnefqCZ40Y5eYB2gbtQx5rciM,17147 +apprise/plugins/bulkvs.py,sha256=UF0whPeQRGhxvWZQjNWY6T2ouIETxt8b60GD7feYJW0,13995 +apprise/plugins/burstsms.py,sha256=CkYPgYN7TK0rBLbPo95CE9c2SpflbIVE9F0wtfwUCbw,16221 +apprise/plugins/chanify.py,sha256=NKVTW2jBgkyeZMuc7xoRtYCWsOW0yDk-segCtB1caTk,6952 +apprise/plugins/chime.py,sha256=bj7GUV4293raiz3J8Tg73cfOqgNwBF_wmrbe8XebvxU,11931 +apprise/plugins/clickatell.py,sha256=rbZ7vp276r5VRQJtslwJLuQ28wy8NZTDQ2BddyJWq3Y,10482 +apprise/plugins/clicksend.py,sha256=vfItj7o1CnMqqpnQQ2ZYXI5mdrKpbEmMAItbguyy-hA,12087 +apprise/plugins/custom_form.py,sha256=_0iskc1jpdy5f0jiKhXIINdE0pgWzZdP-TmzkeKJ1rc,19128 +apprise/plugins/custom_json.py,sha256=dm6cyMw0TU4HGqzWyrj6DMpjoT9Y9WJiZtFlCfvlEzU,14831 +apprise/plugins/custom_xml.py,sha256=AP2JSWrkiuv0zcQDJgITb4_4iYX0EA77-7i6tUxYzjQ,18199 +apprise/plugins/d7networks.py,sha256=a2Yj9dEXZ7MxmV23qjzMMzYXFzgZlxaFaY952TUWqX0,15647 +apprise/plugins/dapnet.py,sha256=gF04ccpPHYX9LvXNdQWcLjYDlE5D9fk8XiKR0if9oh0,13927 +apprise/plugins/dbus.py,sha256=0cGS6sik2UPikngiLC7iDtNcH6PUIkrITXi9rq8uCVQ,14498 +apprise/plugins/dingtalk.py,sha256=j_4c3kJjS4eGuevXpu_YCvnvNgN6ivVIlW6ajRu2QqE,12408 +apprise/plugins/discord.py,sha256=h655izKLimdwVc6A0oF6u2mpend2cNGS6kpgJYk9R28,31308 +apprise/plugins/dot.py,sha256=_iHza1hmocKAPyfJk6J3YbROT1u-zpUeOE-rjUiVitc,20678 +apprise/plugins/email/__init__.py,sha256=kTDw88J0x5MVAJC7UIXI-RpiGgzoXI0YanBXvvciylE,1917 +apprise/plugins/email/base.py,sha256=uCNqozZXv_RrcHAI6z_VX3W_KYnUBId2WE5-e-aUI7o,52811 +apprise/plugins/email/common.py,sha256=oJiGN2GKiXaDbiiJI0CSB32FIf8dqLuVqrJA75KLeJw,2527 +apprise/plugins/email/templates.py,sha256=M5GrxGCMnYzE7xlyBbPdmfoyw8RCyxW9aaPV1zqPZys,10348 +apprise/plugins/emby.py,sha256=8XrCK2_v6pbAjstn9gdJdPMXgLthTUcgEvHzf9Qf90M,22779 +apprise/plugins/enigma2.py,sha256=URxlnKP09-WXOUEopInuQy01tRSAzzr_jSYF1RdzOiw,12174 +apprise/plugins/evolution.py,sha256=deKmwA-dA-wbL9PObR4w5Llu4AGAmAKR1VWp4YqQBso,12229 +apprise/plugins/exotel.py,sha256=WfmJo21ZzeIoMJVA9S5NQzGdu5ZVcE5jZW1TXuIOz2c,20858 +apprise/plugins/fcm/__init__.py,sha256=vgib00h4Yxm8z_4LE_9WQxb5L1PlLOFJ2hreGBBImWo,22484 +apprise/plugins/fcm/color.py,sha256=rqSeq9Bc6gmwKAqOMLNvSJDFUAKIBwDP5lDxpYd1ClA,4609 +apprise/plugins/fcm/common.py,sha256=yaEc5lOrEr5UFHd24CKhNNZW39PKDqR6Uxa8IwXVBP0,1687 +apprise/plugins/fcm/oauth.py,sha256=OGM1u_4_FRsQiNU5BLeQzum9ZIOK-fMSl6FJxEOtQMI,11378 +apprise/plugins/fcm/priority.py,sha256=lc5N6TfPR6OTnNlgS2AnSeQRIW0umPp0gKYi0csgg64,7216 +apprise/plugins/feishu.py,sha256=v9-85JVuMZEfvHxjXvBHBfFX7HFfjiOfcYdpiYsDtTk,7562 +apprise/plugins/flock.py,sha256=N8wvkt8Gl2dqKxsG7nUHBsb9gqdHLL_BAQvHHC6Aur0,13157 +apprise/plugins/fluxer.py,sha256=FWhGwW34L-fdhms5Jye6fwbSmyQU9BaFV0anhZva4hE,36950 +apprise/plugins/fortysixelks.py,sha256=y92jDSginbYBCsSAxGQCPPwd3oyTVPcz8b2jr82jaQc,11860 +apprise/plugins/freemobile.py,sha256=DlvwVaTyiJwY9ppk_6qWYqGE6TV2DOtdkcBeeIPNWs0,6975 +apprise/plugins/glib.py,sha256=asOH5x7zbCkAoCBuVWr6qUxZZC6hL4RIN5yD3-F9S5A,13154 +apprise/plugins/gnome.py,sha256=2Ykzkdncf0UfI4ok-hZIuhQC3__u10U12PBUGbjvWVU,9295 +apprise/plugins/google_chat.py,sha256=NWQhZZRFZ9w1oHfJWlZu5a40oU88Ib9-waGhGma6YF8,13784 +apprise/plugins/gotify.py,sha256=lQWVO2k2TNvRCqbtcskrYn-t03mlgOscC-c95gh8IBY,11227 +apprise/plugins/groupme.py,sha256=S4J3eUdGiWkNvZtd4z8Q9XBSRslIwq8HbhNUygDuqQ0,15426 +apprise/plugins/growl.py,sha256=4iOXfEvipoxaKkdpzDhQYLSufLfwSs4N5gw_vR7-8xM,15155 +apprise/plugins/guilded.py,sha256=ARbx9otLNOMoeYNKftLTu8uuDnkmt91jk61zt9HdgN8,3801 +apprise/plugins/home_assistant.py,sha256=xPc0y3YyyjCjeDHtD-tta0DtwGLZFNhV7Erp1Iv-TVM,21683 +apprise/plugins/httpsms.py,sha256=D5RjOLqgyv6RYpZSBfOILkRLw4_gc3H10a-cWWj0mfQ,11530 +apprise/plugins/humhub.py,sha256=N2jAN_WskSz2DsjnTl3o2WUwJ7m3i44aNNtgBMX79Ds,16499 +apprise/plugins/ifttt.py,sha256=DRZ3AJrg-AdKn6WVWV0wjm8GrhSZaBVdtzQ_sL2rTew,13971 +apprise/plugins/irc/__init__.py,sha256=FnlnCevX7Hxc8zea6r3yqOJimIucLNM6YMDStcfebL8,1493 +apprise/plugins/irc/base.py,sha256=CTfJTKpK8AoxVpCGGMwhcjHdlYDWpED2x4yiknbCilY,17518 +apprise/plugins/irc/client.py,sha256=o7O6lnsoJlCblMiQT-2w9S8RxWcfyFd6zVWi_P7jSok,12872 +apprise/plugins/irc/protocol.py,sha256=MWYYyQTXKiM85t2nYczd5Sthp1EQT8KDnnEOKaWcjYM,6578 +apprise/plugins/irc/state.py,sha256=h9nt0Dvg_4QP7gULXYP8f_QOr5ZnMrOqxbI9sRWvZbs,7459 +apprise/plugins/irc/templates.py,sha256=e5cFGUEddI-0dIck6AwkhX25mD_eGLi8JUqSiifZUCk,2707 +apprise/plugins/jellyfin.py,sha256=Qsqtq0oPXQfZgZekykk6d5TvJBFY7rrBHRpulERczhQ,2155 +apprise/plugins/jira.py,sha256=ZHGZDwneLYryGfD95_3b-fIvS0zXXhLIYN0TYr45L-Y,28701 +apprise/plugins/join.py,sha256=0zDyvW2Q4P8GhmYR-XBwxkujdI6SHXVYG0tlH34riRg,14274 +apprise/plugins/kavenegar.py,sha256=-yRRSVN8PrJgFKlJSaDSMRPyvBUD8kHPd_CegNJcnMY,13010 +apprise/plugins/kodi.py,sha256=e9pjyq8Bs6CD5eex-hMMnQhZ_xngZwYI42R0TUpTIpQ,13136 +apprise/plugins/kumulos.py,sha256=fUqdnxE_WE_FX_gmtqgRPcHfXSd2fleRJCunp8YmQ3k,8529 +apprise/plugins/lametric.py,sha256=TcEAWzRJrtNe5VvVhf6ebuy-7sk1Dt1dByRNjqxKqm0,40372 +apprise/plugins/lark.py,sha256=EfTAbIbRQLjs4vshWgpKVbd8xemgz_-Qza8n_-jjUuc,6472 +apprise/plugins/line.py,sha256=5PeWKaydDyCrHlEjwzP9oQrLm7qv_iY8NXZTw0TISsk,11099 +apprise/plugins/macosx.py,sha256=x5FE8ccOHg3H7r6Q9Q2N24XpHkCUVsk5xbER1Oj7d9Q,8365 +apprise/plugins/mailgun.py,sha256=itS1RER56EUn9nTnQ7hzu9dxp1-l5CIDuBUmssapfZE,26826 +apprise/plugins/mastodon.py,sha256=k3Cnis1YFKmY-tG-2z3iutGPrRYOBdufsMq27xmcRPw,42379 +apprise/plugins/matrix/__init__.py,sha256=KojFSIAfXK7rqDZP3PxAxD8R7S_OyScscjOkwrf7rRo,1590 +apprise/plugins/matrix/base.py,sha256=o70e_mbG9BIbg1rgLmeeHNybjI6-yXIrzJigPGMA9jI,122257 +apprise/plugins/matrix/e2ee.py,sha256=RIhBPJetz6f1WuE44Exb8ahDImL0EOpKrGV48pCJDI0,32337 +apprise/plugins/mattermost.py,sha256=8hXZH8JlS9Btp9_qNWIJXjT9YZa-ra19jQP0z3vQt9Q,31935 +apprise/plugins/messagebird.py,sha256=WQs9_bhOO66ATgTTzAaFy54FSMsCWKLxTBlsWiX4d7E,12622 +apprise/plugins/misskey.py,sha256=Tja2B6FDAih0HB7xzD93HcE8UrvUyZ6o5X1Q38L4DI8,10120 +apprise/plugins/mqtt.py,sha256=SMdmfjskLwBQ7Wd3Fk4_BbQUuA-5_2KW12i23QGzhEY,23617 +apprise/plugins/msg91.py,sha256=Ae4H2WmlMvQlld4Wep9VMr9Dyc5-fCcjPKDfz6j-Nlc,13217 +apprise/plugins/nextcloud.py,sha256=UZVO8mGsD0ngRuHmXs3E-BtNud-BNEmAzu8NznG7DAM,20374 +apprise/plugins/nextcloudtalk.py,sha256=dqx9dwEiwGERuT6X0CcLATCvs9az1hDfJNbKRNU4-uw,11740 +apprise/plugins/notica.py,sha256=sTXv9SyZevH2JY1C0IdAX4nGuLhjIOZbPujS2702sQo,13521 +apprise/plugins/notifiarr.py,sha256=qtahxfExW6kxRgZ5rG_8Jv9DKcdaEoDuMvnsIzVQclI,15956 +apprise/plugins/notificationapi.py,sha256=8TI-ql2Wkl1sgqpvXRkCVqVTVwXtlTgU7PIJS9ApMrs,36173 +apprise/plugins/notifico.py,sha256=YWmt0dNoe3C9T51fv3pNZ_u4kE4xksKTm-4ilu_ni2w,18773 +apprise/plugins/ntfy.py,sha256=3GaUsFqjn1LIAWhtI3xHegstL43TRBWHkKDFbV8m1M4,32875 +apprise/plugins/octopush.py,sha256=HkOiC_cDAOdF3LU5r3eyavVGpzdUK7YU0bzQ2giJekU,16767 +apprise/plugins/office365.py,sha256=3eEU-3braH9BQ41ckKntCNKAWBaE8GQQo78tKiKQTw8,52379 +apprise/plugins/one_signal.py,sha256=LX7uSPo6VzKmo1xIiPSyqPrg7rkDvp3X5m6G71ouDCY,24143 +apprise/plugins/opsgenie.py,sha256=HP_ybGcThF3HCCFchOKlke-_6JRrE0QvbypfWdhI7oo,29445 +apprise/plugins/pagerduty.py,sha256=iZRMvfEO3IhpcCopoK-3EtXGoQsqnGTVXHbZpbW52e0,18988 +apprise/plugins/pagertree.py,sha256=wTuSu0QUk8k-TqZ6qQe8QQRyPGL55zZ56CsDrq5UL2Q,14290 +apprise/plugins/parseplatform.py,sha256=2AXvLFpdtVthgSfCI-anlpRCT3kmqRd-b7_LM42mzxY,11152 +apprise/plugins/plivo.py,sha256=2ounzFO2n7i7EMyYyEXDpvBVrtJRc2K3xQSIj30hEvM,14197 +apprise/plugins/popcorn_notify.py,sha256=-eNCX9OZU1WIQpuBrMiRFbyRf23W8zreRGAgPATQ6dI,11166 +apprise/plugins/postmark.py,sha256=lOeOR5mDMMPyHHvUR8-zYewt9dWAFVg-HbkC28FkIxg,21978 +apprise/plugins/prowl.py,sha256=wKEsp-WvVW_WQ6h3XjpxK7UHtSPCfqN6j2Q1Sw-PiNE,10307 +apprise/plugins/pushbullet.py,sha256=1Ur7f-ZnhF2spx1ECYG9t6jN2-uFQmhIWnrS1_SLckw,16317 +apprise/plugins/pushdeer.py,sha256=OZmBa0905sYt1ZZ7WKAGSDjVvE0sbnZFNR7NRxuuglo,7311 +apprise/plugins/pushed.py,sha256=Y2z0JZFy6QHy7OhY72YNoJoxEdlSnJTW7iqgGG58H54,12763 +apprise/plugins/pushjet.py,sha256=lfdXeCTrRAma5qu1IVmAmHV-kz9O7WHwJY5JWFiPGXU,9656 +apprise/plugins/pushme.py,sha256=vAChu7OTxLdjbGmxBQp_2uCxB-RJMszGWBoJlaYoE2A,7480 +apprise/plugins/pushover.py,sha256=Nh301BPu_E2fX-AN_Alu6cxOWQw5Y0HeQBzdLYICL3U,33385 +apprise/plugins/pushplus.py,sha256=OOwBbfaWGTEtSeD7L-7prMt2WW9E5HLLup7IwLwMDIw,25167 +apprise/plugins/pushsafer.py,sha256=f6F3C-8EKJTy0L_35C5h2TmfaqZqVjoBWYXOeTfo6wo,28452 +apprise/plugins/pushy.py,sha256=DQWsa0PAAnunJkSbUZb2OLUYG5DFuUNNvuJ3kerp5bE,12971 +apprise/plugins/qq.py,sha256=xvVOf7CwGwKIh0-W2QZB2TyPsnJuU9RuV71gRPR2Ndg,5703 +apprise/plugins/reddit.py,sha256=IGdkUbiCShHi07u9Yxnc8S0gDFwZCrX7pci-yGyYQEU,26391 +apprise/plugins/resend.py,sha256=hBB63f-KHG55InjqhK9P5zADr5NzDQtPx6uPFLpC_do,21041 +apprise/plugins/revolt.py,sha256=TkHfENLQkgYqBjKpTuntWrwbp3VxFin28w8hwr0I9e0,15054 +apprise/plugins/ringcentral.py,sha256=IweR6_BbPbgUVix6UuUlARv2InmKh-jsh4KksvfHFtA,29329 +apprise/plugins/rocketchat.py,sha256=Q4EYhF6GOVUp1ze1VHtbaeL9j_11Q2bLRrvQabeZulI,27015 +apprise/plugins/rsyslog.py,sha256=RpVcjnTJrI0wgtNsz6425kbq4Lnwhi0Z2RYImagrS4w,12550 +apprise/plugins/ryver.py,sha256=Yv1b3HMRsPLzE28UC2m5jJrCZsf7O2hmryNEWWHOsok,12298 +apprise/plugins/sendgrid.py,sha256=SSt5iUFNKUw6s_wflXMz8ssK4OGr0WP7KxM3300z9z4,19548 +apprise/plugins/sendpulse.py,sha256=iHnbhixHByucP0iIRjeeZC1ePVUkhaodVf5MbDWmxQ0,29024 +apprise/plugins/serverchan.py,sha256=fkBR7tIAqoZZIe8wssU-Uxbl3CmjHJXWrso5QS7X8vw,6091 +apprise/plugins/ses.py,sha256=-WSsHD6J8HDm9XS7CvRGt-0MSaeJ5eHBBG2xhjRL2HE,38103 +apprise/plugins/seven.py,sha256=Fd1i5Jxa8iberSW76fCDCSRJvuzWM6Ytumk5vpa4LBU,12558 +apprise/plugins/sfr.py,sha256=G-cHoTEWp1ISsBg2-og4B20KGKQRP4J26DrJWn8AzKE,15300 +apprise/plugins/signal_api.py,sha256=0HTwgcW6fp7T9YyKjohpiH3VZX2EWx19qIS713ID3Gs,18760 +apprise/plugins/signl4.py,sha256=XkZ84xyQJ5QtIknt8j_uf9O4D2RmWYFdzF90U0M7ZIo,11573 +apprise/plugins/simplepush.py,sha256=NPfF4UXOrV8xOXzRmSMlu85kW3qi9YpJ5S96-3PmADU,12538 +apprise/plugins/sinch.py,sha256=fKYTLPPH49ZQyuPCNvrr23ETwgvxzyuN40x6mrw8qPc,17529 +apprise/plugins/slack.py,sha256=NYo3HVThjLqAvsDjxXNmetqjRL1xLFdxMUNWn3Q8TW0,64446 +apprise/plugins/smpp.py,sha256=qpt2EM3jLD71TNcjpuLOS9hzCqJ5TnDiIXPYuLcGqcI,12012 +apprise/plugins/smseagle.py,sha256=bmEaKk2xKlvJ-uZyWU38Z4INsZjaAzof13lTX6DzlXw,26455 +apprise/plugins/smsmanager.py,sha256=EgFYERj1t59mGJSDX1QSf5LE2vgBIFpElSEeN-I-yqk,14837 +apprise/plugins/smtp2go.py,sha256=0WjwokoVf9crcd1v0fVoS-rOuOZdTsQMJVITR6R8U7s,21224 +apprise/plugins/sns.py,sha256=2CriEW-xJW5hOw8t2zfU7CblclK9SKSi9h4u9sSOE7Y,30970 +apprise/plugins/sogs.py,sha256=b9Dt2YQyxl-GxGuWwfsJ05ZnmfPRnCVeU-RbPgUWa6g,20537 +apprise/plugins/sparkpost.py,sha256=Hm5tLJSsztLn1xrJq7zc3b4NwbG9Km5j-5FojpS809w,28679 +apprise/plugins/spike.py,sha256=GQvbE7kLgXxvN-6VZ2pEymnUw09o-_2SVlaEbfj0V5M,6011 +apprise/plugins/splunk.py,sha256=DTOi20cbbG_4iI9ImNdKW5Jmu5y9dY8IbJZ2THFiSoA,17153 +apprise/plugins/spugpush.py,sha256=I3O3r7Xeyk7FU3ns4rt7E5ox_QR8vF41kWJ6JnbIWRQ,5664 +apprise/plugins/streamlabs.py,sha256=PUqGbp_q8FZNeuTSKNRsW5asDc9vdl-RmlM_KgBgx_U,16746 +apprise/plugins/synology.py,sha256=drUNwf7EIyDSjqdX68B6sLadAtWG3NvYWU1gqHV1r4s,11923 +apprise/plugins/syslog.py,sha256=0-E8qJeiKJCElWKcsRQkqFW74yUmF7mT699sFabNHTc,10799 +apprise/plugins/techuluspush.py,sha256=LcOdgYtSvZv-QiwxSdpfOHU2DN-5h_B6InaIRpBL3KI,7477 +apprise/plugins/telegram.py,sha256=cv75QZDFAVdEorG28xQM7LQ7qjLvH1xD6XIev2iicdw,41292 +apprise/plugins/threema.py,sha256=E1mATZY52wrAP7YiBUNP68bmRWnzfdcWAhu933_tGAs,12360 +apprise/plugins/twilio.py,sha256=FJDkEpbyrA-ksQnS73LC3aUA7H0BwmIY1Sv7GJYc4V8,22067 +apprise/plugins/twist.py,sha256=A7opy8FKjAH6VL0NGYvF6z7AcpMXsN9xgqvjftbRvcc,27130 +apprise/plugins/twitter.py,sha256=aAxtCYzy3V3S7ma6XzezMuDKpRo74GHH5SXkeLMiLdM,33705 +apprise/plugins/vapid/__init__.py,sha256=s3XFCqPkPPAoLe02EBF00JyU4a0yO2t_urcmRxnLWEw,20833 +apprise/plugins/vapid/subscription.py,sha256=v6IWFIyifxTHCs3Y-lNuEcFUOeZ8Q_sXuPWZhDeuscI,13139 +apprise/plugins/viber.py,sha256=Xfd_23M3SvgoBwyO0u7ZeVOTJ748GFv19zkVwyNiMfs,11681 +apprise/plugins/voipms.py,sha256=h7hf8N47LuI1iFeBO2aMcuBcJwslTnlBUhLUFWVASBo,13497 +apprise/plugins/vonage.py,sha256=ru7FpQUVdNFIboM_c329CJ0y4-c7mTzafubHAqaxOZs,13825 +apprise/plugins/webexteams.py,sha256=HCYh7qWzvMHoDBc0ieH4gJO8aGXX_TBrmJor_OWDlII,23231 +apprise/plugins/wechat.py,sha256=Nxc-KaSJEXkzigkw7IWihmRW0Phwvi0PSz4b7zWhK8k,23743 +apprise/plugins/wecombot.py,sha256=vl0eac0DpEjWbw1eQ4Pil5F0nABSVaFUUJD5kLYuTFI,9224 +apprise/plugins/whatsapp.py,sha256=zooKQsMRibs4r9QSmHVcs9O16Gkp8QqqGC1eT-aVt0U,25038 +apprise/plugins/windows.py,sha256=ozKn8c620kWufxRrgW0G4xIFKrgDP4k0PNufA4b3WFQ,9289 +apprise/plugins/workflows.py,sha256=8emtQTfn02JL1_AQGSWalxeJBr8y1N05FrKSBtPtyQY,25775 +apprise/plugins/wxpusher.py,sha256=2wIK5o3r5u0T5QjS3N4QHhHIl7rONDQsLhK00i9n6oQ,13218 +apprise/plugins/xmpp/__init__.py,sha256=MHHkHXkuAccQmd54fd5jLdpFTrID7fa6XtnqKQx_SUo,1496 +apprise/plugins/xmpp/adapter.py,sha256=OuF-x5czOIOzyGqpxABfbD_z8QhsurGUDi8azaogTwI,38029 +apprise/plugins/xmpp/base.py,sha256=HHmuJEbOUc_iPt9UBkGDFwBNp1xTvOZeNK_9HjJezPo,19056 +apprise/plugins/xmpp/common.py,sha256=BGj3W_1-f3L7S1eDigKlcZ19IJgsy8FB1KVWz5GFj0E,2098 +apprise/plugins/zoom.py,sha256=rMtfCDuWW1ry5aTtvb5dSqX69-Ff-Yvp2MlkcQSXGkc,13507 +apprise/plugins/zulip.py,sha256=j2oUqO_EVLgVDlZ_t_xvRocNFi1LNYK5UwJQ0BXxRsY,14604 +apprise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise/tag.py,sha256=WZcNHW1dkcakCHSS0E-EnslGx0ytCeQ_AdiXmqpJjSQ,7129 +apprise/url.py,sha256=ega596zLuV6s-iRjyYWOd1G-ti46SlUvqz0XPDGgz_4,37674 +apprise/utils/__init__.py,sha256=-XVQG23gk_zU6BF_ugbAan8ChfJmycmuI-gDVLexW88,1406 +apprise/utils/base64.py,sha256=oRzRs78Di2Gh8nOPVw9kDXkgtPb3O74jfmQ_O4lGfXQ,3771 +apprise/utils/cwe312.py,sha256=LcveYa8zGfJTi3y2h7Y2aRarZw288UocU3jb3rlM0pA,7915 +apprise/utils/disk.py,sha256=hKKGA6KMIihqsk9PW4syCtbbOAbCwlraV8Zmub7SJRM,5659 +apprise/utils/format.py,sha256=OV3fIGynyk2Vg54FP8qiqyDP1_jbn1V8VMQOV8gPs1s,6915 +apprise/utils/json.py,sha256=Q1yLIViYOjNTFdzrMKubpEoNZqmnF9zzPxbx_9h1A4k,2222 +apprise/utils/logic.py,sha256=SsFp17B2iRT-szm24X34pBjSWD76iHkCI4jdVn5JRfs,5743 +apprise/utils/module.py,sha256=DYUvpbmcHc0GimpBIwqwDatPzNYVHdKl_r9nCT2KDcQ,2054 +apprise/utils/parse.py,sha256=e3JZAm7Tq9Lx2VZfbzvvOp-6h6uPvrzjQd5E14Esspg,43453 +apprise/utils/pem.py,sha256=FfBjI-I_gjI9ON-K2lId-3gCQrU7L0Cx62USmD8rdOk,28025 +apprise/utils/pgp.py,sha256=-s3CKwBJDEbD83DmjbgQW5WDqqMTWddPRV76eNyeJqo,26460 +apprise/utils/sanitize.py,sha256=ug6bG_Xr_LtF1FRDmD1AznTOCiniKRFKDAeIYgpkGXM,9522 +apprise/utils/singleton.py,sha256=OUSF4C5yzIQm8wNEkXtSw_pTEKVueXfgaLdOe-fiOe4,1795 +apprise/utils/socket.py,sha256=TBLGiW-U1npP3wT5L9dSHVAzT-xGkQoqVXHM9oF0NsI,22228 +apprise/utils/templates.py,sha256=6t1dJmx2l3pqiJEFZLFZWHkpPz2WS92mjRCEG4HSMGA,3245 +apprise/utils/time.py,sha256=tlbQVD6EujJjAHEuG-1N6MMQLBFwiHoGxq9Blo4BUt8,2927 +apprise/utils/wkd.py,sha256=8JqIFCWNMiE6Zm9BbckXvD5sn4_MdOrBnT_PZdqfNqQ,14320 diff --git a/libs/apprise-1.9.8.dist-info/REQUESTED b/libs/apprise-1.11.0.dist-info/REQUESTED similarity index 100% rename from libs/apprise-1.9.8.dist-info/REQUESTED rename to libs/apprise-1.11.0.dist-info/REQUESTED diff --git a/libs/apprise-1.9.8.dist-info/WHEEL b/libs/apprise-1.11.0.dist-info/WHEEL similarity index 100% rename from libs/apprise-1.9.8.dist-info/WHEEL rename to libs/apprise-1.11.0.dist-info/WHEEL diff --git a/libs/apprise-1.9.8.dist-info/entry_points.txt b/libs/apprise-1.11.0.dist-info/entry_points.txt similarity index 100% rename from libs/apprise-1.9.8.dist-info/entry_points.txt rename to libs/apprise-1.11.0.dist-info/entry_points.txt diff --git a/libs/apprise-1.9.8.dist-info/licenses/LICENSE b/libs/apprise-1.11.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/apprise-1.9.8.dist-info/licenses/LICENSE rename to libs/apprise-1.11.0.dist-info/licenses/LICENSE diff --git a/libs/apprise-1.9.8.dist-info/top_level.txt b/libs/apprise-1.11.0.dist-info/top_level.txt similarity index 100% rename from libs/apprise-1.9.8.dist-info/top_level.txt rename to libs/apprise-1.11.0.dist-info/top_level.txt diff --git a/libs/apprise-1.9.8.dist-info/RECORD b/libs/apprise-1.9.8.dist-info/RECORD deleted file mode 100644 index 9f0dfa6a3..000000000 --- a/libs/apprise-1.9.8.dist-info/RECORD +++ /dev/null @@ -1,233 +0,0 @@ -../../bin/apprise,sha256=41ocDdUKbn8QbOtncNxcwagiZ6d2DL1ARZyEgip9OYA,186 -apprise-1.9.8.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -apprise-1.9.8.dist-info/METADATA,sha256=An6sk6bAqQdJRDAfr8KhZhyohL-i_h37eGX6V5R4aVM,56830 -apprise-1.9.8.dist-info/RECORD,, -apprise-1.9.8.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -apprise-1.9.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -apprise-1.9.8.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 -apprise-1.9.8.dist-info/licenses/LICENSE,sha256=msAH6Aa9cZnPMVit_4OhY1GuhcRjc8NTOpi5YZdRA_I,1343 -apprise-1.9.8.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 -apprise/__init__.py,sha256=wZB-1ATAsyV-rjXvPa2MOI4vfYxWO6gsZ_yGoZD7PYk,3851 -apprise/apprise.py,sha256=urhgoEWyI9AzAjbkY6XmNUcsrdU-4KWYYT0CHspHE2g,36087 -apprise/apprise_attachment.py,sha256=PUvsVkFrFodtYY5KKWApGwVzRucTJIcSsRjy0YDE7xM,13642 -apprise/apprise_config.py,sha256=fPiTCS4wEG2R-IOXbVe3Jzd_dJgoOkLVrYndFkMFNDY,17964 -apprise/asset.py,sha256=2Epg10nieZKt5yFEgq2Qq_bTpijiICjaHK2oeGv_tmc,17701 -apprise/assets/NotifyXML-1.0.xsd,sha256=292qQ_IUl5EWDhPyzm9UTT0C2rVvJkyGar8jiODkJs8,986 -apprise/assets/NotifyXML-1.1.xsd,sha256=bjR3CGG4AEXoJjYkGCbDttKHSkPP1FlIWO02E7G59g4,1758 -apprise/assets/themes/default/apprise-failure-128x128.ico,sha256=Mt0ptfHJaN3Wsv5UCNDn9_3lyEDHxVDv1JdaDEI_xCA,67646 -apprise/assets/themes/default/apprise-failure-128x128.png,sha256=66ps8TDPxVH3g9PlObJqF-0x952CjnqQyN3zvpRcOT8,16135 -apprise/assets/themes/default/apprise-failure-256x256.png,sha256=bQBsKKCsKfR9EqgYOZrcVcVa5y8qG58PN2mEqO5eNRI,41931 -apprise/assets/themes/default/apprise-failure-32x32.png,sha256=vH0pZffIDCvkejpr3fJHGXW__8Yc3R_p0bacX6t6l18,2437 -apprise/assets/themes/default/apprise-failure-72x72.png,sha256=EP5A8DHRDr9srgupFSwOoyQ308bNJ8aL192J_L4K-ec,7600 -apprise/assets/themes/default/apprise-info-128x128.ico,sha256=F5_CirmXueRCRI5Z_Crf6TS6jVIXTJlRD83zw1oJ66g,67646 -apprise/assets/themes/default/apprise-info-128x128.png,sha256=bBqRZAgQey-gkmJrnFhPbzjILSrljE59mRkgj3raMQo,16671 -apprise/assets/themes/default/apprise-info-256x256.png,sha256=B5r_O4d9MHCmSWZwfbqQgZSp-ZetTdiBSwKcMTF1aFA,43331 -apprise/assets/themes/default/apprise-info-32x32.png,sha256=lt3NZ95TzkiCNVNlurrB2fE2nriMa1wftl7nrNXmb6c,2485 -apprise/assets/themes/default/apprise-info-72x72.png,sha256=kDnsZpqNUZGqs9t1ECUup7FOfXUIL-rupnQCYJp9So4,7875 -apprise/assets/themes/default/apprise-logo.png,sha256=85ttALudKkLmiqilJT7mUQLUXRFmM1AK89rnwLm313s,160907 -apprise/assets/themes/default/apprise-success-128x128.ico,sha256=uCopPwdQjxgfohKazHaDzYs9y4oiaOpL048PYC6WRlg,67646 -apprise/assets/themes/default/apprise-success-128x128.png,sha256=nvDuU_QqhGlw6cMtdj7Mv-gPgqCEx-0DaaXn1KBLVYg,17446 -apprise/assets/themes/default/apprise-success-256x256.png,sha256=vXfKuxY3n0eeXHKdb9hTxICxOEn7HjAQ4IZpX0HSLzc,48729 -apprise/assets/themes/default/apprise-success-32x32.png,sha256=Jg9pFJh3YPI-LiPBebyJ7Z4Vt7BRecaE8AsRjQVIkME,2471 -apprise/assets/themes/default/apprise-success-72x72.png,sha256=FQbgvIhqKOhEK0yvrhaSpai0R7hrkTt_-GaC2KUgCCk,7858 -apprise/assets/themes/default/apprise-warning-128x128.ico,sha256=6XaQPOx0oWK_xbhr4Yhb7qNazCWwSs9lk2SYR2MHTrQ,67646 -apprise/assets/themes/default/apprise-warning-128x128.png,sha256=pf5c4Ph7jWH7gf39dJoieSj8TzAsY3TXI-sGISGVIW4,16784 -apprise/assets/themes/default/apprise-warning-256x256.png,sha256=SY-xlaiXaj420iEYKC2_fJxU-yj2SuaQg6xfPNi83bw,43708 -apprise/assets/themes/default/apprise-warning-32x32.png,sha256=97R2ywNvcwczhBoWEIgajVtWjgT8fLs4FCCz4wu0dwc,2472 -apprise/assets/themes/default/apprise-warning-72x72.png,sha256=L8moEInkO_OLxoOcuvN7rmrGZo64iJeH20o-24MQghE,7913 -apprise/attachment/__init__.py,sha256=HVj63cGWbHlQrUy8WedaDFtvSRMPKsATb8lvKXfR-6A,1654 -apprise/attachment/base.py,sha256=oAB4Bdfof7tEgGtJUqPtFR9INEPGfvWOwuXf4sARN_s,16463 -apprise/attachment/file.py,sha256=J14LHyloDOVq1rXlqIeX7pkb9MCBH7C2DAEqNLExeGw,4953 -apprise/attachment/http.py,sha256=r7sphh4tb9JRcZ8l2BRJfyGErPeIRDWo--Ae_FuFYOg,13843 -apprise/attachment/memory.py,sha256=6j2kdFV32Zu8mod7Asiiwf6ByaRvf2PVCLIV28UIFYg,6873 -apprise/cli.py,sha256=u_DaRmwlDk81tOaa43VZ_6nBQaINxFCh2Mxc-9B4YWE,37773 -apprise/common.py,sha256=5a2s8yQtqm3BbdhM0cd9aIvZR5yGvwl9ioLOnNbwW7s,6863 -apprise/compat.py,sha256=hjaMSzwCcdodK79bZ54_vxhhO_KhcidIen-tJwE0NQQ,2043 -apprise/config/__init__.py,sha256=BrkAKU9NZ_dN8KuiUwBjYB9U7gU1wVW95uksS9rAbmI,1655 -apprise/config/base.py,sha256=t2gDb8-OAGINKo_aZfxvZsJIUPU3xZeXT9M7SDa9cjs,55848 -apprise/config/file.py,sha256=-y5oCdn1m9Sv8zfLSWwcw0jXj5nwDPv2r1i-eS_jSOE,6119 -apprise/config/http.py,sha256=V2U93piWl94umfKDIDsm66esR4lAmx-VLPvSP187Ed8,9445 -apprise/config/memory.py,sha256=J49cB4MFDvJADgnZVCX1GmNSwrQm-wnFsi6o1nY22vg,2733 -apprise/conversion.py,sha256=O-PjprtH1sMN1z9_sj6AMXsAd9jvbku8hvRdoSh_Ggg,6393 -apprise/decorators/__init__.py,sha256=UJvg7JUgzj50yr_uQqZd1Z2y6My-T9oeR2Luc1nUDJw,1456 -apprise/decorators/base.py,sha256=oXGf9ByHG5HZ8WMqZwTKTmBvUq1RBHFZ5QIljBtqhvQ,7879 -apprise/decorators/notify.py,sha256=yNtbZ3zID6cYnR6b-UTuZ0ILmZETwPO0HK2ZzjpvOUI,5066 -apprise/emojis.py,sha256=_IR06DNsTMAWpYzkZSm_zqVP0MKLtmCvlWS9jv07kE4,87630 -apprise/exception.py,sha256=9mOUrGXxLzwdpMo7XEk4iMUaED5FJU6QiSf9q5-NQdk,2512 -apprise/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -apprise/i18n/apprise.pot,sha256=emswBY3tz-4tnvXR9yqj6B0LvHb_skcWkyhrbbM7h5I,44287 -apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=6TR8Gayv6E8LR9HLdbRnCVzRP_rdyIrIiCuCPt9-CT4,3655 -apprise/i18n/en/LC_MESSAGES/apprise.po,sha256=c3E2UB7PAqwxjb1IZ8ydvh4hL88Wz7YQF8snWepAung,47558 -apprise/locale.py,sha256=Qg0Jr49cPQQctNrzCSuzrWd27YMGgoG7shrAt6kepJU,8983 -apprise/logger.py,sha256=01qEr8aL2RrGnl7DNaawEbBLnGZr0izn9vb6e4e974U,6715 -apprise/manager.py,sha256=mmXPHMWFxEpFOPj3rpdOOYUQcsXNwl0eH7_2jGGHV7g,29624 -apprise/manager_attachment.py,sha256=Zqqq6X-m1qvIKbbqdXvCuuwfbzE7kXSBmDVjKD_Vkvg,2134 -apprise/manager_config.py,sha256=7Sw8ab_JhyxkCVFlkSfElYCacAq7immLrO6RyeOQe-0,2139 -apprise/manager_plugins.py,sha256=L_g-nxr_qyoZ-GyYvOskK2NLXyOBbRJCHFILOVgba_E,2137 -apprise/persistent_store.py,sha256=hr73A9lazx31fA0yz7wKOuPaDSHT-z4BkddC9RSNpiU,61030 -apprise/plugins/__init__.py,sha256=trbtZemgqRGdwGm493UCaRjTV94L8jzhRxgi-7KEQuQ,18665 -apprise/plugins/africas_talking.py,sha256=2U9PW46BwENnV14ZYoaIbTSKhxt_eaSZXGmMcaVtu6w,16624 -apprise/plugins/apprise_api.py,sha256=FPErT1UIlZniLqpT49qyNXGyVgs7lJTteWqZzWtP-tk,18134 -apprise/plugins/aprs.py,sha256=7Vmvv1pHjZWijpEjrgjuGjAb3dQko_kGO-ah4IFg9Io,25117 -apprise/plugins/bark.py,sha256=w-0YJ4Q6-uHYKLCqZKMrhmDzeISrU4tWc7HxSNdTYMw,19003 -apprise/plugins/base.py,sha256=3e4O6YtKc_gnull9Bp77nnTHEIm-a_-VS27QnlYp0P4,37080 -apprise/plugins/bluesky.py,sha256=lot_KKYhvVdwt6hGAUZdTRLGg2oLHkz9bJLpx8LF6u4,23303 -apprise/plugins/brevo.py,sha256=h33VyQ108W09sZwLFc28jvz8Jok0EuvLhTn-MGqlDzU,19388 -apprise/plugins/bulksms.py,sha256=N1xU5C3Jdbxi6bGNQY87i8XjZA12UwxNfq0A48noNuU,17036 -apprise/plugins/bulkvs.py,sha256=L7SASQxVox6QCQDYiVqM631o_7R6Mz3-UyexR0HFnGI,13922 -apprise/plugins/burstsms.py,sha256=HBFd-QYuVcsdLuBEwhzbzwErVuR9fiXD3RG-qUA8WkQ,16149 -apprise/plugins/chanify.py,sha256=lUBM7MXWvjBJNL3_ISIoNvZnArTFpEEnBPJcGBhWcWw,6887 -apprise/plugins/clickatell.py,sha256=R3oc3ZksikJYgV-iAMpc12lyjkvavayUftH6TbKB4K4,10367 -apprise/plugins/clicksend.py,sha256=TM52u60aM9OxRulhhDgrZPaV5o48wS96WlOzzxhEgdA,12014 -apprise/plugins/custom_form.py,sha256=HnMmOQoXBLeApGE9vD8LI_c57JsZkB0V3wd72fPONxw,19121 -apprise/plugins/custom_json.py,sha256=fkzDYCZ_zFT1g_Att9BifvJEMKdBIUuYGjAj50rIN2g,15012 -apprise/plugins/custom_xml.py,sha256=bnyusnF36BG0TD8OfgaU_PM6wD06SplcJmFPJjOqn6A,18441 -apprise/plugins/d7networks.py,sha256=sEVihX4Wm9AiVI2harLkGtqpSB0NLVRkeeVk6jfqBqU,15494 -apprise/plugins/dapnet.py,sha256=FIkaboR2m0Zgae4pl5LjsLeC3C_97pHLKoXtToPRpRM,13855 -apprise/plugins/dbus.py,sha256=SNv-nQlcFQIRgHNTcAr_tfVqcygqfn3k2rnUPOGz5qA,14495 -apprise/plugins/dingtalk.py,sha256=hd2LHBDaPAy241-NXtvgrmpegvpPPEKkEWrFa84yfZc,12343 -apprise/plugins/discord.py,sha256=OfiyW5_PzLXnlUTLre_91Rkwc_Av5Q8LvOGZS5ZDcTQ,30868 -apprise/plugins/dot.py,sha256=fzHv8MC2QSB0nb58Tja1G0WfliAfQPPBMMKRXseP-Vo,20666 -apprise/plugins/email/__init__.py,sha256=kTDw88J0x5MVAJC7UIXI-RpiGgzoXI0YanBXvvciylE,1917 -apprise/plugins/email/base.py,sha256=e_A2INbASt76PxX9_IOpKVjtGW6QiXgwvAoJavhy5PU,41205 -apprise/plugins/email/common.py,sha256=oJiGN2GKiXaDbiiJI0CSB32FIf8dqLuVqrJA75KLeJw,2527 -apprise/plugins/email/templates.py,sha256=tAxROfw2Q7XNmWSgggLnQXttGfY0cGJHQqpeCT9hsck,10353 -apprise/plugins/emby.py,sha256=HpR4WAcp8rwbgvwXpSQ3mJ3lE2plPBkQQ3_udQ10nYk,22567 -apprise/plugins/enigma2.py,sha256=vh8qAGmldV4ln1Y9zzYB3gYfhl07zRkgBnDV6ERP5W0,12109 -apprise/plugins/fcm/__init__.py,sha256=69qDcMxtfh8tZOY8gzZp0oFK9fX2NbMWPiEDC-19bGw,22158 -apprise/plugins/fcm/color.py,sha256=rqSeq9Bc6gmwKAqOMLNvSJDFUAKIBwDP5lDxpYd1ClA,4609 -apprise/plugins/fcm/common.py,sha256=yaEc5lOrEr5UFHd24CKhNNZW39PKDqR6Uxa8IwXVBP0,1687 -apprise/plugins/fcm/oauth.py,sha256=iqs5DRqR8dTWI8EiltzlCAn8aWl-gPZi7st7AErO0BE,11147 -apprise/plugins/fcm/priority.py,sha256=8y1E1iHPCH34sas1sGg5fFmyo_lTQpe0o473hUNjQ_0,6904 -apprise/plugins/feishu.py,sha256=kqPjitwmUf2ca62u_NKtiRF8FVFQ-zyeJkBrjfa4jYQ,7497 -apprise/plugins/flock.py,sha256=3s_xc91Jvvso4j8IzvlmSCX_YkEXQJYOUcjTUMzpBdk,13092 -apprise/plugins/fluxer.py,sha256=IjicrZfA_t93kxPAEtoKVXNQlCxX1Zb4niDpqmtINcY,36147 -apprise/plugins/fortysixelks.py,sha256=5mO6UYgO319A1HfT_Av8jdbXfjhL5qZLAZMRftawQgM,11937 -apprise/plugins/freemobile.py,sha256=RHZHrpwdQJYHr8KGzteipNN-bZLsvEWGqFAYO1cK2qc,6910 -apprise/plugins/glib.py,sha256=rQV1hH7lHRio1b2ZFvZJNTDV5Iwa_F_5d2uS83I8h5E,12920 -apprise/plugins/gnome.py,sha256=2Ykzkdncf0UfI4ok-hZIuhQC3__u10U12PBUGbjvWVU,9295 -apprise/plugins/google_chat.py,sha256=LJI1FLHWRcX3gjspnF3xVCquWt7O0V3JSDt0IcfLjPU,13588 -apprise/plugins/gotify.py,sha256=XgTM2FZlTQwRXD4wfMvYYcIujP8dcEqYM5cDhnj0gN4,11162 -apprise/plugins/growl.py,sha256=A9UYAKrEqBgQzSZlxwiRUN8s7CLRx3M8QowLTY-_KgA,14947 -apprise/plugins/guilded.py,sha256=ARbx9otLNOMoeYNKftLTu8uuDnkmt91jk61zt9HdgN8,3801 -apprise/plugins/home_assistant.py,sha256=CjOVejsI95CdkHDjTw38SxFIoz5BxpiWJj1_1p7gXYk,11669 -apprise/plugins/httpsms.py,sha256=nIC7noesMHLwMXO9vC-92zJx8_v2PFyh6jsivwBR5ow,11457 -apprise/plugins/ifttt.py,sha256=PUsvapfXAreWYyKKTAjDFLHP1Os9vu9VOHPcMKHVnaA,13899 -apprise/plugins/irc/__init__.py,sha256=FnlnCevX7Hxc8zea6r3yqOJimIucLNM6YMDStcfebL8,1493 -apprise/plugins/irc/base.py,sha256=oxjXLj9cpF-vbR0I02nZWQwcA0zlMv7tvxex7Cc4icQ,16860 -apprise/plugins/irc/client.py,sha256=4usfuD7kFARXl2VdviKuG3tQ1DinlrXBoaICbM6QnEg,12788 -apprise/plugins/irc/protocol.py,sha256=4jTetI-FuguaR1R2dBSEoHofYUw7-Q6iMBTFUdDe6zY,6539 -apprise/plugins/irc/state.py,sha256=HI4ZR0wXZUiM5q9Q7rAVW0xpQu7_WIlmBAHbRP0L9KE,7301 -apprise/plugins/irc/templates.py,sha256=tbr4nZ9NR_RlqZXMNfNmtj_F5pxcGkWnsXfxeMLeDYA,2709 -apprise/plugins/jellyfin.py,sha256=Qsqtq0oPXQfZgZekykk6d5TvJBFY7rrBHRpulERczhQ,2155 -apprise/plugins/join.py,sha256=ypse-K0Kp8XpYalogZ3ayVDfxkdZddOFPNuyeY7caEk,14167 -apprise/plugins/kavenegar.py,sha256=onekhFAjWH-3vd1INxSO4p9_VHmKi-19h7ydGdDkjKI,12937 -apprise/plugins/kumulos.py,sha256=L0f5MdNn2gae8-JDy3wDWVNkJqJ9aLyoTbPeAVk1XGg,8464 -apprise/plugins/lametric.py,sha256=FjhugijcodU4j_SgxGuJgbKYLfp5rsPdm9mseJBDTt4,40183 -apprise/plugins/lark.py,sha256=JdAU4YpeXK2bQtvX1BQdqLJ9Dutesavd4USTThiwmy8,6424 -apprise/plugins/line.py,sha256=_Rg4x4LhPB02HWaVJ9znFohOqAMrOvH_1XeOoNf5Dfc,10972 -apprise/plugins/macosx.py,sha256=x5FE8ccOHg3H7r6Q9Q2N24XpHkCUVsk5xbER1Oj7d9Q,8365 -apprise/plugins/mailgun.py,sha256=f7l1AvYP8BeIvXxpYmh9olzydCEz2_lHa3s0kv1n_EQ,26591 -apprise/plugins/mastodon.py,sha256=CXm0UWddiLBJ5_OrMdkfZ5xrsgGkA0kh_hRlVjjrmVc,36655 -apprise/plugins/matrix.py,sha256=JMujdt0XnClQasKcrJcXL2WysmIKyavo7dQyjSyc0AM,65360 -apprise/plugins/mattermost.py,sha256=QvxvsySE5qneIgAIIMR7WhlOI3_TFK4cWuMSKYfNvQQ,25346 -apprise/plugins/messagebird.py,sha256=cydwBE6RD40HXmJP1Fxkd1XeyvaL2FKqarec4UEzv-s,12569 -apprise/plugins/misskey.py,sha256=mNy2RaspVNOxC9AfpNLM30vTHjDAnZq-oxAW_JtK_dI,10055 -apprise/plugins/mqtt.py,sha256=vo1ay92eEz7cWdLk3_POJOESxQxKOjvRP0lRbzLqi0s,21522 -apprise/plugins/msg91.py,sha256=Nehhtoi_JM2rcP-FhFgiT40ivYgB3DovR2QbB6eCoTE,13153 -apprise/plugins/msteams.py,sha256=rT9VTQdzgZDwUN2kt1IzIlMirz00Sjyw68J-XGPBFrc,27533 -apprise/plugins/nextcloud.py,sha256=km6s73zggAqnsJ5klMfgxW0w0eVsnUNRsg6JN0_3d3I,20174 -apprise/plugins/nextcloudtalk.py,sha256=f18YWEhNCkjOz4XjFeX3oeg-JR7u-mItKfXm4YXv3yE,11667 -apprise/plugins/notica.py,sha256=dvQlQx2QBvpzvIcDRVHReP36EiuXAlXgQOC8hpUlOyc,13541 -apprise/plugins/notifiarr.py,sha256=UscluMX7WJXNx74ANH013Ob83RMBIfPTtGmeZwWdRPI,15829 -apprise/plugins/notificationapi.py,sha256=vjWeqrmq44QejcyTFiRVcNV0qVqrkMlMUyv62N0FY7A,33843 -apprise/plugins/notifico.py,sha256=KPE7AIN16INkoFxCxw83nMTzkTjqG-jYJpcHUZMpz00,12619 -apprise/plugins/ntfy.py,sha256=nbT-XHmF8PGYqDMxdhjFaB_Nf2B3JXZEdy32eaH9P0w,32547 -apprise/plugins/office365.py,sha256=XA4V3zlqgVbXVP-wJgVtALGlOkrxKhBOx7uMwTFpD7Y,38391 -apprise/plugins/one_signal.py,sha256=xcb_r2zQ0K4LepBWqZ5n8aSasM1okglJ3pDTaecVaXY,23941 -apprise/plugins/opsgenie.py,sha256=2g3R7EpIUuaEyKei6DvJ4Nd8hhWfyzKPGQIRKcFsKxQ,29465 -apprise/plugins/pagerduty.py,sha256=T-QaKTGI_Hw8Yv59L5m_Iyl_IimF0p648YoxwckhfQA,18843 -apprise/plugins/pagertree.py,sha256=bPdX37D43PnfpjCGeSWRGCvUx4gAeGU2o9SP_MUBnl0,14225 -apprise/plugins/parseplatform.py,sha256=INnv_RTfQeYqOi9K2Pymma8VnqbfbvLajkthhQobX_s,11087 -apprise/plugins/plivo.py,sha256=MKp3wj1SO3BQrJMXUVgVQtmIkbn4zjVLS9jXP4zzv0k,14124 -apprise/plugins/popcorn_notify.py,sha256=DgAt0sA9P4qIEQUQKQ1prz7DyOyZF4_2joNfXDBkaCM,11091 -apprise/plugins/prowl.py,sha256=iSZoy6VHjLnk7zaOIBJznMtd0coE3GKl1CMxE13kiV0,10242 -apprise/plugins/pushbullet.py,sha256=PBc6RFPbi8Ohv8OrZUZRam5rvj8-7fJ00_twpjDXUrA,16236 -apprise/plugins/pushdeer.py,sha256=Ap3R2IVjfcyVzSgpLC5tYym0U4SNoalTtbOgs16ymL4,7246 -apprise/plugins/pushed.py,sha256=PsTUAIQLjaS3vHhYmnLZ47P186_dSeCKbN7JZ2NRLjU,12638 -apprise/plugins/pushjet.py,sha256=tSD5LymHK8EuXrWixYBC3e5idz3BRX2yzsYprOsgY2U,9591 -apprise/plugins/pushme.py,sha256=hfnK-9DQzgrCcHtDxuYxjI7N1_ZgtQpZWnUEkjEaI_E,7415 -apprise/plugins/pushover.py,sha256=Mk7xaT7lvDjrhtMu2YWKLa0SkFb2ywZMIWSRH9BQYHI,22103 -apprise/plugins/pushplus.py,sha256=iHASnXcQ1xdrf8Opz0NA0DJxsOSlVIrUn6ugnlputtY,5683 -apprise/plugins/pushsafer.py,sha256=Q9HKYtpqCajuhbljiZzZ01eho9_YU0vKUelETUWwOzE,28301 -apprise/plugins/pushy.py,sha256=jFPChiF9U1mHVPYsrbicdWSgGR3kPrMgQGIJAf7jQgo,12837 -apprise/plugins/qq.py,sha256=RcUEcwgjrX-dNjfB4k7XHWsiC1HuKUJiMcwuXN32_pM,5655 -apprise/plugins/reddit.py,sha256=yIFnLAWIzbnb7UTx8mio2j9WzJaw5zN54nZNx34AcFo,26183 -apprise/plugins/resend.py,sha256=HnV4hUizz7XFIReLHtYQsRtHV1G6BccI-gk9X4nLqBI,20362 -apprise/plugins/revolt.py,sha256=HEagDzGVrYIlqJXc185aq4ATXSCnfmWUpk-bb-hOBL0,14900 -apprise/plugins/rocketchat.py,sha256=Qcpbd06yo548RlDZnJNSpaeb4J728Ox04BDuiaimWts,26677 -apprise/plugins/rsyslog.py,sha256=lZRnTglk-dM87dRd1hMrnLBYvCWwYWMEiF3zrAPUB7M,12516 -apprise/plugins/ryver.py,sha256=Ns-fy6DOboVycrCGKigx0kSDzSawPRTCxx5YxP1TZoA,12249 -apprise/plugins/sendgrid.py,sha256=9XD98TDubjQOfFppOrVEV3iGYbwYWakG4ZOUPNu2JVw,19228 -apprise/plugins/sendpulse.py,sha256=rMNs1cFvErzALD7lw6p_PyjzxLvO7HRbcTUxfgT7YHA,27428 -apprise/plugins/serverchan.py,sha256=VWtgW8AgIPHqBtJu_Uhpjd8ZSaPCkkFlobZP5ZLLDkw,6026 -apprise/plugins/ses.py,sha256=Hg80nMOVCvXi5OW6rfUp7sjLMe7gWoykxeIo6WYThaM,34848 -apprise/plugins/seven.py,sha256=kfG2Xw55vTaDoOSsAe2SESESThAVydwULFZ5m5blDSM,12451 -apprise/plugins/sfr.py,sha256=UTNdwvYNZ_CojLC0qNwZBT9F1GZQsYICD1Z0LK7QVzw,15185 -apprise/plugins/signal_api.py,sha256=9kqu2_vTX_AViNhn2FcHixg4nNSuFeQrA8FOvGmgRHc,18656 -apprise/plugins/signl4.py,sha256=f7xYu9bseoT3iufQvD7GmhTPRMMTLdOWsfXfXPQ6u3w,11260 -apprise/plugins/simplepush.py,sha256=9j0VqAQ2v8tv6--sZS_2On5CNMK_UBLJRlHDwx6zi0w,12137 -apprise/plugins/sinch.py,sha256=Fxd4VLgDHlGYECWIVmP_bXIQmllEGQ6aJw_0OgJHWew,17456 -apprise/plugins/slack.py,sha256=tQF2konKYyGgp7YQBDI3nhCwV4PpQ-jS1mjUTKmJaIs,48562 -apprise/plugins/smpp.py,sha256=EbWMFnt3ACWAtHzxjZLWSmrQ5kJbUL23LpfON6mvOu0,11759 -apprise/plugins/smseagle.py,sha256=XKCzj6tnxNBy7J5sEeQxNItL4xz1yA-2ShqvVu20fZk,26241 -apprise/plugins/smsmanager.py,sha256=QycgH5UqHvEAoMXMrMJJunHIsALDjYIr6o8df6kj574,14706 -apprise/plugins/smtp2go.py,sha256=IBIy8lQPgIgVioeSDVbITGCq87QjCAYFti1M05j6d78,20956 -apprise/plugins/sns.py,sha256=KxGre-BFLkLdMkJHYiGkGRovpyan8sMSPH13_TeuS2I,24519 -apprise/plugins/sparkpost.py,sha256=qKYuaoCaOJ59T6wnEa3-bGU-ihpnSg8adKulDuYoNpQ,28205 -apprise/plugins/spike.py,sha256=PKSYQvjNCKM9VN5VSa-BCIR5WVyinHoKL2rCVBI3Jc4,5963 -apprise/plugins/splunk.py,sha256=HeBg6FEM1xbuXHNv2XSKkZ74rpaFcUVzTeUegC5xGHI,17182 -apprise/plugins/spugpush.py,sha256=q4pd7G7TBQ2hYd_3qZ5bAdR--nufENbXdtjEpkVK4oc,5616 -apprise/plugins/streamlabs.py,sha256=ys7jnREK9aFowY3rKjz2K_0ufaz2szZczHUCXcZSr7k,16622 -apprise/plugins/synology.py,sha256=2bgOCWMP4i7uUn8Hpxz85e_MsIESS1I23KHLJ45N0ek,11857 -apprise/plugins/syslog.py,sha256=0-E8qJeiKJCElWKcsRQkqFW74yUmF7mT699sFabNHTc,10799 -apprise/plugins/techuluspush.py,sha256=ahi_4sBY3VSECCdPzhDL1snuN0oCEqVMxi5OvxvXbAE,7412 -apprise/plugins/telegram.py,sha256=KBrQXPgYJdbJ--hEiZ0RfN0JbGrXmZ98tCOIDjgdhA4,40886 -apprise/plugins/threema.py,sha256=jjIARxit74jX_WQmom1BPj8-vi5AkK7xM9vNAFssOnA,12287 -apprise/plugins/twilio.py,sha256=wRGstpOOQ_jnpCHfjKm-R5SwUIzQZYwmay8H0srDa9I,21752 -apprise/plugins/twist.py,sha256=RM75TRbioN9PPGvkKY5031IB1uQBTVaTMgyEmjB3mZM,26944 -apprise/plugins/twitter.py,sha256=AApwLbHfvePJinHMToovp_ESTJ0lW8fNcu0VnVaTFqs,31870 -apprise/plugins/vapid/__init__.py,sha256=tgxaZH-QDXSwTJa5GPw9cjW3mc0X1DG0VB_z2Llacd4,20545 -apprise/plugins/vapid/subscription.py,sha256=v6IWFIyifxTHCs3Y-lNuEcFUOeZ8Q_sXuPWZhDeuscI,13139 -apprise/plugins/viber.py,sha256=m6nDkOwkEVfPEDaO7tQdKlv5OT8Zpnv1LzRYGBPUBRE,11511 -apprise/plugins/voipms.py,sha256=U1cRyImG0xQnYEOifKKRzNMFjlEpiy8bjNYKutUUwow,13335 -apprise/plugins/vonage.py,sha256=Nr4mZDPnPbQb5BdAcoa3KHCpQzH3WBBZuO9R0lcTiiI,13752 -apprise/plugins/webexteams.py,sha256=sEbtMh-YyxCyNNPjLpiYPpV4wCudzJOqYolBzSmH4sY,9886 -apprise/plugins/wecombot.py,sha256=G1gBM4JAzlISHmrRBuI2DSodJhPLpZSo3l5UNcwo7yw,9159 -apprise/plugins/whatsapp.py,sha256=EA9eSADSwgtZHIvEfjqg7IRUbmSLwf698RB39mQDx4A,20846 -apprise/plugins/windows.py,sha256=ozKn8c620kWufxRrgW0G4xIFKrgDP4k0PNufA4b3WFQ,9289 -apprise/plugins/workflows.py,sha256=D4wl5QGX4wRA3_2Oljvkhdx8JJMicJpHjPDf0JZP1ag,22918 -apprise/plugins/wxpusher.py,sha256=2PQCHD8BORYkYeQmWbhMSmqZIfyOdPb4PNAsxkliyIY,13119 -apprise/plugins/xbmc.py,sha256=65_OtJvuaf4Wyn1cqo4ehi2BvJmhrUr53gkqLMadQ50,13071 -apprise/plugins/xmpp/__init__.py,sha256=MHHkHXkuAccQmd54fd5jLdpFTrID7fa6XtnqKQx_SUo,1496 -apprise/plugins/xmpp/adapter.py,sha256=DkKJIJOayei96W-jKyCh0DLJFFT5xlnYpv5At-mt6UY,32095 -apprise/plugins/xmpp/base.py,sha256=8NTYZJJte3SzN_VECLkY-sbPLcqvYU3vc4fp-YA-n9U,14730 -apprise/plugins/xmpp/common.py,sha256=P4_esSvRzgUJNbJB4FNfG_Ec42kf6anPr3jWq77wSQI,2106 -apprise/plugins/zulip.py,sha256=psxDXX8luootHqiElSVtvMRFYQ8_XZLMpGS5CYe-2DA,14531 -apprise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -apprise/url.py,sha256=NSS9PwWbj2edxPCWFnFZvc8lnlOE0dA-Yi_c4ysyqhY,35434 -apprise/utils/__init__.py,sha256=-XVQG23gk_zU6BF_ugbAan8ChfJmycmuI-gDVLexW88,1406 -apprise/utils/base64.py,sha256=oRzRs78Di2Gh8nOPVw9kDXkgtPb3O74jfmQ_O4lGfXQ,3771 -apprise/utils/cwe312.py,sha256=6PhiMZqvcWhO9k8cPF0AYo9kBbmX5WEP9Qx4HHE2vTg,7767 -apprise/utils/disk.py,sha256=hKKGA6KMIihqsk9PW4syCtbbOAbCwlraV8Zmub7SJRM,5659 -apprise/utils/format.py,sha256=FxLD6yBcR1PeKAT_a1STo4r5oNv1hUb0DYQGWpZbg0k,6916 -apprise/utils/logic.py,sha256=feRCj1ttRuZADg5_vi9ITcofkV9QuxWOP7BB3mCuo5g,4578 -apprise/utils/module.py,sha256=DYUvpbmcHc0GimpBIwqwDatPzNYVHdKl_r9nCT2KDcQ,2054 -apprise/utils/parse.py,sha256=QWsNdJ39GyHp4TXYZiUk-tKMqsYF2RqLLoRqiSQPhPA,39926 -apprise/utils/pem.py,sha256=FfBjI-I_gjI9ON-K2lId-3gCQrU7L0Cx62USmD8rdOk,28025 -apprise/utils/pgp.py,sha256=qlpYOvyFoZ75ReLDJNqguBeT3K2rrVZPGHBnHUnfhjY,11607 -apprise/utils/sanitize.py,sha256=2SoNrdlX-ZAFQryxgGQixIUWrWO-_lwI3M7ZTYMsJ8I,9532 -apprise/utils/singleton.py,sha256=OUSF4C5yzIQm8wNEkXtSw_pTEKVueXfgaLdOe-fiOe4,1795 -apprise/utils/socket.py,sha256=QHjWbqd4IV2wcTl5a3GZeD8EIkCsA2Wq-EBjPpmrI0E,21903 -apprise/utils/templates.py,sha256=6t1dJmx2l3pqiJEFZLFZWHkpPz2WS92mjRCEG4HSMGA,3245 -apprise/utils/time.py,sha256=W3_y338hpaArJqNr2UIHGjPinEOi2oeBiF2hR3lvF9M,2877 diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py index 1e17ea8c9..bfc93c7ca 100644 --- a/libs/apprise/__init__.py +++ b/libs/apprise/__init__.py @@ -26,9 +26,10 @@ # POSSIBILITY OF SUCH DAMAGE. __title__ = "Apprise" -__description__: str = \ +__description__: str = ( "Push Notifications that work with just about every platform!" -__version__ = "1.9.8" +) +__version__ = "1.11.0" __author__ = "Chris Caron" __email__ = "lead2gold@gmail.com" __license__ = "BSD 2-Clause" @@ -70,6 +71,7 @@ from .manager_config import ConfigurationManager from .manager_plugins import NotificationManager from .persistent_store import PersistentStore from .plugins.base import NotifyBase +from .tag import AppriseTag from .url import PrivacyMode, URLBase # Set default logging handler to avoid "No handler found" warnings. @@ -92,6 +94,7 @@ __all__ = [ "AppriseAttachment", "AppriseConfig", "AppriseLocale", + "AppriseTag", "AttachBase", "AttachmentManager", "ConfigBase", diff --git a/libs/apprise/apprise.py b/libs/apprise/apprise.py index 924b47bb9..12a48198d 100644 --- a/libs/apprise/apprise.py +++ b/libs/apprise/apprise.py @@ -31,7 +31,9 @@ import asyncio from collections.abc import Iterator import concurrent.futures as cf from itertools import chain +import json import os +import time from typing import Any, Optional, Union from . import __version__, common, plugins @@ -46,7 +48,9 @@ from .locale import AppriseLocale from .logger import logger from .manager_plugins import NotificationManager from .plugins.base import NotifyBase +from .tag import AppriseTag from .utils.cwe312 import cwe312_url +from .utils.json import AppriseJSONEncoder from .utils.logic import is_exclusive_match from .utils.parse import parse_list, parse_urls @@ -314,7 +318,6 @@ class Apprise: return False for server in servers: - if isinstance(server, (ConfigBase, NotifyBase, AppriseConfig)): # Go ahead and just add our plugin into our list self.servers.append(server) @@ -370,7 +373,6 @@ class Apprise: # Iterate over our loaded plugins for entry in self.servers: - if isinstance(entry, (ConfigBase, AppriseConfig)): # load our servers servers = entry.servers() @@ -391,6 +393,226 @@ class Apprise: yield server return + @staticmethod + def _extract_filter_retry(tag): + """Return the retry override embedded in a filter tag, or None. + + A filter like "3:endpoint:2" or "endpoint:2" carries ":2" as the + call-level retry count. When present it overrides each matched + server's configured retry for this single notify() call. + """ + if tag is None or tag == common.MATCH_ALL_TAG: + return None + for entry in ( + [tag] if isinstance(tag, (str, AppriseTag)) else list(tag) + ): + if isinstance(entry, (list, tuple, set)): + for tok in parse_list(entry): + ft = AppriseTag.parse(tok) + if ft.retry is not None: + return ft.retry + else: + ft = AppriseTag.parse(str(entry)) + if ft.retry is not None: + return ft.retry + return None + + @staticmethod + def _filter_has_explicit_priority(tag): + """Return True if any token in *tag* carries an explicit priority + prefix. + + When True, notify() dispatches matched servers as a flat batch + (no escalation) because the caller selected an exact priority level. + When False, matched servers are grouped by their own tag priorities + and dispatched in ascending order with early-True exit. + """ + if tag is None or tag == common.MATCH_ALL_TAG: + return False + for entry in ( + [tag] if isinstance(tag, (str, AppriseTag)) else list(tag) + ): + if isinstance(entry, (list, tuple, set)): + for tok in parse_list(entry): + if AppriseTag.parse(tok).has_priority: + return True + else: + for tok in parse_list(str(entry)): + if AppriseTag.parse(tok).has_priority: + return True + return False + + @staticmethod + def _server_priority_for_tag_name(server, tag_name): + """Return the dispatch priority stored on *server* for *tag_name*. + + Looks up the AppriseTag in server.tags whose bare name equals + *tag_name* and returns its priority. Returns 0 when the tag is + absent or stored as a plain string (no explicit priority). + """ + for stag in server.tags: + if str(stag) == tag_name: + return stag.priority if isinstance(stag, AppriseTag) else 0 + return 0 + + @staticmethod + def _match_service_retry(server, tag): + """Return the call-time retry override for *server* given *tag*. + + Iterates the OR tokens in *tag* in order. The first token that + both carries a retry suffix AND matches *server* determines the + override. Returns None when no such token exists. + + Matching follows the same rules as _token_matches_data: + - no priority prefix -> name-only match + - explicit priority -> name + priority-exact match + """ + if tag is None or tag == common.MATCH_ALL_TAG: + return None + for entry in ( + [tag] if isinstance(tag, (str, AppriseTag)) else list(tag) + ): + tokens = ( + parse_list(entry) + if isinstance(entry, (list, tuple, set)) + else parse_list(str(entry)) + ) + for tok in tokens: + ft = AppriseTag.parse(tok) + if ft.retry is None: + continue + tag_name = str(ft) + if not ft.has_priority: + if tag_name in server.tags: + return ft.retry + else: + for stag in server.tags: + if isinstance(stag, AppriseTag): + if ( + str(stag) == tag_name + and stag.priority == ft.priority + ): + return ft.retry + else: + if str(stag).lower() == tag_name: + return ft.retry + return None + + @staticmethod + def _inject_per_service_retries(all_calls, tag): + """Return *all_calls* with per-service _retry_override injected. + + For each (server, kwargs) pair, finds the first filter token in + *tag* that matches the service and carries a retry suffix. When + found, injects that value as _retry_override so that all dispatch + paths (sequential, threadpool, asyncio) pick it up automatically. + Services with no matching retry token are left unchanged. + """ + result = [] + for server, kwargs in all_calls: + retry = Apprise._match_service_retry(server, tag) + if retry is not None: + kwargs = dict(kwargs, _retry_override=retry) + result.append((server, kwargs)) + return result + + @staticmethod + def _build_tag_chains(all_calls, tag): + """Group *all_calls* into per-OR-token escalation chains. + + Returns {chain_key: {priority: [(server, kwargs)]}}. + + Each service is assigned to the chain of the first OR token whose + bare tag name appears in the service's own tags. The chain key is + that bare tag name. Services that don't match any token by name + fall into a catch-all chain keyed as "". + + When *tag* is MATCH_ALL_TAG or None, a single chain "" is built + using the existing _server_priority_for_filter logic. + """ + if tag is None or tag == common.MATCH_ALL_TAG: + chain: dict[int, list] = {} + for server, kwargs in all_calls: + p = Apprise._server_priority_for_filter(server, tag) + chain.setdefault(p, []).append((server, kwargs)) + return {"": chain} + + # Flatten OR tokens; AND groups are kept as a single opaque entry + # that falls through to the catch-all chain. + # + # The CLI wraps each --tag value in a list via parse_list(), so a + # single --tag flag produces a single-element inner list such as + # [["alerts:3"]]. A single-element list is always a plain OR token + # (there is nothing to AND against), so we treat it the same as a + # bare string. A multi-element inner list is a genuine AND condition + # (the server must carry every tag in the group) and falls through to + # the catch-all chain instead of getting its own independent chain. + or_tag_names: list[str] = [] + for entry in ( + [tag] if isinstance(tag, (str, AppriseTag)) else list(tag) + ): + if isinstance(entry, (list, tuple, set)): + flat = parse_list(*entry) if entry else [] + if len(flat) == 1: + # single OR token wrapped in a list (CLI convention) + or_tag_names.append(str(AppriseTag.parse(flat[0]))) + else: + or_tag_names.append("") # AND group placeholder + else: + for tok in parse_list(str(entry)): + or_tag_names.append(str(AppriseTag.parse(tok))) + + chains: dict[str, dict[int, list]] = {} + for server, kwargs in all_calls: + for chain_key in or_tag_names: + if chain_key and chain_key in server.tags: + p = Apprise._server_priority_for_tag_name( + server, chain_key + ) + chains.setdefault(chain_key, {}).setdefault(p, []).append( + (server, kwargs) + ) + break + else: + # fallback: use global priority + p = Apprise._server_priority_for_filter(server, tag) + chains.setdefault("", {}).setdefault(p, []).append( + (server, kwargs) + ) + + return chains + + @staticmethod + def _server_priority_for_filter(server, tag): + """Return the effective dispatch priority for *server* given *tag*. + + The priority comes from the AppriseTag stored on the server whose + name matches one of the tag-filter names. When multiple server tags + match, the minimum (highest-precedence) priority is returned. + Returns 0 when no matching priority tag is found. + """ + if tag is None or tag == common.MATCH_ALL_TAG: + return 0 + + # Flatten the filter to a set of bare lowercase tag names. + filter_names = set() + for entry in ( + [tag] if isinstance(tag, (str, AppriseTag)) else list(tag) + ): + if isinstance(entry, (list, tuple, set)): + for t in parse_list(entry): + filter_names.add(str(AppriseTag.parse(t))) + else: + for t in parse_list(str(entry)): + filter_names.add(str(AppriseTag.parse(t))) + + priorities = [ + stag.priority if isinstance(stag, AppriseTag) else 0 + for stag in server.tags + if str(stag) in filter_names + ] + return min(priorities) if priorities else 0 + def notify( self, body: Union[str, bytes], @@ -417,6 +639,23 @@ class Apprise: sent at all as a result of tag filtering and/or simply having empty configuration files that were read. + A filter tag may carry an optional priority prefix and/or retry suffix: + + "endpoint" -> match all entries; escalate by priority + "3:endpoint" -> match ONLY priority-3 entries; flat dispatch + "endpoint:2" -> match all entries; retry each up to 2 times + "3:endpoint:2" -> exclusive priority-3 match with 2 retries + + When no priority prefix is given, matched services are grouped by + their configured tag priority and dispatched in ascending order + (lowest number = highest urgency). If every service in the lowest + priority group succeeds, Apprise returns True immediately without + running higher-numbered priority groups (escalation chain). + + When an explicit priority prefix IS given (e.g. "3:endpoint"), only + services whose matching tag carries that exact priority are notified, + and all matched services are dispatched as a single flat batch. + Attach can contain a list of attachment URLs. attach can also be represented by an AttachBase() (or list of) object(s). This identifies the products you wish to notify @@ -426,61 +665,245 @@ class Apprise: """ try: - # Process arguments and build synchronous and asynchronous calls - # (this step can throw internal errors). - sequential_calls, parallel_calls = self._create_notify_calls( - body, - title, - notify_type=notify_type, - body_format=body_format, - tag=tag, - match_always=match_always, - attach=attach, - interpret_escapes=interpret_escapes, + all_calls = list( + self._create_notify_gen( + body, + title, + notify_type=notify_type, + body_format=body_format, + tag=tag, + match_always=match_always, + attach=attach, + interpret_escapes=interpret_escapes, + ) ) except TypeError: - # No notifications sent, and there was an internal error. return False - if not sequential_calls and not parallel_calls: - # Nothing to send + if not all_calls: return None - sequential_result = Apprise._notify_sequential(*sequential_calls) - parallel_result = Apprise._notify_parallel_threadpool(*parallel_calls) - return sequential_result and parallel_result + # Inject the per-service call-time retry override. Each matched + # service gets the retry from the first filter token that both matches + # it and carries a retry suffix (e.g. "devops:3" applies retry=3 only + # to devops-tagged services, not to management-tagged services). + all_calls = Apprise._inject_per_service_retries(all_calls, tag) - async def async_notify( - self, - *args: Any, - **kwargs: Any - ) -> Optional[bool]: + if Apprise._filter_has_explicit_priority(tag): + # Tag filter carries an explicit priority prefix (e.g. "2:alerts"). + # Skip the escalation chain: dispatch all matched services as a + # single flat batch regardless of their individual tag priorities. + sequential = [ + (s, k) for s, k in all_calls if not s.asset.async_mode + ] + parallel = [(s, k) for s, k in all_calls if s.asset.async_mode] + seq_ok = ( + Apprise._notify_sequential(*sequential) if sequential else True + ) + par_ok = ( + Apprise._notify_parallel_threadpool(*parallel) + if parallel + else True + ) + return seq_ok and par_ok + + # No explicit priority in the filter -- use per-tag escalation chains. + # + # Each distinct OR token forms an independent chain. Within a chain, + # services are grouped by their configured tag priority. The lowest- + # numbered group (highest urgency) runs first. If every service in + # that group succeeds the chain is done; any failure escalates to the + # next priority group. notify() returns True only when every chain + # finds a fully-successful group. + # + # When multiple chains are active in the same round their current + # priority-group batches run concurrently via a thread pool so that + # one chain's services cannot delay another chain. + chains = Apprise._build_tag_chains(all_calls, tag) + + def _run_batch(batch): + """Dispatch one priority-group batch; True = all services ok.""" + seq = [(s, k) for s, k in batch if not s.asset.async_mode] + par = [(s, k) for s, k in batch if s.asset.async_mode] + seq_ok = Apprise._notify_sequential(*seq) if seq else True + par_ok = Apprise._notify_parallel_threadpool(*par) if par else True + return seq_ok and par_ok + + # Per-chain state: priorities (sorted), groups dict, current index, + # and a flag marking whether a successful group has been found. + chain_states = { + key: { + "priorities": sorted(groups), + "groups": groups, + "idx": 0, + "succeeded": False, + } + for key, groups in chains.items() + } + + while True: + # When abort_on_chain_failure is enabled, stop as soon as any + # chain has exhausted all its priority groups without success. + # With the default (False) all chains run to completion even if + # one has already failed, so every defined URL gets an attempt. + if self.asset.abort_on_chain_failure and any( + not st["succeeded"] and st["idx"] >= len(st["priorities"]) + for st in chain_states.values() + ): + return False + + # Collect chains that still need to try their next priority group. + active = [ + (key, st) + for key, st in chain_states.items() + if not st["succeeded"] and st["idx"] < len(st["priorities"]) + ] + if not active: + break # every chain has either succeeded or been exhausted + + if len(active) == 1: + # Single active chain: dispatch directly, no thread overhead. + _, st = active[0] + batch = st["groups"][st["priorities"][st["idx"]]] + if _run_batch(batch): + st["succeeded"] = True + else: + st["idx"] += 1 # escalate to next priority + else: + # Multiple active chains: run their current-priority batches + # concurrently so independent chains don't block each other. + with cf.ThreadPoolExecutor() as executor: + future_map = { + executor.submit( + _run_batch, + st["groups"][st["priorities"][st["idx"]]], + ): (key, st) + for key, st in active + } + for future in cf.as_completed(future_map): + _, st = future_map[future] + try: + ok = future.result() + except Exception: + logger.exception( + "Unhandled Notification Exception" + ) + ok = False + if ok: + st["succeeded"] = True + else: + st["idx"] += 1 # escalate to next priority + + return all(st["succeeded"] for st in chain_states.values()) + + async def async_notify(self, *args: Any, **kwargs: Any) -> Optional[bool]: """Send a notification to all the plugins previously loaded, for asynchronous callers. The arguments are identical to those of Apprise.notify(). """ + tag = kwargs.get("tag", common.MATCH_ALL_TAG) + try: - # Process arguments and build synchronous and asynchronous calls - # (this step can throw internal errors). - sequential_calls, parallel_calls = self._create_notify_calls( - *args, **kwargs - ) + all_calls = list(self._create_notify_gen(*args, **kwargs)) except TypeError: - # No notifications sent, and there was an internal error. return False - if not sequential_calls and not parallel_calls: - # Nothing to send + if not all_calls: return None - sequential_result = Apprise._notify_sequential(*sequential_calls) - parallel_result = await Apprise._notify_parallel_asyncio( - *parallel_calls - ) - return sequential_result and parallel_result + # Inject per-service call-time retry overrides (same logic as notify). + all_calls = Apprise._inject_per_service_retries(all_calls, tag) + + if Apprise._filter_has_explicit_priority(tag): + # Explicit priority prefix: flat dispatch, no escalation. + sequential = [ + (s, k) for s, k in all_calls if not s.asset.async_mode + ] + parallel = [(s, k) for s, k in all_calls if s.asset.async_mode] + seq_ok = ( + Apprise._notify_sequential(*sequential) if sequential else True + ) + par_ok = ( + await Apprise._notify_parallel_asyncio(*parallel) + if parallel + else True + ) + return seq_ok and par_ok + + # Per-tag independent escalation chains -- same semantics as notify(). + # + # Each chain's current-priority batch is dispatched as a coroutine. + # All active chains' batches run concurrently via asyncio.gather() so + # async services across independent chains can make I/O progress + # simultaneously. Only chains whose current batch failed advance to + # the next priority group (escalation). + chains = Apprise._build_tag_chains(all_calls, tag) + + async def _run_batch_async(batch): + """Dispatch one priority-group batch; True = all services ok.""" + seq = [(s, k) for s, k in batch if not s.asset.async_mode] + par = [(s, k) for s, k in batch if s.asset.async_mode] + # Sequential items (async_mode=False) run blocking in the caller; + # async items are gathered concurrently. + seq_ok = Apprise._notify_sequential(*seq) if seq else True + par_ok = ( + await Apprise._notify_parallel_asyncio(*par) if par else True + ) + return seq_ok and par_ok + + chain_states = { + key: { + "priorities": sorted(groups), + "groups": groups, + "idx": 0, + "succeeded": False, + } + for key, groups in chains.items() + } + + while True: + # Same abort_on_chain_failure guard as notify(). + if self.asset.abort_on_chain_failure and any( + not st["succeeded"] and st["idx"] >= len(st["priorities"]) + for st in chain_states.values() + ): + return False + + active = [ + (key, st) + for key, st in chain_states.items() + if not st["succeeded"] and st["idx"] < len(st["priorities"]) + ] + if not active: + break # every chain has either succeeded or been exhausted + + # Run all active chains' current-priority batches concurrently. + # asyncio.gather() interleaves coroutines so async services across + # different chains can pipeline their I/O simultaneously. + # return_exceptions=True prevents one failing batch from cancelling + # the others; exceptions are treated as delivery failures below. + results = await asyncio.gather( + *( + _run_batch_async(st["groups"][st["priorities"][st["idx"]]]) + for _, st in active + ), + return_exceptions=True, + ) + + for (_, st), ok in zip(active, results): + if isinstance(ok, Exception): + # Escaped exception -- safety net; treat as failure. + logger.exception("Unhandled Notification Exception") + st["idx"] += 1 + elif ok: + st["succeeded"] = True + else: + st["idx"] += 1 # escalate to next priority group + + return all(st["succeeded"] for st in chain_states.values()) def _create_notify_calls(self, *args, **kwargs): """Creates notifications for all the plugins loaded. @@ -528,14 +951,15 @@ class Apprise: try: notify_type = ( - notify_type if isinstance(notify_type, common.NotifyType) + notify_type + if isinstance(notify_type, common.NotifyType) else common.NotifyType(notify_type.lower()) ) except (AttributeError, ValueError, TypeError): err = ( - f"An invalid notification type ({notify_type}) was " - "specified.") + f"An invalid notification type ({notify_type}) was specified." + ) raise TypeError(err) from None try: @@ -596,7 +1020,6 @@ class Apprise: key += "-emojis" if key not in conversion_title_map: - # Prepare our title conversion_title_map[key] = title if title else "" @@ -663,66 +1086,205 @@ class Apprise: @staticmethod def _notify_sequential(*servers_kwargs): - """Process a list of notify() calls sequentially and synchronously.""" + """Process a list of notify() calls sequentially and synchronously. + + Each server is attempted once and then retried up to server.retry + additional times on failure before moving on. When server.wait is + greater than zero, the process sleeps that many seconds between + each retry attempt. + + A per-call retry override may be injected into kwargs under the key + ``_retry_override``; when present it takes precedence over the + server's own retry attribute for this invocation only. + + Exceptions raised by a plugin's notify() -- including those from + third-party @notify-decorated functions that are outside our control + -- are caught here and treated as a delivery failure. The retry + logic still applies, so a plugin that raises on the first attempt + will be retried the configured number of times before giving up. + """ success = True for server, kwargs in servers_kwargs: - try: - # Send notification - result = server.notify(**kwargs) - success = success and result + # Pop the per-call override before forwarding kwargs to the + # plugin so it never sees the internal _retry_override key. + retry = kwargs.pop("_retry_override", getattr(server, "retry", 0)) + wait = getattr(server, "wait", 0.0) - except TypeError: - # These are our internally thrown notifications. - success = False + result = False + for attempt in range(retry + 1): + # Attempt delivery. TypeError comes from Apprise's own + # validation; bare Exception guards against buggy or + # third-party plugins (including @notify decorators) that + # may raise unexpectedly. Both are treated as failure so + # the retry loop can continue. + try: + result = server.notify(**kwargs) + except TypeError: + result = False + except Exception: + logger.exception("Unhandled Notification Exception") + result = False - except Exception: - # A catch all so we don't have to abort early - # just because one of our plugins has a bug in it. - logger.exception("Unhandled Notification Exception") - success = False + if result: + # Delivered successfully; no need to retry this server. + break + + if attempt < retry: + # Delivery failed and retries remain. Log the attempt + # number and pause before the next try. + logger.warning( + "Retry %d/%d for %s", + attempt + 1, + retry, + server.service_name, + ) + if wait > 0: + time.sleep(wait) + + # Optional-service check. + # + # At this point all retry attempts for 'server' have been + # exhausted (the for-loop above has finished). If the final + # result is still False *and* the service is marked optional, + # we overwrite result to True before folding it into the + # running 'success' accumulator. This silently absorbs the + # failure: the caller will not see it as a delivery error. + # + # Interaction with retries: + # The retry loop above has already run. optional= does not + # short-circuit or bypass retries -- it only changes the + # interpretation of the *final* result once all attempts + # are done. A service with retry=3 and optional=True will + # still be attempted four times before the failure is + # absorbed here. + if not result and getattr(server, "optional", False): + logger.info( + "Optional service '%s' failed; ignoring failure.", + server.service_name, + ) + result = True + + # Fold this service's result into the running batch outcome. + # Boolean AND is used so that a single False from any required + # (non-optional) service permanently taints 'success' for the + # whole batch -- even if later services succeed. Optional + # failures are already re-mapped to True above, so they never + # contribute a False here. + success = success and result return success @staticmethod def _notify_parallel_threadpool(*servers_kwargs): - """Process a list of notify() calls in parallel and synchronously.""" + """Process a list of notify() calls in parallel via a thread pool. + + Each server runs in its own thread. Within each thread, the server + is retried up to server.retry additional times on failure with an + optional server.wait second sleep between each attempt. + + Falls back to _notify_sequential() when only a single server is + given to avoid the overhead of spawning a thread pool for one call. + + Exceptions from a plugin's notify() -- including those from + third-party @notify-decorated functions -- are caught inside each + thread and treated as delivery failures so the retry logic can + still run. + """ n_calls = len(servers_kwargs) - # 0-length case if n_calls == 0: return True - # There's no need to use a thread pool for just a single notification + # Avoid thread-pool overhead for a single notification. if n_calls == 1: return Apprise._notify_sequential(servers_kwargs[0]) - # Create log entry logger.info( "Notifying %d service(s) with threads.", len(servers_kwargs) ) + def _call_with_retry(server, kwargs): + """Execute one server's notify() with retry/wait logic. + + Runs inside a worker thread. Pops ``_retry_override`` from + kwargs so it is never forwarded to the plugin's notify() call. + Exceptions are caught and treated as failures so the retry + loop continues even when a plugin raises unexpectedly. + """ + # Pop the per-call override so it stays internal. + retry = kwargs.pop("_retry_override", getattr(server, "retry", 0)) + wait = getattr(server, "wait", 0.0) + + result = False + for attempt in range(retry + 1): + # Same exception handling as _notify_sequential: TypeError + # from Apprise validation and bare Exception for buggy or + # third-party plugins both map to a retriable failure. + try: + result = server.notify(**kwargs) + except TypeError: + result = False + except Exception: + logger.exception("Unhandled Notification Exception") + result = False + + if result: + return True + + if attempt < retry: + logger.warning( + "Retry %d/%d for %s", + attempt + 1, + retry, + server.service_name, + ) + if wait > 0: + time.sleep(wait) + + # Optional-service check (thread-pool path). + # + # All retry attempts for this server have been exhausted by the + # loop above. If the final result is still False and the service + # is tagged as optional, return True from this worker function + # instead of False. The caller (_notify_parallel_threadpool) + # collects each worker's return value via future.result() and + # ANDs them together; returning True here prevents this worker's + # failure from tainting the aggregate result. + # + # This is the thread-pool equivalent of the same check in + # _notify_sequential. See the comment there for a full + # explanation of the getattr() guard and the retry interaction. + if not result and getattr(server, "optional", False): + logger.info( + "Optional service '%s' failed; ignoring failure.", + server.service_name, + ) + # Return True so future.result() in the caller reports + # success for this optional worker thread. + return True + + # Every attempt for this service failed and it is not optional; + # propagate the failure to the caller. + return result + + # Submit all server calls to the thread pool and collect results. with cf.ThreadPoolExecutor() as executor: success = True futures = [ - executor.submit(server.notify, **kwargs) + executor.submit(_call_with_retry, server, kwargs) for (server, kwargs) in servers_kwargs ] for future in cf.as_completed(futures): + # future.result() re-raises any exception that escaped + # _call_with_retry (should not happen given the inner + # try/except, but guard here as a safety net). try: - result = future.result() - success = success and result - - except TypeError: - # These are our internally thrown notifications. - success = False - + success = success and future.result() except Exception: - # A catch all so we don't have to abort early - # just because one of our plugins has a bug in it. logger.exception("Unhandled Notification Exception") success = False @@ -730,45 +1292,162 @@ class Apprise: @staticmethod async def _notify_parallel_asyncio(*servers_kwargs): - """Process a list of async_notify() calls in parallel and - asynchronously.""" + """Process a list of async_notify() calls concurrently via asyncio. + + All coroutines are gathered with asyncio.gather(). Each server is + retried up to server.retry additional times on failure with an + optional asyncio.sleep(server.wait) between attempts. + + Unlike the thread-pool path, there is no single-server optimisation + here because asyncio can pipeline work across coroutines while one + is awaiting I/O. + + Exceptions from a plugin's async_notify() -- including those from + third-party @notify-decorated coroutines -- are caught inside each + coroutine and treated as delivery failures so the retry loop can + still run for that service. + """ n_calls = len(servers_kwargs) - # 0-length case if n_calls == 0: return True - # (Unlike with the thread pool, we don't optimize for the single- - # notification case because asyncio can do useful work while waiting - # for that thread to complete) - - # Create log entry logger.info( "Notifying %d service(s) asynchronously.", len(servers_kwargs) ) async def do_call(server, kwargs): - return await server.async_notify(**kwargs) + """Coroutine driving one server's async_notify() with retry/wait. + Pops ``_retry_override`` from kwargs so it is never forwarded + to the plugin. Exceptions are caught and treated as failures + so the retry loop continues even when a plugin raises + unexpectedly (e.g. a third-party @notify-decorated coroutine). + """ + # Pop the per-call override so it stays internal. + retry = kwargs.pop("_retry_override", getattr(server, "retry", 0)) + wait = getattr(server, "wait", 0.0) + + result = False + for attempt in range(retry + 1): + # Mirror the exception handling from the synchronous paths: + # TypeError from Apprise's own validation layer and bare + # Exception for any plugin that raises unexpectedly are both + # treated as retriable failures rather than hard crashes. + try: + result = await server.async_notify(**kwargs) + except TypeError: + result = False + except Exception: + logger.exception("Unhandled Notification Exception") + result = False + + if result: + return True + + if attempt < retry: + logger.warning( + "Retry %d/%d for %s", + attempt + 1, + retry, + server.service_name, + ) + if wait > 0: + await asyncio.sleep(wait) + + # Optional-service check (asyncio coroutine path). + # + # All retry attempts have been exhausted by the async loop + # above. If the final result is still False and the service + # is tagged optional, return True from this coroutine so that + # asyncio.gather() receives a truthy value for this task. + # The caller inspects all gathered results with all(); a True + # here ensures this coroutine does not lower the aggregate + # result for the batch. + # + # This is the asyncio equivalent of the same check in + # _notify_sequential and _call_with_retry. See the comment + # in _notify_sequential for a full explanation of the getattr() + # guard and the interaction with the retry count. + if not result and getattr(server, "optional", False): + logger.info( + "Optional service '%s' failed; ignoring failure.", + server.service_name, + ) + # Return True so asyncio.gather() sees a success value + # for this optional coroutine. + return True + + # Every attempt for this service failed and it is not optional; + # propagate the failure to the caller. + return result + + # Run all coroutines concurrently. return_exceptions=True ensures + # that one coroutine raising does not cancel the others; any escaped + # exception (beyond what do_call already handles) is caught below. cors = (do_call(server, kwargs) for (server, kwargs) in servers_kwargs) results = await asyncio.gather(*cors, return_exceptions=True) - if any( - isinstance(status, Exception) and not isinstance(status, TypeError) - for status in results - ): - # A catch all so we don't have to abort early just because - # one of our plugins has a bug in it. - logger.exception("Unhandled Notification Exception") - return False - - if any(isinstance(status, TypeError) for status in results): - # These are our internally thrown notifications. + if any(isinstance(status, Exception) for status in results): + # Safety net: an exception escaped do_call's own try/except. + # Log each one and treat the whole batch as failed. + for status in results: + if isinstance(status, Exception): + logger.error( + "Unhandled Notification Exception: %s", status + ) return False return all(results) + def json( + self, + lang: Optional[str] = None, + show_requirements: bool = False, + show_disabled: bool = False, + indent: Optional[int] = None, + path: Optional[str] = None, + ) -> Union[str, bool]: + """Returns a json response associated with the Apprise object.""" + details = self.details( + lang=lang, + show_requirements=show_requirements, + show_disabled=show_disabled, + ) + + if not path: + return json.dumps( + details, + separators=(",", ":"), + indent=indent, + cls=AppriseJSONEncoder, + ) + + with open(path, "w") as fp: + try: + json.dump( + details, + fp, + separators=(",", ":"), + indent=indent, + cls=AppriseJSONEncoder, + ensure_ascii=False, + ) + + except (OSError, EOFError) as e: + logger.error("Apprise details dumpfile inaccessible: %s", path) + logger.debug("Apprise details dump Exception: %s", e) + + # Early Exit + return False + + finally: + # Reduce memory + del details + + return True + def details( self, lang: Optional[str] = None, @@ -828,10 +1507,12 @@ class Apprise: secure_protocols = (secure_protocols,) # Add our protocol details to our content - content.update({ - "protocols": protocols, - "secure_protocols": secure_protocols, - }) + content.update( + { + "protocols": protocols, + "secure_protocols": secure_protocols, + } + ) if not lang: # Simply return our results @@ -922,11 +1603,13 @@ class Apprise: offset = prev_offset + len(servers) if offset >= index: - return servers[( - index - if prev_offset == -1 - else (index - prev_offset - 1) - )] + return servers[ + ( + index + if prev_offset == -1 + else (index - prev_offset - 1) + ) + ] else: offset = prev_offset + 1 @@ -968,7 +1651,8 @@ class Apprise: location = state.get("location") self.location = ( - location if isinstance(location, ContentLocation) + location + if isinstance(location, ContentLocation) else ContentLocation(location) if location is not None else None @@ -989,14 +1673,16 @@ class Apprise: This includes those found inside configuration. """ - return chain(*[ - ( - [s] - if not isinstance(s, (ConfigBase, AppriseConfig)) - else iter(s.servers()) - ) - for s in self.servers - ]) + return chain( + *[ + ( + [s] + if not isinstance(s, (ConfigBase, AppriseConfig)) + else iter(s.servers()) + ) + for s in self.servers + ] + ) def __len__(self) -> int: """Returns the number of servers loaded; this includes those found @@ -1005,9 +1691,11 @@ class Apprise: This funtion nnever actually counts the Config entry themselves (if they exist), only what they contain. """ - return sum(( + return sum( + ( 1 if not isinstance(s, (ConfigBase, AppriseConfig)) else len(s.servers()) ) - for s in self.servers) + for s in self.servers + ) diff --git a/libs/apprise/apprise_attachment.py b/libs/apprise/apprise_attachment.py index 64c58a2ce..eebaf6a59 100644 --- a/libs/apprise/apprise_attachment.py +++ b/libs/apprise/apprise_attachment.py @@ -45,8 +45,9 @@ class AppriseAttachment: def __init__( self, - paths: Optional[Union[str, list[ - Union[str, AttachBase, "AppriseAttachment"]]]] = None, + paths: Optional[ + Union[str, list[Union[str, AttachBase, "AppriseAttachment"]]] + ] = None, asset: Optional[AppriseAsset] = None, cache: Union[bool, int] = True, location: Optional[Union[str, ContentLocation]] = None, @@ -105,7 +106,8 @@ class AppriseAttachment: if location: try: self.location = ( - location if isinstance(location, ContentLocation) + location + if isinstance(location, ContentLocation) else ContentLocation(location.lower()) ) diff --git a/libs/apprise/apprise_config.py b/libs/apprise/apprise_config.py index 58551db7b..f34f061cd 100644 --- a/libs/apprise/apprise_config.py +++ b/libs/apprise/apprise_config.py @@ -35,6 +35,7 @@ from .config.base import ConfigBase from .logger import logger from .manager_config import ConfigurationManager from .url import URLBase +from .utils.cwe312 import cwe312_url from .utils.logic import is_exclusive_match from .utils.parse import GET_SCHEMA_RE, parse_list @@ -203,7 +204,6 @@ class AppriseConfig: # Iterate over our configuration for config in configs: - if isinstance(config, ConfigBase): # Go ahead and just add our configuration into our list self.configs.append(config) @@ -296,8 +296,10 @@ class AppriseConfig: insecure_includes=insecure_includes, ) - if not (instance.config_format and - instance.config_format.value in common.CONFIG_FORMATS): + if not ( + instance.config_format + and instance.config_format.value in common.CONFIG_FORMATS + ): logger.warning( "The format of the configuration could not be detected." ) @@ -345,7 +347,6 @@ class AppriseConfig: response = [] for entry in self.configs: - # Apply our tag matching based on our defined logic if is_exclusive_match( logic=tag, @@ -388,7 +389,7 @@ class AppriseConfig: # Some basic validation if schema not in C_MGR: - logger.warning(f"Unsupported schema {schema}.") + logger.error(f"Unsupported schema {schema}.") return None # Parse our url details of the server object as dictionary containing @@ -397,7 +398,14 @@ class AppriseConfig: if not results: # Failed to parse the server URL - logger.warning(f"Unparseable URL {url}.") + # CWE-312 (Secure Logging) Handling + secure_logging = ( + asset.secure_logging + if isinstance(asset, AppriseAsset) + else True + ) + loggable_url = url if not secure_logging else cwe312_url(url) + logger.error(f"Unparseable URL {loggable_url}.") return None # Build a list of tags to associate with the newly added notifications @@ -426,7 +434,13 @@ class AppriseConfig: except Exception: # the arguments are invalid or can not be used. - logger.warning(f"Could not load URL: {url}") + # CWE-312 (Secure Logging) Handling + loggable_url = ( + url + if not results["asset"].secure_logging + else cwe312_url(url) + ) + logger.error(f"Could not load URL: {loggable_url}") return None else: diff --git a/libs/apprise/asset.py b/libs/apprise/asset.py index 548ca1d16..e30ceb424 100644 --- a/libs/apprise/asset.py +++ b/libs/apprise/asset.py @@ -147,6 +147,23 @@ class AppriseAsset: # to a new line. interpret_escapes = False + # Default number of retries after a first notification failure. + # Individual plugins may override this via their ?retry= URL parameter + # or the 'retry:' YAML key. Clamped to [0, APPRISE_MAX_SERVICE_RETRY]. + default_service_retry = 0 + + # Default wait (in seconds) between retry attempts. Individual plugins + # may override this via their ?wait= URL parameter or the 'wait:' YAML + # key. Clamped to [0.0, APPRISE_MAX_SERVICE_WAIT]. + default_service_wait = 0.5 + + # When True, as soon as any independent OR tag chain exhausts all its + # priority groups without success, notify() / async_notify() return False + # immediately without running further escalation rounds for the surviving + # chains. When False (the default), all chains are allowed to complete + # regardless of whether another chain has already failed. + abort_on_chain_failure = False + # Defines the encoding of the content passed into Apprise encoding = "utf-8" @@ -173,6 +190,13 @@ class AppriseAsset: # that you leave this option as is otherwise. secure_logging = True + # By default, HTTP redirects (3xx responses) are followed, matching the + # behaviour of the underlying requests library. Set this to False to + # disable redirect following globally across all plugins without having + # to add redirect=no to every individual URL. Individual URLs can still + # override this via the redirect= parameter. + http_redirects = True + # Optionally specify one or more path to attempt to scan for Python modules # By default, no paths are scanned. __plugin_paths = [] @@ -223,7 +247,7 @@ class AppriseAsset: storage_salt: Optional[Union[str, bytes]] = None, storage_idlen: Optional[int] = None, timezone: Optional[Union[str, tzinfo]] = None, - **kwargs: Any + **kwargs: Any, ) -> None: """Asset Initialization.""" # Assign default arguments if specified @@ -248,14 +272,16 @@ class AppriseAsset: # Define how our persistent storage behaves try: self.__storage_mode = ( - storage_mode if isinstance(storage_mode, NotifyFormat) + storage_mode + if isinstance(storage_mode, NotifyFormat) else PersistentStoreMode(storage_mode.lower()) ) except (AttributeError, ValueError, TypeError): err = ( f"An invalid persistent store mode ({storage_mode}) was " - "specified.") + "specified." + ) raise AttributeError(err) from None if isinstance(storage_idlen, int): @@ -277,7 +303,8 @@ class AppriseAsset: self._tzinfo = zoneinfo(timezone) if not self._tzinfo: raise AttributeError( - "AppriseAsset timezone provided is invalid") from None + "AppriseAsset timezone provided is invalid" + ) from None else: # Default our timezone to what is detected on the system self._tzinfo = datetime.now().astimezone().tzinfo @@ -322,8 +349,7 @@ class AppriseAsset: # Attempt to get the type, otherwise return a default grey # if we couldn't look up the entry - color = self.html_notify_map.get( - notify_type, self.default_html_color) + color = self.html_notify_map.get(notify_type, self.default_html_color) if color_type is None: # This is the default return type return color @@ -344,8 +370,7 @@ class AppriseAsset: def ascii(self, notify_type: NotifyType) -> str: """Returns an ascii representation based on passed in notify type.""" # look our response up - return self.ascii_notify_map.get( - notify_type, self.default_ascii_chars) + return self.ascii_notify_map.get(notify_type, self.default_ascii_chars) def image_url( self, diff --git a/libs/apprise/attachment/base.py b/libs/apprise/attachment/base.py index 5020958c9..093d3d6ce 100644 --- a/libs/apprise/attachment/base.py +++ b/libs/apprise/attachment/base.py @@ -173,15 +173,16 @@ class AttachBase(URLBase): # Validate mimetype if specified if self._mimetype and ( - next( - ( - t - for t in mimetypes.types_map.values() - if self._mimetype == t - ), - None, - ) - is None): + next( + ( + t + for t in mimetypes.types_map.values() + if self._mimetype == t + ), + None, + ) + is None + ): err = f"An invalid mime-type ({mimetype}) was specified." self.logger.warning(err) raise TypeError(err) @@ -275,7 +276,6 @@ class AttachBase(URLBase): and os.path.isfile(self.download_path) and cache ): - # We have enough reason to look further into our cached content # and verify it has not expired. if cache is True: @@ -316,7 +316,7 @@ class AttachBase(URLBase): else base64.b64encode(f.read()) ) - except (FileNotFoundError): + except FileNotFoundError: # We no longer have a path to open raise exception.AppriseFileNotFound("Attachment Missing") from None @@ -328,7 +328,8 @@ class AttachBase(URLBase): ) self.logger.debug(f"I/O Exception: {e!s}") raise exception.AppriseDiskIOError( - "Attachment Access Error") from e + "Attachment Access Error" + ) from e def invalidate(self): """Release any temporary data that may be open by child classes. diff --git a/libs/apprise/attachment/file.py b/libs/apprise/attachment/file.py index 0e96d3999..2dd2598ef 100644 --- a/libs/apprise/attachment/file.py +++ b/libs/apprise/attachment/file.py @@ -107,7 +107,6 @@ class AttachFile(AttachBase): self.max_file_size > 0 and os.path.getsize(self.dirty_path) > self.max_file_size ): - # The content to attach is to large self.logger.error( "Content exceeds allowable maximum file length" diff --git a/libs/apprise/attachment/http.py b/libs/apprise/attachment/http.py index 633f898fd..f91bbd166 100644 --- a/libs/apprise/attachment/http.py +++ b/libs/apprise/attachment/http.py @@ -151,12 +151,25 @@ class AttachHTTP(AttachBase): params=self.qsd, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, stream=True, ) as r: - # Handle Errors r.raise_for_status() + # raise_for_status() only covers 4xx/5xx; when redirect + # following is disabled any 3xx must be treated as a + # failure so we do not silently stream a redirect stub. + # Using a status-code range rather than r.is_redirect + # catches 3xx responses that lack a Location header. + if not self.redirects and 300 <= r.status_code < 400: + self.logger.error( + "HTTP redirect encountered but redirect " + "following is disabled:" + f" {self.url(privacy=True)}" + ) + return False + # Get our file-size (if known) try: file_size = int(r.headers.get("Content-Length", "0")) @@ -169,7 +182,6 @@ class AttachHTTP(AttachBase): self.max_file_size > 0 and file_size > self.max_file_size ): - # The content retrieved is to large self.logger.error( "HTTP response exceeds allowable maximum file" @@ -195,8 +207,7 @@ class AttachHTTP(AttachBase): # to False or it isn't compatible with Microsoft Windows # instances. In lieu of this, __del__ will clean up the # file for us. - self._temp_file = \ - NamedTemporaryFile(delete=False) # noqa: SIM115 + self._temp_file = NamedTemporaryFile(delete=False) # noqa: SIM115 # Get our chunk size chunk_size = self.chunk_size diff --git a/libs/apprise/attachment/memory.py b/libs/apprise/attachment/memory.py index c7ebdbd60..5072e8d2a 100644 --- a/libs/apprise/attachment/memory.py +++ b/libs/apprise/attachment/memory.py @@ -162,7 +162,8 @@ class AttachMemory(AttachBase): def invalidate(self): """Removes data.""" - self._data.truncate(0) + if not self._data.closed: + self._data.truncate(0) return def exists(self): diff --git a/libs/apprise/cli.py b/libs/apprise/cli.py index dbe11886c..874d89686 100644 --- a/libs/apprise/cli.py +++ b/libs/apprise/cli.py @@ -58,7 +58,7 @@ from .common import ( ) from .logger import logger from .utils.disk import bytes_to_str, dir_size, path_decode -from .utils.parse import parse_list +from .utils.parse import GET_SCHEMA_RE, parse_list # By default we allow looking 1 level down recursively in Apprise configuration # files. @@ -294,31 +294,33 @@ class CustomHelpCommand(click.Command): formatter.write_text("") formatter.write_text("Actions:") - actions = [( - "storage", - "Access the persistent storage disk administration", - [ - ( - "list", + actions = [ + ( + "storage", + "Access the persistent storage disk administration", + [ ( - "List all URL IDs associated with detected URL(s)." - " This is also the default action run if nothing is" - " provided" + "list", + ( + "List all URL IDs associated with detected" + " URL(s). This is also the default action" + " run if nothing is provided" + ), ), - ), - ( - "prune", ( - "Eliminates stale entries found based on " - "--storage-prune-days (-SPD)" + "prune", + ( + "Eliminates stale entries found based on " + "--storage-prune-days (-SPD)" + ), ), - ), - ( - "clean", - "Removes any persistent data created by Apprise", - ), - ], - )] + ( + "clean", + "Removes any persistent data created by Apprise", + ), + ], + ) + ] # # Some variables @@ -458,7 +460,7 @@ class CustomHelpCommand(click.Command): metavar="MODE", help=( "Specify the persistent storage operational mode " - f'(default={DEFAULT_STORAGE_MODE.value}). ' + f"(default={DEFAULT_STORAGE_MODE.value}). " 'Possible values are: "{}".'.format( '", "'.join(mode.value for mode in PERSISTENT_STORE_MODE_CHOICES) ) @@ -808,6 +810,27 @@ def main( # Track if we are performing a storage action storage_action = bool(urls and "storage".startswith(urls[0])) + # Save the original state; the storage block below always needs these. + _in_storage_mode = storage_action + _raw_urls = urls + + if storage_action: + # Scan storage args for URL-filter entries. If any are found, + # remap urls to only those entries and flip storage_action so the + # priority chain (rule #1 below) loads them into 'a' (our Apprise + # instance). This mirrors the sending notification process. + _sfp = list(urls[1:]) + if _sfp and any( + m.startswith(_sfp[0]) for m in PERSISTENT_STORAGE_MODES + ): + # Skip the action word ("list", "prune", "clear") + _sfp = _sfp[1:] + _url_filters = [_f for _f in _sfp if GET_SCHEMA_RE.match(_f)] + if _url_filters: + # Hand off to rule #1 by replacing urls and clearing the flag. + urls = _url_filters + storage_action = False + if details: # Print details and exit results = a.details(show_requirements=True, show_disabled=True) @@ -961,6 +984,10 @@ def main( ) ) + # Restore storage_action in case it was flipped above to allow URL- + # filter entries to be loaded via rule #1. + storage_action = _in_storage_mode + if not dry_run and not (a or storage_action): click.echo( "You must specify at least one server URL or populated " @@ -996,8 +1023,10 @@ def main( # 5 characters are already reserved for the counter on the left (columns, _) = shutil.get_terminal_size(fallback=(80, 24)) - # Pop 'storage' off of the head of our list - filter_uids = urls[1:] + # Pop 'storage' off of the head of the original url list. + # _raw_urls always holds the unmodified list even when urls was + # remapped to URL-filter entries for the priority chain above. + filter_uids = _raw_urls[1:] action = PERSISTENT_STORAGE_MODES[0] if filter_uids: @@ -1015,16 +1044,33 @@ def main( filter_uids = filter_uids[1:] action = action_ - # Get our detected URL IDs + # True when URL-filter entries were present in the storage args; + # detected by whether urls was remapped to a new list object above. + _had_url_filters = _raw_urls is not urls + + # Build the uid-prefix filter list from non-URL entries only. + # URL-filter entries were already loaded into 'a' by rule #1. + uid_filter_list = [ + _f.lower() for _f in filter_uids if not GET_SCHEMA_RE.match(_f) + ] + + # Single pass: collect plugin uids, applying the uid-prefix filter uids = {} for plugin in a if not tags else a.find(tag=tags): id_ = plugin.url_id() if not id_: + # No persistent storage uid; skip this plugin continue - if filter_uids and next( - (False for n in filter_uids if id_.startswith(n)), True + if uid_filter_list and not next( + (u for u in uid_filter_list if id_.startswith(u)), None ): + # Plugin uid does not match any uid-prefix filter + logger.trace( + "Storage: plugin uid '%s' excluded by filter %s", + id_, + uid_filter_list, + ) continue if id_ not in uids: @@ -1040,12 +1086,31 @@ def main( uids[id_]["plugins"].append(plugin) if action == PersistentStorageMode.LIST: - detected_uid = PersistentStore.disk_scan( - # Use our asset path as it has already been properly parsed - path=asset.storage_path, - # Provide filter if specified - namespace=filter_uids, - ) + # Scope disk_scan based on what filters were provided: + # - URL filters: scope to resolved plugin uids plus any + # plain uid-prefix strings; skip scan if none resolved + # - Plain-string or no filter: namespace=uid_filter_list + # (empty list means return all on-disk directories) + if _had_url_filters: + # Derive namespace from the plugins resolved by URL filters + _disk_ns = list(uids.keys()) + uid_filter_list + if not _disk_ns: + # URL filters yielded no plugins; skip the disk scan + detected_uid = {} + else: + detected_uid = PersistentStore.disk_scan( + # Use our asset path as already properly parsed + path=asset.storage_path, + namespace=_disk_ns, + ) + else: + detected_uid = PersistentStore.disk_scan( + # Use our asset path as it has already been properly + # parsed + path=asset.storage_path, + # Provide filter if specified; empty = all directories + namespace=uid_filter_list, + ) for id_ in detected_uid: size, _ = dir_size(os.path.join(asset.storage_path, id_)) if id_ in uids: @@ -1094,26 +1159,47 @@ def main( ( url if len(url) <= (columns - 8) - else f"{url[:columns - 11]}..." + else f"{url[: columns - 11]}..." ), ) ) if entry.tags: click.echo( - "{:>10}: {}".format("tags", ", ".join(entry.tags)) + "{:>10}: {}".format( + "tags", + ", ".join(str(t) for t in entry.tags), + ) ) else: # PersistentStorageMode.PRUNE or PersistentStorageMode.CLEAR if action == PersistentStorageMode.CLEAR: storage_prune_days = 0 + # Derive the namespace for disk_prune. The scoping rules + # mirror those for disk_scan: + # - URL filters: resolved plugin uids + any uid-prefix + # strings; empty means no-op (nothing to prune). + # - Tag filters: same scoping — resolved plugin uids + any + # uid-prefix strings; empty means no-op. + # - Plain uid-prefix or no filter: pass through directly. + if _had_url_filters or tags: + # Scope to the uids that the filtered plugins resolved + # to. If none resolved (e.g. unknown tag), exit early + # -- disk_prune() with namespace=None targets ALL + # namespaces, so passing None here is not a no-op. + _prune_ns = list(uids.keys()) + uid_filter_list + if not _prune_ns: + ctx.exit(0) + else: + _prune_ns = uid_filter_list or None + # clean up storage results = PersistentStore.disk_prune( # Use our asset path as it has already been properly parsed path=asset.storage_path, # Provide our namespaces if they exist - namespace=filter_uids if filter_uids else None, + namespace=_prune_ns, # Convert expiry from days to seconds expires=storage_prune_days * 60 * 60 * 24, action=not dry_run, @@ -1158,7 +1244,7 @@ def main( ( url if len(url) <= (columns - 8) - else f"{url[:columns - 9]}..." + else f"{url[: columns - 9]}..." ), ) ) @@ -1172,7 +1258,12 @@ def main( ) if server.tags: - click.echo("{:>10}: {}".format("tags", ", ".join(server.tags))) + click.echo( + "{:>10}: {}".format( + "tags", + ", ".join(str(t) for t in server.tags), + ) + ) # Initialize a default response of nothing matched, otherwise # if we matched at least one entry, we can return True diff --git a/libs/apprise/common.py b/libs/apprise/common.py index ec62eaf59..5b44bd356 100644 --- a/libs/apprise/common.py +++ b/libs/apprise/common.py @@ -27,6 +27,10 @@ from enum import Enum +# isoformat is spelled out for compatibility with Python v3.6 +AWARE_DATE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z" +NAIVE_DATE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + class NotifyType(str, Enum): """A simple mapping of notification types most commonly used with all types @@ -53,8 +57,9 @@ class NotifyImageSize(str, Enum): # Define our image sizes so we can verify if we need to -NOTIFY_IMAGE_SIZES: frozenset[str] = \ - frozenset(e.value for e in NotifyImageSize) +NOTIFY_IMAGE_SIZES: frozenset[str] = frozenset( + e.value for e in NotifyImageSize +) class NotifyFormat(str, Enum): @@ -130,8 +135,9 @@ class ContentIncludeMode(str, Enum): # Define our file inclusion types so we can verify if we need to -CONTENT_INCLUDE_MODES: frozenset[str] = \ - frozenset(e.value for e in ContentIncludeMode) +CONTENT_INCLUDE_MODES: frozenset[str] = frozenset( + e.value for e in ContentIncludeMode +) class ContentLocation(str, Enum): @@ -174,8 +180,9 @@ class PersistentStoreMode(str, Enum): # Define our persistent storage modes so we can verify if we need to -PERSISTENT_STORE_MODES: frozenset[str] = \ - frozenset(e.value for e in PersistentStoreMode) +PERSISTENT_STORE_MODES: frozenset[str] = frozenset( + e.value for e in PersistentStoreMode +) class PersistentStoreState(str, Enum): @@ -194,8 +201,9 @@ class PersistentStoreState(str, Enum): # Define our persistent storage states so we can verify if we need to -PERSISTENT_STORE_STATES: frozenset[str] = \ - frozenset(e.value for e in PersistentStoreState) +PERSISTENT_STORE_STATES: frozenset[str] = frozenset( + e.value for e in PersistentStoreState +) # This is a reserved tag that is automatically assigned to every # Notification Plugin @@ -204,3 +212,13 @@ MATCH_ALL_TAG = "all" # Will cause notification to trigger under any circumstance even if an # exclusive tagging was provided. MATCH_ALWAYS_TAG = "always" + +# Maximum number of retries a service may attempt after an initial failure. +# This cap prevents runaway retry loops; the effective value is clamped to +# [0, APPRISE_MAX_SERVICE_RETRY] everywhere it is consumed. +APPRISE_MAX_SERVICE_RETRY = 10 + +# Maximum inter-retry wait (in seconds). Prevents accidental multi-hour +# sleeps from a misconfigured wait= value; clamped to +# [0.0, APPRISE_MAX_SERVICE_WAIT] everywhere it is consumed. +APPRISE_MAX_SERVICE_WAIT = 20.0 diff --git a/libs/apprise/config/base.py b/libs/apprise/config/base.py index cfe306f9e..119f32584 100644 --- a/libs/apprise/config/base.py +++ b/libs/apprise/config/base.py @@ -38,6 +38,7 @@ from ..asset import AppriseAsset from ..logger import logging from ..manager_config import ConfigurationManager from ..manager_plugins import NotificationManager +from ..tag import AppriseTag from ..url import URLBase from ..utils.cwe312 import cwe312_url from ..utils.parse import GET_SCHEMA_RE, parse_bool, parse_list, parse_urls @@ -149,7 +150,8 @@ class ConfigBase(URLBase): if fmt: try: self.config_format = ( - fmt if isinstance(fmt, common.ConfigFormat) + fmt + if isinstance(fmt, common.ConfigFormat) else common.ConfigFormat(fmt.lower()) ) @@ -226,7 +228,6 @@ class ConfigBase(URLBase): # Configuration files were detected; recursively populate them # If we have been configured to do so for url in configs: - if self.recursion > 0: # Attempt to acquire the schema at the very least to allow # our configuration based urls. @@ -247,7 +248,7 @@ class ConfigBase(URLBase): # Some basic validation if schema not in C_MGR: - ConfigBase.logger.warning( + ConfigBase.logger.error( f"Unsupported include schema {schema}." ) continue @@ -262,7 +263,7 @@ class ConfigBase(URLBase): results = C_MGR[schema].parse_url(url) if not results: # Failed to parse the server URL - self.logger.warning( + self.logger.error( f"Unparseable include URL {loggable_url}" ) continue @@ -276,7 +277,6 @@ class ConfigBase(URLBase): ) or C_MGR[ schema ].allow_cross_includes == common.ContentIncludeMode.NEVER: - # Prevent the loading if insecure base protocols ConfigBase.logger.warning( f"Including {schema}:// based configuration is" @@ -306,7 +306,7 @@ class ConfigBase(URLBase): except Exception as e: # the arguments are invalid or can not be used. - self.logger.warning( + self.logger.error( f"Could not load include URL: {loggable_url}" ) self.logger.debug(f"Loading Exception: {e!s}") @@ -519,10 +519,12 @@ class ConfigBase(URLBase): # definitions (accepting commas) followed by an equal sign we know # we're dealing with a TEXT format. - # Define what a valid line should look like + # Define what a valid line should look like. + # The tag group allows an optional "N:" priority prefix so that + # "2:endpoint=ntfy://..." is recognised as TEXT format. valid_line_re = re.compile( r"^\s*(?P([;#]+(?P.*))|" - r"(?P((?P[ \t,a-z0-9_-]+)=)?[a-z0-9]+://.*)|" + r"(?P((?P[ \t,a-z0-9_:-]+)=)?[a-z0-9]+://.*)|" r"((?P[a-z0-9]+):.*))?$", re.I, ) @@ -543,7 +545,6 @@ class ConfigBase(URLBase): # iterate over each line of the file to attempt to detect it # stop the moment a the type has been determined for line, entry in enumerate(content, start=1): - result = valid_line_re.match(entry) if not result: # Invalid syntax @@ -599,7 +600,8 @@ class ConfigBase(URLBase): try: config_format = ( - config_format if isinstance(config_format, common.ConfigFormat) + config_format + if isinstance(config_format, common.ConfigFormat) else common.ConfigFormat(config_format.lower()) ) @@ -667,10 +669,12 @@ class ConfigBase(URLBase): # Prepare our Asset Object asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset() - # Define what a valid line should look like + # Define what a valid line should look like. + # The tags group allows an optional leading "N:" priority prefix so + # that entries like "2:endpoint=ntfy://..." are parsed correctly. valid_line_re = re.compile( r"^\s*(?P([;#]+(?P.*))|" - r"(\s*(?P[a-z0-9, \t_-]+)\s*=|=)?\s*" + r"(\s*(?P[a-z0-9, \t_:-]+)\s*=|=)?\s*" r"((?P[a-z0-9]{1,32}://.*)|(?P[a-z0-9, \t_-]+))|" r"include\s+(?P.+))?\s*$", re.I, @@ -761,8 +765,9 @@ class ConfigBase(URLBase): url, secure_logging=asset.secure_logging ) if results is None: - # Failed to parse the server URL - ConfigBase.logger.warning( + # url_to_dict() already logged an error with the URL; + # repeat at debug level with line number for context. + ConfigBase.logger.debug( f"Unparseable URL {loggable_url} on line {line}." ) continue @@ -771,15 +776,29 @@ class ConfigBase(URLBase): # notifications if any were set results["tag"] = set(parse_list(result.group("tags"), cast=str)) + # A retry count on a service tag (e.g. "alerts:3=slack://...") is + # not valid here: per-service retry belongs on the URL (?retry=N) + # and call-time retry overrides belong on the notify() filter. + if any( + AppriseTag.parse(t).retry is not None for t in results["tag"] + ): + ConfigBase.logger.warning( + "You can not specify a retry count as part of a service" + f" tag on line {line}; use ?retry=N in the URL instead." + ) + continue + # Set our Asset Object results["asset"] = asset # Store our preloaded entries - preloaded.append({ - "results": results, - "line": line, - "loggable_url": loggable_url, - }) + preloaded.append( + { + "results": results, + "line": line, + "loggable_url": loggable_url, + } + ) # # Normalize Tag Groups @@ -818,7 +837,7 @@ class ConfigBase(URLBase): except Exception as e: # the arguments are invalid or can not be used. - ConfigBase.logger.warning( + ConfigBase.logger.error( "Could not load URL {} on line {}.".format( entry["loggable_url"], entry["line"] ) @@ -907,14 +926,16 @@ class ConfigBase(URLBase): default_timezone = zoneinfo(re.sub(r"[^\w/-]+", "", raw_tz)) if not default_timezone: ConfigBase.logger.warning( - 'Ignored invalid timezone "%s"', raw_tz) + 'Ignored invalid timezone "%s"', raw_tz + ) default_timezone = asset.tzinfo else: asset._tzinfo = default_timezone elif raw_tz is not None: ConfigBase.logger.warning( - 'Ignored invalid timezone "%r"', raw_tz) + 'Ignored invalid timezone "%r"', raw_tz + ) # Iterate over remaining tokens for k, v in tokens.items(): @@ -928,7 +949,6 @@ class ConfigBase(URLBase): hasattr(asset, k) and isinstance(getattr(asset, k), (bool, str)) ): - # We can't set a function or non-string set value ConfigBase.logger.warning(f'Invalid asset key "{k}".') continue @@ -940,7 +960,6 @@ class ConfigBase(URLBase): if isinstance(v, (bool, str)) and isinstance( getattr(asset, k), bool ): - # If the object in the Asset is a boolean, then # we want to convert the specified string to # match that. @@ -1043,7 +1062,6 @@ class ConfigBase(URLBase): # Iterate over each config URL for _no, url in enumerate(includes): - if isinstance(url, str): # Support a single inline string or multiple ones separated by # a comma and/or space @@ -1063,7 +1081,6 @@ class ConfigBase(URLBase): # Iterate over each URL for no, url in enumerate(urls): - # Our results object is what we use to instantiate our object if # we can. Reset it to None on each iteration results = [] @@ -1089,7 +1106,9 @@ class ConfigBase(URLBase): url, secure_logging=asset.secure_logging ) if results_ is None: - ConfigBase.logger.warning( + # url_to_dict() already logged an error with the URL; + # repeat at debug level with entry number for context. + ConfigBase.logger.debug( f"Unparseable URL {loggable_url}, entry #{no + 1}" ) continue @@ -1241,9 +1260,12 @@ class ConfigBase(URLBase): | global_tags ) if "tags" in results_: - ConfigBase.logger.warning(( - "URL #{}: {} contains both 'tag' and 'tags' " - "keyword").format(no + 1, url)) + ConfigBase.logger.warning( + ( + "URL #{}: {} contains both 'tag' and 'tags' " + "keyword" + ).format(no + 1, url) + ) del results_["tags"] elif "tags" in results_: @@ -1259,6 +1281,22 @@ class ConfigBase(URLBase): # Just use the global settings results_["tag"] = global_tags + # A retry count on a service tag is not valid here: per-service + # retry belongs on the URL (retry: key) and call-time retry + # overrides belong on the notify() filter. + _retry_tags = [ + t + for t in results_["tag"] + if AppriseTag.parse(t).retry is not None + ] + if _retry_tags: + ConfigBase.logger.warning( + "You can not specify a retry count as part of a" + " service tag (YAML entry #{}, item #{}); use the" + " 'retry:' key instead.".format(no + 1, entry) + ) + continue + for key in list(results_.keys()): # Strip out any tokens we know that we can't accept and # warn the user @@ -1277,7 +1315,8 @@ class ConfigBase(URLBase): url, os.linesep, os.linesep.join( - [f'{k}="{a}"' for k, a in results_.items()]), + [f'{k}="{a}"' for k, a in results_.items()] + ), ) # Prepare our Asset Object @@ -1287,11 +1326,13 @@ class ConfigBase(URLBase): results_ = URLBase.post_process_parse_url_results(results_) # Store our preloaded entries - preloaded.append({ - "results": results_, - "entry": no + 1, - "item": entry, - }) + preloaded.append( + { + "results": results_, + "entry": no + 1, + "item": entry, + } + ) # # Normalize Tag Groups @@ -1331,7 +1372,7 @@ class ConfigBase(URLBase): except Exception as e: # the arguments are invalid or can not be used. - ConfigBase.logger.warning( + ConfigBase.logger.error( "Could not load Apprise YAML configuration " "entry #{}, item #{}".format(entry["entry"], entry["item"]) ) @@ -1384,7 +1425,6 @@ class ConfigBase(URLBase): tokens = tokens.copy() for kw, meta in N_MGR[schema].template_kwargs.items(): - # Determine our prefix: prefix = meta.get("prefix", "+") @@ -1432,7 +1472,6 @@ class ConfigBase(URLBase): class_templates = plugins.details(N_MGR[schema]) for key in list(tokens.keys()): - if key not in class_templates["args"]: # No need to handle non-arg entries continue @@ -1472,7 +1511,6 @@ class ConfigBase(URLBase): if re.search( r"^(choice:)?string", meta.get("type"), re.IGNORECASE ) and not isinstance(value, str): - # Ensure our format is as expected value = str(value) diff --git a/libs/apprise/config/file.py b/libs/apprise/config/file.py index a68946669..572700f70 100644 --- a/libs/apprise/config/file.py +++ b/libs/apprise/config/file.py @@ -101,7 +101,6 @@ class ConfigFile(ConfigBase): self.max_buffer_size > 0 and os.path.getsize(self.path) > self.max_buffer_size ): - # Content exceeds maximum buffer size self.logger.error( "File size exceeds maximum allowable buffer length" @@ -149,7 +148,6 @@ class ConfigFile(ConfigBase): self.config_format is None and re.match(r"^.*\.ya?ml\s*$", self.path, re.I) is not None ): - # YAML Filename Detected self.default_config_format = ConfigFormat.YAML diff --git a/libs/apprise/config/http.py b/libs/apprise/config/http.py index a062136ff..2830d9089 100644 --- a/libs/apprise/config/http.py +++ b/libs/apprise/config/http.py @@ -186,12 +186,25 @@ class ConfigHTTP(ConfigBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, stream=True, ) as r: - # Handle Errors r.raise_for_status() + # raise_for_status() only covers 4xx/5xx; when redirect + # following is disabled any 3xx must be treated as a failure + # so we do not silently return a redirect stub as config. + # Using a status-code range rather than r.is_redirect + # catches 3xx responses that lack a Location header. + if not self.redirects and 300 <= r.status_code < 400: + self.logger.error( + "HTTP redirect encountered but redirect " + "following is disabled:" + f" {self.url(privacy=True)}" + ) + return None + # Get our file-size (if known) try: file_size = int(r.headers.get("Content-Length", "0")) @@ -204,7 +217,6 @@ class ConfigHTTP(ConfigBase): self.max_buffer_size > 0 and file_size > self.max_buffer_size ): - # Provide warning of data truncation self.logger.error( "HTTP config response exceeds maximum buffer length " @@ -235,12 +247,10 @@ class ConfigHTTP(ConfigBase): ) if self.config_format is None and content_type: if MIME_IS_YAML.match(content_type) is not None: - # YAML data detected based on header content self.default_config_format = ConfigFormat.YAML elif MIME_IS_TEXT.match(content_type) is not None: - # TEXT data detected based on header content self.default_config_format = ConfigFormat.TEXT diff --git a/libs/apprise/conversion.py b/libs/apprise/conversion.py index e4930f61d..e8216a497 100644 --- a/libs/apprise/conversion.py +++ b/libs/apprise/conversion.py @@ -176,7 +176,6 @@ class HTMLConverter(HTMLParser): # initialize our previous flag if self._do_store: - # Tidy our whitespace content = self.WS_TRIM.sub(" ", data) self._result.append(content) diff --git a/libs/apprise/decorators/base.py b/libs/apprise/decorators/base.py index 1fcdb15f2..faf59e424 100644 --- a/libs/apprise/decorators/base.py +++ b/libs/apprise/decorators/base.py @@ -120,7 +120,6 @@ class CustomNotifyPlugin(NotifyBase): # `Apprise.details()` to correctly differentiate one custom plugin # that was loaded from another class CustomNotifyPluginWrapper(CustomNotifyPlugin): - # Our Service Name service_name = ( name @@ -193,7 +192,8 @@ class CustomNotifyPlugin(NotifyBase): N_MGR[self.secure_protocol].service_name, ) self.logger.debug( - "%s Exception: %s", N_MGR[self.secure_protocol], e) + "%s Exception: %s", N_MGR[self.secure_protocol], e + ) return False if response: diff --git a/libs/apprise/emojis.py b/libs/apprise/emojis.py index cf6c4ae54..dcfbde28c 100644 --- a/libs/apprise/emojis.py +++ b/libs/apprise/emojis.py @@ -2146,15 +2146,17 @@ EMOJI_MAP = { DELIM + r"wales" + DELIM: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", } -# Define our singlton +# Define our singletons EMOJI_COMPILED_MAP = None +# List of (compiled_fullmatch_pattern, emoji) for resolving alternation keys +_EMOJI_PATTERN_LIST = None def apply_emojis(content): """Takes the content and swaps any matched emoji's found with their utf-8 encoded mapping.""" - global EMOJI_COMPILED_MAP + global EMOJI_COMPILED_MAP, _EMOJI_PATTERN_LIST if EMOJI_COMPILED_MAP is None: t_start = time.time() @@ -2162,10 +2164,27 @@ def apply_emojis(content): EMOJI_COMPILED_MAP = re.compile( r"(" + "|".join(EMOJI_MAP.keys()) + r")", re.IGNORECASE ) + + # - EMOJI_MAP keys are regex patterns (e.g. ":(\+1|thumbsup):") + # - x.group() from a match cannot be used as a dict key directly. + # + # Build a per-entry compiled pattern for fullmatch lookups so + # alternation/optional-group keys resolve correctly. + _EMOJI_PATTERN_LIST = [ + (re.compile(r"(?:" + pat + r")", re.IGNORECASE), emoji) + for pat, emoji in EMOJI_MAP.items() + ] logger.trace(f"Emoji engine loaded in {time.time() - t_start:.4f}s") + def _lookup(m): + text = m.group() + for pat, emoji in _EMOJI_PATTERN_LIST: + if pat.fullmatch(text): + return emoji + return text + try: - return EMOJI_COMPILED_MAP.sub(lambda x: EMOJI_MAP[x.group()], content) + return EMOJI_COMPILED_MAP.sub(_lookup, content) except TypeError: # No change; but force string return diff --git a/libs/apprise/i18n/apprise.pot b/libs/apprise/i18n/apprise.pot index 37194e5d0..9f0548b99 100644 --- a/libs/apprise/i18n/apprise.pot +++ b/libs/apprise/i18n/apprise.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-03-08 16:43-0400\n" +"POT-Creation-Date: 2026-05-29 13:49-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,15 +17,19 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: apprise/attachment/base.py:96 apprise/url.py:141 +#: apprise/attachment/base.py:96 apprise/url.py:148 msgid "Verify SSL" msgstr "" -#: apprise/url.py:151 +#: apprise/url.py:158 +msgid "Follow Redirects" +msgstr "" + +#: apprise/url.py:168 msgid "Socket Read Timeout" msgstr "" -#: apprise/url.py:165 +#: apprise/url.py:182 msgid "Socket Connect Timeout" msgstr "" @@ -53,19 +57,19 @@ msgstr "" msgid "Memory" msgstr "" -#: apprise/plugins/__init__.py:280 +#: apprise/plugins/__init__.py:278 msgid "Schema" msgstr "" -#: apprise/plugins/__init__.py:401 +#: apprise/plugins/__init__.py:398 msgid "No dependencies." msgstr "" -#: apprise/plugins/__init__.py:404 +#: apprise/plugins/__init__.py:401 msgid "Packages are required to function." msgstr "" -#: apprise/plugins/__init__.py:408 +#: apprise/plugins/__init__.py:405 msgid "Packages are recommended to improve functionality." msgstr "" @@ -73,20 +77,23 @@ msgstr "" msgid "App User Name" msgstr "" -#: apprise/plugins/africas_talking.py:138 apprise/plugins/brevo.py:112 +#: apprise/plugins/africas_talking.py:138 apprise/plugins/brevo.py:157 #: apprise/plugins/burstsms.py:104 apprise/plugins/clicksend.py:98 -#: apprise/plugins/dot.py:121 apprise/plugins/fcm/__init__.py:143 -#: apprise/plugins/httpsms.py:77 apprise/plugins/join.py:140 -#: apprise/plugins/kavenegar.py:115 apprise/plugins/kumulos.py:87 -#: apprise/plugins/mailgun.py:146 apprise/plugins/messagebird.py:78 -#: apprise/plugins/one_signal.py:113 apprise/plugins/opsgenie.py:236 +#: apprise/plugins/dot.py:136 apprise/plugins/evolution.py:105 +#: apprise/plugins/exotel.py:213 apprise/plugins/fcm/__init__.py:143 +#: apprise/plugins/httpsms.py:77 apprise/plugins/jira.py:230 +#: apprise/plugins/join.py:140 apprise/plugins/kavenegar.py:115 +#: apprise/plugins/kumulos.py:87 apprise/plugins/mailgun.py:146 +#: apprise/plugins/messagebird.py:78 apprise/plugins/octopush.py:136 +#: apprise/plugins/one_signal.py:113 apprise/plugins/opsgenie.py:228 #: apprise/plugins/pagerduty.py:131 apprise/plugins/popcorn_notify.py:71 -#: apprise/plugins/prowl.py:121 apprise/plugins/resend.py:107 +#: apprise/plugins/postmark.py:117 apprise/plugins/prowl.py:121 +#: apprise/plugins/resend.py:107 apprise/plugins/resend.py:145 #: apprise/plugins/sendgrid.py:116 apprise/plugins/seven.py:75 #: apprise/plugins/simplepush.py:101 apprise/plugins/smsmanager.py:106 #: apprise/plugins/smtp2go.py:118 apprise/plugins/sparkpost.py:169 #: apprise/plugins/splunk.py:165 apprise/plugins/techuluspush.py:97 -#: apprise/plugins/twilio.py:197 apprise/plugins/vapid/__init__.py:152 +#: apprise/plugins/twilio.py:194 apprise/plugins/vapid/__init__.py:152 #: apprise/plugins/vonage.py:80 msgid "API Key" msgstr "" @@ -96,153 +103,166 @@ msgid "Target Phone" msgstr "" #: apprise/plugins/africas_talking.py:150 apprise/plugins/aprs.py:187 -#: apprise/plugins/bark.py:159 apprise/plugins/brevo.py:129 +#: apprise/plugins/bark.py:159 apprise/plugins/brevo.py:174 #: apprise/plugins/bulksms.py:137 apprise/plugins/bulkvs.py:110 #: apprise/plugins/burstsms.py:131 apprise/plugins/clickatell.py:90 #: apprise/plugins/clicksend.py:112 apprise/plugins/d7networks.py:110 #: apprise/plugins/dapnet.py:138 apprise/plugins/dingtalk.py:111 -#: apprise/plugins/email/base.py:140 apprise/plugins/fcm/__init__.py:168 +#: apprise/plugins/email/base.py:164 apprise/plugins/evolution.py:134 +#: apprise/plugins/exotel.py:190 apprise/plugins/fcm/__init__.py:168 #: apprise/plugins/flock.py:125 apprise/plugins/fortysixelks.py:111 -#: apprise/plugins/httpsms.py:97 apprise/plugins/irc/base.py:149 +#: apprise/plugins/home_assistant.py:153 apprise/plugins/httpsms.py:97 +#: apprise/plugins/humhub.py:149 apprise/plugins/irc/base.py:149 #: apprise/plugins/join.py:172 apprise/plugins/kavenegar.py:135 #: apprise/plugins/line.py:93 apprise/plugins/mailgun.py:157 -#: apprise/plugins/mastodon.py:190 apprise/plugins/matrix.py:262 -#: apprise/plugins/mattermost.py:185 apprise/plugins/messagebird.py:99 -#: apprise/plugins/mqtt.py:174 apprise/plugins/msg91.py:123 +#: apprise/plugins/mastodon.py:202 apprise/plugins/matrix/base.py:307 +#: apprise/plugins/mattermost.py:189 apprise/plugins/messagebird.py:99 +#: apprise/plugins/mqtt.py:177 apprise/plugins/msg91.py:123 #: apprise/plugins/nextcloud.py:148 apprise/plugins/nextcloudtalk.py:101 -#: apprise/plugins/notifiarr.py:103 apprise/plugins/notificationapi.py:187 -#: apprise/plugins/ntfy.py:241 apprise/plugins/office365.py:148 -#: apprise/plugins/one_signal.py:141 apprise/plugins/plivo.py:114 -#: apprise/plugins/popcorn_notify.py:89 apprise/plugins/pushbullet.py:105 -#: apprise/plugins/pushed.py:107 apprise/plugins/pushover.py:206 +#: apprise/plugins/notifiarr.py:103 apprise/plugins/notificationapi.py:192 +#: apprise/plugins/ntfy.py:241 apprise/plugins/octopush.py:153 +#: apprise/plugins/office365.py:287 apprise/plugins/one_signal.py:141 +#: apprise/plugins/plivo.py:114 apprise/plugins/popcorn_notify.py:89 +#: apprise/plugins/postmark.py:133 apprise/plugins/pushbullet.py:105 +#: apprise/plugins/pushed.py:107 apprise/plugins/pushover.py:252 #: apprise/plugins/pushsafer.py:376 apprise/plugins/pushy.py:97 #: apprise/plugins/reddit.py:170 apprise/plugins/resend.py:124 -#: apprise/plugins/revolt.py:112 apprise/plugins/rocketchat.py:166 -#: apprise/plugins/sendgrid.py:133 apprise/plugins/sendpulse.py:136 -#: apprise/plugins/seven.py:88 apprise/plugins/sfr.py:113 -#: apprise/plugins/signal_api.py:138 apprise/plugins/sinch.py:140 -#: apprise/plugins/slack.py:248 apprise/plugins/smpp.py:128 -#: apprise/plugins/smseagle.py:172 apprise/plugins/smsmanager.py:119 -#: apprise/plugins/sns.py:138 apprise/plugins/telegram.py:359 -#: apprise/plugins/threema.py:115 apprise/plugins/twilio.py:174 -#: apprise/plugins/twist.py:123 apprise/plugins/twitter.py:168 -#: apprise/plugins/vapid/__init__.py:158 apprise/plugins/voipms.py:107 -#: apprise/plugins/vonage.py:108 apprise/plugins/whatsapp.py:126 -#: apprise/plugins/wxpusher.py:139 apprise/plugins/xmpp/base.py:118 +#: apprise/plugins/revolt.py:112 apprise/plugins/ringcentral.py:219 +#: apprise/plugins/rocketchat.py:167 apprise/plugins/sendgrid.py:133 +#: apprise/plugins/sendpulse.py:139 apprise/plugins/seven.py:88 +#: apprise/plugins/sfr.py:113 apprise/plugins/signal_api.py:138 +#: apprise/plugins/sinch.py:140 apprise/plugins/slack.py:273 +#: apprise/plugins/smpp.py:128 apprise/plugins/smseagle.py:172 +#: apprise/plugins/smsmanager.py:119 apprise/plugins/sns.py:170 +#: apprise/plugins/telegram.py:359 apprise/plugins/threema.py:115 +#: apprise/plugins/twilio.py:174 apprise/plugins/twist.py:123 +#: apprise/plugins/twitter.py:193 apprise/plugins/vapid/__init__.py:158 +#: apprise/plugins/voipms.py:108 apprise/plugins/vonage.py:108 +#: apprise/plugins/wechat.py:218 apprise/plugins/whatsapp.py:166 +#: apprise/plugins/wxpusher.py:139 apprise/plugins/xmpp/base.py:134 #: apprise/plugins/zulip.py:153 msgid "Targets" msgstr "" -#: apprise/plugins/africas_talking.py:168 +#: apprise/plugins/africas_talking.py:166 msgid "From" msgstr "" -#: apprise/plugins/africas_talking.py:174 apprise/plugins/bulksms.py:170 -#: apprise/plugins/bulkvs.py:131 apprise/plugins/burstsms.py:166 -#: apprise/plugins/clicksend.py:130 apprise/plugins/d7networks.py:128 -#: apprise/plugins/dapnet.py:167 apprise/plugins/mailgun.py:194 -#: apprise/plugins/mastodon.py:215 apprise/plugins/one_signal.py:161 -#: apprise/plugins/opsgenie.py:288 apprise/plugins/plivo.py:137 -#: apprise/plugins/popcorn_notify.py:104 apprise/plugins/signal_api.py:155 -#: apprise/plugins/smseagle.py:190 apprise/plugins/smsmanager.py:152 -#: apprise/plugins/smtp2go.py:151 apprise/plugins/sparkpost.py:209 -#: apprise/plugins/twitter.py:193 -msgid "Batch Mode" -msgstr "" - -#: apprise/plugins/africas_talking.py:179 +#: apprise/plugins/africas_talking.py:172 msgid "SMS Mode" msgstr "" +#: apprise/plugins/africas_talking.py:181 apprise/plugins/bulksms.py:170 +#: apprise/plugins/bulkvs.py:131 apprise/plugins/burstsms.py:166 +#: apprise/plugins/clicksend.py:130 apprise/plugins/d7networks.py:144 +#: apprise/plugins/dapnet.py:167 apprise/plugins/exotel.py:222 +#: apprise/plugins/home_assistant.py:170 apprise/plugins/jira.py:282 +#: apprise/plugins/mailgun.py:194 apprise/plugins/mastodon.py:247 +#: apprise/plugins/octopush.py:173 apprise/plugins/one_signal.py:185 +#: apprise/plugins/opsgenie.py:307 apprise/plugins/plivo.py:137 +#: apprise/plugins/popcorn_notify.py:104 apprise/plugins/signal_api.py:160 +#: apprise/plugins/smseagle.py:211 apprise/plugins/smsmanager.py:152 +#: apprise/plugins/smtp2go.py:151 apprise/plugins/sparkpost.py:209 +#: apprise/plugins/twitter.py:218 +msgid "Batch Mode" +msgstr "" + #: apprise/plugins/apprise_api.py:102 apprise/plugins/bark.py:134 -#: apprise/plugins/custom_form.py:121 apprise/plugins/custom_json.py:103 -#: apprise/plugins/custom_xml.py:103 apprise/plugins/emby.py:85 -#: apprise/plugins/enigma2.py:110 apprise/plugins/fluxer.py:159 -#: apprise/plugins/gotify.py:129 apprise/plugins/growl.py:140 -#: apprise/plugins/home_assistant.py:79 apprise/plugins/irc/base.py:117 -#: apprise/plugins/lametric.py:460 apprise/plugins/mastodon.py:168 -#: apprise/plugins/matrix.py:220 apprise/plugins/mattermost.py:151 -#: apprise/plugins/misskey.py:117 apprise/plugins/mqtt.py:147 -#: apprise/plugins/nextcloud.py:116 apprise/plugins/nextcloudtalk.py:74 -#: apprise/plugins/notica.py:126 apprise/plugins/ntfy.py:211 +#: apprise/plugins/custom_form.py:130 apprise/plugins/custom_json.py:112 +#: apprise/plugins/custom_xml.py:112 apprise/plugins/emby.py:85 +#: apprise/plugins/enigma2.py:110 apprise/plugins/evolution.py:111 +#: apprise/plugins/fluxer.py:159 apprise/plugins/gotify.py:129 +#: apprise/plugins/growl.py:140 apprise/plugins/home_assistant.py:122 +#: apprise/plugins/humhub.py:138 apprise/plugins/irc/base.py:117 +#: apprise/plugins/kodi.py:96 apprise/plugins/lametric.py:460 +#: apprise/plugins/mastodon.py:180 apprise/plugins/matrix/base.py:262 +#: apprise/plugins/mattermost.py:155 apprise/plugins/misskey.py:117 +#: apprise/plugins/mqtt.py:149 apprise/plugins/nextcloud.py:116 +#: apprise/plugins/nextcloudtalk.py:74 apprise/plugins/notica.py:126 +#: apprise/plugins/notifico.py:166 apprise/plugins/ntfy.py:211 #: apprise/plugins/parseplatform.py:90 apprise/plugins/pushdeer.py:75 #: apprise/plugins/pushjet.py:71 apprise/plugins/rocketchat.py:120 #: apprise/plugins/rsyslog.py:180 apprise/plugins/signal_api.py:97 -#: apprise/plugins/smseagle.py:135 apprise/plugins/synology.py:83 -#: apprise/plugins/workflows.py:125 apprise/plugins/xbmc.py:96 -#: apprise/plugins/xmpp/base.py:96 +#: apprise/plugins/smseagle.py:135 apprise/plugins/sogs.py:207 +#: apprise/plugins/synology.py:83 apprise/plugins/workflows.py:126 +#: apprise/plugins/xmpp/base.py:101 msgid "Hostname" msgstr "" #: apprise/plugins/apprise_api.py:107 apprise/plugins/bark.py:139 -#: apprise/plugins/custom_form.py:126 apprise/plugins/custom_json.py:108 -#: apprise/plugins/custom_xml.py:108 apprise/plugins/email/base.py:129 +#: apprise/plugins/custom_form.py:135 apprise/plugins/custom_json.py:117 +#: apprise/plugins/custom_xml.py:117 apprise/plugins/email/base.py:153 #: apprise/plugins/emby.py:90 apprise/plugins/enigma2.py:115 -#: apprise/plugins/fluxer.py:163 apprise/plugins/gotify.py:140 -#: apprise/plugins/growl.py:145 apprise/plugins/home_assistant.py:84 -#: apprise/plugins/irc/base.py:122 apprise/plugins/lametric.py:464 -#: apprise/plugins/mastodon.py:178 apprise/plugins/matrix.py:224 -#: apprise/plugins/mattermost.py:167 apprise/plugins/misskey.py:127 -#: apprise/plugins/mqtt.py:152 apprise/plugins/nextcloud.py:121 -#: apprise/plugins/nextcloudtalk.py:79 apprise/plugins/notica.py:130 +#: apprise/plugins/evolution.py:116 apprise/plugins/fluxer.py:163 +#: apprise/plugins/gotify.py:140 apprise/plugins/growl.py:145 +#: apprise/plugins/home_assistant.py:127 apprise/plugins/humhub.py:143 +#: apprise/plugins/irc/base.py:122 apprise/plugins/kodi.py:101 +#: apprise/plugins/lametric.py:464 apprise/plugins/mastodon.py:190 +#: apprise/plugins/matrix/base.py:267 apprise/plugins/mattermost.py:171 +#: apprise/plugins/misskey.py:127 apprise/plugins/mqtt.py:154 +#: apprise/plugins/nextcloud.py:121 apprise/plugins/nextcloudtalk.py:79 +#: apprise/plugins/notica.py:130 apprise/plugins/notifico.py:170 #: apprise/plugins/ntfy.py:215 apprise/plugins/parseplatform.py:95 #: apprise/plugins/pushdeer.py:79 apprise/plugins/pushjet.py:76 #: apprise/plugins/rocketchat.py:125 apprise/plugins/rsyslog.py:185 #: apprise/plugins/signal_api.py:102 apprise/plugins/smpp.py:108 -#: apprise/plugins/smseagle.py:140 apprise/plugins/synology.py:88 -#: apprise/plugins/workflows.py:130 apprise/plugins/xbmc.py:101 -#: apprise/plugins/xmpp/base.py:101 +#: apprise/plugins/smseagle.py:140 apprise/plugins/sogs.py:212 +#: apprise/plugins/synology.py:88 apprise/plugins/workflows.py:131 +#: apprise/plugins/xmpp/base.py:106 msgid "Port" msgstr "" #: apprise/plugins/apprise_api.py:113 apprise/plugins/bark.py:145 -#: apprise/plugins/bluesky.py:119 apprise/plugins/custom_form.py:132 -#: apprise/plugins/custom_json.py:114 apprise/plugins/custom_xml.py:114 +#: apprise/plugins/bluesky.py:119 apprise/plugins/custom_form.py:141 +#: apprise/plugins/custom_json.py:123 apprise/plugins/custom_xml.py:123 #: apprise/plugins/emby.py:97 apprise/plugins/enigma2.py:121 -#: apprise/plugins/freemobile.py:78 apprise/plugins/home_assistant.py:90 -#: apprise/plugins/lametric.py:471 apprise/plugins/matrix.py:230 +#: apprise/plugins/freemobile.py:78 apprise/plugins/home_assistant.py:133 +#: apprise/plugins/jira.py:236 apprise/plugins/kodi.py:107 +#: apprise/plugins/lametric.py:471 apprise/plugins/matrix/base.py:273 #: apprise/plugins/nextcloud.py:127 apprise/plugins/nextcloudtalk.py:85 -#: apprise/plugins/notica.py:136 apprise/plugins/ntfy.py:221 -#: apprise/plugins/opsgenie.py:242 apprise/plugins/pushjet.py:88 -#: apprise/plugins/rocketchat.py:131 apprise/plugins/signal_api.py:108 -#: apprise/plugins/smpp.py:92 apprise/plugins/synology.py:94 -#: apprise/plugins/xbmc.py:107 +#: apprise/plugins/notica.py:136 apprise/plugins/notifico.py:176 +#: apprise/plugins/ntfy.py:221 apprise/plugins/opsgenie.py:234 +#: apprise/plugins/pushjet.py:88 apprise/plugins/rocketchat.py:131 +#: apprise/plugins/signal_api.py:108 apprise/plugins/smpp.py:92 +#: apprise/plugins/synology.py:94 msgid "Username" msgstr "" #: apprise/plugins/apprise_api.py:117 apprise/plugins/aprs.py:172 #: apprise/plugins/bark.py:149 apprise/plugins/bluesky.py:124 #: apprise/plugins/bulksms.py:117 apprise/plugins/bulkvs.py:90 -#: apprise/plugins/custom_form.py:136 apprise/plugins/custom_json.py:118 -#: apprise/plugins/custom_xml.py:118 apprise/plugins/dapnet.py:123 -#: apprise/plugins/email/base.py:119 apprise/plugins/emby.py:101 +#: apprise/plugins/custom_form.py:145 apprise/plugins/custom_json.py:127 +#: apprise/plugins/custom_xml.py:127 apprise/plugins/dapnet.py:123 +#: apprise/plugins/email/base.py:143 apprise/plugins/emby.py:101 #: apprise/plugins/enigma2.py:125 apprise/plugins/freemobile.py:83 -#: apprise/plugins/growl.py:151 apprise/plugins/home_assistant.py:94 -#: apprise/plugins/irc/base.py:132 apprise/plugins/matrix.py:234 -#: apprise/plugins/mqtt.py:163 apprise/plugins/nextcloud.py:131 +#: apprise/plugins/growl.py:151 apprise/plugins/home_assistant.py:137 +#: apprise/plugins/humhub.py:133 apprise/plugins/irc/base.py:132 +#: apprise/plugins/kodi.py:111 apprise/plugins/matrix/base.py:277 +#: apprise/plugins/mqtt.py:165 apprise/plugins/nextcloud.py:131 #: apprise/plugins/nextcloudtalk.py:90 apprise/plugins/notica.py:140 -#: apprise/plugins/ntfy.py:225 apprise/plugins/pushjet.py:92 -#: apprise/plugins/reddit.py:145 apprise/plugins/rocketchat.py:135 -#: apprise/plugins/signal_api.py:112 apprise/plugins/simplepush.py:108 -#: apprise/plugins/smpp.py:97 apprise/plugins/synology.py:98 -#: apprise/plugins/twist.py:101 apprise/plugins/voipms.py:88 -#: apprise/plugins/xbmc.py:111 apprise/plugins/xmpp/base.py:112 +#: apprise/plugins/notifico.py:180 apprise/plugins/ntfy.py:225 +#: apprise/plugins/pushjet.py:92 apprise/plugins/reddit.py:145 +#: apprise/plugins/rocketchat.py:135 apprise/plugins/signal_api.py:112 +#: apprise/plugins/simplepush.py:108 apprise/plugins/smpp.py:97 +#: apprise/plugins/synology.py:98 apprise/plugins/twist.py:101 +#: apprise/plugins/voipms.py:88 apprise/plugins/xmpp/base.py:117 msgid "Password" msgstr "" #: apprise/plugins/apprise_api.py:122 apprise/plugins/chanify.py:74 -#: apprise/plugins/dingtalk.py:93 apprise/plugins/feishu.py:80 -#: apprise/plugins/gotify.py:123 apprise/plugins/mattermost.py:157 -#: apprise/plugins/notica.py:119 apprise/plugins/notifiarr.py:91 -#: apprise/plugins/ntfy.py:230 apprise/plugins/pushme.py:62 -#: apprise/plugins/ryver.py:99 apprise/plugins/serverchan.py:70 -#: apprise/plugins/slack.py:294 apprise/plugins/synology.py:103 -#: apprise/plugins/webexteams.py:116 apprise/plugins/zulip.py:136 +#: apprise/plugins/dingtalk.py:93 apprise/plugins/exotel.py:166 +#: apprise/plugins/feishu.py:80 apprise/plugins/gotify.py:123 +#: apprise/plugins/mattermost.py:161 apprise/plugins/notica.py:119 +#: apprise/plugins/notifiarr.py:91 apprise/plugins/ntfy.py:230 +#: apprise/plugins/pushme.py:62 apprise/plugins/ryver.py:99 +#: apprise/plugins/serverchan.py:70 apprise/plugins/slack.py:323 +#: apprise/plugins/synology.py:103 apprise/plugins/zulip.py:136 msgid "Token" msgstr "" -#: apprise/plugins/apprise_api.py:136 apprise/plugins/ntfy.py:288 -#: apprise/plugins/opsgenie.py:307 apprise/plugins/pagertree.py:133 +#: apprise/plugins/apprise_api.py:136 apprise/plugins/jira.py:301 +#: apprise/plugins/ntfy.py:288 apprise/plugins/opsgenie.py:294 +#: apprise/plugins/pagertree.py:133 msgid "Tags" msgstr "" @@ -250,8 +270,8 @@ msgstr "" msgid "Query Method" msgstr "" -#: apprise/plugins/apprise_api.py:154 apprise/plugins/custom_form.py:165 -#: apprise/plugins/custom_json.py:141 apprise/plugins/custom_xml.py:141 +#: apprise/plugins/apprise_api.py:154 apprise/plugins/custom_form.py:174 +#: apprise/plugins/custom_json.py:150 apprise/plugins/custom_xml.py:150 #: apprise/plugins/enigma2.py:153 apprise/plugins/nextcloud.py:181 #: apprise/plugins/nextcloudtalk.py:122 apprise/plugins/notica.py:156 #: apprise/plugins/pagertree.py:142 apprise/plugins/synology.py:128 @@ -260,15 +280,15 @@ msgstr "" #: apprise/plugins/aprs.py:167 apprise/plugins/bulksms.py:112 #: apprise/plugins/bulkvs.py:85 apprise/plugins/clicksend.py:93 -#: apprise/plugins/dapnet.py:118 apprise/plugins/email/base.py:115 -#: apprise/plugins/mailgun.py:136 apprise/plugins/mqtt.py:158 -#: apprise/plugins/reddit.py:140 apprise/plugins/sendpulse.py:108 +#: apprise/plugins/dapnet.py:118 apprise/plugins/email/base.py:139 +#: apprise/plugins/mailgun.py:136 apprise/plugins/mqtt.py:160 +#: apprise/plugins/reddit.py:140 apprise/plugins/sendpulse.py:110 #: apprise/plugins/smtp2go.py:108 apprise/plugins/sparkpost.py:159 msgid "User Name" msgstr "" #: apprise/plugins/aprs.py:178 apprise/plugins/aprs.py:199 -#: apprise/plugins/dapnet.py:129 apprise/plugins/dapnet.py:150 +#: apprise/plugins/dapnet.py:129 apprise/plugins/dapnet.py:162 msgid "Target Callsign" msgstr "" @@ -281,160 +301,200 @@ msgid "Locale" msgstr "" #: apprise/plugins/bark.py:154 apprise/plugins/fcm/__init__.py:157 -#: apprise/plugins/pushbullet.py:89 apprise/plugins/pushover.py:200 -#: apprise/plugins/pushsafer.py:366 apprise/plugins/pushy.py:85 +#: apprise/plugins/home_assistant.py:148 apprise/plugins/pushbullet.py:89 +#: apprise/plugins/pushover.py:240 apprise/plugins/pushsafer.py:366 +#: apprise/plugins/pushy.py:85 msgid "Target Device" msgstr "" -#: apprise/plugins/bark.py:174 apprise/plugins/lametric.py:516 -#: apprise/plugins/macosx.py:124 apprise/plugins/pushover.py:223 +#: apprise/plugins/bark.py:171 apprise/plugins/lametric.py:516 +#: apprise/plugins/macosx.py:124 apprise/plugins/pushover.py:269 #: apprise/plugins/pushsafer.py:392 apprise/plugins/pushy.py:110 msgid "Sound" msgstr "" -#: apprise/plugins/bark.py:179 +#: apprise/plugins/bark.py:176 msgid "Level" msgstr "" -#: apprise/plugins/bark.py:184 +#: apprise/plugins/bark.py:181 msgid "Volume" msgstr "" -#: apprise/plugins/bark.py:190 apprise/plugins/ntfy.py:270 +#: apprise/plugins/bark.py:187 apprise/plugins/ntfy.py:270 #: apprise/plugins/pagerduty.py:172 msgid "Click" msgstr "" -#: apprise/plugins/bark.py:194 apprise/plugins/pushy.py:114 +#: apprise/plugins/bark.py:191 apprise/plugins/pushy.py:114 msgid "Badge" msgstr "" -#: apprise/plugins/bark.py:199 +#: apprise/plugins/bark.py:196 msgid "Category" msgstr "" -#: apprise/plugins/bark.py:203 apprise/plugins/join.py:158 +#: apprise/plugins/bark.py:200 apprise/plugins/join.py:158 #: apprise/plugins/pagerduty.py:163 msgid "Group" msgstr "" -#: apprise/plugins/bark.py:207 apprise/plugins/dbus.py:225 +#: apprise/plugins/bark.py:204 apprise/plugins/dbus.py:228 #: apprise/plugins/discord.py:196 apprise/plugins/fcm/__init__.py:197 #: apprise/plugins/flock.py:136 apprise/plugins/fluxer.py:250 -#: apprise/plugins/glib.py:187 apprise/plugins/gnome.py:154 -#: apprise/plugins/growl.py:175 apprise/plugins/join.py:182 -#: apprise/plugins/line.py:108 apprise/plugins/macosx.py:115 -#: apprise/plugins/matrix.py:273 apprise/plugins/mattermost.py:211 -#: apprise/plugins/msteams.py:200 apprise/plugins/notifiarr.py:125 -#: apprise/plugins/ntfy.py:256 apprise/plugins/one_signal.py:155 +#: apprise/plugins/glib.py:188 apprise/plugins/gnome.py:154 +#: apprise/plugins/growl.py:175 apprise/plugins/join.py:183 +#: apprise/plugins/kodi.py:129 apprise/plugins/line.py:108 +#: apprise/plugins/macosx.py:115 apprise/plugins/matrix/base.py:318 +#: apprise/plugins/mattermost.py:212 apprise/plugins/notifiarr.py:125 +#: apprise/plugins/ntfy.py:256 apprise/plugins/one_signal.py:164 #: apprise/plugins/pagerduty.py:191 apprise/plugins/ryver.py:124 -#: apprise/plugins/slack.py:259 apprise/plugins/telegram.py:370 +#: apprise/plugins/slack.py:291 apprise/plugins/telegram.py:370 #: apprise/plugins/vapid/__init__.py:204 apprise/plugins/windows.py:106 -#: apprise/plugins/workflows.py:162 apprise/plugins/xbmc.py:129 +#: apprise/plugins/workflows.py:163 msgid "Include Image" msgstr "" -#: apprise/plugins/bark.py:213 apprise/plugins/mattermost.py:207 -#: apprise/plugins/revolt.py:128 +#: apprise/plugins/bark.py:210 apprise/plugins/mattermost.py:208 +#: apprise/plugins/revolt.py:129 msgid "Icon URL" msgstr "" -#: apprise/plugins/bark.py:217 apprise/plugins/streamlabs.py:119 +#: apprise/plugins/bark.py:214 apprise/plugins/streamlabs.py:119 msgid "Call" msgstr "" -#: apprise/plugins/base.py:192 +#: apprise/plugins/base.py:305 msgid "Overflow Mode" msgstr "" -#: apprise/plugins/base.py:207 +#: apprise/plugins/base.py:320 msgid "Notify Format" msgstr "" -#: apprise/plugins/base.py:217 +#: apprise/plugins/base.py:330 msgid "Interpret Emojis" msgstr "" -#: apprise/plugins/base.py:227 +#: apprise/plugins/base.py:340 msgid "Persistent Storage" msgstr "" -#: apprise/plugins/base.py:237 +#: apprise/plugins/base.py:350 msgid "Timezone" msgstr "" -#: apprise/plugins/brevo.py:119 apprise/plugins/resend.py:114 -#: apprise/plugins/sendgrid.py:123 +#: apprise/plugins/base.py:359 +msgid "Service Retry" +msgstr "" + +#: apprise/plugins/base.py:367 +msgid "Inter-Retry Wait" +msgstr "" + +#: apprise/plugins/base.py:375 +msgid "Optional Service" +msgstr "" + +#: apprise/plugins/blink1.py:151 +msgid "blink(1)" +msgstr "" + +#: apprise/plugins/blink1.py:178 +msgid "Serial Number" +msgstr "" + +#: apprise/plugins/blink1.py:188 +msgid "Duration (ms)" +msgstr "" + +#: apprise/plugins/blink1.py:195 +msgid "Fade Time (ms)" +msgstr "" + +#: apprise/plugins/blink1.py:202 +msgid "LED Number" +msgstr "" + +#: apprise/plugins/brevo.py:164 apprise/plugins/postmark.py:123 +#: apprise/plugins/resend.py:114 apprise/plugins/sendgrid.py:123 msgid "Source Email" msgstr "" -#: apprise/plugins/brevo.py:124 apprise/plugins/email/base.py:135 -#: apprise/plugins/mailgun.py:152 apprise/plugins/notificationapi.py:172 -#: apprise/plugins/office365.py:143 apprise/plugins/one_signal.py:124 -#: apprise/plugins/popcorn_notify.py:84 apprise/plugins/pushbullet.py:100 -#: apprise/plugins/pushsafer.py:371 apprise/plugins/resend.py:119 -#: apprise/plugins/sendgrid.py:128 apprise/plugins/sendpulse.py:131 -#: apprise/plugins/slack.py:231 apprise/plugins/threema.py:105 +#: apprise/plugins/brevo.py:169 apprise/plugins/email/base.py:159 +#: apprise/plugins/mailgun.py:152 apprise/plugins/notificationapi.py:177 +#: apprise/plugins/office365.py:282 apprise/plugins/one_signal.py:124 +#: apprise/plugins/popcorn_notify.py:84 apprise/plugins/postmark.py:128 +#: apprise/plugins/pushbullet.py:100 apprise/plugins/pushsafer.py:371 +#: apprise/plugins/resend.py:119 apprise/plugins/sendgrid.py:128 +#: apprise/plugins/sendpulse.py:134 apprise/plugins/slack.py:256 +#: apprise/plugins/threema.py:105 msgid "Target Email" msgstr "" -#: apprise/plugins/brevo.py:143 apprise/plugins/email/base.py:165 -#: apprise/plugins/mailgun.py:186 apprise/plugins/notificationapi.py:218 -#: apprise/plugins/office365.py:162 apprise/plugins/resend.py:138 -#: apprise/plugins/sendgrid.py:147 apprise/plugins/sendpulse.py:152 -#: apprise/plugins/ses.py:206 apprise/plugins/smtp2go.py:143 -#: apprise/plugins/sparkpost.py:201 +#: apprise/plugins/brevo.py:185 apprise/plugins/postmark.py:149 +#: apprise/plugins/ses.py:205 +msgid "Reply To Email" +msgstr "" + +#: apprise/plugins/brevo.py:193 apprise/plugins/email/base.py:240 +#: apprise/plugins/mailgun.py:186 apprise/plugins/notificationapi.py:243 +#: apprise/plugins/office365.py:307 apprise/plugins/postmark.py:157 +#: apprise/plugins/resend.py:158 apprise/plugins/sendgrid.py:153 +#: apprise/plugins/sendpulse.py:169 apprise/plugins/ses.py:232 +#: apprise/plugins/smtp2go.py:143 apprise/plugins/sparkpost.py:201 msgid "Carbon Copy" msgstr "" -#: apprise/plugins/brevo.py:147 apprise/plugins/email/base.py:169 -#: apprise/plugins/mailgun.py:190 apprise/plugins/notificationapi.py:222 -#: apprise/plugins/office365.py:166 apprise/plugins/resend.py:142 -#: apprise/plugins/sendgrid.py:151 apprise/plugins/sendpulse.py:156 -#: apprise/plugins/ses.py:210 apprise/plugins/smtp2go.py:147 -#: apprise/plugins/sparkpost.py:205 +#: apprise/plugins/brevo.py:197 apprise/plugins/email/base.py:244 +#: apprise/plugins/mailgun.py:190 apprise/plugins/notificationapi.py:247 +#: apprise/plugins/office365.py:311 apprise/plugins/postmark.py:161 +#: apprise/plugins/resend.py:162 apprise/plugins/sendgrid.py:157 +#: apprise/plugins/sendpulse.py:173 apprise/plugins/ses.py:236 +#: apprise/plugins/smtp2go.py:147 apprise/plugins/sparkpost.py:205 msgid "Blind Carbon Copy" msgstr "" -#: apprise/plugins/brevo.py:151 apprise/plugins/ses.py:196 -msgid "Reply To Email" -msgstr "" - #: apprise/plugins/bulksms.py:123 apprise/plugins/bulkvs.py:103 #: apprise/plugins/burstsms.py:124 apprise/plugins/clickatell.py:83 #: apprise/plugins/clicksend.py:105 apprise/plugins/d7networks.py:103 -#: apprise/plugins/dingtalk.py:106 apprise/plugins/httpsms.py:90 +#: apprise/plugins/dingtalk.py:106 apprise/plugins/evolution.py:127 +#: apprise/plugins/exotel.py:183 apprise/plugins/httpsms.py:90 #: apprise/plugins/kavenegar.py:128 apprise/plugins/messagebird.py:92 -#: apprise/plugins/msg91.py:116 apprise/plugins/plivo.py:107 -#: apprise/plugins/popcorn_notify.py:77 apprise/plugins/seven.py:81 +#: apprise/plugins/msg91.py:116 apprise/plugins/octopush.py:146 +#: apprise/plugins/plivo.py:107 apprise/plugins/popcorn_notify.py:77 +#: apprise/plugins/ringcentral.py:213 apprise/plugins/seven.py:81 #: apprise/plugins/signal_api.py:124 apprise/plugins/sinch.py:127 #: apprise/plugins/smpp.py:121 apprise/plugins/smseagle.py:151 -#: apprise/plugins/smsmanager.py:112 apprise/plugins/sns.py:125 +#: apprise/plugins/smsmanager.py:112 apprise/plugins/sns.py:157 #: apprise/plugins/threema.py:98 apprise/plugins/twilio.py:161 -#: apprise/plugins/voipms.py:100 apprise/plugins/vonage.py:101 -#: apprise/plugins/whatsapp.py:119 +#: apprise/plugins/voipms.py:101 apprise/plugins/vonage.py:101 +#: apprise/plugins/whatsapp.py:147 msgid "Target Phone No" msgstr "" #: apprise/plugins/bulksms.py:130 apprise/plugins/nextcloud.py:142 +#: apprise/plugins/pushover.py:246 msgid "Target Group" msgstr "" -#: apprise/plugins/bulksms.py:152 apprise/plugins/bulkvs.py:96 -#: apprise/plugins/bulkvs.py:125 apprise/plugins/clickatell.py:78 +#: apprise/plugins/bulksms.py:149 apprise/plugins/bulkvs.py:96 +#: apprise/plugins/bulkvs.py:122 apprise/plugins/clickatell.py:78 #: apprise/plugins/fortysixelks.py:100 apprise/plugins/httpsms.py:83 -#: apprise/plugins/httpsms.py:115 apprise/plugins/signal_api.py:117 -#: apprise/plugins/sinch.py:120 apprise/plugins/smpp.py:114 -#: apprise/plugins/smsmanager.py:137 apprise/plugins/twilio.py:154 -#: apprise/plugins/voipms.py:94 apprise/plugins/vonage.py:94 +#: apprise/plugins/httpsms.py:115 apprise/plugins/ringcentral.py:207 +#: apprise/plugins/signal_api.py:117 apprise/plugins/sinch.py:120 +#: apprise/plugins/smpp.py:114 apprise/plugins/smsmanager.py:134 +#: apprise/plugins/twilio.py:154 apprise/plugins/voipms.py:94 +#: apprise/plugins/vonage.py:94 msgid "From Phone No" msgstr "" -#: apprise/plugins/bulksms.py:158 +#: apprise/plugins/bulksms.py:155 msgid "Route Group" msgstr "" -#: apprise/plugins/bulksms.py:165 apprise/plugins/d7networks.py:123 +#: apprise/plugins/bulksms.py:162 apprise/plugins/d7networks.py:136 +#: apprise/plugins/exotel.py:228 msgid "Unicode Characters" msgstr "" @@ -447,11 +507,11 @@ msgstr "" msgid "Sender ID" msgstr "" -#: apprise/plugins/burstsms.py:155 +#: apprise/plugins/burstsms.py:152 msgid "Country" msgstr "" -#: apprise/plugins/burstsms.py:164 +#: apprise/plugins/burstsms.py:161 msgid "validity" msgstr "" @@ -459,6 +519,18 @@ msgstr "" msgid "Chanify" msgstr "" +#: apprise/plugins/chime.py:130 apprise/plugins/discord.py:130 +#: apprise/plugins/fluxer.py:174 apprise/plugins/ifttt.py:103 +#: apprise/plugins/zoom.py:139 +msgid "Webhook ID" +msgstr "" + +#: apprise/plugins/chime.py:136 apprise/plugins/discord.py:136 +#: apprise/plugins/fluxer.py:181 apprise/plugins/google_chat.py:118 +#: apprise/plugins/webexteams.py:171 +msgid "Webhook Token" +msgstr "" + #: apprise/plugins/clickatell.py:45 msgid "Clickatell" msgstr "" @@ -467,22 +539,22 @@ msgstr "" msgid "API Token" msgstr "" -#: apprise/plugins/custom_form.py:148 apprise/plugins/custom_json.py:130 -#: apprise/plugins/custom_xml.py:130 +#: apprise/plugins/custom_form.py:157 apprise/plugins/custom_json.py:139 +#: apprise/plugins/custom_xml.py:139 msgid "Fetch Method" msgstr "" -#: apprise/plugins/custom_form.py:154 +#: apprise/plugins/custom_form.py:163 msgid "Attach File As" msgstr "" -#: apprise/plugins/custom_form.py:169 apprise/plugins/custom_json.py:145 -#: apprise/plugins/custom_xml.py:145 apprise/plugins/pagertree.py:146 +#: apprise/plugins/custom_form.py:178 apprise/plugins/custom_json.py:154 +#: apprise/plugins/custom_xml.py:154 apprise/plugins/pagertree.py:146 msgid "Payload Extras" msgstr "" -#: apprise/plugins/custom_form.py:173 apprise/plugins/custom_json.py:149 -#: apprise/plugins/custom_xml.py:149 +#: apprise/plugins/custom_form.py:182 apprise/plugins/custom_json.py:158 +#: apprise/plugins/custom_xml.py:158 msgid "GET Params" msgstr "" @@ -490,41 +562,42 @@ msgstr "" msgid "API Access Token" msgstr "" -#: apprise/plugins/d7networks.py:141 apprise/plugins/seven.py:107 +#: apprise/plugins/d7networks.py:127 apprise/plugins/seven.py:105 msgid "Originating Address" msgstr "" -#: apprise/plugins/dapnet.py:155 apprise/plugins/gotify.py:153 -#: apprise/plugins/growl.py:163 apprise/plugins/join.py:188 +#: apprise/plugins/dapnet.py:150 apprise/plugins/exotel.py:240 +#: apprise/plugins/gotify.py:153 apprise/plugins/growl.py:163 +#: apprise/plugins/jira.py:287 apprise/plugins/join.py:189 #: apprise/plugins/lametric.py:494 apprise/plugins/ntfy.py:282 -#: apprise/plugins/opsgenie.py:293 apprise/plugins/prowl.py:141 -#: apprise/plugins/pushover.py:217 apprise/plugins/pushsafer.py:387 -#: apprise/plugins/smseagle.py:210 +#: apprise/plugins/opsgenie.py:280 apprise/plugins/prowl.py:141 +#: apprise/plugins/pushover.py:263 apprise/plugins/pushsafer.py:387 +#: apprise/plugins/smseagle.py:187 msgid "Priority" msgstr "" -#: apprise/plugins/dapnet.py:161 +#: apprise/plugins/dapnet.py:156 msgid "Transmitter Groups" msgstr "" -#: apprise/plugins/dbus.py:153 +#: apprise/plugins/dbus.py:156 msgid "libdbus-1.so.x must be installed." msgstr "" -#: apprise/plugins/dbus.py:157 apprise/plugins/glib.py:126 +#: apprise/plugins/dbus.py:160 msgid "DBus Notification" msgstr "" -#: apprise/plugins/dbus.py:201 apprise/plugins/glib.py:163 +#: apprise/plugins/dbus.py:204 apprise/plugins/glib.py:165 #: apprise/plugins/gnome.py:142 apprise/plugins/pagertree.py:128 msgid "Urgency" msgstr "" -#: apprise/plugins/dbus.py:213 apprise/plugins/glib.py:175 +#: apprise/plugins/dbus.py:216 apprise/plugins/glib.py:176 msgid "X-Axis" msgstr "" -#: apprise/plugins/dbus.py:219 apprise/plugins/glib.py:181 +#: apprise/plugins/dbus.py:222 apprise/plugins/glib.py:182 msgid "Y-Axis" msgstr "" @@ -534,21 +607,11 @@ msgstr "" #: apprise/plugins/discord.py:125 apprise/plugins/flock.py:106 #: apprise/plugins/fluxer.py:169 apprise/plugins/ryver.py:106 -#: apprise/plugins/slack.py:186 apprise/plugins/viber.py:95 +#: apprise/plugins/slack.py:208 apprise/plugins/viber.py:99 #: apprise/plugins/zulip.py:124 msgid "Bot Name" msgstr "" -#: apprise/plugins/discord.py:130 apprise/plugins/fluxer.py:174 -#: apprise/plugins/ifttt.py:103 -msgid "Webhook ID" -msgstr "" - -#: apprise/plugins/discord.py:136 apprise/plugins/fluxer.py:181 -#: apprise/plugins/google_chat.py:118 -msgid "Webhook Token" -msgstr "" - #: apprise/plugins/discord.py:149 apprise/plugins/fluxer.py:201 msgid "Text To Speech" msgstr "" @@ -563,7 +626,7 @@ msgid "Avatar URL" msgstr "" #: apprise/plugins/discord.py:163 apprise/plugins/fluxer.py:215 -#: apprise/plugins/pushover.py:229 +#: apprise/plugins/pushover.py:275 msgid "URL" msgstr "" @@ -591,52 +654,56 @@ msgstr "" msgid "Ping Users/Roles" msgstr "" -#: apprise/plugins/dot.py:127 +#: apprise/plugins/dot.py:142 msgid "Device Serial Number" msgstr "" -#: apprise/plugins/dot.py:133 +#: apprise/plugins/dot.py:155 msgid "API Mode" msgstr "" -#: apprise/plugins/dot.py:147 +#: apprise/plugins/dot.py:161 msgid "Refresh Now" msgstr "" -#: apprise/plugins/dot.py:153 +#: apprise/plugins/dot.py:167 msgid "Text Signature" msgstr "" -#: apprise/plugins/dot.py:157 +#: apprise/plugins/dot.py:171 msgid "Icon Base64 (Text API)" msgstr "" -#: apprise/plugins/dot.py:161 +#: apprise/plugins/dot.py:175 msgid "Image Base64 (Image API)" msgstr "" -#: apprise/plugins/dot.py:166 +#: apprise/plugins/dot.py:180 msgid "Link" msgstr "" -#: apprise/plugins/dot.py:170 +#: apprise/plugins/dot.py:184 msgid "Border" msgstr "" -#: apprise/plugins/dot.py:177 +#: apprise/plugins/dot.py:191 msgid "Dither Type" msgstr "" -#: apprise/plugins/dot.py:183 +#: apprise/plugins/dot.py:197 msgid "Dither Kernel" msgstr "" +#: apprise/plugins/dot.py:203 +msgid "Task Key" +msgstr "" + #: apprise/plugins/emby.py:112 msgid "Modal" msgstr "" #: apprise/plugins/enigma2.py:130 apprise/plugins/gotify.py:134 -#: apprise/plugins/mattermost.py:163 apprise/plugins/notica.py:145 +#: apprise/plugins/mattermost.py:167 apprise/plugins/notica.py:145 msgid "Path" msgstr "" @@ -644,11 +711,31 @@ msgstr "" msgid "Server Timeout" msgstr "" +#: apprise/plugins/evolution.py:122 +msgid "Instance Name" +msgstr "" + +#: apprise/plugins/exotel.py:160 apprise/plugins/sinch.py:106 +#: apprise/plugins/twilio.py:140 +msgid "Account SID" +msgstr "" + +#: apprise/plugins/exotel.py:172 +msgid "From Phone No / Sender ID" +msgstr "" + +#: apprise/plugins/exotel.py:233 apprise/plugins/jira.py:275 +#: apprise/plugins/mailgun.py:176 apprise/plugins/notificationapi.py:212 +#: apprise/plugins/opsgenie.py:273 apprise/plugins/pagerduty.py:176 +#: apprise/plugins/sparkpost.py:191 +msgid "Region Name" +msgstr "" + #: apprise/plugins/feishu.py:49 msgid "Feishu" msgstr "" -#: apprise/plugins/flock.py:99 apprise/plugins/twitter.py:150 +#: apprise/plugins/flock.py:99 apprise/plugins/twitter.py:175 msgid "Access Key" msgstr "" @@ -662,8 +749,10 @@ msgstr "" #: apprise/plugins/fcm/__init__.py:182 apprise/plugins/fcm/__init__.py:188 #: apprise/plugins/fluxer.py:195 apprise/plugins/lametric.py:510 -#: apprise/plugins/mattermost.py:223 apprise/plugins/notificationapi.py:209 -#: apprise/plugins/ntfy.py:296 apprise/plugins/vapid/__init__.py:169 +#: apprise/plugins/mattermost.py:224 apprise/plugins/notificationapi.py:218 +#: apprise/plugins/ntfy.py:296 apprise/plugins/office365.py:319 +#: apprise/plugins/sns.py:202 apprise/plugins/vapid/__init__.py:169 +#: apprise/plugins/webexteams.py:197 apprise/plugins/zoom.py:158 msgid "Mode" msgstr "" @@ -687,10 +776,14 @@ msgstr "" msgid "Free-Mobile" msgstr "" -#: apprise/plugins/glib.py:122 +#: apprise/plugins/glib.py:123 msgid "libdbus-1.so.x or libdbus-2.so.x must be installed." msgstr "" +#: apprise/plugins/glib.py:127 +msgid "GLib Notification" +msgstr "" + #: apprise/plugins/gnome.py:100 msgid "A local Gnome environment is required." msgstr "" @@ -711,8 +804,21 @@ msgstr "" msgid "Thread Key" msgstr "" +#: apprise/plugins/groupme.py:123 +msgid "Bot ID" +msgstr "" + +#: apprise/plugins/groupme.py:130 apprise/plugins/line.py:82 +#: apprise/plugins/mastodon.py:185 apprise/plugins/matrix/base.py:282 +#: apprise/plugins/misskey.py:122 apprise/plugins/pushbullet.py:83 +#: apprise/plugins/pushover.py:234 apprise/plugins/smseagle.py:146 +#: apprise/plugins/spugpush.py:68 apprise/plugins/streamlabs.py:105 +#: apprise/plugins/whatsapp.py:121 +msgid "Access Token" +msgstr "" + #: apprise/plugins/growl.py:169 apprise/plugins/mqtt.py:195 -#: apprise/plugins/msteams.py:206 apprise/plugins/nextcloud.py:163 +#: apprise/plugins/nextcloud.py:163 msgid "Version" msgstr "" @@ -720,14 +826,22 @@ msgstr "" msgid "Sticky" msgstr "" -#: apprise/plugins/home_assistant.py:99 +#: apprise/plugins/home_assistant.py:142 msgid "Long-Lived Access Token" msgstr "" -#: apprise/plugins/home_assistant.py:113 +#: apprise/plugins/home_assistant.py:165 msgid "Notification ID" msgstr "" +#: apprise/plugins/home_assistant.py:182 +msgid "Path Prefix" +msgstr "" + +#: apprise/plugins/humhub.py:127 +msgid "Token or Username" +msgstr "" + #: apprise/plugins/ifttt.py:109 msgid "Events" msgstr "" @@ -740,6 +854,55 @@ msgstr "" msgid "Remove Tokens" msgstr "" +#: apprise/plugins/jira.py:240 apprise/plugins/opsgenie.py:238 +msgid "Target Escalation" +msgstr "" + +#: apprise/plugins/jira.py:246 apprise/plugins/opsgenie.py:244 +msgid "Target Schedule" +msgstr "" + +#: apprise/plugins/irc/base.py:137 apprise/plugins/jira.py:252 +#: apprise/plugins/line.py:88 apprise/plugins/mastodon.py:196 +#: apprise/plugins/matrix/base.py:289 apprise/plugins/nextcloud.py:136 +#: apprise/plugins/one_signal.py:129 apprise/plugins/opsgenie.py:250 +#: apprise/plugins/pushed.py:95 apprise/plugins/rocketchat.py:156 +#: apprise/plugins/slack.py:261 apprise/plugins/twitter.py:187 +#: apprise/plugins/wechat.py:198 apprise/plugins/xmpp/base.py:123 +#: apprise/plugins/zulip.py:143 +msgid "Target User" +msgstr "" + +#: apprise/plugins/jira.py:258 apprise/plugins/opsgenie.py:256 +msgid "Target Team" +msgstr "" + +#: apprise/plugins/jira.py:264 apprise/plugins/opsgenie.py:262 +msgid "Targets " +msgstr "" + +#: apprise/plugins/jira.py:293 apprise/plugins/opsgenie.py:286 +msgid "Entity" +msgstr "" + +#: apprise/plugins/jira.py:297 apprise/plugins/opsgenie.py:290 +msgid "Alias" +msgstr "" + +#: apprise/plugins/jira.py:308 apprise/plugins/opsgenie.py:298 +#: apprise/plugins/pagertree.py:118 apprise/plugins/splunk.py:202 +msgid "Action" +msgstr "" + +#: apprise/plugins/jira.py:319 apprise/plugins/opsgenie.py:317 +msgid "Details" +msgstr "" + +#: apprise/plugins/jira.py:323 apprise/plugins/opsgenie.py:321 +#: apprise/plugins/splunk.py:213 +msgid "Action Mapping" +msgstr "" + #: apprise/plugins/join.py:147 msgid "Device ID" msgstr "" @@ -753,6 +916,11 @@ msgstr "" msgid "Source Phone No" msgstr "" +#: apprise/plugins/kodi.py:123 apprise/plugins/streamlabs.py:141 +#: apprise/plugins/windows.py:100 +msgid "Duration" +msgstr "" + #: apprise/plugins/kumulos.py:99 msgid "Server Key" msgstr "" @@ -795,23 +963,6 @@ msgstr "" msgid "Bot Token" msgstr "" -#: apprise/plugins/line.py:82 apprise/plugins/mastodon.py:173 -#: apprise/plugins/matrix.py:239 apprise/plugins/misskey.py:122 -#: apprise/plugins/pushbullet.py:83 apprise/plugins/pushover.py:194 -#: apprise/plugins/smseagle.py:146 apprise/plugins/spugpush.py:68 -#: apprise/plugins/streamlabs.py:105 apprise/plugins/whatsapp.py:99 -msgid "Access Token" -msgstr "" - -#: apprise/plugins/irc/base.py:137 apprise/plugins/line.py:88 -#: apprise/plugins/mastodon.py:184 apprise/plugins/matrix.py:244 -#: apprise/plugins/nextcloud.py:136 apprise/plugins/one_signal.py:129 -#: apprise/plugins/opsgenie.py:258 apprise/plugins/pushed.py:95 -#: apprise/plugins/rocketchat.py:155 apprise/plugins/slack.py:236 -#: apprise/plugins/twitter.py:162 apprise/plugins/zulip.py:143 -msgid "Target User" -msgstr "" - #: apprise/plugins/macosx.py:65 msgid "" "Only works with Mac OS X 10.8 and higher. Additionally requires that " @@ -826,105 +977,76 @@ msgstr "" msgid "Open/Click URL" msgstr "" -#: apprise/plugins/email/base.py:124 apprise/plugins/mailgun.py:141 -#: apprise/plugins/sendpulse.py:112 apprise/plugins/smtp2go.py:113 +#: apprise/plugins/email/base.py:148 apprise/plugins/mailgun.py:141 +#: apprise/plugins/sendpulse.py:115 apprise/plugins/smtp2go.py:113 #: apprise/plugins/sparkpost.py:164 msgid "Domain" msgstr "" -#: apprise/plugins/email/base.py:160 apprise/plugins/mailgun.py:168 -#: apprise/plugins/resend.py:154 apprise/plugins/ses.py:201 -#: apprise/plugins/smtp2go.py:135 apprise/plugins/sparkpost.py:186 +#: apprise/plugins/email/base.py:179 apprise/plugins/mailgun.py:168 +#: apprise/plugins/postmark.py:144 apprise/plugins/resend.py:140 +#: apprise/plugins/ses.py:210 apprise/plugins/smtp2go.py:135 +#: apprise/plugins/sparkpost.py:186 msgid "From Name" msgstr "" -#: apprise/plugins/mailgun.py:176 apprise/plugins/notificationapi.py:203 -#: apprise/plugins/opsgenie.py:281 apprise/plugins/pagerduty.py:176 -#: apprise/plugins/sparkpost.py:191 -msgid "Region Name" -msgstr "" - -#: apprise/plugins/email/base.py:209 apprise/plugins/mailgun.py:204 +#: apprise/plugins/email/base.py:253 apprise/plugins/mailgun.py:204 #: apprise/plugins/smtp2go.py:161 apprise/plugins/sparkpost.py:219 msgid "Email Header" msgstr "" -#: apprise/plugins/mailgun.py:208 apprise/plugins/msteams.py:222 -#: apprise/plugins/notificationapi.py:246 apprise/plugins/sparkpost.py:223 -#: apprise/plugins/workflows.py:201 +#: apprise/plugins/mailgun.py:208 apprise/plugins/notificationapi.py:256 +#: apprise/plugins/slack.py:340 apprise/plugins/sparkpost.py:223 +#: apprise/plugins/workflows.py:202 msgid "Template Tokens" msgstr "" -#: apprise/plugins/mastodon.py:204 apprise/plugins/misskey.py:143 +#: apprise/plugins/mastodon.py:216 apprise/plugins/misskey.py:143 msgid "Visibility" msgstr "" -#: apprise/plugins/mastodon.py:210 apprise/plugins/twitter.py:185 -msgid "Cache Results" -msgstr "" - -#: apprise/plugins/mastodon.py:220 -msgid "Sensitive Attachments" -msgstr "" - -#: apprise/plugins/mastodon.py:225 +#: apprise/plugins/mastodon.py:222 msgid "Spoiler Text" msgstr "" -#: apprise/plugins/mastodon.py:229 +#: apprise/plugins/mastodon.py:226 msgid "Idempotency-Key" msgstr "" -#: apprise/plugins/mastodon.py:233 +#: apprise/plugins/mastodon.py:230 msgid "Language Code" msgstr "" -#: apprise/plugins/matrix.py:250 apprise/plugins/rocketchat.py:161 -msgid "Target Room ID" +#: apprise/plugins/mastodon.py:234 apprise/plugins/twitter.py:210 +msgid "Cache Results" msgstr "" -#: apprise/plugins/matrix.py:256 -msgid "Target Room Alias" +#: apprise/plugins/mastodon.py:239 +msgid "Sensitive Attachments" msgstr "" -#: apprise/plugins/matrix.py:279 -msgid "Server Discovery" +#: apprise/plugins/mastodon.py:255 +msgid "Ping Users/Tags" msgstr "" -#: apprise/plugins/matrix.py:284 -msgid "Force Home Server on Room IDs" -msgstr "" - -#: apprise/plugins/matrix.py:289 apprise/plugins/rocketchat.py:177 -#: apprise/plugins/ryver.py:118 -msgid "Webhook Mode" -msgstr "" - -#: apprise/plugins/matrix.py:295 -msgid "Matrix API Verion" -msgstr "" - -#: apprise/plugins/matrix.py:301 apprise/plugins/notificationapi.py:154 -msgid "Message Type" -msgstr "" - -#: apprise/plugins/irc/base.py:128 apprise/plugins/mattermost.py:147 -#: apprise/plugins/xmpp/base.py:107 +#: apprise/plugins/irc/base.py:128 apprise/plugins/mattermost.py:151 +#: apprise/plugins/xmpp/base.py:112 msgid "User" msgstr "" -#: apprise/plugins/irc/base.py:143 apprise/plugins/mattermost.py:173 +#: apprise/plugins/irc/base.py:143 apprise/plugins/mattermost.py:177 #: apprise/plugins/notifiarr.py:97 apprise/plugins/pushbullet.py:94 -#: apprise/plugins/pushed.py:101 apprise/plugins/rocketchat.py:149 -#: apprise/plugins/slack.py:242 apprise/plugins/twist.py:112 +#: apprise/plugins/pushed.py:101 apprise/plugins/rocketchat.py:150 +#: apprise/plugins/slack.py:267 apprise/plugins/twist.py:112 +#: apprise/plugins/xmpp/base.py:128 msgid "Target Channel" msgstr "" -#: apprise/plugins/mattermost.py:179 apprise/plugins/twist.py:118 +#: apprise/plugins/mattermost.py:183 apprise/plugins/twist.py:118 msgid "Target Channel ID" msgstr "" -#: apprise/plugins/mqtt.py:169 +#: apprise/plugins/mqtt.py:171 msgid "Target Queue" msgstr "" @@ -932,8 +1054,9 @@ msgstr "" msgid "QOS" msgstr "" -#: apprise/plugins/mqtt.py:201 apprise/plugins/notificationapi.py:161 -#: apprise/plugins/office365.py:130 apprise/plugins/sendpulse.py:117 +#: apprise/plugins/mqtt.py:201 apprise/plugins/notificationapi.py:166 +#: apprise/plugins/office365.py:269 apprise/plugins/ringcentral.py:192 +#: apprise/plugins/sendpulse.py:120 msgid "Client ID" msgstr "" @@ -945,7 +1068,7 @@ msgstr "" msgid "Retain Messages" msgstr "" -#: apprise/plugins/msg91.py:102 apprise/plugins/sendpulse.py:161 +#: apprise/plugins/msg91.py:102 apprise/plugins/sendpulse.py:156 msgid "Template ID" msgstr "" @@ -957,34 +1080,10 @@ msgstr "" msgid "Short URL" msgstr "" -#: apprise/plugins/msg91.py:148 apprise/plugins/whatsapp.py:168 +#: apprise/plugins/msg91.py:148 apprise/plugins/whatsapp.py:203 msgid "Template Mapping" msgstr "" -#: apprise/plugins/msteams.py:151 -msgid "Team Name" -msgstr "" - -#: apprise/plugins/msteams.py:159 apprise/plugins/slack.py:203 -msgid "Token A" -msgstr "" - -#: apprise/plugins/msteams.py:168 apprise/plugins/slack.py:211 -msgid "Token B" -msgstr "" - -#: apprise/plugins/msteams.py:177 apprise/plugins/slack.py:219 -msgid "Token C" -msgstr "" - -#: apprise/plugins/msteams.py:186 -msgid "Token D" -msgstr "" - -#: apprise/plugins/msteams.py:212 apprise/plugins/workflows.py:180 -msgid "Template Path" -msgstr "" - #: apprise/plugins/nextcloud.py:169 apprise/plugins/nextcloudtalk.py:113 msgid "URL Prefix" msgstr "" @@ -1005,46 +1104,50 @@ msgstr "" msgid "Source" msgstr "" -#: apprise/plugins/notificationapi.py:166 apprise/plugins/office365.py:137 -#: apprise/plugins/sendpulse.py:124 +#: apprise/plugins/matrix/base.py:352 apprise/plugins/notificationapi.py:159 +msgid "Message Type" +msgstr "" + +#: apprise/plugins/notificationapi.py:171 apprise/plugins/office365.py:276 +#: apprise/plugins/ringcentral.py:199 apprise/plugins/sendpulse.py:127 msgid "Client Secret" msgstr "" -#: apprise/plugins/notificationapi.py:177 +#: apprise/plugins/notificationapi.py:182 msgid "Target ID" msgstr "" -#: apprise/plugins/notificationapi.py:182 +#: apprise/plugins/notificationapi.py:187 msgid "Target SMS" msgstr "" -#: apprise/plugins/notificationapi.py:198 +#: apprise/plugins/notificationapi.py:207 msgid "Channels" msgstr "" -#: apprise/plugins/email/base.py:185 apprise/plugins/notificationapi.py:226 -#: apprise/plugins/resend.py:146 +#: apprise/plugins/email/base.py:196 apprise/plugins/notificationapi.py:223 +#: apprise/plugins/office365.py:315 apprise/plugins/resend.py:150 msgid "Reply To" msgstr "" -#: apprise/plugins/email/base.py:155 apprise/plugins/notificationapi.py:231 -#: apprise/plugins/sendpulse.py:147 apprise/plugins/ses.py:154 +#: apprise/plugins/email/base.py:174 apprise/plugins/notificationapi.py:228 +#: apprise/plugins/sendpulse.py:150 apprise/plugins/ses.py:163 msgid "From Email" msgstr "" -#: apprise/plugins/fcm/__init__.py:153 apprise/plugins/notifico.py:124 +#: apprise/plugins/fcm/__init__.py:153 apprise/plugins/notifico.py:187 msgid "Project ID" msgstr "" -#: apprise/plugins/notifico.py:133 +#: apprise/plugins/notifico.py:196 msgid "Message Hook" msgstr "" -#: apprise/plugins/notifico.py:148 +#: apprise/plugins/notifico.py:211 msgid "IRC Colors" msgstr "" -#: apprise/plugins/notifico.py:154 +#: apprise/plugins/notifico.py:217 msgid "Prefix" msgstr "" @@ -1076,15 +1179,39 @@ msgstr "" msgid "Authentication Type" msgstr "" -#: apprise/plugins/office365.py:118 +#: apprise/plugins/octopush.py:130 +msgid "API Login" +msgstr "" + +#: apprise/plugins/octopush.py:142 +msgid "Sender" +msgstr "" + +#: apprise/plugins/octopush.py:178 +msgid "Accept Replies" +msgstr "" + +#: apprise/plugins/octopush.py:183 +msgid "Purpose" +msgstr "" + +#: apprise/plugins/octopush.py:189 +msgid "Type" +msgstr "" + +#: apprise/plugins/office365.py:257 msgid "Tenant Domain" msgstr "" -#: apprise/plugins/office365.py:125 +#: apprise/plugins/office365.py:264 msgid "Account Email or Object ID" msgstr "" -#: apprise/plugins/one_signal.py:108 apprise/plugins/sendgrid.py:157 +#: apprise/plugins/office365.py:325 +msgid "Save to Sent Items" +msgstr "" + +#: apprise/plugins/one_signal.py:108 apprise/plugins/sendgrid.py:146 msgid "Template" msgstr "" @@ -1096,23 +1223,23 @@ msgstr "" msgid "Include Segment" msgstr "" -#: apprise/plugins/one_signal.py:166 -msgid "Enable Contents" -msgstr "" - -#: apprise/plugins/one_signal.py:172 -msgid "Decode Template Args" -msgstr "" - -#: apprise/plugins/one_signal.py:181 +#: apprise/plugins/one_signal.py:155 msgid "Subtitle" msgstr "" -#: apprise/plugins/one_signal.py:185 apprise/plugins/sfr.py:125 -#: apprise/plugins/whatsapp.py:130 +#: apprise/plugins/one_signal.py:159 apprise/plugins/sfr.py:125 +#: apprise/plugins/whatsapp.py:141 msgid "Language" msgstr "" +#: apprise/plugins/one_signal.py:170 +msgid "Enable Contents" +msgstr "" + +#: apprise/plugins/one_signal.py:176 +msgid "Decode Template Args" +msgstr "" + #: apprise/plugins/one_signal.py:195 msgid "Custom Data" msgstr "" @@ -1121,43 +1248,6 @@ msgstr "" msgid "Postback Data" msgstr "" -#: apprise/plugins/opsgenie.py:246 -msgid "Target Escalation" -msgstr "" - -#: apprise/plugins/opsgenie.py:252 -msgid "Target Schedule" -msgstr "" - -#: apprise/plugins/opsgenie.py:264 -msgid "Target Team" -msgstr "" - -#: apprise/plugins/opsgenie.py:270 -msgid "Targets " -msgstr "" - -#: apprise/plugins/opsgenie.py:299 -msgid "Entity" -msgstr "" - -#: apprise/plugins/opsgenie.py:303 -msgid "Alias" -msgstr "" - -#: apprise/plugins/opsgenie.py:314 apprise/plugins/pagertree.py:118 -#: apprise/plugins/splunk.py:202 -msgid "Action" -msgstr "" - -#: apprise/plugins/opsgenie.py:325 -msgid "Details" -msgstr "" - -#: apprise/plugins/opsgenie.py:329 apprise/plugins/splunk.py:213 -msgid "Action Mapping" -msgstr "" - #: apprise/plugins/pagerduty.py:138 apprise/plugins/spike.py:68 msgid "Integration Key" msgstr "" @@ -1227,35 +1317,55 @@ msgstr "" msgid "Secret Key" msgstr "" -#: apprise/plugins/pushme.py:81 apprise/plugins/signal_api.py:160 -#: apprise/plugins/smseagle.py:195 +#: apprise/plugins/pushme.py:81 apprise/plugins/signal_api.py:152 +#: apprise/plugins/smseagle.py:193 msgid "Show Status" msgstr "" -#: apprise/plugins/pushover.py:188 +#: apprise/plugins/pushover.py:228 msgid "User Key" msgstr "" -#: apprise/plugins/pushover.py:234 +#: apprise/plugins/pushover.py:280 msgid "URL Title" msgstr "" -#: apprise/plugins/pushover.py:239 -msgid "Retry" +#: apprise/plugins/pushover.py:285 +msgid "Emergency Retry Interval" msgstr "" -#: apprise/plugins/pushover.py:245 +#: apprise/plugins/pushover.py:291 msgid "Expire" msgstr "" -#: apprise/plugins/pushplus.py:48 +#: apprise/plugins/pushover.py:301 +msgid "Encryption Key" +msgstr "" + +#: apprise/plugins/pushover.py:308 +msgid "E2EE" +msgstr "" + +#: apprise/plugins/pushplus.py:172 msgid "Pushplus" msgstr "" -#: apprise/plugins/pushplus.py:68 apprise/plugins/qq.py:66 +#: apprise/plugins/pushplus.py:210 apprise/plugins/qq.py:66 msgid "User Token" msgstr "" +#: apprise/plugins/pushplus.py:219 +msgid "Group Topics" +msgstr "" + +#: apprise/plugins/pushplus.py:240 +msgid "Channel" +msgstr "" + +#: apprise/plugins/pushplus.py:257 +msgid "Webhook Name" +msgstr "" + #: apprise/plugins/pushsafer.py:360 msgid "Private Key" msgstr "" @@ -1269,7 +1379,7 @@ msgid "Secret API Key" msgstr "" #: apprise/plugins/fcm/__init__.py:162 apprise/plugins/pushy.py:91 -#: apprise/plugins/sns.py:131 apprise/plugins/wxpusher.py:128 +#: apprise/plugins/sns.py:163 apprise/plugins/wxpusher.py:128 msgid "Target Topic" msgstr "" @@ -1285,51 +1395,76 @@ msgstr "" msgid "Target Subreddit" msgstr "" -#: apprise/plugins/reddit.py:185 +#: apprise/plugins/reddit.py:182 msgid "Kind" msgstr "" -#: apprise/plugins/reddit.py:191 +#: apprise/plugins/reddit.py:188 msgid "Flair ID" msgstr "" -#: apprise/plugins/reddit.py:196 +#: apprise/plugins/reddit.py:193 msgid "Flair Text" msgstr "" -#: apprise/plugins/reddit.py:201 +#: apprise/plugins/reddit.py:198 msgid "NSFW" msgstr "" -#: apprise/plugins/reddit.py:207 +#: apprise/plugins/reddit.py:204 msgid "Is Ad?" msgstr "" -#: apprise/plugins/reddit.py:213 +#: apprise/plugins/reddit.py:210 msgid "Send Replies" msgstr "" -#: apprise/plugins/reddit.py:219 +#: apprise/plugins/reddit.py:216 msgid "Is Spoiler" msgstr "" -#: apprise/plugins/reddit.py:225 +#: apprise/plugins/reddit.py:222 msgid "Resubmit Flag" msgstr "" +#: apprise/plugins/resend.py:135 +msgid "From Address" +msgstr "" + #: apprise/plugins/revolt.py:104 msgid "Channel ID" msgstr "" -#: apprise/plugins/revolt.py:130 +#: apprise/plugins/revolt.py:131 msgid "Embed URL" msgstr "" -#: apprise/plugins/rocketchat.py:145 +#: apprise/plugins/ringcentral.py:186 +msgid "Token / Password" +msgstr "" + +#: apprise/plugins/ringcentral.py:239 +msgid "Environment" +msgstr "" + +#: apprise/plugins/ringcentral.py:252 +msgid "Authentication Mode" +msgstr "" + +#: apprise/plugins/rocketchat.py:146 msgid "Webhook" msgstr "" -#: apprise/plugins/rocketchat.py:182 +#: apprise/plugins/matrix/base.py:295 apprise/plugins/rocketchat.py:162 +msgid "Target Room ID" +msgstr "" + +#: apprise/plugins/matrix/base.py:334 apprise/plugins/rocketchat.py:178 +#: apprise/plugins/ryver.py:118 +msgid "Webhook Mode" +msgstr "" + +#: apprise/plugins/rocketchat.py:183 msgid "Use Avatar" msgstr "" @@ -1345,33 +1480,37 @@ msgstr "" msgid "Organization" msgstr "" -#: apprise/plugins/sendgrid.py:166 apprise/plugins/sendpulse.py:175 +#: apprise/plugins/sendgrid.py:166 apprise/plugins/sendpulse.py:182 msgid "Template Data" msgstr "" -#: apprise/plugins/ses.py:160 apprise/plugins/sns.py:106 +#: apprise/plugins/ses.py:157 apprise/plugins/sns.py:132 +msgid "Session Token" +msgstr "" + +#: apprise/plugins/ses.py:169 apprise/plugins/sns.py:138 msgid "Access Key ID" msgstr "" -#: apprise/plugins/ses.py:166 apprise/plugins/sns.py:112 +#: apprise/plugins/ses.py:175 apprise/plugins/sns.py:144 msgid "Secret Access Key" msgstr "" -#: apprise/plugins/ses.py:172 apprise/plugins/sinch.py:160 -#: apprise/plugins/sns.py:118 +#: apprise/plugins/ses.py:181 apprise/plugins/sinch.py:157 +#: apprise/plugins/sns.py:150 msgid "Region" msgstr "" -#: apprise/plugins/ses.py:179 apprise/plugins/smtp2go.py:124 +#: apprise/plugins/ses.py:188 apprise/plugins/smtp2go.py:124 #: apprise/plugins/sparkpost.py:175 msgid "Target Emails" msgstr "" -#: apprise/plugins/seven.py:115 apprise/plugins/smseagle.py:205 +#: apprise/plugins/seven.py:113 apprise/plugins/smseagle.py:203 msgid "Flash" msgstr "" -#: apprise/plugins/seven.py:119 +#: apprise/plugins/seven.py:117 msgid "Label" msgstr "" @@ -1412,30 +1551,31 @@ msgid "TTS Voice" msgstr "" #: apprise/plugins/signal_api.py:131 apprise/plugins/smseagle.py:158 +#: apprise/plugins/whatsapp.py:159 msgid "Target Group ID" msgstr "" -#: apprise/plugins/signl4.py:86 +#: apprise/plugins/signl4.py:89 msgid "Service" msgstr "" -#: apprise/plugins/signl4.py:90 +#: apprise/plugins/signl4.py:93 msgid "Location" msgstr "" -#: apprise/plugins/signl4.py:94 +#: apprise/plugins/signl4.py:97 msgid "Alerting Scenario" msgstr "" -#: apprise/plugins/signl4.py:98 +#: apprise/plugins/signl4.py:101 msgid "Filtering" msgstr "" -#: apprise/plugins/signl4.py:103 +#: apprise/plugins/signl4.py:106 msgid "External ID" msgstr "" -#: apprise/plugins/signl4.py:107 +#: apprise/plugins/signl4.py:110 msgid "Status" msgstr "" @@ -1447,38 +1587,54 @@ msgstr "" msgid "Event" msgstr "" -#: apprise/plugins/sinch.py:106 apprise/plugins/twilio.py:140 -msgid "Account SID" -msgstr "" - #: apprise/plugins/sinch.py:134 apprise/plugins/twilio.py:168 msgid "Target Short Code" msgstr "" -#: apprise/plugins/slack.py:194 +#: apprise/plugins/slack.py:216 msgid "OAuth Access Token" msgstr "" #: apprise/plugins/slack.py:225 +msgid "Token A" +msgstr "" + +#: apprise/plugins/slack.py:234 +msgid "Token B" +msgstr "" + +#: apprise/plugins/slack.py:243 +msgid "Token C" +msgstr "" + +#: apprise/plugins/slack.py:250 msgid "Target Encoded ID" msgstr "" -#: apprise/plugins/slack.py:265 +#: apprise/plugins/slack.py:279 +msgid "Workflow Path" +msgstr "" + +#: apprise/plugins/slack.py:297 msgid "Include Footer" msgstr "" -#: apprise/plugins/slack.py:273 +#: apprise/plugins/slack.py:305 msgid "Use Blocks" msgstr "" -#: apprise/plugins/slack.py:282 +#: apprise/plugins/slack.py:311 msgid "Include Timestamp" msgstr "" -#: apprise/plugins/slack.py:288 apprise/plugins/twitter.py:179 +#: apprise/plugins/slack.py:317 apprise/plugins/twitter.py:204 msgid "Message Mode" msgstr "" +#: apprise/plugins/slack.py:330 apprise/plugins/workflows.py:181 +msgid "Template Path" +msgstr "" + #: apprise/plugins/smpp.py:61 msgid "SMPP" msgstr "" @@ -1491,14 +1647,26 @@ msgstr "" msgid "Target Contact" msgstr "" -#: apprise/plugins/smseagle.py:200 +#: apprise/plugins/smseagle.py:198 msgid "Test Only" msgstr "" -#: apprise/plugins/smsmanager.py:146 +#: apprise/plugins/smsmanager.py:143 msgid "Gateway" msgstr "" +#: apprise/plugins/sogs.py:219 +msgid "Public Key" +msgstr "" + +#: apprise/plugins/sogs.py:226 +msgid "Seed" +msgstr "" + +#: apprise/plugins/sogs.py:233 +msgid "Rooms" +msgstr "" + #: apprise/plugins/spike.py:48 msgid "Spike.sh" msgstr "" @@ -1531,11 +1699,6 @@ msgstr "" msgid "Sound Link" msgstr "" -#: apprise/plugins/streamlabs.py:141 apprise/plugins/windows.py:100 -#: apprise/plugins/xbmc.py:123 -msgid "Duration" -msgstr "" - #: apprise/plugins/streamlabs.py:147 msgid "Special Text Color" msgstr "" @@ -1600,19 +1763,19 @@ msgstr "" msgid "Target Threema ID" msgstr "" -#: apprise/plugins/twilio.py:203 +#: apprise/plugins/twilio.py:200 msgid "Notification Method: sms or call" msgstr "" -#: apprise/plugins/twitter.py:138 +#: apprise/plugins/twitter.py:163 msgid "Consumer Key" msgstr "" -#: apprise/plugins/twitter.py:144 +#: apprise/plugins/twitter.py:169 msgid "Consumer Secret" msgstr "" -#: apprise/plugins/twitter.py:156 +#: apprise/plugins/twitter.py:181 msgid "Access Secret" msgstr "" @@ -1628,7 +1791,7 @@ msgstr "" msgid "Receiver IDs" msgstr "" -#: apprise/plugins/viber.py:101 +#: apprise/plugins/viber.py:105 msgid "Bot Avatar URL" msgstr "" @@ -1636,19 +1799,47 @@ msgstr "" msgid "User Email" msgstr "" -#: apprise/plugins/vapid/__init__.py:179 apprise/plugins/vonage.py:136 +#: apprise/plugins/vapid/__init__.py:179 apprise/plugins/vonage.py:133 msgid "ttl" msgstr "" +#: apprise/plugins/webexteams.py:178 +msgid "Bot Access Token" +msgstr "" + +#: apprise/plugins/webexteams.py:184 +msgid "Room IDs" +msgstr "" + +#: apprise/plugins/wechat.py:174 +msgid "Corp ID" +msgstr "" + +#: apprise/plugins/wechat.py:181 +msgid "App Secret" +msgstr "" + +#: apprise/plugins/wechat.py:189 +msgid "Agent ID" +msgstr "" + +#: apprise/plugins/wechat.py:205 +msgid "Target Department" +msgstr "" + +#: apprise/plugins/wechat.py:212 +msgid "Target Tag" +msgstr "" + #: apprise/plugins/wecombot.py:99 msgid "Bot Webhook Key" msgstr "" -#: apprise/plugins/whatsapp.py:106 +#: apprise/plugins/whatsapp.py:128 msgid "Template Name" msgstr "" -#: apprise/plugins/whatsapp.py:112 +#: apprise/plugins/whatsapp.py:134 msgid "From Phone ID" msgstr "" @@ -1656,23 +1847,23 @@ msgstr "" msgid "A local Microsoft Windows environment is required." msgstr "" -#: apprise/plugins/workflows.py:137 +#: apprise/plugins/workflows.py:138 msgid "Workflow ID" msgstr "" -#: apprise/plugins/workflows.py:145 +#: apprise/plugins/workflows.py:146 msgid "Signature" msgstr "" -#: apprise/plugins/workflows.py:168 +#: apprise/plugins/workflows.py:169 msgid "Use Power Automate URL" msgstr "" -#: apprise/plugins/workflows.py:175 +#: apprise/plugins/workflows.py:176 msgid "Wrap Text" msgstr "" -#: apprise/plugins/workflows.py:190 +#: apprise/plugins/workflows.py:191 msgid "API Version" msgstr "" @@ -1684,30 +1875,42 @@ msgstr "" msgid "Target User ID" msgstr "" +#: apprise/plugins/zoom.py:145 +msgid "Verification Token" +msgstr "" + #: apprise/plugins/zulip.py:148 msgid "Target Stream" msgstr "" -#: apprise/plugins/email/base.py:150 -msgid "To Email" -msgstr "" - -#: apprise/plugins/email/base.py:173 +#: apprise/plugins/email/base.py:184 msgid "SMTP Server" msgstr "" -#: apprise/plugins/email/base.py:178 apprise/plugins/xmpp/base.py:129 +#: apprise/plugins/email/base.py:189 apprise/plugins/xmpp/base.py:149 msgid "Secure Mode" msgstr "" -#: apprise/plugins/email/base.py:190 -msgid "PGP Encryption" +#: apprise/plugins/email/base.py:201 +msgid "PGP Security Mode" msgstr "" -#: apprise/plugins/email/base.py:196 +#: apprise/plugins/email/base.py:208 msgid "PGP Public Key Path" msgstr "" +#: apprise/plugins/email/base.py:221 +msgid "PGP Private Key Path" +msgstr "" + +#: apprise/plugins/email/base.py:229 +msgid "Web Key Directory" +msgstr "" + +#: apprise/plugins/email/base.py:235 +msgid "To Email" +msgstr "" + #: apprise/plugins/fcm/__init__.py:148 msgid "OAuth2 KeyFile" msgstr "" @@ -1740,6 +1943,30 @@ msgstr "" msgid "Auth Mode" msgstr "" +#: apprise/plugins/matrix/base.py:301 +msgid "Target Room Alias" +msgstr "" + +#: apprise/plugins/matrix/base.py:324 +msgid "Server Discovery" +msgstr "" + +#: apprise/plugins/matrix/base.py:329 +msgid "Force Home Server on Room IDs" +msgstr "" + +#: apprise/plugins/matrix/base.py:340 +msgid "Webhook Path" +msgstr "" + +#: apprise/plugins/matrix/base.py:346 +msgid "Matrix API Verion" +msgstr "" + +#: apprise/plugins/matrix/base.py:358 +msgid "End-to-End Encryption" +msgstr "" + #: apprise/plugins/vapid/__init__.py:193 msgid "PEM Private KeyFile" msgstr "" @@ -1748,15 +1975,27 @@ msgstr "" msgid "Subscripion File" msgstr "" -#: apprise/plugins/xmpp/base.py:136 +#: apprise/plugins/xmpp/base.py:144 +msgid "XMPP Server" +msgstr "" + +#: apprise/plugins/xmpp/base.py:156 msgid "Get Roster" msgstr "" -#: apprise/plugins/xmpp/base.py:141 +#: apprise/plugins/xmpp/base.py:161 msgid "Use Subject" msgstr "" -#: apprise/plugins/xmpp/base.py:146 +#: apprise/plugins/xmpp/base.py:166 msgid "Keep Connection Alive" msgstr "" +#: apprise/plugins/xmpp/base.py:172 +msgid "MUC Nickname" +msgstr "" + +#: apprise/plugins/xmpp/base.py:176 +msgid "SCRAM-PLUS Channel Binding" +msgstr "" + diff --git a/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo b/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo index 01f1f728d..4a0716f1d 100644 Binary files a/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo and b/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo differ diff --git a/libs/apprise/i18n/en/LC_MESSAGES/apprise.po b/libs/apprise/i18n/en/LC_MESSAGES/apprise.po index 22a192335..6abadfd9e 100644 --- a/libs/apprise/i18n/en/LC_MESSAGES/apprise.po +++ b/libs/apprise/i18n/en/LC_MESSAGES/apprise.po @@ -5,9 +5,9 @@ # msgid "" msgstr "" -"Project-Id-Version: apprise 1.9.8\n" +"Project-Id-Version: apprise 1.11.0\n" "Report-Msgid-Bugs-To: lead2gold@gmail.com\n" -"POT-Creation-Date: 2026-03-08 16:43-0400\n" +"POT-Creation-Date: 2026-05-29 13:49-0400\n" "PO-Revision-Date: 2019-05-24 20:00-0400\n" "Last-Translator: Chris Caron \n" "Language: en\n" @@ -18,16 +18,20 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: apprise/attachment/base.py:96 apprise/url.py:141 +#: apprise/attachment/base.py:96 apprise/url.py:148 msgid "Verify SSL" msgstr "Verify SSL" -#: apprise/url.py:151 +#: apprise/url.py:158 +msgid "Follow Redirects" +msgstr "" + +#: apprise/url.py:168 #, fuzzy msgid "Socket Read Timeout" msgstr "Server Timeout" -#: apprise/url.py:165 +#: apprise/url.py:182 #, fuzzy msgid "Socket Connect Timeout" msgstr "Server Timeout" @@ -56,19 +60,19 @@ msgstr "" msgid "Memory" msgstr "" -#: apprise/plugins/__init__.py:280 +#: apprise/plugins/__init__.py:278 msgid "Schema" msgstr "Schema" -#: apprise/plugins/__init__.py:401 +#: apprise/plugins/__init__.py:398 msgid "No dependencies." msgstr "" -#: apprise/plugins/__init__.py:404 +#: apprise/plugins/__init__.py:401 msgid "Packages are required to function." msgstr "" -#: apprise/plugins/__init__.py:408 +#: apprise/plugins/__init__.py:405 msgid "Packages are recommended to improve functionality." msgstr "" @@ -77,20 +81,23 @@ msgstr "" msgid "App User Name" msgstr "User Name" -#: apprise/plugins/africas_talking.py:138 apprise/plugins/brevo.py:112 +#: apprise/plugins/africas_talking.py:138 apprise/plugins/brevo.py:157 #: apprise/plugins/burstsms.py:104 apprise/plugins/clicksend.py:98 -#: apprise/plugins/dot.py:121 apprise/plugins/fcm/__init__.py:143 -#: apprise/plugins/httpsms.py:77 apprise/plugins/join.py:140 -#: apprise/plugins/kavenegar.py:115 apprise/plugins/kumulos.py:87 -#: apprise/plugins/mailgun.py:146 apprise/plugins/messagebird.py:78 -#: apprise/plugins/one_signal.py:113 apprise/plugins/opsgenie.py:236 +#: apprise/plugins/dot.py:136 apprise/plugins/evolution.py:105 +#: apprise/plugins/exotel.py:213 apprise/plugins/fcm/__init__.py:143 +#: apprise/plugins/httpsms.py:77 apprise/plugins/jira.py:230 +#: apprise/plugins/join.py:140 apprise/plugins/kavenegar.py:115 +#: apprise/plugins/kumulos.py:87 apprise/plugins/mailgun.py:146 +#: apprise/plugins/messagebird.py:78 apprise/plugins/octopush.py:136 +#: apprise/plugins/one_signal.py:113 apprise/plugins/opsgenie.py:228 #: apprise/plugins/pagerduty.py:131 apprise/plugins/popcorn_notify.py:71 -#: apprise/plugins/prowl.py:121 apprise/plugins/resend.py:107 +#: apprise/plugins/postmark.py:117 apprise/plugins/prowl.py:121 +#: apprise/plugins/resend.py:107 apprise/plugins/resend.py:145 #: apprise/plugins/sendgrid.py:116 apprise/plugins/seven.py:75 #: apprise/plugins/simplepush.py:101 apprise/plugins/smsmanager.py:106 #: apprise/plugins/smtp2go.py:118 apprise/plugins/sparkpost.py:169 #: apprise/plugins/splunk.py:165 apprise/plugins/techuluspush.py:97 -#: apprise/plugins/twilio.py:197 apprise/plugins/vapid/__init__.py:152 +#: apprise/plugins/twilio.py:194 apprise/plugins/vapid/__init__.py:152 #: apprise/plugins/vonage.py:80 msgid "API Key" msgstr "API Key" @@ -101,156 +108,169 @@ msgid "Target Phone" msgstr "Target Phone No" #: apprise/plugins/africas_talking.py:150 apprise/plugins/aprs.py:187 -#: apprise/plugins/bark.py:159 apprise/plugins/brevo.py:129 +#: apprise/plugins/bark.py:159 apprise/plugins/brevo.py:174 #: apprise/plugins/bulksms.py:137 apprise/plugins/bulkvs.py:110 #: apprise/plugins/burstsms.py:131 apprise/plugins/clickatell.py:90 #: apprise/plugins/clicksend.py:112 apprise/plugins/d7networks.py:110 #: apprise/plugins/dapnet.py:138 apprise/plugins/dingtalk.py:111 -#: apprise/plugins/email/base.py:140 apprise/plugins/fcm/__init__.py:168 +#: apprise/plugins/email/base.py:164 apprise/plugins/evolution.py:134 +#: apprise/plugins/exotel.py:190 apprise/plugins/fcm/__init__.py:168 #: apprise/plugins/flock.py:125 apprise/plugins/fortysixelks.py:111 -#: apprise/plugins/httpsms.py:97 apprise/plugins/irc/base.py:149 +#: apprise/plugins/home_assistant.py:153 apprise/plugins/httpsms.py:97 +#: apprise/plugins/humhub.py:149 apprise/plugins/irc/base.py:149 #: apprise/plugins/join.py:172 apprise/plugins/kavenegar.py:135 #: apprise/plugins/line.py:93 apprise/plugins/mailgun.py:157 -#: apprise/plugins/mastodon.py:190 apprise/plugins/matrix.py:262 -#: apprise/plugins/mattermost.py:185 apprise/plugins/messagebird.py:99 -#: apprise/plugins/mqtt.py:174 apprise/plugins/msg91.py:123 +#: apprise/plugins/mastodon.py:202 apprise/plugins/matrix/base.py:307 +#: apprise/plugins/mattermost.py:189 apprise/plugins/messagebird.py:99 +#: apprise/plugins/mqtt.py:177 apprise/plugins/msg91.py:123 #: apprise/plugins/nextcloud.py:148 apprise/plugins/nextcloudtalk.py:101 -#: apprise/plugins/notifiarr.py:103 apprise/plugins/notificationapi.py:187 -#: apprise/plugins/ntfy.py:241 apprise/plugins/office365.py:148 -#: apprise/plugins/one_signal.py:141 apprise/plugins/plivo.py:114 -#: apprise/plugins/popcorn_notify.py:89 apprise/plugins/pushbullet.py:105 -#: apprise/plugins/pushed.py:107 apprise/plugins/pushover.py:206 +#: apprise/plugins/notifiarr.py:103 apprise/plugins/notificationapi.py:192 +#: apprise/plugins/ntfy.py:241 apprise/plugins/octopush.py:153 +#: apprise/plugins/office365.py:287 apprise/plugins/one_signal.py:141 +#: apprise/plugins/plivo.py:114 apprise/plugins/popcorn_notify.py:89 +#: apprise/plugins/postmark.py:133 apprise/plugins/pushbullet.py:105 +#: apprise/plugins/pushed.py:107 apprise/plugins/pushover.py:252 #: apprise/plugins/pushsafer.py:376 apprise/plugins/pushy.py:97 #: apprise/plugins/reddit.py:170 apprise/plugins/resend.py:124 -#: apprise/plugins/revolt.py:112 apprise/plugins/rocketchat.py:166 -#: apprise/plugins/sendgrid.py:133 apprise/plugins/sendpulse.py:136 -#: apprise/plugins/seven.py:88 apprise/plugins/sfr.py:113 -#: apprise/plugins/signal_api.py:138 apprise/plugins/sinch.py:140 -#: apprise/plugins/slack.py:248 apprise/plugins/smpp.py:128 -#: apprise/plugins/smseagle.py:172 apprise/plugins/smsmanager.py:119 -#: apprise/plugins/sns.py:138 apprise/plugins/telegram.py:359 -#: apprise/plugins/threema.py:115 apprise/plugins/twilio.py:174 -#: apprise/plugins/twist.py:123 apprise/plugins/twitter.py:168 -#: apprise/plugins/vapid/__init__.py:158 apprise/plugins/voipms.py:107 -#: apprise/plugins/vonage.py:108 apprise/plugins/whatsapp.py:126 -#: apprise/plugins/wxpusher.py:139 apprise/plugins/xmpp/base.py:118 +#: apprise/plugins/revolt.py:112 apprise/plugins/ringcentral.py:219 +#: apprise/plugins/rocketchat.py:167 apprise/plugins/sendgrid.py:133 +#: apprise/plugins/sendpulse.py:139 apprise/plugins/seven.py:88 +#: apprise/plugins/sfr.py:113 apprise/plugins/signal_api.py:138 +#: apprise/plugins/sinch.py:140 apprise/plugins/slack.py:273 +#: apprise/plugins/smpp.py:128 apprise/plugins/smseagle.py:172 +#: apprise/plugins/smsmanager.py:119 apprise/plugins/sns.py:170 +#: apprise/plugins/telegram.py:359 apprise/plugins/threema.py:115 +#: apprise/plugins/twilio.py:174 apprise/plugins/twist.py:123 +#: apprise/plugins/twitter.py:193 apprise/plugins/vapid/__init__.py:158 +#: apprise/plugins/voipms.py:108 apprise/plugins/vonage.py:108 +#: apprise/plugins/wechat.py:218 apprise/plugins/whatsapp.py:166 +#: apprise/plugins/wxpusher.py:139 apprise/plugins/xmpp/base.py:134 #: apprise/plugins/zulip.py:153 msgid "Targets" msgstr "Targets" -#: apprise/plugins/africas_talking.py:168 +#: apprise/plugins/africas_talking.py:166 #, fuzzy msgid "From" msgstr "Rooms" -#: apprise/plugins/africas_talking.py:174 apprise/plugins/bulksms.py:170 -#: apprise/plugins/bulkvs.py:131 apprise/plugins/burstsms.py:166 -#: apprise/plugins/clicksend.py:130 apprise/plugins/d7networks.py:128 -#: apprise/plugins/dapnet.py:167 apprise/plugins/mailgun.py:194 -#: apprise/plugins/mastodon.py:215 apprise/plugins/one_signal.py:161 -#: apprise/plugins/opsgenie.py:288 apprise/plugins/plivo.py:137 -#: apprise/plugins/popcorn_notify.py:104 apprise/plugins/signal_api.py:155 -#: apprise/plugins/smseagle.py:190 apprise/plugins/smsmanager.py:152 -#: apprise/plugins/smtp2go.py:151 apprise/plugins/sparkpost.py:209 -#: apprise/plugins/twitter.py:193 -#, fuzzy -msgid "Batch Mode" -msgstr "Webhook Mode" - -#: apprise/plugins/africas_talking.py:179 +#: apprise/plugins/africas_talking.py:172 #, fuzzy msgid "SMS Mode" msgstr "Secure Mode" +#: apprise/plugins/africas_talking.py:181 apprise/plugins/bulksms.py:170 +#: apprise/plugins/bulkvs.py:131 apprise/plugins/burstsms.py:166 +#: apprise/plugins/clicksend.py:130 apprise/plugins/d7networks.py:144 +#: apprise/plugins/dapnet.py:167 apprise/plugins/exotel.py:222 +#: apprise/plugins/home_assistant.py:170 apprise/plugins/jira.py:282 +#: apprise/plugins/mailgun.py:194 apprise/plugins/mastodon.py:247 +#: apprise/plugins/octopush.py:173 apprise/plugins/one_signal.py:185 +#: apprise/plugins/opsgenie.py:307 apprise/plugins/plivo.py:137 +#: apprise/plugins/popcorn_notify.py:104 apprise/plugins/signal_api.py:160 +#: apprise/plugins/smseagle.py:211 apprise/plugins/smsmanager.py:152 +#: apprise/plugins/smtp2go.py:151 apprise/plugins/sparkpost.py:209 +#: apprise/plugins/twitter.py:218 +#, fuzzy +msgid "Batch Mode" +msgstr "Webhook Mode" + #: apprise/plugins/apprise_api.py:102 apprise/plugins/bark.py:134 -#: apprise/plugins/custom_form.py:121 apprise/plugins/custom_json.py:103 -#: apprise/plugins/custom_xml.py:103 apprise/plugins/emby.py:85 -#: apprise/plugins/enigma2.py:110 apprise/plugins/fluxer.py:159 -#: apprise/plugins/gotify.py:129 apprise/plugins/growl.py:140 -#: apprise/plugins/home_assistant.py:79 apprise/plugins/irc/base.py:117 -#: apprise/plugins/lametric.py:460 apprise/plugins/mastodon.py:168 -#: apprise/plugins/matrix.py:220 apprise/plugins/mattermost.py:151 -#: apprise/plugins/misskey.py:117 apprise/plugins/mqtt.py:147 -#: apprise/plugins/nextcloud.py:116 apprise/plugins/nextcloudtalk.py:74 -#: apprise/plugins/notica.py:126 apprise/plugins/ntfy.py:211 +#: apprise/plugins/custom_form.py:130 apprise/plugins/custom_json.py:112 +#: apprise/plugins/custom_xml.py:112 apprise/plugins/emby.py:85 +#: apprise/plugins/enigma2.py:110 apprise/plugins/evolution.py:111 +#: apprise/plugins/fluxer.py:159 apprise/plugins/gotify.py:129 +#: apprise/plugins/growl.py:140 apprise/plugins/home_assistant.py:122 +#: apprise/plugins/humhub.py:138 apprise/plugins/irc/base.py:117 +#: apprise/plugins/kodi.py:96 apprise/plugins/lametric.py:460 +#: apprise/plugins/mastodon.py:180 apprise/plugins/matrix/base.py:262 +#: apprise/plugins/mattermost.py:155 apprise/plugins/misskey.py:117 +#: apprise/plugins/mqtt.py:149 apprise/plugins/nextcloud.py:116 +#: apprise/plugins/nextcloudtalk.py:74 apprise/plugins/notica.py:126 +#: apprise/plugins/notifico.py:166 apprise/plugins/ntfy.py:211 #: apprise/plugins/parseplatform.py:90 apprise/plugins/pushdeer.py:75 #: apprise/plugins/pushjet.py:71 apprise/plugins/rocketchat.py:120 #: apprise/plugins/rsyslog.py:180 apprise/plugins/signal_api.py:97 -#: apprise/plugins/smseagle.py:135 apprise/plugins/synology.py:83 -#: apprise/plugins/workflows.py:125 apprise/plugins/xbmc.py:96 -#: apprise/plugins/xmpp/base.py:96 +#: apprise/plugins/smseagle.py:135 apprise/plugins/sogs.py:207 +#: apprise/plugins/synology.py:83 apprise/plugins/workflows.py:126 +#: apprise/plugins/xmpp/base.py:101 msgid "Hostname" msgstr "Hostname" #: apprise/plugins/apprise_api.py:107 apprise/plugins/bark.py:139 -#: apprise/plugins/custom_form.py:126 apprise/plugins/custom_json.py:108 -#: apprise/plugins/custom_xml.py:108 apprise/plugins/email/base.py:129 +#: apprise/plugins/custom_form.py:135 apprise/plugins/custom_json.py:117 +#: apprise/plugins/custom_xml.py:117 apprise/plugins/email/base.py:153 #: apprise/plugins/emby.py:90 apprise/plugins/enigma2.py:115 -#: apprise/plugins/fluxer.py:163 apprise/plugins/gotify.py:140 -#: apprise/plugins/growl.py:145 apprise/plugins/home_assistant.py:84 -#: apprise/plugins/irc/base.py:122 apprise/plugins/lametric.py:464 -#: apprise/plugins/mastodon.py:178 apprise/plugins/matrix.py:224 -#: apprise/plugins/mattermost.py:167 apprise/plugins/misskey.py:127 -#: apprise/plugins/mqtt.py:152 apprise/plugins/nextcloud.py:121 -#: apprise/plugins/nextcloudtalk.py:79 apprise/plugins/notica.py:130 +#: apprise/plugins/evolution.py:116 apprise/plugins/fluxer.py:163 +#: apprise/plugins/gotify.py:140 apprise/plugins/growl.py:145 +#: apprise/plugins/home_assistant.py:127 apprise/plugins/humhub.py:143 +#: apprise/plugins/irc/base.py:122 apprise/plugins/kodi.py:101 +#: apprise/plugins/lametric.py:464 apprise/plugins/mastodon.py:190 +#: apprise/plugins/matrix/base.py:267 apprise/plugins/mattermost.py:171 +#: apprise/plugins/misskey.py:127 apprise/plugins/mqtt.py:154 +#: apprise/plugins/nextcloud.py:121 apprise/plugins/nextcloudtalk.py:79 +#: apprise/plugins/notica.py:130 apprise/plugins/notifico.py:170 #: apprise/plugins/ntfy.py:215 apprise/plugins/parseplatform.py:95 #: apprise/plugins/pushdeer.py:79 apprise/plugins/pushjet.py:76 #: apprise/plugins/rocketchat.py:125 apprise/plugins/rsyslog.py:185 #: apprise/plugins/signal_api.py:102 apprise/plugins/smpp.py:108 -#: apprise/plugins/smseagle.py:140 apprise/plugins/synology.py:88 -#: apprise/plugins/workflows.py:130 apprise/plugins/xbmc.py:101 -#: apprise/plugins/xmpp/base.py:101 +#: apprise/plugins/smseagle.py:140 apprise/plugins/sogs.py:212 +#: apprise/plugins/synology.py:88 apprise/plugins/workflows.py:131 +#: apprise/plugins/xmpp/base.py:106 msgid "Port" msgstr "Port" #: apprise/plugins/apprise_api.py:113 apprise/plugins/bark.py:145 -#: apprise/plugins/bluesky.py:119 apprise/plugins/custom_form.py:132 -#: apprise/plugins/custom_json.py:114 apprise/plugins/custom_xml.py:114 +#: apprise/plugins/bluesky.py:119 apprise/plugins/custom_form.py:141 +#: apprise/plugins/custom_json.py:123 apprise/plugins/custom_xml.py:123 #: apprise/plugins/emby.py:97 apprise/plugins/enigma2.py:121 -#: apprise/plugins/freemobile.py:78 apprise/plugins/home_assistant.py:90 -#: apprise/plugins/lametric.py:471 apprise/plugins/matrix.py:230 +#: apprise/plugins/freemobile.py:78 apprise/plugins/home_assistant.py:133 +#: apprise/plugins/jira.py:236 apprise/plugins/kodi.py:107 +#: apprise/plugins/lametric.py:471 apprise/plugins/matrix/base.py:273 #: apprise/plugins/nextcloud.py:127 apprise/plugins/nextcloudtalk.py:85 -#: apprise/plugins/notica.py:136 apprise/plugins/ntfy.py:221 -#: apprise/plugins/opsgenie.py:242 apprise/plugins/pushjet.py:88 -#: apprise/plugins/rocketchat.py:131 apprise/plugins/signal_api.py:108 -#: apprise/plugins/smpp.py:92 apprise/plugins/synology.py:94 -#: apprise/plugins/xbmc.py:107 +#: apprise/plugins/notica.py:136 apprise/plugins/notifico.py:176 +#: apprise/plugins/ntfy.py:221 apprise/plugins/opsgenie.py:234 +#: apprise/plugins/pushjet.py:88 apprise/plugins/rocketchat.py:131 +#: apprise/plugins/signal_api.py:108 apprise/plugins/smpp.py:92 +#: apprise/plugins/synology.py:94 msgid "Username" msgstr "Username" #: apprise/plugins/apprise_api.py:117 apprise/plugins/aprs.py:172 #: apprise/plugins/bark.py:149 apprise/plugins/bluesky.py:124 #: apprise/plugins/bulksms.py:117 apprise/plugins/bulkvs.py:90 -#: apprise/plugins/custom_form.py:136 apprise/plugins/custom_json.py:118 -#: apprise/plugins/custom_xml.py:118 apprise/plugins/dapnet.py:123 -#: apprise/plugins/email/base.py:119 apprise/plugins/emby.py:101 +#: apprise/plugins/custom_form.py:145 apprise/plugins/custom_json.py:127 +#: apprise/plugins/custom_xml.py:127 apprise/plugins/dapnet.py:123 +#: apprise/plugins/email/base.py:143 apprise/plugins/emby.py:101 #: apprise/plugins/enigma2.py:125 apprise/plugins/freemobile.py:83 -#: apprise/plugins/growl.py:151 apprise/plugins/home_assistant.py:94 -#: apprise/plugins/irc/base.py:132 apprise/plugins/matrix.py:234 -#: apprise/plugins/mqtt.py:163 apprise/plugins/nextcloud.py:131 +#: apprise/plugins/growl.py:151 apprise/plugins/home_assistant.py:137 +#: apprise/plugins/humhub.py:133 apprise/plugins/irc/base.py:132 +#: apprise/plugins/kodi.py:111 apprise/plugins/matrix/base.py:277 +#: apprise/plugins/mqtt.py:165 apprise/plugins/nextcloud.py:131 #: apprise/plugins/nextcloudtalk.py:90 apprise/plugins/notica.py:140 -#: apprise/plugins/ntfy.py:225 apprise/plugins/pushjet.py:92 -#: apprise/plugins/reddit.py:145 apprise/plugins/rocketchat.py:135 -#: apprise/plugins/signal_api.py:112 apprise/plugins/simplepush.py:108 -#: apprise/plugins/smpp.py:97 apprise/plugins/synology.py:98 -#: apprise/plugins/twist.py:101 apprise/plugins/voipms.py:88 -#: apprise/plugins/xbmc.py:111 apprise/plugins/xmpp/base.py:112 +#: apprise/plugins/notifico.py:180 apprise/plugins/ntfy.py:225 +#: apprise/plugins/pushjet.py:92 apprise/plugins/reddit.py:145 +#: apprise/plugins/rocketchat.py:135 apprise/plugins/signal_api.py:112 +#: apprise/plugins/simplepush.py:108 apprise/plugins/smpp.py:97 +#: apprise/plugins/synology.py:98 apprise/plugins/twist.py:101 +#: apprise/plugins/voipms.py:88 apprise/plugins/xmpp/base.py:117 msgid "Password" msgstr "Password" #: apprise/plugins/apprise_api.py:122 apprise/plugins/chanify.py:74 -#: apprise/plugins/dingtalk.py:93 apprise/plugins/feishu.py:80 -#: apprise/plugins/gotify.py:123 apprise/plugins/mattermost.py:157 -#: apprise/plugins/notica.py:119 apprise/plugins/notifiarr.py:91 -#: apprise/plugins/ntfy.py:230 apprise/plugins/pushme.py:62 -#: apprise/plugins/ryver.py:99 apprise/plugins/serverchan.py:70 -#: apprise/plugins/slack.py:294 apprise/plugins/synology.py:103 -#: apprise/plugins/webexteams.py:116 apprise/plugins/zulip.py:136 +#: apprise/plugins/dingtalk.py:93 apprise/plugins/exotel.py:166 +#: apprise/plugins/feishu.py:80 apprise/plugins/gotify.py:123 +#: apprise/plugins/mattermost.py:161 apprise/plugins/notica.py:119 +#: apprise/plugins/notifiarr.py:91 apprise/plugins/ntfy.py:230 +#: apprise/plugins/pushme.py:62 apprise/plugins/ryver.py:99 +#: apprise/plugins/serverchan.py:70 apprise/plugins/slack.py:323 +#: apprise/plugins/synology.py:103 apprise/plugins/zulip.py:136 msgid "Token" msgstr "Token" -#: apprise/plugins/apprise_api.py:136 apprise/plugins/ntfy.py:288 -#: apprise/plugins/opsgenie.py:307 apprise/plugins/pagertree.py:133 +#: apprise/plugins/apprise_api.py:136 apprise/plugins/jira.py:301 +#: apprise/plugins/ntfy.py:288 apprise/plugins/opsgenie.py:294 +#: apprise/plugins/pagertree.py:133 #, fuzzy msgid "Tags" msgstr "Targets" @@ -259,8 +279,8 @@ msgstr "Targets" msgid "Query Method" msgstr "" -#: apprise/plugins/apprise_api.py:154 apprise/plugins/custom_form.py:165 -#: apprise/plugins/custom_json.py:141 apprise/plugins/custom_xml.py:141 +#: apprise/plugins/apprise_api.py:154 apprise/plugins/custom_form.py:174 +#: apprise/plugins/custom_json.py:150 apprise/plugins/custom_xml.py:150 #: apprise/plugins/enigma2.py:153 apprise/plugins/nextcloud.py:181 #: apprise/plugins/nextcloudtalk.py:122 apprise/plugins/notica.py:156 #: apprise/plugins/pagertree.py:142 apprise/plugins/synology.py:128 @@ -269,15 +289,15 @@ msgstr "HTTP Header" #: apprise/plugins/aprs.py:167 apprise/plugins/bulksms.py:112 #: apprise/plugins/bulkvs.py:85 apprise/plugins/clicksend.py:93 -#: apprise/plugins/dapnet.py:118 apprise/plugins/email/base.py:115 -#: apprise/plugins/mailgun.py:136 apprise/plugins/mqtt.py:158 -#: apprise/plugins/reddit.py:140 apprise/plugins/sendpulse.py:108 +#: apprise/plugins/dapnet.py:118 apprise/plugins/email/base.py:139 +#: apprise/plugins/mailgun.py:136 apprise/plugins/mqtt.py:160 +#: apprise/plugins/reddit.py:140 apprise/plugins/sendpulse.py:110 #: apprise/plugins/smtp2go.py:108 apprise/plugins/sparkpost.py:159 msgid "User Name" msgstr "User Name" #: apprise/plugins/aprs.py:178 apprise/plugins/aprs.py:199 -#: apprise/plugins/dapnet.py:129 apprise/plugins/dapnet.py:150 +#: apprise/plugins/dapnet.py:129 apprise/plugins/dapnet.py:162 #, fuzzy msgid "Target Callsign" msgstr "Target Emails" @@ -291,166 +311,211 @@ msgid "Locale" msgstr "" #: apprise/plugins/bark.py:154 apprise/plugins/fcm/__init__.py:157 -#: apprise/plugins/pushbullet.py:89 apprise/plugins/pushover.py:200 -#: apprise/plugins/pushsafer.py:366 apprise/plugins/pushy.py:85 +#: apprise/plugins/home_assistant.py:148 apprise/plugins/pushbullet.py:89 +#: apprise/plugins/pushover.py:240 apprise/plugins/pushsafer.py:366 +#: apprise/plugins/pushy.py:85 msgid "Target Device" msgstr "Target Device" -#: apprise/plugins/bark.py:174 apprise/plugins/lametric.py:516 -#: apprise/plugins/macosx.py:124 apprise/plugins/pushover.py:223 +#: apprise/plugins/bark.py:171 apprise/plugins/lametric.py:516 +#: apprise/plugins/macosx.py:124 apprise/plugins/pushover.py:269 #: apprise/plugins/pushsafer.py:392 apprise/plugins/pushy.py:110 msgid "Sound" msgstr "Sound" -#: apprise/plugins/bark.py:179 +#: apprise/plugins/bark.py:176 msgid "Level" msgstr "" -#: apprise/plugins/bark.py:184 +#: apprise/plugins/bark.py:181 msgid "Volume" msgstr "" -#: apprise/plugins/bark.py:190 apprise/plugins/ntfy.py:270 +#: apprise/plugins/bark.py:187 apprise/plugins/ntfy.py:270 #: apprise/plugins/pagerduty.py:172 msgid "Click" msgstr "" -#: apprise/plugins/bark.py:194 apprise/plugins/pushy.py:114 +#: apprise/plugins/bark.py:191 apprise/plugins/pushy.py:114 msgid "Badge" msgstr "" -#: apprise/plugins/bark.py:199 +#: apprise/plugins/bark.py:196 msgid "Category" msgstr "" -#: apprise/plugins/bark.py:203 apprise/plugins/join.py:158 +#: apprise/plugins/bark.py:200 apprise/plugins/join.py:158 #: apprise/plugins/pagerduty.py:163 msgid "Group" msgstr "Group" -#: apprise/plugins/bark.py:207 apprise/plugins/dbus.py:225 +#: apprise/plugins/bark.py:204 apprise/plugins/dbus.py:228 #: apprise/plugins/discord.py:196 apprise/plugins/fcm/__init__.py:197 #: apprise/plugins/flock.py:136 apprise/plugins/fluxer.py:250 -#: apprise/plugins/glib.py:187 apprise/plugins/gnome.py:154 -#: apprise/plugins/growl.py:175 apprise/plugins/join.py:182 -#: apprise/plugins/line.py:108 apprise/plugins/macosx.py:115 -#: apprise/plugins/matrix.py:273 apprise/plugins/mattermost.py:211 -#: apprise/plugins/msteams.py:200 apprise/plugins/notifiarr.py:125 -#: apprise/plugins/ntfy.py:256 apprise/plugins/one_signal.py:155 +#: apprise/plugins/glib.py:188 apprise/plugins/gnome.py:154 +#: apprise/plugins/growl.py:175 apprise/plugins/join.py:183 +#: apprise/plugins/kodi.py:129 apprise/plugins/line.py:108 +#: apprise/plugins/macosx.py:115 apprise/plugins/matrix/base.py:318 +#: apprise/plugins/mattermost.py:212 apprise/plugins/notifiarr.py:125 +#: apprise/plugins/ntfy.py:256 apprise/plugins/one_signal.py:164 #: apprise/plugins/pagerduty.py:191 apprise/plugins/ryver.py:124 -#: apprise/plugins/slack.py:259 apprise/plugins/telegram.py:370 +#: apprise/plugins/slack.py:291 apprise/plugins/telegram.py:370 #: apprise/plugins/vapid/__init__.py:204 apprise/plugins/windows.py:106 -#: apprise/plugins/workflows.py:162 apprise/plugins/xbmc.py:129 +#: apprise/plugins/workflows.py:163 msgid "Include Image" msgstr "Include Image" -#: apprise/plugins/bark.py:213 apprise/plugins/mattermost.py:207 -#: apprise/plugins/revolt.py:128 +#: apprise/plugins/bark.py:210 apprise/plugins/mattermost.py:208 +#: apprise/plugins/revolt.py:129 msgid "Icon URL" msgstr "" -#: apprise/plugins/bark.py:217 apprise/plugins/streamlabs.py:119 +#: apprise/plugins/bark.py:214 apprise/plugins/streamlabs.py:119 msgid "Call" msgstr "" -#: apprise/plugins/base.py:192 +#: apprise/plugins/base.py:305 msgid "Overflow Mode" msgstr "Overflow Mode" -#: apprise/plugins/base.py:207 +#: apprise/plugins/base.py:320 msgid "Notify Format" msgstr "Notify Format" -#: apprise/plugins/base.py:217 +#: apprise/plugins/base.py:330 #, fuzzy msgid "Interpret Emojis" msgstr "Target Emails" -#: apprise/plugins/base.py:227 +#: apprise/plugins/base.py:340 msgid "Persistent Storage" msgstr "" -#: apprise/plugins/base.py:237 +#: apprise/plugins/base.py:350 #, fuzzy msgid "Timezone" msgstr "Server Timeout" -#: apprise/plugins/brevo.py:119 apprise/plugins/resend.py:114 -#: apprise/plugins/sendgrid.py:123 +#: apprise/plugins/base.py:359 +#, fuzzy +msgid "Service Retry" +msgstr "Device ID" + +#: apprise/plugins/base.py:367 +#, fuzzy +msgid "Inter-Retry Wait" +msgstr "Target Emails" + +#: apprise/plugins/base.py:375 +#, fuzzy +msgid "Optional Service" +msgstr "Device ID" + +#: apprise/plugins/blink1.py:151 +msgid "blink(1)" +msgstr "" + +#: apprise/plugins/blink1.py:178 +#, fuzzy +msgid "Serial Number" +msgstr "Device ID" + +#: apprise/plugins/blink1.py:188 +#, fuzzy +msgid "Duration (ms)" +msgstr "Duration" + +#: apprise/plugins/blink1.py:195 +msgid "Fade Time (ms)" +msgstr "" + +#: apprise/plugins/blink1.py:202 +msgid "LED Number" +msgstr "" + +#: apprise/plugins/brevo.py:164 apprise/plugins/postmark.py:123 +#: apprise/plugins/resend.py:114 apprise/plugins/sendgrid.py:123 #, fuzzy msgid "Source Email" msgstr "Source JID" -#: apprise/plugins/brevo.py:124 apprise/plugins/email/base.py:135 -#: apprise/plugins/mailgun.py:152 apprise/plugins/notificationapi.py:172 -#: apprise/plugins/office365.py:143 apprise/plugins/one_signal.py:124 -#: apprise/plugins/popcorn_notify.py:84 apprise/plugins/pushbullet.py:100 -#: apprise/plugins/pushsafer.py:371 apprise/plugins/resend.py:119 -#: apprise/plugins/sendgrid.py:128 apprise/plugins/sendpulse.py:131 -#: apprise/plugins/slack.py:231 apprise/plugins/threema.py:105 +#: apprise/plugins/brevo.py:169 apprise/plugins/email/base.py:159 +#: apprise/plugins/mailgun.py:152 apprise/plugins/notificationapi.py:177 +#: apprise/plugins/office365.py:282 apprise/plugins/one_signal.py:124 +#: apprise/plugins/popcorn_notify.py:84 apprise/plugins/postmark.py:128 +#: apprise/plugins/pushbullet.py:100 apprise/plugins/pushsafer.py:371 +#: apprise/plugins/resend.py:119 apprise/plugins/sendgrid.py:128 +#: apprise/plugins/sendpulse.py:134 apprise/plugins/slack.py:256 +#: apprise/plugins/threema.py:105 msgid "Target Email" msgstr "Target Email" -#: apprise/plugins/brevo.py:143 apprise/plugins/email/base.py:165 -#: apprise/plugins/mailgun.py:186 apprise/plugins/notificationapi.py:218 -#: apprise/plugins/office365.py:162 apprise/plugins/resend.py:138 -#: apprise/plugins/sendgrid.py:147 apprise/plugins/sendpulse.py:152 -#: apprise/plugins/ses.py:206 apprise/plugins/smtp2go.py:143 -#: apprise/plugins/sparkpost.py:201 -msgid "Carbon Copy" -msgstr "" - -#: apprise/plugins/brevo.py:147 apprise/plugins/email/base.py:169 -#: apprise/plugins/mailgun.py:190 apprise/plugins/notificationapi.py:222 -#: apprise/plugins/office365.py:166 apprise/plugins/resend.py:142 -#: apprise/plugins/sendgrid.py:151 apprise/plugins/sendpulse.py:156 -#: apprise/plugins/ses.py:210 apprise/plugins/smtp2go.py:147 -#: apprise/plugins/sparkpost.py:205 -msgid "Blind Carbon Copy" -msgstr "" - -#: apprise/plugins/brevo.py:151 apprise/plugins/ses.py:196 +#: apprise/plugins/brevo.py:185 apprise/plugins/postmark.py:149 +#: apprise/plugins/ses.py:205 #, fuzzy msgid "Reply To Email" msgstr "To Email" +#: apprise/plugins/brevo.py:193 apprise/plugins/email/base.py:240 +#: apprise/plugins/mailgun.py:186 apprise/plugins/notificationapi.py:243 +#: apprise/plugins/office365.py:307 apprise/plugins/postmark.py:157 +#: apprise/plugins/resend.py:158 apprise/plugins/sendgrid.py:153 +#: apprise/plugins/sendpulse.py:169 apprise/plugins/ses.py:232 +#: apprise/plugins/smtp2go.py:143 apprise/plugins/sparkpost.py:201 +msgid "Carbon Copy" +msgstr "" + +#: apprise/plugins/brevo.py:197 apprise/plugins/email/base.py:244 +#: apprise/plugins/mailgun.py:190 apprise/plugins/notificationapi.py:247 +#: apprise/plugins/office365.py:311 apprise/plugins/postmark.py:161 +#: apprise/plugins/resend.py:162 apprise/plugins/sendgrid.py:157 +#: apprise/plugins/sendpulse.py:173 apprise/plugins/ses.py:236 +#: apprise/plugins/smtp2go.py:147 apprise/plugins/sparkpost.py:205 +msgid "Blind Carbon Copy" +msgstr "" + #: apprise/plugins/bulksms.py:123 apprise/plugins/bulkvs.py:103 #: apprise/plugins/burstsms.py:124 apprise/plugins/clickatell.py:83 #: apprise/plugins/clicksend.py:105 apprise/plugins/d7networks.py:103 -#: apprise/plugins/dingtalk.py:106 apprise/plugins/httpsms.py:90 +#: apprise/plugins/dingtalk.py:106 apprise/plugins/evolution.py:127 +#: apprise/plugins/exotel.py:183 apprise/plugins/httpsms.py:90 #: apprise/plugins/kavenegar.py:128 apprise/plugins/messagebird.py:92 -#: apprise/plugins/msg91.py:116 apprise/plugins/plivo.py:107 -#: apprise/plugins/popcorn_notify.py:77 apprise/plugins/seven.py:81 +#: apprise/plugins/msg91.py:116 apprise/plugins/octopush.py:146 +#: apprise/plugins/plivo.py:107 apprise/plugins/popcorn_notify.py:77 +#: apprise/plugins/ringcentral.py:213 apprise/plugins/seven.py:81 #: apprise/plugins/signal_api.py:124 apprise/plugins/sinch.py:127 #: apprise/plugins/smpp.py:121 apprise/plugins/smseagle.py:151 -#: apprise/plugins/smsmanager.py:112 apprise/plugins/sns.py:125 +#: apprise/plugins/smsmanager.py:112 apprise/plugins/sns.py:157 #: apprise/plugins/threema.py:98 apprise/plugins/twilio.py:161 -#: apprise/plugins/voipms.py:100 apprise/plugins/vonage.py:101 -#: apprise/plugins/whatsapp.py:119 +#: apprise/plugins/voipms.py:101 apprise/plugins/vonage.py:101 +#: apprise/plugins/whatsapp.py:147 msgid "Target Phone No" msgstr "Target Phone No" #: apprise/plugins/bulksms.py:130 apprise/plugins/nextcloud.py:142 +#: apprise/plugins/pushover.py:246 #, fuzzy msgid "Target Group" msgstr "Target Topic" -#: apprise/plugins/bulksms.py:152 apprise/plugins/bulkvs.py:96 -#: apprise/plugins/bulkvs.py:125 apprise/plugins/clickatell.py:78 +#: apprise/plugins/bulksms.py:149 apprise/plugins/bulkvs.py:96 +#: apprise/plugins/bulkvs.py:122 apprise/plugins/clickatell.py:78 #: apprise/plugins/fortysixelks.py:100 apprise/plugins/httpsms.py:83 -#: apprise/plugins/httpsms.py:115 apprise/plugins/signal_api.py:117 -#: apprise/plugins/sinch.py:120 apprise/plugins/smpp.py:114 -#: apprise/plugins/smsmanager.py:137 apprise/plugins/twilio.py:154 -#: apprise/plugins/voipms.py:94 apprise/plugins/vonage.py:94 +#: apprise/plugins/httpsms.py:115 apprise/plugins/ringcentral.py:207 +#: apprise/plugins/signal_api.py:117 apprise/plugins/sinch.py:120 +#: apprise/plugins/smpp.py:114 apprise/plugins/smsmanager.py:134 +#: apprise/plugins/twilio.py:154 apprise/plugins/voipms.py:94 +#: apprise/plugins/vonage.py:94 msgid "From Phone No" msgstr "From Phone No" -#: apprise/plugins/bulksms.py:158 +#: apprise/plugins/bulksms.py:155 #, fuzzy msgid "Route Group" msgstr "Group" -#: apprise/plugins/bulksms.py:165 apprise/plugins/d7networks.py:123 +#: apprise/plugins/bulksms.py:162 apprise/plugins/d7networks.py:136 +#: apprise/plugins/exotel.py:228 msgid "Unicode Characters" msgstr "" @@ -465,11 +530,11 @@ msgstr "Application Secret" msgid "Sender ID" msgstr "To User ID" -#: apprise/plugins/burstsms.py:155 +#: apprise/plugins/burstsms.py:152 msgid "Country" msgstr "" -#: apprise/plugins/burstsms.py:164 +#: apprise/plugins/burstsms.py:161 msgid "validity" msgstr "" @@ -477,6 +542,18 @@ msgstr "" msgid "Chanify" msgstr "" +#: apprise/plugins/chime.py:130 apprise/plugins/discord.py:130 +#: apprise/plugins/fluxer.py:174 apprise/plugins/ifttt.py:103 +#: apprise/plugins/zoom.py:139 +msgid "Webhook ID" +msgstr "Webhook ID" + +#: apprise/plugins/chime.py:136 apprise/plugins/discord.py:136 +#: apprise/plugins/fluxer.py:181 apprise/plugins/google_chat.py:118 +#: apprise/plugins/webexteams.py:171 +msgid "Webhook Token" +msgstr "Webhook Token" + #: apprise/plugins/clickatell.py:45 msgid "Clickatell" msgstr "" @@ -486,22 +563,22 @@ msgstr "" msgid "API Token" msgstr "API Key" -#: apprise/plugins/custom_form.py:148 apprise/plugins/custom_json.py:130 -#: apprise/plugins/custom_xml.py:130 +#: apprise/plugins/custom_form.py:157 apprise/plugins/custom_json.py:139 +#: apprise/plugins/custom_xml.py:139 msgid "Fetch Method" msgstr "" -#: apprise/plugins/custom_form.py:154 +#: apprise/plugins/custom_form.py:163 msgid "Attach File As" msgstr "" -#: apprise/plugins/custom_form.py:169 apprise/plugins/custom_json.py:145 -#: apprise/plugins/custom_xml.py:145 apprise/plugins/pagertree.py:146 +#: apprise/plugins/custom_form.py:178 apprise/plugins/custom_json.py:154 +#: apprise/plugins/custom_xml.py:154 apprise/plugins/pagertree.py:146 msgid "Payload Extras" msgstr "" -#: apprise/plugins/custom_form.py:173 apprise/plugins/custom_json.py:149 -#: apprise/plugins/custom_xml.py:149 +#: apprise/plugins/custom_form.py:182 apprise/plugins/custom_json.py:158 +#: apprise/plugins/custom_xml.py:158 msgid "GET Params" msgstr "" @@ -510,41 +587,42 @@ msgstr "" msgid "API Access Token" msgstr "Access Token" -#: apprise/plugins/d7networks.py:141 apprise/plugins/seven.py:107 +#: apprise/plugins/d7networks.py:127 apprise/plugins/seven.py:105 msgid "Originating Address" msgstr "" -#: apprise/plugins/dapnet.py:155 apprise/plugins/gotify.py:153 -#: apprise/plugins/growl.py:163 apprise/plugins/join.py:188 +#: apprise/plugins/dapnet.py:150 apprise/plugins/exotel.py:240 +#: apprise/plugins/gotify.py:153 apprise/plugins/growl.py:163 +#: apprise/plugins/jira.py:287 apprise/plugins/join.py:189 #: apprise/plugins/lametric.py:494 apprise/plugins/ntfy.py:282 -#: apprise/plugins/opsgenie.py:293 apprise/plugins/prowl.py:141 -#: apprise/plugins/pushover.py:217 apprise/plugins/pushsafer.py:387 -#: apprise/plugins/smseagle.py:210 +#: apprise/plugins/opsgenie.py:280 apprise/plugins/prowl.py:141 +#: apprise/plugins/pushover.py:263 apprise/plugins/pushsafer.py:387 +#: apprise/plugins/smseagle.py:187 msgid "Priority" msgstr "Priority" -#: apprise/plugins/dapnet.py:161 +#: apprise/plugins/dapnet.py:156 msgid "Transmitter Groups" msgstr "" -#: apprise/plugins/dbus.py:153 +#: apprise/plugins/dbus.py:156 msgid "libdbus-1.so.x must be installed." msgstr "" -#: apprise/plugins/dbus.py:157 apprise/plugins/glib.py:126 +#: apprise/plugins/dbus.py:160 msgid "DBus Notification" msgstr "" -#: apprise/plugins/dbus.py:201 apprise/plugins/glib.py:163 +#: apprise/plugins/dbus.py:204 apprise/plugins/glib.py:165 #: apprise/plugins/gnome.py:142 apprise/plugins/pagertree.py:128 msgid "Urgency" msgstr "Urgency" -#: apprise/plugins/dbus.py:213 apprise/plugins/glib.py:175 +#: apprise/plugins/dbus.py:216 apprise/plugins/glib.py:176 msgid "X-Axis" msgstr "X-Axis" -#: apprise/plugins/dbus.py:219 apprise/plugins/glib.py:181 +#: apprise/plugins/dbus.py:222 apprise/plugins/glib.py:182 msgid "Y-Axis" msgstr "Y-Axis" @@ -555,21 +633,11 @@ msgstr "Secret Key" #: apprise/plugins/discord.py:125 apprise/plugins/flock.py:106 #: apprise/plugins/fluxer.py:169 apprise/plugins/ryver.py:106 -#: apprise/plugins/slack.py:186 apprise/plugins/viber.py:95 +#: apprise/plugins/slack.py:208 apprise/plugins/viber.py:99 #: apprise/plugins/zulip.py:124 msgid "Bot Name" msgstr "Bot Name" -#: apprise/plugins/discord.py:130 apprise/plugins/fluxer.py:174 -#: apprise/plugins/ifttt.py:103 -msgid "Webhook ID" -msgstr "Webhook ID" - -#: apprise/plugins/discord.py:136 apprise/plugins/fluxer.py:181 -#: apprise/plugins/google_chat.py:118 -msgid "Webhook Token" -msgstr "Webhook Token" - #: apprise/plugins/discord.py:149 apprise/plugins/fluxer.py:201 msgid "Text To Speech" msgstr "Text To Speech" @@ -585,7 +653,7 @@ msgid "Avatar URL" msgstr "Avatar Image" #: apprise/plugins/discord.py:163 apprise/plugins/fluxer.py:215 -#: apprise/plugins/pushover.py:229 +#: apprise/plugins/pushover.py:275 msgid "URL" msgstr "" @@ -614,55 +682,60 @@ msgstr "" msgid "Ping Users/Roles" msgstr "" -#: apprise/plugins/dot.py:127 +#: apprise/plugins/dot.py:142 #, fuzzy msgid "Device Serial Number" msgstr "Device ID" -#: apprise/plugins/dot.py:133 +#: apprise/plugins/dot.py:155 #, fuzzy msgid "API Mode" msgstr "API Key" -#: apprise/plugins/dot.py:147 +#: apprise/plugins/dot.py:161 msgid "Refresh Now" msgstr "" -#: apprise/plugins/dot.py:153 +#: apprise/plugins/dot.py:167 msgid "Text Signature" msgstr "" -#: apprise/plugins/dot.py:157 +#: apprise/plugins/dot.py:171 msgid "Icon Base64 (Text API)" msgstr "" -#: apprise/plugins/dot.py:161 +#: apprise/plugins/dot.py:175 msgid "Image Base64 (Image API)" msgstr "" -#: apprise/plugins/dot.py:166 +#: apprise/plugins/dot.py:180 msgid "Link" msgstr "" -#: apprise/plugins/dot.py:170 +#: apprise/plugins/dot.py:184 #, fuzzy msgid "Border" msgstr "Modal" -#: apprise/plugins/dot.py:177 +#: apprise/plugins/dot.py:191 msgid "Dither Type" msgstr "" -#: apprise/plugins/dot.py:183 +#: apprise/plugins/dot.py:197 msgid "Dither Kernel" msgstr "" +#: apprise/plugins/dot.py:203 +#, fuzzy +msgid "Task Key" +msgstr "Secret Key" + #: apprise/plugins/emby.py:112 msgid "Modal" msgstr "Modal" #: apprise/plugins/enigma2.py:130 apprise/plugins/gotify.py:134 -#: apprise/plugins/mattermost.py:163 apprise/plugins/notica.py:145 +#: apprise/plugins/mattermost.py:167 apprise/plugins/notica.py:145 msgid "Path" msgstr "" @@ -670,11 +743,33 @@ msgstr "" msgid "Server Timeout" msgstr "Server Timeout" +#: apprise/plugins/evolution.py:122 +#, fuzzy +msgid "Instance Name" +msgstr "Device ID" + +#: apprise/plugins/exotel.py:160 apprise/plugins/sinch.py:106 +#: apprise/plugins/twilio.py:140 +msgid "Account SID" +msgstr "Account SID" + +#: apprise/plugins/exotel.py:172 +#, fuzzy +msgid "From Phone No / Sender ID" +msgstr "From Phone No" + +#: apprise/plugins/exotel.py:233 apprise/plugins/jira.py:275 +#: apprise/plugins/mailgun.py:176 apprise/plugins/notificationapi.py:212 +#: apprise/plugins/opsgenie.py:273 apprise/plugins/pagerduty.py:176 +#: apprise/plugins/sparkpost.py:191 +msgid "Region Name" +msgstr "Region Name" + #: apprise/plugins/feishu.py:49 msgid "Feishu" msgstr "" -#: apprise/plugins/flock.py:99 apprise/plugins/twitter.py:150 +#: apprise/plugins/flock.py:99 apprise/plugins/twitter.py:175 msgid "Access Key" msgstr "Access Key" @@ -688,8 +783,10 @@ msgstr "To Channel ID" #: apprise/plugins/fcm/__init__.py:182 apprise/plugins/fcm/__init__.py:188 #: apprise/plugins/fluxer.py:195 apprise/plugins/lametric.py:510 -#: apprise/plugins/mattermost.py:223 apprise/plugins/notificationapi.py:209 -#: apprise/plugins/ntfy.py:296 apprise/plugins/vapid/__init__.py:169 +#: apprise/plugins/mattermost.py:224 apprise/plugins/notificationapi.py:218 +#: apprise/plugins/ntfy.py:296 apprise/plugins/office365.py:319 +#: apprise/plugins/sns.py:202 apprise/plugins/vapid/__init__.py:169 +#: apprise/plugins/webexteams.py:197 apprise/plugins/zoom.py:158 #, fuzzy msgid "Mode" msgstr "Modal" @@ -717,10 +814,15 @@ msgstr "Password" msgid "Free-Mobile" msgstr "" -#: apprise/plugins/glib.py:122 +#: apprise/plugins/glib.py:123 msgid "libdbus-1.so.x or libdbus-2.so.x must be installed." msgstr "" +#: apprise/plugins/glib.py:127 +#, fuzzy +msgid "GLib Notification" +msgstr "Duration" + #: apprise/plugins/gnome.py:100 msgid "A local Gnome environment is required." msgstr "" @@ -743,8 +845,22 @@ msgstr "Webhook Token" msgid "Thread Key" msgstr "Secret Key" +#: apprise/plugins/groupme.py:123 +#, fuzzy +msgid "Bot ID" +msgstr "Webhook ID" + +#: apprise/plugins/groupme.py:130 apprise/plugins/line.py:82 +#: apprise/plugins/mastodon.py:185 apprise/plugins/matrix/base.py:282 +#: apprise/plugins/misskey.py:122 apprise/plugins/pushbullet.py:83 +#: apprise/plugins/pushover.py:234 apprise/plugins/smseagle.py:146 +#: apprise/plugins/spugpush.py:68 apprise/plugins/streamlabs.py:105 +#: apprise/plugins/whatsapp.py:121 +msgid "Access Token" +msgstr "Access Token" + #: apprise/plugins/growl.py:169 apprise/plugins/mqtt.py:195 -#: apprise/plugins/msteams.py:206 apprise/plugins/nextcloud.py:163 +#: apprise/plugins/nextcloud.py:163 msgid "Version" msgstr "Version" @@ -752,15 +868,24 @@ msgstr "Version" msgid "Sticky" msgstr "" -#: apprise/plugins/home_assistant.py:99 +#: apprise/plugins/home_assistant.py:142 #, fuzzy msgid "Long-Lived Access Token" msgstr "Access Token" -#: apprise/plugins/home_assistant.py:113 +#: apprise/plugins/home_assistant.py:165 msgid "Notification ID" msgstr "" +#: apprise/plugins/home_assistant.py:182 +msgid "Path Prefix" +msgstr "" + +#: apprise/plugins/humhub.py:127 +#, fuzzy +msgid "Token or Username" +msgstr "Username" + #: apprise/plugins/ifttt.py:109 msgid "Events" msgstr "Events" @@ -773,6 +898,61 @@ msgstr "Add Tokens" msgid "Remove Tokens" msgstr "Remove Tokens" +#: apprise/plugins/jira.py:240 apprise/plugins/opsgenie.py:238 +#, fuzzy +msgid "Target Escalation" +msgstr "Target Chat ID" + +#: apprise/plugins/jira.py:246 apprise/plugins/opsgenie.py:244 +#, fuzzy +msgid "Target Schedule" +msgstr "Target Channel" + +#: apprise/plugins/irc/base.py:137 apprise/plugins/jira.py:252 +#: apprise/plugins/line.py:88 apprise/plugins/mastodon.py:196 +#: apprise/plugins/matrix/base.py:289 apprise/plugins/nextcloud.py:136 +#: apprise/plugins/one_signal.py:129 apprise/plugins/opsgenie.py:250 +#: apprise/plugins/pushed.py:95 apprise/plugins/rocketchat.py:156 +#: apprise/plugins/slack.py:261 apprise/plugins/twitter.py:187 +#: apprise/plugins/wechat.py:198 apprise/plugins/xmpp/base.py:123 +#: apprise/plugins/zulip.py:143 +msgid "Target User" +msgstr "Target User" + +#: apprise/plugins/jira.py:258 apprise/plugins/opsgenie.py:256 +#, fuzzy +msgid "Target Team" +msgstr "Target Email" + +#: apprise/plugins/jira.py:264 apprise/plugins/opsgenie.py:262 +#, fuzzy +msgid "Targets " +msgstr "Targets" + +#: apprise/plugins/jira.py:293 apprise/plugins/opsgenie.py:286 +msgid "Entity" +msgstr "" + +#: apprise/plugins/jira.py:297 apprise/plugins/opsgenie.py:290 +msgid "Alias" +msgstr "" + +#: apprise/plugins/jira.py:308 apprise/plugins/opsgenie.py:298 +#: apprise/plugins/pagertree.py:118 apprise/plugins/splunk.py:202 +#, fuzzy +msgid "Action" +msgstr "Duration" + +#: apprise/plugins/jira.py:319 apprise/plugins/opsgenie.py:317 +#, fuzzy +msgid "Details" +msgstr "Target Emails" + +#: apprise/plugins/jira.py:323 apprise/plugins/opsgenie.py:321 +#: apprise/plugins/splunk.py:213 +msgid "Action Mapping" +msgstr "" + #: apprise/plugins/join.py:147 msgid "Device ID" msgstr "Device ID" @@ -788,6 +968,11 @@ msgstr "Device ID" msgid "Source Phone No" msgstr "Target Phone No" +#: apprise/plugins/kodi.py:123 apprise/plugins/streamlabs.py:141 +#: apprise/plugins/windows.py:100 +msgid "Duration" +msgstr "Duration" + #: apprise/plugins/kumulos.py:99 #, fuzzy msgid "Server Key" @@ -834,23 +1019,6 @@ msgstr "" msgid "Bot Token" msgstr "Bot Token" -#: apprise/plugins/line.py:82 apprise/plugins/mastodon.py:173 -#: apprise/plugins/matrix.py:239 apprise/plugins/misskey.py:122 -#: apprise/plugins/pushbullet.py:83 apprise/plugins/pushover.py:194 -#: apprise/plugins/smseagle.py:146 apprise/plugins/spugpush.py:68 -#: apprise/plugins/streamlabs.py:105 apprise/plugins/whatsapp.py:99 -msgid "Access Token" -msgstr "Access Token" - -#: apprise/plugins/irc/base.py:137 apprise/plugins/line.py:88 -#: apprise/plugins/mastodon.py:184 apprise/plugins/matrix.py:244 -#: apprise/plugins/nextcloud.py:136 apprise/plugins/one_signal.py:129 -#: apprise/plugins/opsgenie.py:258 apprise/plugins/pushed.py:95 -#: apprise/plugins/rocketchat.py:155 apprise/plugins/slack.py:236 -#: apprise/plugins/twitter.py:162 apprise/plugins/zulip.py:143 -msgid "Target User" -msgstr "Target User" - #: apprise/plugins/macosx.py:65 msgid "" "Only works with Mac OS X 10.8 and higher. Additionally requires that /usr/" @@ -865,110 +1033,80 @@ msgstr "" msgid "Open/Click URL" msgstr "" -#: apprise/plugins/email/base.py:124 apprise/plugins/mailgun.py:141 -#: apprise/plugins/sendpulse.py:112 apprise/plugins/smtp2go.py:113 +#: apprise/plugins/email/base.py:148 apprise/plugins/mailgun.py:141 +#: apprise/plugins/sendpulse.py:115 apprise/plugins/smtp2go.py:113 #: apprise/plugins/sparkpost.py:164 msgid "Domain" msgstr "Domain" -#: apprise/plugins/email/base.py:160 apprise/plugins/mailgun.py:168 -#: apprise/plugins/resend.py:154 apprise/plugins/ses.py:201 -#: apprise/plugins/smtp2go.py:135 apprise/plugins/sparkpost.py:186 +#: apprise/plugins/email/base.py:179 apprise/plugins/mailgun.py:168 +#: apprise/plugins/postmark.py:144 apprise/plugins/resend.py:140 +#: apprise/plugins/ses.py:210 apprise/plugins/smtp2go.py:135 +#: apprise/plugins/sparkpost.py:186 msgid "From Name" msgstr "From Name" -#: apprise/plugins/mailgun.py:176 apprise/plugins/notificationapi.py:203 -#: apprise/plugins/opsgenie.py:281 apprise/plugins/pagerduty.py:176 -#: apprise/plugins/sparkpost.py:191 -msgid "Region Name" -msgstr "Region Name" - -#: apprise/plugins/email/base.py:209 apprise/plugins/mailgun.py:204 +#: apprise/plugins/email/base.py:253 apprise/plugins/mailgun.py:204 #: apprise/plugins/smtp2go.py:161 apprise/plugins/sparkpost.py:219 #, fuzzy msgid "Email Header" msgstr "HTTP Header" -#: apprise/plugins/mailgun.py:208 apprise/plugins/msteams.py:222 -#: apprise/plugins/notificationapi.py:246 apprise/plugins/sparkpost.py:223 -#: apprise/plugins/workflows.py:201 +#: apprise/plugins/mailgun.py:208 apprise/plugins/notificationapi.py:256 +#: apprise/plugins/slack.py:340 apprise/plugins/sparkpost.py:223 +#: apprise/plugins/workflows.py:202 #, fuzzy msgid "Template Tokens" msgstr "Remove Tokens" -#: apprise/plugins/mastodon.py:204 apprise/plugins/misskey.py:143 +#: apprise/plugins/mastodon.py:216 apprise/plugins/misskey.py:143 msgid "Visibility" msgstr "" -#: apprise/plugins/mastodon.py:210 apprise/plugins/twitter.py:185 -msgid "Cache Results" -msgstr "" - -#: apprise/plugins/mastodon.py:220 -msgid "Sensitive Attachments" -msgstr "" - -#: apprise/plugins/mastodon.py:225 +#: apprise/plugins/mastodon.py:222 msgid "Spoiler Text" msgstr "" -#: apprise/plugins/mastodon.py:229 +#: apprise/plugins/mastodon.py:226 msgid "Idempotency-Key" msgstr "" -#: apprise/plugins/mastodon.py:233 +#: apprise/plugins/mastodon.py:230 msgid "Language Code" msgstr "" -#: apprise/plugins/matrix.py:250 apprise/plugins/rocketchat.py:161 -msgid "Target Room ID" -msgstr "Target Room ID" - -#: apprise/plugins/matrix.py:256 -msgid "Target Room Alias" -msgstr "Target Room Alias" - -#: apprise/plugins/matrix.py:279 -#, fuzzy -msgid "Server Discovery" -msgstr "Server Timeout" - -#: apprise/plugins/matrix.py:284 -msgid "Force Home Server on Room IDs" +#: apprise/plugins/mastodon.py:234 apprise/plugins/twitter.py:210 +msgid "Cache Results" msgstr "" -#: apprise/plugins/matrix.py:289 apprise/plugins/rocketchat.py:177 -#: apprise/plugins/ryver.py:118 -msgid "Webhook Mode" -msgstr "Webhook Mode" - -#: apprise/plugins/matrix.py:295 -msgid "Matrix API Verion" +#: apprise/plugins/mastodon.py:239 +msgid "Sensitive Attachments" msgstr "" -#: apprise/plugins/matrix.py:301 apprise/plugins/notificationapi.py:154 -msgid "Message Type" +#: apprise/plugins/mastodon.py:255 +msgid "Ping Users/Tags" msgstr "" -#: apprise/plugins/irc/base.py:128 apprise/plugins/mattermost.py:147 -#: apprise/plugins/xmpp/base.py:107 +#: apprise/plugins/irc/base.py:128 apprise/plugins/mattermost.py:151 +#: apprise/plugins/xmpp/base.py:112 #, fuzzy msgid "User" msgstr "Username" -#: apprise/plugins/irc/base.py:143 apprise/plugins/mattermost.py:173 +#: apprise/plugins/irc/base.py:143 apprise/plugins/mattermost.py:177 #: apprise/plugins/notifiarr.py:97 apprise/plugins/pushbullet.py:94 -#: apprise/plugins/pushed.py:101 apprise/plugins/rocketchat.py:149 -#: apprise/plugins/slack.py:242 apprise/plugins/twist.py:112 +#: apprise/plugins/pushed.py:101 apprise/plugins/rocketchat.py:150 +#: apprise/plugins/slack.py:267 apprise/plugins/twist.py:112 +#: apprise/plugins/xmpp/base.py:128 msgid "Target Channel" msgstr "Target Channel" -#: apprise/plugins/mattermost.py:179 apprise/plugins/twist.py:118 +#: apprise/plugins/mattermost.py:183 apprise/plugins/twist.py:118 #, fuzzy msgid "Target Channel ID" msgstr "Target Channel" -#: apprise/plugins/mqtt.py:169 +#: apprise/plugins/mqtt.py:171 #, fuzzy msgid "Target Queue" msgstr "Target User" @@ -977,8 +1115,9 @@ msgstr "Target User" msgid "QOS" msgstr "" -#: apprise/plugins/mqtt.py:201 apprise/plugins/notificationapi.py:161 -#: apprise/plugins/office365.py:130 apprise/plugins/sendpulse.py:117 +#: apprise/plugins/mqtt.py:201 apprise/plugins/notificationapi.py:166 +#: apprise/plugins/office365.py:269 apprise/plugins/ringcentral.py:192 +#: apprise/plugins/sendpulse.py:120 #, fuzzy msgid "Client ID" msgstr "Account SID" @@ -991,7 +1130,7 @@ msgstr "" msgid "Retain Messages" msgstr "" -#: apprise/plugins/msg91.py:102 apprise/plugins/sendpulse.py:161 +#: apprise/plugins/msg91.py:102 apprise/plugins/sendpulse.py:156 msgid "Template ID" msgstr "" @@ -1004,36 +1143,10 @@ msgstr "Application Key" msgid "Short URL" msgstr "" -#: apprise/plugins/msg91.py:148 apprise/plugins/whatsapp.py:168 +#: apprise/plugins/msg91.py:148 apprise/plugins/whatsapp.py:203 msgid "Template Mapping" msgstr "" -#: apprise/plugins/msteams.py:151 -#, fuzzy -msgid "Team Name" -msgstr "Bot Name" - -#: apprise/plugins/msteams.py:159 apprise/plugins/slack.py:203 -msgid "Token A" -msgstr "Token A" - -#: apprise/plugins/msteams.py:168 apprise/plugins/slack.py:211 -msgid "Token B" -msgstr "Token B" - -#: apprise/plugins/msteams.py:177 apprise/plugins/slack.py:219 -msgid "Token C" -msgstr "Token C" - -#: apprise/plugins/msteams.py:186 -#, fuzzy -msgid "Token D" -msgstr "Token C" - -#: apprise/plugins/msteams.py:212 apprise/plugins/workflows.py:180 -msgid "Template Path" -msgstr "" - #: apprise/plugins/nextcloud.py:169 apprise/plugins/nextcloudtalk.py:113 msgid "URL Prefix" msgstr "" @@ -1056,50 +1169,54 @@ msgstr "" msgid "Source" msgstr "Source JID" -#: apprise/plugins/notificationapi.py:166 apprise/plugins/office365.py:137 -#: apprise/plugins/sendpulse.py:124 +#: apprise/plugins/matrix/base.py:352 apprise/plugins/notificationapi.py:159 +msgid "Message Type" +msgstr "" + +#: apprise/plugins/notificationapi.py:171 apprise/plugins/office365.py:276 +#: apprise/plugins/ringcentral.py:199 apprise/plugins/sendpulse.py:127 #, fuzzy msgid "Client Secret" msgstr "Access Secret" -#: apprise/plugins/notificationapi.py:177 +#: apprise/plugins/notificationapi.py:182 #, fuzzy msgid "Target ID" msgstr "Target User" -#: apprise/plugins/notificationapi.py:182 +#: apprise/plugins/notificationapi.py:187 #, fuzzy msgid "Target SMS" msgstr "Targets" -#: apprise/plugins/notificationapi.py:198 +#: apprise/plugins/notificationapi.py:207 msgid "Channels" msgstr "Channels" -#: apprise/plugins/email/base.py:185 apprise/plugins/notificationapi.py:226 -#: apprise/plugins/resend.py:146 +#: apprise/plugins/email/base.py:196 apprise/plugins/notificationapi.py:223 +#: apprise/plugins/office365.py:315 apprise/plugins/resend.py:150 msgid "Reply To" msgstr "" -#: apprise/plugins/email/base.py:155 apprise/plugins/notificationapi.py:231 -#: apprise/plugins/sendpulse.py:147 apprise/plugins/ses.py:154 +#: apprise/plugins/email/base.py:174 apprise/plugins/notificationapi.py:228 +#: apprise/plugins/sendpulse.py:150 apprise/plugins/ses.py:163 msgid "From Email" msgstr "From Email" -#: apprise/plugins/fcm/__init__.py:153 apprise/plugins/notifico.py:124 +#: apprise/plugins/fcm/__init__.py:153 apprise/plugins/notifico.py:187 #, fuzzy msgid "Project ID" msgstr "Target JID" -#: apprise/plugins/notifico.py:133 +#: apprise/plugins/notifico.py:196 msgid "Message Hook" msgstr "" -#: apprise/plugins/notifico.py:148 +#: apprise/plugins/notifico.py:211 msgid "IRC Colors" msgstr "" -#: apprise/plugins/notifico.py:154 +#: apprise/plugins/notifico.py:217 msgid "Prefix" msgstr "" @@ -1134,16 +1251,42 @@ msgstr "Duration" msgid "Authentication Type" msgstr "Authorization Token" -#: apprise/plugins/office365.py:118 +#: apprise/plugins/octopush.py:130 +#, fuzzy +msgid "API Login" +msgstr "API Key" + +#: apprise/plugins/octopush.py:142 +#, fuzzy +msgid "Sender" +msgstr "To User ID" + +#: apprise/plugins/octopush.py:178 +msgid "Accept Replies" +msgstr "" + +#: apprise/plugins/octopush.py:183 +msgid "Purpose" +msgstr "" + +#: apprise/plugins/octopush.py:189 +msgid "Type" +msgstr "" + +#: apprise/plugins/office365.py:257 #, fuzzy msgid "Tenant Domain" msgstr "Domain" -#: apprise/plugins/office365.py:125 +#: apprise/plugins/office365.py:264 msgid "Account Email or Object ID" msgstr "" -#: apprise/plugins/one_signal.py:108 apprise/plugins/sendgrid.py:157 +#: apprise/plugins/office365.py:325 +msgid "Save to Sent Items" +msgstr "" + +#: apprise/plugins/one_signal.py:108 apprise/plugins/sendgrid.py:146 msgid "Template" msgstr "" @@ -1157,23 +1300,23 @@ msgstr "Target Tag ID" msgid "Include Segment" msgstr "Include Image" -#: apprise/plugins/one_signal.py:166 -msgid "Enable Contents" -msgstr "" - -#: apprise/plugins/one_signal.py:172 -msgid "Decode Template Args" -msgstr "" - -#: apprise/plugins/one_signal.py:181 +#: apprise/plugins/one_signal.py:155 msgid "Subtitle" msgstr "" -#: apprise/plugins/one_signal.py:185 apprise/plugins/sfr.py:125 -#: apprise/plugins/whatsapp.py:130 +#: apprise/plugins/one_signal.py:159 apprise/plugins/sfr.py:125 +#: apprise/plugins/whatsapp.py:141 msgid "Language" msgstr "" +#: apprise/plugins/one_signal.py:170 +msgid "Enable Contents" +msgstr "" + +#: apprise/plugins/one_signal.py:176 +msgid "Decode Template Args" +msgstr "" + #: apprise/plugins/one_signal.py:195 msgid "Custom Data" msgstr "" @@ -1182,49 +1325,6 @@ msgstr "" msgid "Postback Data" msgstr "" -#: apprise/plugins/opsgenie.py:246 -#, fuzzy -msgid "Target Escalation" -msgstr "Target Chat ID" - -#: apprise/plugins/opsgenie.py:252 -#, fuzzy -msgid "Target Schedule" -msgstr "Target Channel" - -#: apprise/plugins/opsgenie.py:264 -#, fuzzy -msgid "Target Team" -msgstr "Target Email" - -#: apprise/plugins/opsgenie.py:270 -#, fuzzy -msgid "Targets " -msgstr "Targets" - -#: apprise/plugins/opsgenie.py:299 -msgid "Entity" -msgstr "" - -#: apprise/plugins/opsgenie.py:303 -msgid "Alias" -msgstr "" - -#: apprise/plugins/opsgenie.py:314 apprise/plugins/pagertree.py:118 -#: apprise/plugins/splunk.py:202 -#, fuzzy -msgid "Action" -msgstr "Duration" - -#: apprise/plugins/opsgenie.py:325 -#, fuzzy -msgid "Details" -msgstr "Target Emails" - -#: apprise/plugins/opsgenie.py:329 apprise/plugins/splunk.py:213 -msgid "Action Mapping" -msgstr "" - #: apprise/plugins/pagerduty.py:138 apprise/plugins/spike.py:68 #, fuzzy msgid "Integration Key" @@ -1301,36 +1401,59 @@ msgstr "Application Secret" msgid "Secret Key" msgstr "Secret Key" -#: apprise/plugins/pushme.py:81 apprise/plugins/signal_api.py:160 -#: apprise/plugins/smseagle.py:195 +#: apprise/plugins/pushme.py:81 apprise/plugins/signal_api.py:152 +#: apprise/plugins/smseagle.py:193 msgid "Show Status" msgstr "" -#: apprise/plugins/pushover.py:188 +#: apprise/plugins/pushover.py:228 msgid "User Key" msgstr "User Key" -#: apprise/plugins/pushover.py:234 +#: apprise/plugins/pushover.py:280 msgid "URL Title" msgstr "" -#: apprise/plugins/pushover.py:239 -msgid "Retry" +#: apprise/plugins/pushover.py:285 +msgid "Emergency Retry Interval" msgstr "" -#: apprise/plugins/pushover.py:245 +#: apprise/plugins/pushover.py:291 msgid "Expire" msgstr "" -#: apprise/plugins/pushplus.py:48 +#: apprise/plugins/pushover.py:301 +#, fuzzy +msgid "Encryption Key" +msgstr "Application Key" + +#: apprise/plugins/pushover.py:308 +msgid "E2EE" +msgstr "" + +#: apprise/plugins/pushplus.py:172 msgid "Pushplus" msgstr "" -#: apprise/plugins/pushplus.py:68 apprise/plugins/qq.py:66 +#: apprise/plugins/pushplus.py:210 apprise/plugins/qq.py:66 #, fuzzy msgid "User Token" msgstr "User Key" +#: apprise/plugins/pushplus.py:219 +msgid "Group Topics" +msgstr "" + +#: apprise/plugins/pushplus.py:240 +#, fuzzy +msgid "Channel" +msgstr "Channels" + +#: apprise/plugins/pushplus.py:257 +#, fuzzy +msgid "Webhook Name" +msgstr "Webhook Mode" + #: apprise/plugins/pushsafer.py:360 #, fuzzy msgid "Private Key" @@ -1347,7 +1470,7 @@ msgid "Secret API Key" msgstr "Secret Key" #: apprise/plugins/fcm/__init__.py:162 apprise/plugins/pushy.py:91 -#: apprise/plugins/sns.py:131 apprise/plugins/wxpusher.py:128 +#: apprise/plugins/sns.py:163 apprise/plugins/wxpusher.py:128 msgid "Target Topic" msgstr "Target Topic" @@ -1365,52 +1488,81 @@ msgstr "Application Key" msgid "Target Subreddit" msgstr "Target User" -#: apprise/plugins/reddit.py:185 +#: apprise/plugins/reddit.py:182 msgid "Kind" msgstr "" -#: apprise/plugins/reddit.py:191 +#: apprise/plugins/reddit.py:188 msgid "Flair ID" msgstr "" -#: apprise/plugins/reddit.py:196 +#: apprise/plugins/reddit.py:193 msgid "Flair Text" msgstr "" -#: apprise/plugins/reddit.py:201 +#: apprise/plugins/reddit.py:198 msgid "NSFW" msgstr "" -#: apprise/plugins/reddit.py:207 +#: apprise/plugins/reddit.py:204 msgid "Is Ad?" msgstr "" -#: apprise/plugins/reddit.py:213 +#: apprise/plugins/reddit.py:210 msgid "Send Replies" msgstr "" -#: apprise/plugins/reddit.py:219 +#: apprise/plugins/reddit.py:216 msgid "Is Spoiler" msgstr "" -#: apprise/plugins/reddit.py:225 +#: apprise/plugins/reddit.py:222 msgid "Resubmit Flag" msgstr "" +#: apprise/plugins/resend.py:135 +#, fuzzy +msgid "From Address" +msgstr "From Name" + #: apprise/plugins/revolt.py:104 #, fuzzy msgid "Channel ID" msgstr "To Channel ID" -#: apprise/plugins/revolt.py:130 +#: apprise/plugins/revolt.py:131 msgid "Embed URL" msgstr "" -#: apprise/plugins/rocketchat.py:145 +#: apprise/plugins/ringcentral.py:186 +#, fuzzy +msgid "Token / Password" +msgstr "Password" + +#: apprise/plugins/ringcentral.py:239 +#, fuzzy +msgid "Environment" +msgstr "Events" + +#: apprise/plugins/ringcentral.py:252 +#, fuzzy +msgid "Authentication Mode" +msgstr "Application Key" + +#: apprise/plugins/rocketchat.py:146 msgid "Webhook" msgstr "Webhook" -#: apprise/plugins/rocketchat.py:182 +#: apprise/plugins/matrix/base.py:295 apprise/plugins/rocketchat.py:162 +msgid "Target Room ID" +msgstr "Target Room ID" + +#: apprise/plugins/matrix/base.py:334 apprise/plugins/rocketchat.py:178 +#: apprise/plugins/ryver.py:118 +msgid "Webhook Mode" +msgstr "Webhook Mode" + +#: apprise/plugins/rocketchat.py:183 msgid "Use Avatar" msgstr "Use Avatar" @@ -1426,33 +1578,38 @@ msgstr "" msgid "Organization" msgstr "Organization" -#: apprise/plugins/sendgrid.py:166 apprise/plugins/sendpulse.py:175 +#: apprise/plugins/sendgrid.py:166 apprise/plugins/sendpulse.py:182 msgid "Template Data" msgstr "" -#: apprise/plugins/ses.py:160 apprise/plugins/sns.py:106 +#: apprise/plugins/ses.py:157 apprise/plugins/sns.py:132 +#, fuzzy +msgid "Session Token" +msgstr "Access Token" + +#: apprise/plugins/ses.py:169 apprise/plugins/sns.py:138 msgid "Access Key ID" msgstr "Access Key ID" -#: apprise/plugins/ses.py:166 apprise/plugins/sns.py:112 +#: apprise/plugins/ses.py:175 apprise/plugins/sns.py:144 msgid "Secret Access Key" msgstr "Secret Access Key" -#: apprise/plugins/ses.py:172 apprise/plugins/sinch.py:160 -#: apprise/plugins/sns.py:118 +#: apprise/plugins/ses.py:181 apprise/plugins/sinch.py:157 +#: apprise/plugins/sns.py:150 msgid "Region" msgstr "Region" -#: apprise/plugins/ses.py:179 apprise/plugins/smtp2go.py:124 +#: apprise/plugins/ses.py:188 apprise/plugins/smtp2go.py:124 #: apprise/plugins/sparkpost.py:175 msgid "Target Emails" msgstr "Target Emails" -#: apprise/plugins/seven.py:115 apprise/plugins/smseagle.py:205 +#: apprise/plugins/seven.py:113 apprise/plugins/smseagle.py:203 msgid "Flash" msgstr "" -#: apprise/plugins/seven.py:119 +#: apprise/plugins/seven.py:117 msgid "Label" msgstr "" @@ -1499,34 +1656,35 @@ msgid "TTS Voice" msgstr "Target Device" #: apprise/plugins/signal_api.py:131 apprise/plugins/smseagle.py:158 +#: apprise/plugins/whatsapp.py:159 #, fuzzy msgid "Target Group ID" msgstr "Target Room ID" -#: apprise/plugins/signl4.py:86 +#: apprise/plugins/signl4.py:89 #, fuzzy msgid "Service" msgstr "Device ID" -#: apprise/plugins/signl4.py:90 +#: apprise/plugins/signl4.py:93 #, fuzzy msgid "Location" msgstr "Duration" -#: apprise/plugins/signl4.py:94 +#: apprise/plugins/signl4.py:97 msgid "Alerting Scenario" msgstr "" -#: apprise/plugins/signl4.py:98 +#: apprise/plugins/signl4.py:101 msgid "Filtering" msgstr "" -#: apprise/plugins/signl4.py:103 +#: apprise/plugins/signl4.py:106 #, fuzzy msgid "External ID" msgstr "To User ID" -#: apprise/plugins/signl4.py:107 +#: apprise/plugins/signl4.py:110 #, fuzzy msgid "Status" msgstr "Targets" @@ -1540,42 +1698,59 @@ msgstr "" msgid "Event" msgstr "Events" -#: apprise/plugins/sinch.py:106 apprise/plugins/twilio.py:140 -msgid "Account SID" -msgstr "Account SID" - #: apprise/plugins/sinch.py:134 apprise/plugins/twilio.py:168 msgid "Target Short Code" msgstr "Target Short Code" -#: apprise/plugins/slack.py:194 +#: apprise/plugins/slack.py:216 #, fuzzy msgid "OAuth Access Token" msgstr "Access Token" #: apprise/plugins/slack.py:225 +msgid "Token A" +msgstr "Token A" + +#: apprise/plugins/slack.py:234 +msgid "Token B" +msgstr "Token B" + +#: apprise/plugins/slack.py:243 +msgid "Token C" +msgstr "Token C" + +#: apprise/plugins/slack.py:250 msgid "Target Encoded ID" msgstr "Target Encoded ID" -#: apprise/plugins/slack.py:265 +#: apprise/plugins/slack.py:279 +#, fuzzy +msgid "Workflow Path" +msgstr "Overflow Mode" + +#: apprise/plugins/slack.py:297 #, fuzzy msgid "Include Footer" msgstr "Include Image" -#: apprise/plugins/slack.py:273 +#: apprise/plugins/slack.py:305 msgid "Use Blocks" msgstr "" -#: apprise/plugins/slack.py:282 +#: apprise/plugins/slack.py:311 #, fuzzy msgid "Include Timestamp" msgstr "Include Image" -#: apprise/plugins/slack.py:288 apprise/plugins/twitter.py:179 +#: apprise/plugins/slack.py:317 apprise/plugins/twitter.py:204 #, fuzzy msgid "Message Mode" msgstr "Secure Mode" +#: apprise/plugins/slack.py:330 apprise/plugins/workflows.py:181 +msgid "Template Path" +msgstr "" + #: apprise/plugins/smpp.py:61 msgid "SMPP" msgstr "" @@ -1590,14 +1765,29 @@ msgstr "Hostname" msgid "Target Contact" msgstr "Target Chat ID" -#: apprise/plugins/smseagle.py:200 +#: apprise/plugins/smseagle.py:198 msgid "Test Only" msgstr "" -#: apprise/plugins/smsmanager.py:146 +#: apprise/plugins/smsmanager.py:143 msgid "Gateway" msgstr "" +#: apprise/plugins/sogs.py:219 +#, fuzzy +msgid "Public Key" +msgstr "API Key" + +#: apprise/plugins/sogs.py:226 +#, fuzzy +msgid "Seed" +msgstr "To User ID" + +#: apprise/plugins/sogs.py:233 +#, fuzzy +msgid "Rooms" +msgstr "Target Room ID" + #: apprise/plugins/spike.py:48 msgid "Spike.sh" msgstr "" @@ -1632,11 +1822,6 @@ msgstr "" msgid "Sound Link" msgstr "Sound" -#: apprise/plugins/streamlabs.py:141 apprise/plugins/windows.py:100 -#: apprise/plugins/xbmc.py:123 -msgid "Duration" -msgstr "Duration" - #: apprise/plugins/streamlabs.py:147 msgid "Special Text Color" msgstr "" @@ -1705,19 +1890,19 @@ msgstr "" msgid "Target Threema ID" msgstr "Target Tag ID" -#: apprise/plugins/twilio.py:203 +#: apprise/plugins/twilio.py:200 msgid "Notification Method: sms or call" msgstr "" -#: apprise/plugins/twitter.py:138 +#: apprise/plugins/twitter.py:163 msgid "Consumer Key" msgstr "Consumer Key" -#: apprise/plugins/twitter.py:144 +#: apprise/plugins/twitter.py:169 msgid "Consumer Secret" msgstr "Consumer Secret" -#: apprise/plugins/twitter.py:156 +#: apprise/plugins/twitter.py:181 msgid "Access Secret" msgstr "Access Secret" @@ -1734,7 +1919,7 @@ msgstr "Application Key" msgid "Receiver IDs" msgstr "" -#: apprise/plugins/viber.py:101 +#: apprise/plugins/viber.py:105 #, fuzzy msgid "Bot Avatar URL" msgstr "Avatar Image" @@ -1744,20 +1929,54 @@ msgstr "Avatar Image" msgid "User Email" msgstr "From Email" -#: apprise/plugins/vapid/__init__.py:179 apprise/plugins/vonage.py:136 +#: apprise/plugins/vapid/__init__.py:179 apprise/plugins/vonage.py:133 msgid "ttl" msgstr "" +#: apprise/plugins/webexteams.py:178 +#, fuzzy +msgid "Bot Access Token" +msgstr "Access Token" + +#: apprise/plugins/webexteams.py:184 +#, fuzzy +msgid "Room IDs" +msgstr "Target Room ID" + +#: apprise/plugins/wechat.py:174 +msgid "Corp ID" +msgstr "" + +#: apprise/plugins/wechat.py:181 +#, fuzzy +msgid "App Secret" +msgstr "Application Secret" + +#: apprise/plugins/wechat.py:189 +#, fuzzy +msgid "Agent ID" +msgstr "Target User" + +#: apprise/plugins/wechat.py:205 +#, fuzzy +msgid "Target Department" +msgstr "Target Device" + +#: apprise/plugins/wechat.py:212 +#, fuzzy +msgid "Target Tag" +msgstr "Target Email" + #: apprise/plugins/wecombot.py:99 #, fuzzy msgid "Bot Webhook Key" msgstr "Webhook Token" -#: apprise/plugins/whatsapp.py:106 +#: apprise/plugins/whatsapp.py:128 msgid "Template Name" msgstr "" -#: apprise/plugins/whatsapp.py:112 +#: apprise/plugins/whatsapp.py:134 #, fuzzy msgid "From Phone ID" msgstr "From Phone No" @@ -1766,24 +1985,24 @@ msgstr "From Phone No" msgid "A local Microsoft Windows environment is required." msgstr "" -#: apprise/plugins/workflows.py:137 +#: apprise/plugins/workflows.py:138 #, fuzzy msgid "Workflow ID" msgstr "Overflow Mode" -#: apprise/plugins/workflows.py:145 +#: apprise/plugins/workflows.py:146 msgid "Signature" msgstr "" -#: apprise/plugins/workflows.py:168 +#: apprise/plugins/workflows.py:169 msgid "Use Power Automate URL" msgstr "" -#: apprise/plugins/workflows.py:175 +#: apprise/plugins/workflows.py:176 msgid "Wrap Text" msgstr "" -#: apprise/plugins/workflows.py:190 +#: apprise/plugins/workflows.py:191 #, fuzzy msgid "API Version" msgstr "Version" @@ -1798,31 +2017,46 @@ msgstr "Auth Token" msgid "Target User ID" msgstr "Target User" +#: apprise/plugins/zoom.py:145 +#, fuzzy +msgid "Verification Token" +msgstr "Application Key" + #: apprise/plugins/zulip.py:148 #, fuzzy msgid "Target Stream" msgstr "Target User" -#: apprise/plugins/email/base.py:150 -msgid "To Email" -msgstr "To Email" - -#: apprise/plugins/email/base.py:173 +#: apprise/plugins/email/base.py:184 msgid "SMTP Server" msgstr "SMTP Server" -#: apprise/plugins/email/base.py:178 apprise/plugins/xmpp/base.py:129 +#: apprise/plugins/email/base.py:189 apprise/plugins/xmpp/base.py:149 msgid "Secure Mode" msgstr "Secure Mode" -#: apprise/plugins/email/base.py:190 -msgid "PGP Encryption" -msgstr "" +#: apprise/plugins/email/base.py:201 +#, fuzzy +msgid "PGP Security Mode" +msgstr "Secure Mode" -#: apprise/plugins/email/base.py:196 +#: apprise/plugins/email/base.py:208 msgid "PGP Public Key Path" msgstr "" +#: apprise/plugins/email/base.py:221 +#, fuzzy +msgid "PGP Private Key Path" +msgstr "Provider Key" + +#: apprise/plugins/email/base.py:229 +msgid "Web Key Directory" +msgstr "" + +#: apprise/plugins/email/base.py:235 +msgid "To Email" +msgstr "To Email" + #: apprise/plugins/fcm/__init__.py:148 msgid "OAuth2 KeyFile" msgstr "" @@ -1859,6 +2093,32 @@ msgstr "Channels" msgid "Auth Mode" msgstr "Webhook Mode" +#: apprise/plugins/matrix/base.py:301 +msgid "Target Room Alias" +msgstr "Target Room Alias" + +#: apprise/plugins/matrix/base.py:324 +#, fuzzy +msgid "Server Discovery" +msgstr "Server Timeout" + +#: apprise/plugins/matrix/base.py:329 +msgid "Force Home Server on Room IDs" +msgstr "" + +#: apprise/plugins/matrix/base.py:340 +#, fuzzy +msgid "Webhook Path" +msgstr "Webhook" + +#: apprise/plugins/matrix/base.py:346 +msgid "Matrix API Verion" +msgstr "" + +#: apprise/plugins/matrix/base.py:358 +msgid "End-to-End Encryption" +msgstr "" + #: apprise/plugins/vapid/__init__.py:193 msgid "PEM Private KeyFile" msgstr "" @@ -1867,16 +2127,30 @@ msgstr "" msgid "Subscripion File" msgstr "" -#: apprise/plugins/xmpp/base.py:136 +#: apprise/plugins/xmpp/base.py:144 +#, fuzzy +msgid "XMPP Server" +msgstr "SMTP Server" + +#: apprise/plugins/xmpp/base.py:156 #, fuzzy msgid "Get Roster" msgstr "Target User" -#: apprise/plugins/xmpp/base.py:141 +#: apprise/plugins/xmpp/base.py:161 msgid "Use Subject" msgstr "" -#: apprise/plugins/xmpp/base.py:146 +#: apprise/plugins/xmpp/base.py:166 #, fuzzy msgid "Keep Connection Alive" msgstr "Server Timeout" + +#: apprise/plugins/xmpp/base.py:172 +#, fuzzy +msgid "MUC Nickname" +msgstr "Username" + +#: apprise/plugins/xmpp/base.py:176 +msgid "SCRAM-PLUS Channel Binding" +msgstr "" diff --git a/libs/apprise/i18n/es/LC_MESSAGES/apprise.mo b/libs/apprise/i18n/es/LC_MESSAGES/apprise.mo new file mode 100644 index 000000000..b5dc3d03e Binary files /dev/null and b/libs/apprise/i18n/es/LC_MESSAGES/apprise.mo differ diff --git a/libs/apprise/i18n/es/LC_MESSAGES/apprise.po b/libs/apprise/i18n/es/LC_MESSAGES/apprise.po new file mode 100644 index 000000000..b93f79def --- /dev/null +++ b/libs/apprise/i18n/es/LC_MESSAGES/apprise.po @@ -0,0 +1,2162 @@ +# Spanish translations for apprise. +# Copyright (C) 2026 Chris Caron +# This file is distributed under the same license as the apprise project. +# Chris Caron , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: apprise 1.11.0\n" +"Report-Msgid-Bugs-To: lead2gold@gmail.com\n" +"POT-Creation-Date: 2026-05-29 13:49-0400\n" +"PO-Revision-Date: 2019-05-24 20:00-0400\n" +"Last-Translator: Chris Caron \n" +"Language: es\n" +"Language-Team: es \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: apprise/attachment/base.py:96 apprise/url.py:148 +msgid "Verify SSL" +msgstr "Verificar SSL" + +#: apprise/url.py:158 +msgid "Follow Redirects" +msgstr "" + +#: apprise/url.py:168 +#, fuzzy +msgid "Socket Read Timeout" +msgstr "Tiempo de Espera de Lectura del Socket" + +#: apprise/url.py:182 +#, fuzzy +msgid "Socket Connect Timeout" +msgstr "Tiempo de Espera de Conexion del Socket" + +#: apprise/attachment/base.py:82 +msgid "Cache Age" +msgstr "Antiguedad del Cache" + +#: apprise/attachment/base.py:88 +msgid "Forced Mime Type" +msgstr "Tipo MIME Forzado" + +#: apprise/attachment/base.py:92 +msgid "Forced File Name" +msgstr "Nombre de Archivo Forzado" + +#: apprise/attachment/file.py:41 apprise/config/file.py:41 +msgid "Local File" +msgstr "Archivo Local" + +#: apprise/attachment/http.py:46 apprise/config/http.py:54 +msgid "Web Based" +msgstr "Basado en Web" + +#: apprise/attachment/memory.py:44 apprise/config/memory.py:37 +msgid "Memory" +msgstr "Memoria" + +#: apprise/plugins/__init__.py:278 +msgid "Schema" +msgstr "Esquema" + +#: apprise/plugins/__init__.py:398 +msgid "No dependencies." +msgstr "Sin dependencias." + +#: apprise/plugins/__init__.py:401 +msgid "Packages are required to function." +msgstr "Se requieren paquetes para funcionar." + +#: apprise/plugins/__init__.py:405 +msgid "Packages are recommended to improve functionality." +msgstr "Se recomiendan paquetes para mejorar la funcionalidad." + +#: apprise/plugins/africas_talking.py:132 +#, fuzzy +msgid "App User Name" +msgstr "Nombre de Usuario de la Aplicacion" + +#: apprise/plugins/africas_talking.py:138 apprise/plugins/brevo.py:157 +#: apprise/plugins/burstsms.py:104 apprise/plugins/clicksend.py:98 +#: apprise/plugins/dot.py:136 apprise/plugins/evolution.py:105 +#: apprise/plugins/exotel.py:213 apprise/plugins/fcm/__init__.py:143 +#: apprise/plugins/httpsms.py:77 apprise/plugins/jira.py:230 +#: apprise/plugins/join.py:140 apprise/plugins/kavenegar.py:115 +#: apprise/plugins/kumulos.py:87 apprise/plugins/mailgun.py:146 +#: apprise/plugins/messagebird.py:78 apprise/plugins/octopush.py:136 +#: apprise/plugins/one_signal.py:113 apprise/plugins/opsgenie.py:228 +#: apprise/plugins/pagerduty.py:131 apprise/plugins/popcorn_notify.py:71 +#: apprise/plugins/postmark.py:117 apprise/plugins/prowl.py:121 +#: apprise/plugins/resend.py:107 apprise/plugins/resend.py:145 +#: apprise/plugins/sendgrid.py:116 apprise/plugins/seven.py:75 +#: apprise/plugins/simplepush.py:101 apprise/plugins/smsmanager.py:106 +#: apprise/plugins/smtp2go.py:118 apprise/plugins/sparkpost.py:169 +#: apprise/plugins/splunk.py:165 apprise/plugins/techuluspush.py:97 +#: apprise/plugins/twilio.py:194 apprise/plugins/vapid/__init__.py:152 +#: apprise/plugins/vonage.py:80 +msgid "API Key" +msgstr "Clave de API" + +#: apprise/plugins/africas_talking.py:145 apprise/plugins/fortysixelks.py:106 +#, fuzzy +msgid "Target Phone" +msgstr "Telefono de Destino" + +#: apprise/plugins/africas_talking.py:150 apprise/plugins/aprs.py:187 +#: apprise/plugins/bark.py:159 apprise/plugins/brevo.py:174 +#: apprise/plugins/bulksms.py:137 apprise/plugins/bulkvs.py:110 +#: apprise/plugins/burstsms.py:131 apprise/plugins/clickatell.py:90 +#: apprise/plugins/clicksend.py:112 apprise/plugins/d7networks.py:110 +#: apprise/plugins/dapnet.py:138 apprise/plugins/dingtalk.py:111 +#: apprise/plugins/email/base.py:164 apprise/plugins/evolution.py:134 +#: apprise/plugins/exotel.py:190 apprise/plugins/fcm/__init__.py:168 +#: apprise/plugins/flock.py:125 apprise/plugins/fortysixelks.py:111 +#: apprise/plugins/home_assistant.py:153 apprise/plugins/httpsms.py:97 +#: apprise/plugins/humhub.py:149 apprise/plugins/irc/base.py:149 +#: apprise/plugins/join.py:172 apprise/plugins/kavenegar.py:135 +#: apprise/plugins/line.py:93 apprise/plugins/mailgun.py:157 +#: apprise/plugins/mastodon.py:202 apprise/plugins/matrix/base.py:307 +#: apprise/plugins/mattermost.py:189 apprise/plugins/messagebird.py:99 +#: apprise/plugins/mqtt.py:177 apprise/plugins/msg91.py:123 +#: apprise/plugins/nextcloud.py:148 apprise/plugins/nextcloudtalk.py:101 +#: apprise/plugins/notifiarr.py:103 apprise/plugins/notificationapi.py:192 +#: apprise/plugins/ntfy.py:241 apprise/plugins/octopush.py:153 +#: apprise/plugins/office365.py:287 apprise/plugins/one_signal.py:141 +#: apprise/plugins/plivo.py:114 apprise/plugins/popcorn_notify.py:89 +#: apprise/plugins/postmark.py:133 apprise/plugins/pushbullet.py:105 +#: apprise/plugins/pushed.py:107 apprise/plugins/pushover.py:252 +#: apprise/plugins/pushsafer.py:376 apprise/plugins/pushy.py:97 +#: apprise/plugins/reddit.py:170 apprise/plugins/resend.py:124 +#: apprise/plugins/revolt.py:112 apprise/plugins/ringcentral.py:219 +#: apprise/plugins/rocketchat.py:167 apprise/plugins/sendgrid.py:133 +#: apprise/plugins/sendpulse.py:139 apprise/plugins/seven.py:88 +#: apprise/plugins/sfr.py:113 apprise/plugins/signal_api.py:138 +#: apprise/plugins/sinch.py:140 apprise/plugins/slack.py:273 +#: apprise/plugins/smpp.py:128 apprise/plugins/smseagle.py:172 +#: apprise/plugins/smsmanager.py:119 apprise/plugins/sns.py:170 +#: apprise/plugins/telegram.py:359 apprise/plugins/threema.py:115 +#: apprise/plugins/twilio.py:174 apprise/plugins/twist.py:123 +#: apprise/plugins/twitter.py:193 apprise/plugins/vapid/__init__.py:158 +#: apprise/plugins/voipms.py:108 apprise/plugins/vonage.py:108 +#: apprise/plugins/wechat.py:218 apprise/plugins/whatsapp.py:166 +#: apprise/plugins/wxpusher.py:139 apprise/plugins/xmpp/base.py:134 +#: apprise/plugins/zulip.py:153 +msgid "Targets" +msgstr "Destinatarios" + +#: apprise/plugins/africas_talking.py:166 +#, fuzzy +msgid "From" +msgstr "De" + +#: apprise/plugins/africas_talking.py:172 +#, fuzzy +msgid "SMS Mode" +msgstr "Modo SMS" + +#: apprise/plugins/africas_talking.py:181 apprise/plugins/bulksms.py:170 +#: apprise/plugins/bulkvs.py:131 apprise/plugins/burstsms.py:166 +#: apprise/plugins/clicksend.py:130 apprise/plugins/d7networks.py:144 +#: apprise/plugins/dapnet.py:167 apprise/plugins/exotel.py:222 +#: apprise/plugins/home_assistant.py:170 apprise/plugins/jira.py:282 +#: apprise/plugins/mailgun.py:194 apprise/plugins/mastodon.py:247 +#: apprise/plugins/octopush.py:173 apprise/plugins/one_signal.py:185 +#: apprise/plugins/opsgenie.py:307 apprise/plugins/plivo.py:137 +#: apprise/plugins/popcorn_notify.py:104 apprise/plugins/signal_api.py:160 +#: apprise/plugins/smseagle.py:211 apprise/plugins/smsmanager.py:152 +#: apprise/plugins/smtp2go.py:151 apprise/plugins/sparkpost.py:209 +#: apprise/plugins/twitter.py:218 +#, fuzzy +msgid "Batch Mode" +msgstr "Modo por Lotes" + +#: apprise/plugins/apprise_api.py:102 apprise/plugins/bark.py:134 +#: apprise/plugins/custom_form.py:130 apprise/plugins/custom_json.py:112 +#: apprise/plugins/custom_xml.py:112 apprise/plugins/emby.py:85 +#: apprise/plugins/enigma2.py:110 apprise/plugins/evolution.py:111 +#: apprise/plugins/fluxer.py:159 apprise/plugins/gotify.py:129 +#: apprise/plugins/growl.py:140 apprise/plugins/home_assistant.py:122 +#: apprise/plugins/humhub.py:138 apprise/plugins/irc/base.py:117 +#: apprise/plugins/kodi.py:96 apprise/plugins/lametric.py:460 +#: apprise/plugins/mastodon.py:180 apprise/plugins/matrix/base.py:262 +#: apprise/plugins/mattermost.py:155 apprise/plugins/misskey.py:117 +#: apprise/plugins/mqtt.py:149 apprise/plugins/nextcloud.py:116 +#: apprise/plugins/nextcloudtalk.py:74 apprise/plugins/notica.py:126 +#: apprise/plugins/notifico.py:166 apprise/plugins/ntfy.py:211 +#: apprise/plugins/parseplatform.py:90 apprise/plugins/pushdeer.py:75 +#: apprise/plugins/pushjet.py:71 apprise/plugins/rocketchat.py:120 +#: apprise/plugins/rsyslog.py:180 apprise/plugins/signal_api.py:97 +#: apprise/plugins/smseagle.py:135 apprise/plugins/sogs.py:207 +#: apprise/plugins/synology.py:83 apprise/plugins/workflows.py:126 +#: apprise/plugins/xmpp/base.py:101 +msgid "Hostname" +msgstr "Nombre del Host" + +#: apprise/plugins/apprise_api.py:107 apprise/plugins/bark.py:139 +#: apprise/plugins/custom_form.py:135 apprise/plugins/custom_json.py:117 +#: apprise/plugins/custom_xml.py:117 apprise/plugins/email/base.py:153 +#: apprise/plugins/emby.py:90 apprise/plugins/enigma2.py:115 +#: apprise/plugins/evolution.py:116 apprise/plugins/fluxer.py:163 +#: apprise/plugins/gotify.py:140 apprise/plugins/growl.py:145 +#: apprise/plugins/home_assistant.py:127 apprise/plugins/humhub.py:143 +#: apprise/plugins/irc/base.py:122 apprise/plugins/kodi.py:101 +#: apprise/plugins/lametric.py:464 apprise/plugins/mastodon.py:190 +#: apprise/plugins/matrix/base.py:267 apprise/plugins/mattermost.py:171 +#: apprise/plugins/misskey.py:127 apprise/plugins/mqtt.py:154 +#: apprise/plugins/nextcloud.py:121 apprise/plugins/nextcloudtalk.py:79 +#: apprise/plugins/notica.py:130 apprise/plugins/notifico.py:170 +#: apprise/plugins/ntfy.py:215 apprise/plugins/parseplatform.py:95 +#: apprise/plugins/pushdeer.py:79 apprise/plugins/pushjet.py:76 +#: apprise/plugins/rocketchat.py:125 apprise/plugins/rsyslog.py:185 +#: apprise/plugins/signal_api.py:102 apprise/plugins/smpp.py:108 +#: apprise/plugins/smseagle.py:140 apprise/plugins/sogs.py:212 +#: apprise/plugins/synology.py:88 apprise/plugins/workflows.py:131 +#: apprise/plugins/xmpp/base.py:106 +msgid "Port" +msgstr "Puerto" + +#: apprise/plugins/apprise_api.py:113 apprise/plugins/bark.py:145 +#: apprise/plugins/bluesky.py:119 apprise/plugins/custom_form.py:141 +#: apprise/plugins/custom_json.py:123 apprise/plugins/custom_xml.py:123 +#: apprise/plugins/emby.py:97 apprise/plugins/enigma2.py:121 +#: apprise/plugins/freemobile.py:78 apprise/plugins/home_assistant.py:133 +#: apprise/plugins/jira.py:236 apprise/plugins/kodi.py:107 +#: apprise/plugins/lametric.py:471 apprise/plugins/matrix/base.py:273 +#: apprise/plugins/nextcloud.py:127 apprise/plugins/nextcloudtalk.py:85 +#: apprise/plugins/notica.py:136 apprise/plugins/notifico.py:176 +#: apprise/plugins/ntfy.py:221 apprise/plugins/opsgenie.py:234 +#: apprise/plugins/pushjet.py:88 apprise/plugins/rocketchat.py:131 +#: apprise/plugins/signal_api.py:108 apprise/plugins/smpp.py:92 +#: apprise/plugins/synology.py:94 +msgid "Username" +msgstr "Nombre de Usuario" + +#: apprise/plugins/apprise_api.py:117 apprise/plugins/aprs.py:172 +#: apprise/plugins/bark.py:149 apprise/plugins/bluesky.py:124 +#: apprise/plugins/bulksms.py:117 apprise/plugins/bulkvs.py:90 +#: apprise/plugins/custom_form.py:145 apprise/plugins/custom_json.py:127 +#: apprise/plugins/custom_xml.py:127 apprise/plugins/dapnet.py:123 +#: apprise/plugins/email/base.py:143 apprise/plugins/emby.py:101 +#: apprise/plugins/enigma2.py:125 apprise/plugins/freemobile.py:83 +#: apprise/plugins/growl.py:151 apprise/plugins/home_assistant.py:137 +#: apprise/plugins/humhub.py:133 apprise/plugins/irc/base.py:132 +#: apprise/plugins/kodi.py:111 apprise/plugins/matrix/base.py:277 +#: apprise/plugins/mqtt.py:165 apprise/plugins/nextcloud.py:131 +#: apprise/plugins/nextcloudtalk.py:90 apprise/plugins/notica.py:140 +#: apprise/plugins/notifico.py:180 apprise/plugins/ntfy.py:225 +#: apprise/plugins/pushjet.py:92 apprise/plugins/reddit.py:145 +#: apprise/plugins/rocketchat.py:135 apprise/plugins/signal_api.py:112 +#: apprise/plugins/simplepush.py:108 apprise/plugins/smpp.py:97 +#: apprise/plugins/synology.py:98 apprise/plugins/twist.py:101 +#: apprise/plugins/voipms.py:88 apprise/plugins/xmpp/base.py:117 +msgid "Password" +msgstr "Contrasena" + +#: apprise/plugins/apprise_api.py:122 apprise/plugins/chanify.py:74 +#: apprise/plugins/dingtalk.py:93 apprise/plugins/exotel.py:166 +#: apprise/plugins/feishu.py:80 apprise/plugins/gotify.py:123 +#: apprise/plugins/mattermost.py:161 apprise/plugins/notica.py:119 +#: apprise/plugins/notifiarr.py:91 apprise/plugins/ntfy.py:230 +#: apprise/plugins/pushme.py:62 apprise/plugins/ryver.py:99 +#: apprise/plugins/serverchan.py:70 apprise/plugins/slack.py:323 +#: apprise/plugins/synology.py:103 apprise/plugins/zulip.py:136 +msgid "Token" +msgstr "Token" + +#: apprise/plugins/apprise_api.py:136 apprise/plugins/jira.py:301 +#: apprise/plugins/ntfy.py:288 apprise/plugins/opsgenie.py:294 +#: apprise/plugins/pagertree.py:133 +#, fuzzy +msgid "Tags" +msgstr "Etiquetas" + +#: apprise/plugins/apprise_api.py:140 +msgid "Query Method" +msgstr "Metodo de Consulta" + +#: apprise/plugins/apprise_api.py:154 apprise/plugins/custom_form.py:174 +#: apprise/plugins/custom_json.py:150 apprise/plugins/custom_xml.py:150 +#: apprise/plugins/enigma2.py:153 apprise/plugins/nextcloud.py:181 +#: apprise/plugins/nextcloudtalk.py:122 apprise/plugins/notica.py:156 +#: apprise/plugins/pagertree.py:142 apprise/plugins/synology.py:128 +msgid "HTTP Header" +msgstr "Cabecera HTTP" + +#: apprise/plugins/aprs.py:167 apprise/plugins/bulksms.py:112 +#: apprise/plugins/bulkvs.py:85 apprise/plugins/clicksend.py:93 +#: apprise/plugins/dapnet.py:118 apprise/plugins/email/base.py:139 +#: apprise/plugins/mailgun.py:136 apprise/plugins/mqtt.py:160 +#: apprise/plugins/reddit.py:140 apprise/plugins/sendpulse.py:110 +#: apprise/plugins/smtp2go.py:108 apprise/plugins/sparkpost.py:159 +msgid "User Name" +msgstr "Nombre de Usuario" + +#: apprise/plugins/aprs.py:178 apprise/plugins/aprs.py:199 +#: apprise/plugins/dapnet.py:129 apprise/plugins/dapnet.py:162 +#, fuzzy +msgid "Target Callsign" +msgstr "Indicativo de Destino" + +#: apprise/plugins/aprs.py:204 +msgid "Resend Delay" +msgstr "Retraso de Reenvio" + +#: apprise/plugins/aprs.py:211 +msgid "Locale" +msgstr "Configuracion Regional" + +#: apprise/plugins/bark.py:154 apprise/plugins/fcm/__init__.py:157 +#: apprise/plugins/home_assistant.py:148 apprise/plugins/pushbullet.py:89 +#: apprise/plugins/pushover.py:240 apprise/plugins/pushsafer.py:366 +#: apprise/plugins/pushy.py:85 +msgid "Target Device" +msgstr "Dispositivo de Destino" + +#: apprise/plugins/bark.py:171 apprise/plugins/lametric.py:516 +#: apprise/plugins/macosx.py:124 apprise/plugins/pushover.py:269 +#: apprise/plugins/pushsafer.py:392 apprise/plugins/pushy.py:110 +msgid "Sound" +msgstr "Sonido" + +#: apprise/plugins/bark.py:176 +msgid "Level" +msgstr "Nivel" + +#: apprise/plugins/bark.py:181 +msgid "Volume" +msgstr "Volumen" + +#: apprise/plugins/bark.py:187 apprise/plugins/ntfy.py:270 +#: apprise/plugins/pagerduty.py:172 +msgid "Click" +msgstr "Clic" + +#: apprise/plugins/bark.py:191 apprise/plugins/pushy.py:114 +msgid "Badge" +msgstr "Insignia" + +#: apprise/plugins/bark.py:196 +msgid "Category" +msgstr "Categoria" + +#: apprise/plugins/bark.py:200 apprise/plugins/join.py:158 +#: apprise/plugins/pagerduty.py:163 +msgid "Group" +msgstr "Grupo" + +#: apprise/plugins/bark.py:204 apprise/plugins/dbus.py:228 +#: apprise/plugins/discord.py:196 apprise/plugins/fcm/__init__.py:197 +#: apprise/plugins/flock.py:136 apprise/plugins/fluxer.py:250 +#: apprise/plugins/glib.py:188 apprise/plugins/gnome.py:154 +#: apprise/plugins/growl.py:175 apprise/plugins/join.py:183 +#: apprise/plugins/kodi.py:129 apprise/plugins/line.py:108 +#: apprise/plugins/macosx.py:115 apprise/plugins/matrix/base.py:318 +#: apprise/plugins/mattermost.py:212 apprise/plugins/notifiarr.py:125 +#: apprise/plugins/ntfy.py:256 apprise/plugins/one_signal.py:164 +#: apprise/plugins/pagerduty.py:191 apprise/plugins/ryver.py:124 +#: apprise/plugins/slack.py:291 apprise/plugins/telegram.py:370 +#: apprise/plugins/vapid/__init__.py:204 apprise/plugins/windows.py:106 +#: apprise/plugins/workflows.py:163 +msgid "Include Image" +msgstr "Incluir Imagen" + +#: apprise/plugins/bark.py:210 apprise/plugins/mattermost.py:208 +#: apprise/plugins/revolt.py:129 +msgid "Icon URL" +msgstr "URL del Icono" + +#: apprise/plugins/bark.py:214 apprise/plugins/streamlabs.py:119 +msgid "Call" +msgstr "Llamada" + +#: apprise/plugins/base.py:305 +msgid "Overflow Mode" +msgstr "Modo de Desbordamiento" + +#: apprise/plugins/base.py:320 +msgid "Notify Format" +msgstr "Formato de Notificacion" + +#: apprise/plugins/base.py:330 +#, fuzzy +msgid "Interpret Emojis" +msgstr "Interpretar Emojis" + +#: apprise/plugins/base.py:340 +msgid "Persistent Storage" +msgstr "Almacenamiento Persistente" + +#: apprise/plugins/base.py:350 +#, fuzzy +msgid "Timezone" +msgstr "Zona Horaria" + +#: apprise/plugins/base.py:359 +#, fuzzy +msgid "Service Retry" +msgstr "Servicio" + +#: apprise/plugins/base.py:367 +#, fuzzy +msgid "Inter-Retry Wait" +msgstr "Interpretar Emojis" + +#: apprise/plugins/base.py:375 +#, fuzzy +msgid "Optional Service" +msgstr "Servicio" + +#: apprise/plugins/blink1.py:151 +#, fuzzy +msgid "blink(1)" +msgstr "Enlace" + +#: apprise/plugins/blink1.py:178 +#, fuzzy +msgid "Serial Number" +msgstr "Numero de Serie del Dispositivo" + +#: apprise/plugins/blink1.py:188 +#, fuzzy +msgid "Duration (ms)" +msgstr "Duracion" + +#: apprise/plugins/blink1.py:195 +msgid "Fade Time (ms)" +msgstr "" + +#: apprise/plugins/blink1.py:202 +msgid "LED Number" +msgstr "" + +#: apprise/plugins/brevo.py:164 apprise/plugins/postmark.py:123 +#: apprise/plugins/resend.py:114 apprise/plugins/sendgrid.py:123 +#, fuzzy +msgid "Source Email" +msgstr "Correo de Origen" + +#: apprise/plugins/brevo.py:169 apprise/plugins/email/base.py:159 +#: apprise/plugins/mailgun.py:152 apprise/plugins/notificationapi.py:177 +#: apprise/plugins/office365.py:282 apprise/plugins/one_signal.py:124 +#: apprise/plugins/popcorn_notify.py:84 apprise/plugins/postmark.py:128 +#: apprise/plugins/pushbullet.py:100 apprise/plugins/pushsafer.py:371 +#: apprise/plugins/resend.py:119 apprise/plugins/sendgrid.py:128 +#: apprise/plugins/sendpulse.py:134 apprise/plugins/slack.py:256 +#: apprise/plugins/threema.py:105 +msgid "Target Email" +msgstr "Correo de Destino" + +#: apprise/plugins/brevo.py:185 apprise/plugins/postmark.py:149 +#: apprise/plugins/ses.py:205 +#, fuzzy +msgid "Reply To Email" +msgstr "Responder a Correo" + +#: apprise/plugins/brevo.py:193 apprise/plugins/email/base.py:240 +#: apprise/plugins/mailgun.py:186 apprise/plugins/notificationapi.py:243 +#: apprise/plugins/office365.py:307 apprise/plugins/postmark.py:157 +#: apprise/plugins/resend.py:158 apprise/plugins/sendgrid.py:153 +#: apprise/plugins/sendpulse.py:169 apprise/plugins/ses.py:232 +#: apprise/plugins/smtp2go.py:143 apprise/plugins/sparkpost.py:201 +msgid "Carbon Copy" +msgstr "Copia de Carbono" + +#: apprise/plugins/brevo.py:197 apprise/plugins/email/base.py:244 +#: apprise/plugins/mailgun.py:190 apprise/plugins/notificationapi.py:247 +#: apprise/plugins/office365.py:311 apprise/plugins/postmark.py:161 +#: apprise/plugins/resend.py:162 apprise/plugins/sendgrid.py:157 +#: apprise/plugins/sendpulse.py:173 apprise/plugins/ses.py:236 +#: apprise/plugins/smtp2go.py:147 apprise/plugins/sparkpost.py:205 +msgid "Blind Carbon Copy" +msgstr "Copia de Carbono Oculta" + +#: apprise/plugins/bulksms.py:123 apprise/plugins/bulkvs.py:103 +#: apprise/plugins/burstsms.py:124 apprise/plugins/clickatell.py:83 +#: apprise/plugins/clicksend.py:105 apprise/plugins/d7networks.py:103 +#: apprise/plugins/dingtalk.py:106 apprise/plugins/evolution.py:127 +#: apprise/plugins/exotel.py:183 apprise/plugins/httpsms.py:90 +#: apprise/plugins/kavenegar.py:128 apprise/plugins/messagebird.py:92 +#: apprise/plugins/msg91.py:116 apprise/plugins/octopush.py:146 +#: apprise/plugins/plivo.py:107 apprise/plugins/popcorn_notify.py:77 +#: apprise/plugins/ringcentral.py:213 apprise/plugins/seven.py:81 +#: apprise/plugins/signal_api.py:124 apprise/plugins/sinch.py:127 +#: apprise/plugins/smpp.py:121 apprise/plugins/smseagle.py:151 +#: apprise/plugins/smsmanager.py:112 apprise/plugins/sns.py:157 +#: apprise/plugins/threema.py:98 apprise/plugins/twilio.py:161 +#: apprise/plugins/voipms.py:101 apprise/plugins/vonage.py:101 +#: apprise/plugins/whatsapp.py:147 +msgid "Target Phone No" +msgstr "Numero de Telefono de Destino" + +#: apprise/plugins/bulksms.py:130 apprise/plugins/nextcloud.py:142 +#: apprise/plugins/pushover.py:246 +#, fuzzy +msgid "Target Group" +msgstr "Grupo de Destino" + +#: apprise/plugins/bulksms.py:149 apprise/plugins/bulkvs.py:96 +#: apprise/plugins/bulkvs.py:122 apprise/plugins/clickatell.py:78 +#: apprise/plugins/fortysixelks.py:100 apprise/plugins/httpsms.py:83 +#: apprise/plugins/httpsms.py:115 apprise/plugins/ringcentral.py:207 +#: apprise/plugins/signal_api.py:117 apprise/plugins/sinch.py:120 +#: apprise/plugins/smpp.py:114 apprise/plugins/smsmanager.py:134 +#: apprise/plugins/twilio.py:154 apprise/plugins/voipms.py:94 +#: apprise/plugins/vonage.py:94 +msgid "From Phone No" +msgstr "Numero de Telefono de Origen" + +#: apprise/plugins/bulksms.py:155 +#, fuzzy +msgid "Route Group" +msgstr "Grupo de Enrutamiento" + +#: apprise/plugins/bulksms.py:162 apprise/plugins/d7networks.py:136 +#: apprise/plugins/exotel.py:228 +msgid "Unicode Characters" +msgstr "Caracteres Unicode" + +#: apprise/plugins/burstsms.py:111 apprise/plugins/threema.py:92 +#: apprise/plugins/vonage.py:87 +#, fuzzy +msgid "API Secret" +msgstr "Secreto de API" + +#: apprise/plugins/burstsms.py:118 +#, fuzzy +msgid "Sender ID" +msgstr "ID del Remitente" + +#: apprise/plugins/burstsms.py:152 +msgid "Country" +msgstr "Pais" + +#: apprise/plugins/burstsms.py:161 +msgid "validity" +msgstr "validez" + +#: apprise/plugins/chanify.py:47 +msgid "Chanify" +msgstr "Chanify" + +#: apprise/plugins/chime.py:130 apprise/plugins/discord.py:130 +#: apprise/plugins/fluxer.py:174 apprise/plugins/ifttt.py:103 +#: apprise/plugins/zoom.py:139 +msgid "Webhook ID" +msgstr "ID del Webhook" + +#: apprise/plugins/chime.py:136 apprise/plugins/discord.py:136 +#: apprise/plugins/fluxer.py:181 apprise/plugins/google_chat.py:118 +#: apprise/plugins/webexteams.py:171 +msgid "Webhook Token" +msgstr "Token del Webhook" + +#: apprise/plugins/clickatell.py:45 +msgid "Clickatell" +msgstr "Clickatell" + +#: apprise/plugins/clickatell.py:72 apprise/plugins/rocketchat.py:140 +#, fuzzy +msgid "API Token" +msgstr "Token de API" + +#: apprise/plugins/custom_form.py:157 apprise/plugins/custom_json.py:139 +#: apprise/plugins/custom_xml.py:139 +msgid "Fetch Method" +msgstr "Metodo de Obtencion" + +#: apprise/plugins/custom_form.py:163 +msgid "Attach File As" +msgstr "Adjuntar Archivo Como" + +#: apprise/plugins/custom_form.py:178 apprise/plugins/custom_json.py:154 +#: apprise/plugins/custom_xml.py:154 apprise/plugins/pagertree.py:146 +msgid "Payload Extras" +msgstr "Extras del Payload" + +#: apprise/plugins/custom_form.py:182 apprise/plugins/custom_json.py:158 +#: apprise/plugins/custom_xml.py:158 +msgid "GET Params" +msgstr "Parametros GET" + +#: apprise/plugins/d7networks.py:97 +#, fuzzy +msgid "API Access Token" +msgstr "Token de Acceso a la API" + +#: apprise/plugins/d7networks.py:127 apprise/plugins/seven.py:105 +msgid "Originating Address" +msgstr "Direccion de Origen" + +#: apprise/plugins/dapnet.py:150 apprise/plugins/exotel.py:240 +#: apprise/plugins/gotify.py:153 apprise/plugins/growl.py:163 +#: apprise/plugins/jira.py:287 apprise/plugins/join.py:189 +#: apprise/plugins/lametric.py:494 apprise/plugins/ntfy.py:282 +#: apprise/plugins/opsgenie.py:280 apprise/plugins/prowl.py:141 +#: apprise/plugins/pushover.py:263 apprise/plugins/pushsafer.py:387 +#: apprise/plugins/smseagle.py:187 +msgid "Priority" +msgstr "Prioridad" + +#: apprise/plugins/dapnet.py:156 +msgid "Transmitter Groups" +msgstr "Grupos de Transmisores" + +#: apprise/plugins/dbus.py:156 +msgid "libdbus-1.so.x must be installed." +msgstr "libdbus-1.so.x debe estar instalado." + +#: apprise/plugins/dbus.py:160 +msgid "DBus Notification" +msgstr "Notificacion DBus" + +#: apprise/plugins/dbus.py:204 apprise/plugins/glib.py:165 +#: apprise/plugins/gnome.py:142 apprise/plugins/pagertree.py:128 +msgid "Urgency" +msgstr "Urgencia" + +#: apprise/plugins/dbus.py:216 apprise/plugins/glib.py:176 +msgid "X-Axis" +msgstr "Eje X" + +#: apprise/plugins/dbus.py:222 apprise/plugins/glib.py:182 +msgid "Y-Axis" +msgstr "Eje Y" + +#: apprise/plugins/dingtalk.py:100 apprise/plugins/signl4.py:76 +#, fuzzy +msgid "Secret" +msgstr "Secreto" + +#: apprise/plugins/discord.py:125 apprise/plugins/flock.py:106 +#: apprise/plugins/fluxer.py:169 apprise/plugins/ryver.py:106 +#: apprise/plugins/slack.py:208 apprise/plugins/viber.py:99 +#: apprise/plugins/zulip.py:124 +msgid "Bot Name" +msgstr "Nombre del Bot" + +#: apprise/plugins/discord.py:149 apprise/plugins/fluxer.py:201 +msgid "Text To Speech" +msgstr "Texto a Voz" + +#: apprise/plugins/discord.py:154 apprise/plugins/fluxer.py:206 +msgid "Avatar Image" +msgstr "Imagen de Avatar" + +#: apprise/plugins/discord.py:159 apprise/plugins/fluxer.py:211 +#: apprise/plugins/ntfy.py:262 +#, fuzzy +msgid "Avatar URL" +msgstr "URL del Avatar" + +#: apprise/plugins/discord.py:163 apprise/plugins/fluxer.py:215 +#: apprise/plugins/pushover.py:275 +msgid "URL" +msgstr "URL" + +#: apprise/plugins/discord.py:172 apprise/plugins/fluxer.py:222 +msgid "Thread ID" +msgstr "ID del Hilo" + +#: apprise/plugins/discord.py:176 apprise/plugins/fluxer.py:230 +msgid "Display Footer" +msgstr "Mostrar Pie de Pagina" + +#: apprise/plugins/discord.py:181 apprise/plugins/fluxer.py:235 +msgid "Footer Logo" +msgstr "Logo del Pie de Pagina" + +#: apprise/plugins/discord.py:186 apprise/plugins/fluxer.py:240 +#, fuzzy +msgid "Use Fields" +msgstr "Usar Campos" + +#: apprise/plugins/discord.py:191 apprise/plugins/fluxer.py:245 +msgid "Discord Flags" +msgstr "Banderas de Discord" + +#: apprise/plugins/discord.py:205 apprise/plugins/fluxer.py:256 +msgid "Ping Users/Roles" +msgstr "Mencionar Usuarios/Roles" + +#: apprise/plugins/dot.py:142 +#, fuzzy +msgid "Device Serial Number" +msgstr "Numero de Serie del Dispositivo" + +#: apprise/plugins/dot.py:155 +#, fuzzy +msgid "API Mode" +msgstr "Modo de API" + +#: apprise/plugins/dot.py:161 +msgid "Refresh Now" +msgstr "Actualizar Ahora" + +#: apprise/plugins/dot.py:167 +msgid "Text Signature" +msgstr "Firma de Texto" + +#: apprise/plugins/dot.py:171 +msgid "Icon Base64 (Text API)" +msgstr "Icono Base64 (API de Texto)" + +#: apprise/plugins/dot.py:175 +msgid "Image Base64 (Image API)" +msgstr "Imagen Base64 (API de Imagen)" + +#: apprise/plugins/dot.py:180 +msgid "Link" +msgstr "Enlace" + +#: apprise/plugins/dot.py:184 +#, fuzzy +msgid "Border" +msgstr "Borde" + +#: apprise/plugins/dot.py:191 +msgid "Dither Type" +msgstr "Tipo de Tramado" + +#: apprise/plugins/dot.py:197 +msgid "Dither Kernel" +msgstr "Kernel de Tramado" + +#: apprise/plugins/dot.py:203 +#, fuzzy +msgid "Task Key" +msgstr "Clave del Hilo" + +#: apprise/plugins/emby.py:112 +msgid "Modal" +msgstr "Modal" + +#: apprise/plugins/enigma2.py:130 apprise/plugins/gotify.py:134 +#: apprise/plugins/mattermost.py:167 apprise/plugins/notica.py:145 +msgid "Path" +msgstr "Ruta" + +#: apprise/plugins/enigma2.py:140 +msgid "Server Timeout" +msgstr "Tiempo de Espera del Servidor" + +#: apprise/plugins/evolution.py:122 +#, fuzzy +msgid "Instance Name" +msgstr "Nombre del Dispositivo" + +#: apprise/plugins/exotel.py:160 apprise/plugins/sinch.py:106 +#: apprise/plugins/twilio.py:140 +msgid "Account SID" +msgstr "SID de la Cuenta" + +#: apprise/plugins/exotel.py:172 +#, fuzzy +msgid "From Phone No / Sender ID" +msgstr "Numero de Telefono de Origen" + +#: apprise/plugins/exotel.py:233 apprise/plugins/jira.py:275 +#: apprise/plugins/mailgun.py:176 apprise/plugins/notificationapi.py:212 +#: apprise/plugins/opsgenie.py:273 apprise/plugins/pagerduty.py:176 +#: apprise/plugins/sparkpost.py:191 +msgid "Region Name" +msgstr "Nombre de Region" + +#: apprise/plugins/feishu.py:49 +msgid "Feishu" +msgstr "Feishu" + +#: apprise/plugins/flock.py:99 apprise/plugins/twitter.py:175 +msgid "Access Key" +msgstr "Clave de Acceso" + +#: apprise/plugins/flock.py:111 +msgid "To User ID" +msgstr "ID del Usuario de Destino" + +#: apprise/plugins/flock.py:118 +msgid "To Channel ID" +msgstr "ID del Canal de Destino" + +#: apprise/plugins/fcm/__init__.py:182 apprise/plugins/fcm/__init__.py:188 +#: apprise/plugins/fluxer.py:195 apprise/plugins/lametric.py:510 +#: apprise/plugins/mattermost.py:224 apprise/plugins/notificationapi.py:218 +#: apprise/plugins/ntfy.py:296 apprise/plugins/office365.py:319 +#: apprise/plugins/sns.py:202 apprise/plugins/vapid/__init__.py:169 +#: apprise/plugins/webexteams.py:197 apprise/plugins/zoom.py:158 +#, fuzzy +msgid "Mode" +msgstr "Modo" + +#: apprise/plugins/fluxer.py:226 +#, fuzzy +msgid "Thread Name" +msgstr "Nombre del Hilo" + +#: apprise/plugins/fortysixelks.py:58 +msgid "46elks" +msgstr "46elks" + +#: apprise/plugins/fortysixelks.py:89 +#, fuzzy +msgid "API Username" +msgstr "Usuario de API" + +#: apprise/plugins/fortysixelks.py:94 +#, fuzzy +msgid "API Password" +msgstr "Contrasena de API" + +#: apprise/plugins/freemobile.py:48 +msgid "Free-Mobile" +msgstr "Free-Mobile" + +#: apprise/plugins/glib.py:123 +msgid "libdbus-1.so.x or libdbus-2.so.x must be installed." +msgstr "libdbus-1.so.x o libdbus-2.so.x debe estar instalado." + +#: apprise/plugins/glib.py:127 +#, fuzzy +msgid "GLib Notification" +msgstr "Notificacion DBus" + +#: apprise/plugins/gnome.py:100 +msgid "A local Gnome environment is required." +msgstr "Se requiere un entorno local de Gnome." + +#: apprise/plugins/gnome.py:104 +msgid "Gnome Notification" +msgstr "Notificacion de Gnome" + +#: apprise/plugins/google_chat.py:106 +msgid "Workspace" +msgstr "Espacio de Trabajo" + +#: apprise/plugins/google_chat.py:112 +#, fuzzy +msgid "Webhook Key" +msgstr "Clave del Webhook" + +#: apprise/plugins/google_chat.py:124 +#, fuzzy +msgid "Thread Key" +msgstr "Clave del Hilo" + +#: apprise/plugins/groupme.py:123 +#, fuzzy +msgid "Bot ID" +msgstr "ID del Webhook" + +#: apprise/plugins/groupme.py:130 apprise/plugins/line.py:82 +#: apprise/plugins/mastodon.py:185 apprise/plugins/matrix/base.py:282 +#: apprise/plugins/misskey.py:122 apprise/plugins/pushbullet.py:83 +#: apprise/plugins/pushover.py:234 apprise/plugins/smseagle.py:146 +#: apprise/plugins/spugpush.py:68 apprise/plugins/streamlabs.py:105 +#: apprise/plugins/whatsapp.py:121 +msgid "Access Token" +msgstr "Token de Acceso" + +#: apprise/plugins/growl.py:169 apprise/plugins/mqtt.py:195 +#: apprise/plugins/nextcloud.py:163 +msgid "Version" +msgstr "Version" + +#: apprise/plugins/growl.py:181 +msgid "Sticky" +msgstr "Fijado" + +#: apprise/plugins/home_assistant.py:142 +#, fuzzy +msgid "Long-Lived Access Token" +msgstr "Token de Acceso de Larga Duracion" + +#: apprise/plugins/home_assistant.py:165 +msgid "Notification ID" +msgstr "ID de Notificacion" + +#: apprise/plugins/home_assistant.py:182 +#, fuzzy +msgid "Path Prefix" +msgstr "Prefijo" + +#: apprise/plugins/humhub.py:127 +#, fuzzy +msgid "Token or Username" +msgstr "Nombre de Usuario" + +#: apprise/plugins/ifttt.py:109 +msgid "Events" +msgstr "Eventos" + +#: apprise/plugins/ifttt.py:129 +msgid "Add Tokens" +msgstr "Agregar Tokens" + +#: apprise/plugins/ifttt.py:133 +msgid "Remove Tokens" +msgstr "Eliminar Tokens" + +#: apprise/plugins/jira.py:240 apprise/plugins/opsgenie.py:238 +#, fuzzy +msgid "Target Escalation" +msgstr "Escalada de Destino" + +#: apprise/plugins/jira.py:246 apprise/plugins/opsgenie.py:244 +#, fuzzy +msgid "Target Schedule" +msgstr "Programacion de Destino" + +#: apprise/plugins/irc/base.py:137 apprise/plugins/jira.py:252 +#: apprise/plugins/line.py:88 apprise/plugins/mastodon.py:196 +#: apprise/plugins/matrix/base.py:289 apprise/plugins/nextcloud.py:136 +#: apprise/plugins/one_signal.py:129 apprise/plugins/opsgenie.py:250 +#: apprise/plugins/pushed.py:95 apprise/plugins/rocketchat.py:156 +#: apprise/plugins/slack.py:261 apprise/plugins/twitter.py:187 +#: apprise/plugins/wechat.py:198 apprise/plugins/xmpp/base.py:123 +#: apprise/plugins/zulip.py:143 +msgid "Target User" +msgstr "Usuario de Destino" + +#: apprise/plugins/jira.py:258 apprise/plugins/opsgenie.py:256 +#, fuzzy +msgid "Target Team" +msgstr "Equipo de Destino" + +#: apprise/plugins/jira.py:264 apprise/plugins/opsgenie.py:262 +#, fuzzy +msgid "Targets " +msgstr "Destinatarios" + +#: apprise/plugins/jira.py:293 apprise/plugins/opsgenie.py:286 +msgid "Entity" +msgstr "Entidad" + +#: apprise/plugins/jira.py:297 apprise/plugins/opsgenie.py:290 +msgid "Alias" +msgstr "Alias" + +#: apprise/plugins/jira.py:308 apprise/plugins/opsgenie.py:298 +#: apprise/plugins/pagertree.py:118 apprise/plugins/splunk.py:202 +#, fuzzy +msgid "Action" +msgstr "Accion" + +#: apprise/plugins/jira.py:319 apprise/plugins/opsgenie.py:317 +#, fuzzy +msgid "Details" +msgstr "Detalles" + +#: apprise/plugins/jira.py:323 apprise/plugins/opsgenie.py:321 +#: apprise/plugins/splunk.py:213 +msgid "Action Mapping" +msgstr "Mapeo de Acciones" + +#: apprise/plugins/join.py:147 +msgid "Device ID" +msgstr "ID del Dispositivo" + +#: apprise/plugins/join.py:153 +#, fuzzy +msgid "Device Name" +msgstr "Nombre del Dispositivo" + +#: apprise/plugins/kavenegar.py:122 apprise/plugins/messagebird.py:85 +#: apprise/plugins/plivo.py:100 +#, fuzzy +msgid "Source Phone No" +msgstr "Numero de Telefono de Origen" + +#: apprise/plugins/kodi.py:123 apprise/plugins/streamlabs.py:141 +#: apprise/plugins/windows.py:100 +msgid "Duration" +msgstr "Duracion" + +#: apprise/plugins/kumulos.py:99 +#, fuzzy +msgid "Server Key" +msgstr "Clave del Servidor" + +#: apprise/plugins/lametric.py:436 +#, fuzzy +msgid "Device API Key" +msgstr "Clave de API del Dispositivo" + +#: apprise/plugins/lametric.py:442 apprise/plugins/one_signal.py:102 +#: apprise/plugins/parseplatform.py:101 +msgid "App ID" +msgstr "ID de Aplicacion" + +#: apprise/plugins/lametric.py:448 +#, fuzzy +msgid "App Version" +msgstr "Version de Aplicacion" + +#: apprise/plugins/lametric.py:455 +#, fuzzy +msgid "App Access Token" +msgstr "Token de Acceso de Aplicacion" + +#: apprise/plugins/lametric.py:500 +msgid "Custom Icon" +msgstr "Icono Personalizado" + +#: apprise/plugins/lametric.py:504 +msgid "Icon Type" +msgstr "Tipo de Icono" + +#: apprise/plugins/lametric.py:521 +msgid "Cycles" +msgstr "Ciclos" + +#: apprise/plugins/lark.py:47 +msgid "Lark (Feishu)" +msgstr "Lark (Feishu)" + +#: apprise/plugins/lark.py:67 apprise/plugins/revolt.py:98 +#: apprise/plugins/telegram.py:344 +msgid "Bot Token" +msgstr "Token del Bot" + +#: apprise/plugins/macosx.py:65 +msgid "" +"Only works with Mac OS X 10.8 and higher. Additionally requires that /usr/" +"local/bin/terminal-notifier is locally accessible." +msgstr "" + +#: apprise/plugins/macosx.py:72 +msgid "MacOSX Notification" +msgstr "Notificacion de MacOSX" + +#: apprise/plugins/macosx.py:128 +msgid "Open/Click URL" +msgstr "URL de Apertura/Clic" + +#: apprise/plugins/email/base.py:148 apprise/plugins/mailgun.py:141 +#: apprise/plugins/sendpulse.py:115 apprise/plugins/smtp2go.py:113 +#: apprise/plugins/sparkpost.py:164 +msgid "Domain" +msgstr "Dominio" + +#: apprise/plugins/email/base.py:179 apprise/plugins/mailgun.py:168 +#: apprise/plugins/postmark.py:144 apprise/plugins/resend.py:140 +#: apprise/plugins/ses.py:210 apprise/plugins/smtp2go.py:135 +#: apprise/plugins/sparkpost.py:186 +msgid "From Name" +msgstr "Nombre de Origen" + +#: apprise/plugins/email/base.py:253 apprise/plugins/mailgun.py:204 +#: apprise/plugins/smtp2go.py:161 apprise/plugins/sparkpost.py:219 +#, fuzzy +msgid "Email Header" +msgstr "Cabecera de Correo" + +#: apprise/plugins/mailgun.py:208 apprise/plugins/notificationapi.py:256 +#: apprise/plugins/slack.py:340 apprise/plugins/sparkpost.py:223 +#: apprise/plugins/workflows.py:202 +#, fuzzy +msgid "Template Tokens" +msgstr "Tokens de Plantilla" + +#: apprise/plugins/mastodon.py:216 apprise/plugins/misskey.py:143 +msgid "Visibility" +msgstr "Visibilidad" + +#: apprise/plugins/mastodon.py:222 +msgid "Spoiler Text" +msgstr "Texto de Spoiler" + +#: apprise/plugins/mastodon.py:226 +msgid "Idempotency-Key" +msgstr "Clave de Idempotencia" + +#: apprise/plugins/mastodon.py:230 +msgid "Language Code" +msgstr "Codigo de Idioma" + +#: apprise/plugins/mastodon.py:234 apprise/plugins/twitter.py:210 +msgid "Cache Results" +msgstr "Almacenar Resultados en Cache" + +#: apprise/plugins/mastodon.py:239 +msgid "Sensitive Attachments" +msgstr "Archivos Adjuntos Sensibles" + +#: apprise/plugins/mastodon.py:255 +#, fuzzy +msgid "Ping Users/Tags" +msgstr "Mencionar Usuarios/Roles" + +#: apprise/plugins/irc/base.py:128 apprise/plugins/mattermost.py:151 +#: apprise/plugins/xmpp/base.py:112 +#, fuzzy +msgid "User" +msgstr "Usuario" + +#: apprise/plugins/irc/base.py:143 apprise/plugins/mattermost.py:177 +#: apprise/plugins/notifiarr.py:97 apprise/plugins/pushbullet.py:94 +#: apprise/plugins/pushed.py:101 apprise/plugins/rocketchat.py:150 +#: apprise/plugins/slack.py:267 apprise/plugins/twist.py:112 +#: apprise/plugins/xmpp/base.py:128 +msgid "Target Channel" +msgstr "Canal de Destino" + +#: apprise/plugins/mattermost.py:183 apprise/plugins/twist.py:118 +#, fuzzy +msgid "Target Channel ID" +msgstr "ID del Canal de Destino" + +#: apprise/plugins/mqtt.py:171 +msgid "Target Queue" +msgstr "" + +#: apprise/plugins/mqtt.py:188 +msgid "QOS" +msgstr "QoS" + +#: apprise/plugins/mqtt.py:201 apprise/plugins/notificationapi.py:166 +#: apprise/plugins/office365.py:269 apprise/plugins/ringcentral.py:192 +#: apprise/plugins/sendpulse.py:120 +#, fuzzy +msgid "Client ID" +msgstr "ID del Cliente" + +#: apprise/plugins/mqtt.py:205 +msgid "Use Session" +msgstr "Usar Sesion" + +#: apprise/plugins/mqtt.py:210 +msgid "Retain Messages" +msgstr "Retener Mensajes" + +#: apprise/plugins/msg91.py:102 apprise/plugins/sendpulse.py:156 +msgid "Template ID" +msgstr "ID de Plantilla" + +#: apprise/plugins/msg91.py:109 +#, fuzzy +msgid "Authentication Key" +msgstr "Clave de Autenticacion" + +#: apprise/plugins/msg91.py:138 +msgid "Short URL" +msgstr "URL Corta" + +#: apprise/plugins/msg91.py:148 apprise/plugins/whatsapp.py:203 +msgid "Template Mapping" +msgstr "Mapeo de Plantilla" + +#: apprise/plugins/nextcloud.py:169 apprise/plugins/nextcloudtalk.py:113 +msgid "URL Prefix" +msgstr "Prefijo de URL" + +#: apprise/plugins/nextcloudtalk.py:43 +msgid "Nextcloud Talk" +msgstr "Nextcloud Talk" + +#: apprise/plugins/nextcloudtalk.py:96 +#, fuzzy +msgid "Room ID" +msgstr "ID de la Sala" + +#: apprise/plugins/notifiarr.py:121 +msgid "Discord Event ID" +msgstr "ID de Evento de Discord" + +#: apprise/plugins/notifiarr.py:131 apprise/plugins/pagerduty.py:145 +#, fuzzy +msgid "Source" +msgstr "Origen" + +#: apprise/plugins/matrix/base.py:352 apprise/plugins/notificationapi.py:159 +msgid "Message Type" +msgstr "Tipo de Mensaje" + +#: apprise/plugins/notificationapi.py:171 apprise/plugins/office365.py:276 +#: apprise/plugins/ringcentral.py:199 apprise/plugins/sendpulse.py:127 +#, fuzzy +msgid "Client Secret" +msgstr "Secreto del Cliente" + +#: apprise/plugins/notificationapi.py:182 +#, fuzzy +msgid "Target ID" +msgstr "ID de Destino" + +#: apprise/plugins/notificationapi.py:187 +#, fuzzy +msgid "Target SMS" +msgstr "SMS de Destino" + +#: apprise/plugins/notificationapi.py:207 +msgid "Channels" +msgstr "Canales" + +#: apprise/plugins/email/base.py:196 apprise/plugins/notificationapi.py:223 +#: apprise/plugins/office365.py:315 apprise/plugins/resend.py:150 +msgid "Reply To" +msgstr "Responder A" + +#: apprise/plugins/email/base.py:174 apprise/plugins/notificationapi.py:228 +#: apprise/plugins/sendpulse.py:150 apprise/plugins/ses.py:163 +msgid "From Email" +msgstr "Correo de Origen" + +#: apprise/plugins/fcm/__init__.py:153 apprise/plugins/notifico.py:187 +#, fuzzy +msgid "Project ID" +msgstr "ID del Proyecto" + +#: apprise/plugins/notifico.py:196 +msgid "Message Hook" +msgstr "Hook de Mensaje" + +#: apprise/plugins/notifico.py:211 +msgid "IRC Colors" +msgstr "Colores IRC" + +#: apprise/plugins/notifico.py:217 +msgid "Prefix" +msgstr "Prefijo" + +#: apprise/plugins/ntfy.py:235 +msgid "Topic" +msgstr "Tema" + +#: apprise/plugins/ntfy.py:252 +msgid "Attach" +msgstr "Adjuntar" + +#: apprise/plugins/ntfy.py:266 +msgid "Attach Filename" +msgstr "Nombre del Archivo Adjunto" + +#: apprise/plugins/ntfy.py:274 +msgid "Delay" +msgstr "Retraso" + +#: apprise/plugins/ntfy.py:278 apprise/plugins/twist.py:107 +#, fuzzy +msgid "Email" +msgstr "Correo Electronico" + +#: apprise/plugins/ntfy.py:292 +#, fuzzy +msgid "Actions" +msgstr "Acciones" + +#: apprise/plugins/ntfy.py:305 +#, fuzzy +msgid "Authentication Type" +msgstr "Tipo de Autenticacion" + +#: apprise/plugins/octopush.py:130 +#, fuzzy +msgid "API Login" +msgstr "Token de API" + +#: apprise/plugins/octopush.py:142 +#, fuzzy +msgid "Sender" +msgstr "ID del Remitente" + +#: apprise/plugins/octopush.py:178 +#, fuzzy +msgid "Accept Replies" +msgstr "Enviar Respuestas" + +#: apprise/plugins/octopush.py:183 +msgid "Purpose" +msgstr "" + +#: apprise/plugins/octopush.py:189 +#, fuzzy +msgid "Type" +msgstr "Tipo de Icono" + +#: apprise/plugins/office365.py:257 +#, fuzzy +msgid "Tenant Domain" +msgstr "Dominio del Inquilino" + +#: apprise/plugins/office365.py:264 +msgid "Account Email or Object ID" +msgstr "Correo de la Cuenta o ID del Objeto" + +#: apprise/plugins/office365.py:325 +#, fuzzy +msgid "Save to Sent Items" +msgstr "Entradas de Datos" + +#: apprise/plugins/one_signal.py:108 apprise/plugins/sendgrid.py:146 +msgid "Template" +msgstr "Plantilla" + +#: apprise/plugins/one_signal.py:119 +#, fuzzy +msgid "Target Player ID" +msgstr "ID del Jugador de Destino" + +#: apprise/plugins/one_signal.py:135 +#, fuzzy +msgid "Include Segment" +msgstr "Incluir Segmento" + +#: apprise/plugins/one_signal.py:155 +msgid "Subtitle" +msgstr "Subtitulo" + +#: apprise/plugins/one_signal.py:159 apprise/plugins/sfr.py:125 +#: apprise/plugins/whatsapp.py:141 +msgid "Language" +msgstr "Idioma" + +#: apprise/plugins/one_signal.py:170 +msgid "Enable Contents" +msgstr "Habilitar Contenidos" + +#: apprise/plugins/one_signal.py:176 +msgid "Decode Template Args" +msgstr "Decodificar Argumentos de Plantilla" + +#: apprise/plugins/one_signal.py:195 +msgid "Custom Data" +msgstr "Datos Personalizados" + +#: apprise/plugins/one_signal.py:199 +msgid "Postback Data" +msgstr "Datos de Postback" + +#: apprise/plugins/pagerduty.py:138 apprise/plugins/spike.py:68 +#, fuzzy +msgid "Integration Key" +msgstr "Clave de Integracion" + +#: apprise/plugins/pagerduty.py:151 +#, fuzzy +msgid "Component" +msgstr "Componente" + +#: apprise/plugins/pagerduty.py:167 +msgid "Class" +msgstr "Clase" + +#: apprise/plugins/pagerduty.py:185 +msgid "Severity" +msgstr "Severidad" + +#: apprise/plugins/pagerduty.py:202 +#, fuzzy +msgid "Custom Details" +msgstr "Detalles Personalizados" + +#: apprise/plugins/pagertree.py:105 +msgid "Integration ID" +msgstr "ID de Integracion" + +#: apprise/plugins/pagertree.py:124 +msgid "Third Party ID" +msgstr "ID de Terceros" + +#: apprise/plugins/pagertree.py:150 +msgid "Meta Extras" +msgstr "Extras de Meta" + +#: apprise/plugins/parseplatform.py:107 +#, fuzzy +msgid "Master Key" +msgstr "Clave Maestra" + +#: apprise/plugins/parseplatform.py:120 +#, fuzzy +msgid "Device" +msgstr "Dispositivo" + +#: apprise/plugins/plivo.py:88 +#, fuzzy +msgid "Auth ID" +msgstr "ID de Autenticacion" + +#: apprise/plugins/plivo.py:94 apprise/plugins/sinch.py:113 +#: apprise/plugins/twilio.py:147 +msgid "Auth Token" +msgstr "Token de Autenticacion" + +#: apprise/plugins/prowl.py:128 +msgid "Provider Key" +msgstr "Clave del Proveedor" + +#: apprise/plugins/pushdeer.py:85 +#, fuzzy +msgid "Pushkey" +msgstr "Clave Push" + +#: apprise/plugins/pushed.py:83 +msgid "Application Key" +msgstr "Clave de Aplicacion" + +#: apprise/plugins/pushed.py:89 apprise/plugins/reddit.py:158 +msgid "Application Secret" +msgstr "Secreto de Aplicacion" + +#: apprise/plugins/pushjet.py:82 +msgid "Secret Key" +msgstr "Clave Secreta" + +#: apprise/plugins/pushme.py:81 apprise/plugins/signal_api.py:152 +#: apprise/plugins/smseagle.py:193 +msgid "Show Status" +msgstr "Mostrar Estado" + +#: apprise/plugins/pushover.py:228 +msgid "User Key" +msgstr "Clave de Usuario" + +#: apprise/plugins/pushover.py:280 +msgid "URL Title" +msgstr "Titulo de URL" + +#: apprise/plugins/pushover.py:285 +msgid "Emergency Retry Interval" +msgstr "" + +#: apprise/plugins/pushover.py:291 +msgid "Expire" +msgstr "Expirar" + +#: apprise/plugins/pushover.py:301 +#, fuzzy +msgid "Encryption Key" +msgstr "Cifrado PGP" + +#: apprise/plugins/pushover.py:308 +msgid "E2EE" +msgstr "" + +#: apprise/plugins/pushplus.py:172 +msgid "Pushplus" +msgstr "Pushplus" + +#: apprise/plugins/pushplus.py:210 apprise/plugins/qq.py:66 +#, fuzzy +msgid "User Token" +msgstr "Token de Usuario" + +#: apprise/plugins/pushplus.py:219 +msgid "Group Topics" +msgstr "" + +#: apprise/plugins/pushplus.py:240 +#, fuzzy +msgid "Channel" +msgstr "Canales" + +#: apprise/plugins/pushplus.py:257 +#, fuzzy +msgid "Webhook Name" +msgstr "Modo Webhook" + +#: apprise/plugins/pushsafer.py:360 +#, fuzzy +msgid "Private Key" +msgstr "Clave Privada" + +#: apprise/plugins/pushsafer.py:397 +#, fuzzy +msgid "Vibration" +msgstr "Vibracion" + +#: apprise/plugins/pushy.py:79 +#, fuzzy +msgid "Secret API Key" +msgstr "Clave Secreta de API" + +#: apprise/plugins/fcm/__init__.py:162 apprise/plugins/pushy.py:91 +#: apprise/plugins/sns.py:163 apprise/plugins/wxpusher.py:128 +msgid "Target Topic" +msgstr "Tema de Destino" + +#: apprise/plugins/qq.py:46 +msgid "QQ Push" +msgstr "QQ Push" + +#: apprise/plugins/reddit.py:151 +#, fuzzy +msgid "Application ID" +msgstr "ID de Aplicacion" + +#: apprise/plugins/reddit.py:165 +#, fuzzy +msgid "Target Subreddit" +msgstr "Subreddit de Destino" + +#: apprise/plugins/reddit.py:182 +msgid "Kind" +msgstr "Tipo" + +#: apprise/plugins/reddit.py:188 +msgid "Flair ID" +msgstr "ID del Flair" + +#: apprise/plugins/reddit.py:193 +msgid "Flair Text" +msgstr "Texto del Flair" + +#: apprise/plugins/reddit.py:198 +msgid "NSFW" +msgstr "Contenido Adulto" + +#: apprise/plugins/reddit.py:204 +msgid "Is Ad?" +msgstr "Es Anuncio?" + +#: apprise/plugins/reddit.py:210 +msgid "Send Replies" +msgstr "Enviar Respuestas" + +#: apprise/plugins/reddit.py:216 +msgid "Is Spoiler" +msgstr "Es Spoiler" + +#: apprise/plugins/reddit.py:222 +msgid "Resubmit Flag" +msgstr "Bandera de Reenvio" + +#: apprise/plugins/resend.py:135 +#, fuzzy +msgid "From Address" +msgstr "Nombre de Origen" + +#: apprise/plugins/revolt.py:104 +#, fuzzy +msgid "Channel ID" +msgstr "ID del Canal" + +#: apprise/plugins/revolt.py:131 +msgid "Embed URL" +msgstr "URL Incrustada" + +#: apprise/plugins/ringcentral.py:186 +#, fuzzy +msgid "Token / Password" +msgstr "Contrasena" + +#: apprise/plugins/ringcentral.py:239 +#, fuzzy +msgid "Environment" +msgstr "Evento" + +#: apprise/plugins/ringcentral.py:252 +#, fuzzy +msgid "Authentication Mode" +msgstr "Token de Autenticacion" + +#: apprise/plugins/rocketchat.py:146 +msgid "Webhook" +msgstr "Webhook" + +#: apprise/plugins/matrix/base.py:295 apprise/plugins/rocketchat.py:162 +msgid "Target Room ID" +msgstr "ID de Sala de Destino" + +#: apprise/plugins/matrix/base.py:334 apprise/plugins/rocketchat.py:178 +#: apprise/plugins/ryver.py:118 +msgid "Webhook Mode" +msgstr "Modo Webhook" + +#: apprise/plugins/rocketchat.py:183 +msgid "Use Avatar" +msgstr "Usar Avatar" + +#: apprise/plugins/rsyslog.py:173 apprise/plugins/syslog.py:144 +msgid "Facility" +msgstr "Instalacion" + +#: apprise/plugins/rsyslog.py:203 apprise/plugins/syslog.py:161 +msgid "Log PID" +msgstr "PID del Log" + +#: apprise/plugins/ryver.py:93 apprise/plugins/zulip.py:130 +msgid "Organization" +msgstr "Organizacion" + +#: apprise/plugins/sendgrid.py:166 apprise/plugins/sendpulse.py:182 +msgid "Template Data" +msgstr "Datos de Plantilla" + +#: apprise/plugins/ses.py:157 apprise/plugins/sns.py:132 +#, fuzzy +msgid "Session Token" +msgstr "Token de Acceso" + +#: apprise/plugins/ses.py:169 apprise/plugins/sns.py:138 +msgid "Access Key ID" +msgstr "ID de Clave de Acceso" + +#: apprise/plugins/ses.py:175 apprise/plugins/sns.py:144 +msgid "Secret Access Key" +msgstr "Clave de Acceso Secreta" + +#: apprise/plugins/ses.py:181 apprise/plugins/sinch.py:157 +#: apprise/plugins/sns.py:150 +msgid "Region" +msgstr "Region" + +#: apprise/plugins/ses.py:188 apprise/plugins/smtp2go.py:124 +#: apprise/plugins/sparkpost.py:175 +msgid "Target Emails" +msgstr "Correos de Destino" + +#: apprise/plugins/seven.py:113 apprise/plugins/smseagle.py:203 +msgid "Flash" +msgstr "Flash" + +#: apprise/plugins/seven.py:117 +msgid "Label" +msgstr "Etiqueta" + +#: apprise/plugins/sfr.py:58 +msgid "Société Française du Radiotéléphone" +msgstr "" + +#: apprise/plugins/sfr.py:90 +#, fuzzy +msgid "Service ID" +msgstr "ID del Servicio" + +#: apprise/plugins/sfr.py:95 +#, fuzzy +msgid "Service Password" +msgstr "Contrasena del Servicio" + +#: apprise/plugins/sfr.py:101 +#, fuzzy +msgid "Space ID" +msgstr "ID del Espacio" + +#: apprise/plugins/sfr.py:107 +msgid "Recipient Phone Number" +msgstr "Numero de Telefono del Destinatario" + +#: apprise/plugins/sfr.py:131 +#, fuzzy +msgid "Sender Name" +msgstr "Nombre del Remitente" + +#: apprise/plugins/sfr.py:138 +msgid "Media Type" +msgstr "Tipo de Medio" + +#: apprise/plugins/sfr.py:145 +#, fuzzy +msgid "Timeout" +msgstr "Tiempo de Espera" + +#: apprise/plugins/sfr.py:151 +#, fuzzy +msgid "TTS Voice" +msgstr "Voz TTS" + +#: apprise/plugins/signal_api.py:131 apprise/plugins/smseagle.py:158 +#: apprise/plugins/whatsapp.py:159 +#, fuzzy +msgid "Target Group ID" +msgstr "ID del Grupo de Destino" + +#: apprise/plugins/signl4.py:89 +#, fuzzy +msgid "Service" +msgstr "Servicio" + +#: apprise/plugins/signl4.py:93 +#, fuzzy +msgid "Location" +msgstr "Ubicacion" + +#: apprise/plugins/signl4.py:97 +msgid "Alerting Scenario" +msgstr "Escenario de Alerta" + +#: apprise/plugins/signl4.py:101 +msgid "Filtering" +msgstr "Filtrado" + +#: apprise/plugins/signl4.py:106 +#, fuzzy +msgid "External ID" +msgstr "ID Externo" + +#: apprise/plugins/signl4.py:110 +#, fuzzy +msgid "Status" +msgstr "Estado" + +#: apprise/plugins/simplepush.py:113 +msgid "Salt" +msgstr "Salt" + +#: apprise/plugins/simplepush.py:126 +#, fuzzy +msgid "Event" +msgstr "Evento" + +#: apprise/plugins/sinch.py:134 apprise/plugins/twilio.py:168 +msgid "Target Short Code" +msgstr "Codigo Corto de Destino" + +#: apprise/plugins/slack.py:216 +#, fuzzy +msgid "OAuth Access Token" +msgstr "Token de Acceso OAuth" + +#: apprise/plugins/slack.py:225 +msgid "Token A" +msgstr "Token A" + +#: apprise/plugins/slack.py:234 +msgid "Token B" +msgstr "Token B" + +#: apprise/plugins/slack.py:243 +msgid "Token C" +msgstr "Token C" + +#: apprise/plugins/slack.py:250 +msgid "Target Encoded ID" +msgstr "ID Codificado de Destino" + +#: apprise/plugins/slack.py:279 +#, fuzzy +msgid "Workflow Path" +msgstr "ID del Flujo de Trabajo" + +#: apprise/plugins/slack.py:297 +#, fuzzy +msgid "Include Footer" +msgstr "Incluir Pie de Pagina" + +#: apprise/plugins/slack.py:305 +msgid "Use Blocks" +msgstr "Usar Bloques" + +#: apprise/plugins/slack.py:311 +#, fuzzy +msgid "Include Timestamp" +msgstr "Incluir Marca de Tiempo" + +#: apprise/plugins/slack.py:317 apprise/plugins/twitter.py:204 +#, fuzzy +msgid "Message Mode" +msgstr "Modo de Mensaje" + +#: apprise/plugins/slack.py:330 apprise/plugins/workflows.py:181 +msgid "Template Path" +msgstr "Ruta de Plantilla" + +#: apprise/plugins/smpp.py:61 +msgid "SMPP" +msgstr "SMPP" + +#: apprise/plugins/smpp.py:103 +#, fuzzy +msgid "Host" +msgstr "Host" + +#: apprise/plugins/smseagle.py:165 +#, fuzzy +msgid "Target Contact" +msgstr "Contacto de Destino" + +#: apprise/plugins/smseagle.py:198 +msgid "Test Only" +msgstr "Solo Prueba" + +#: apprise/plugins/smsmanager.py:143 +msgid "Gateway" +msgstr "Pasarela" + +#: apprise/plugins/sogs.py:219 +#, fuzzy +msgid "Public Key" +msgstr "Clave de API" + +#: apprise/plugins/sogs.py:226 +#, fuzzy +msgid "Seed" +msgstr "ID del Remitente" + +#: apprise/plugins/sogs.py:233 +#, fuzzy +msgid "Rooms" +msgstr "ID de la Sala" + +#: apprise/plugins/spike.py:48 +msgid "Spike.sh" +msgstr "Spike.sh" + +#: apprise/plugins/splunk.py:117 +msgid "Splunk On-Call" +msgstr "Splunk On-Call" + +#: apprise/plugins/splunk.py:172 +#, fuzzy +msgid "Target Routing Key" +msgstr "Clave de Enrutamiento de Destino" + +#: apprise/plugins/splunk.py:179 +msgid "Entity ID" +msgstr "ID de Entidad" + +#: apprise/plugins/spugpush.py:48 +msgid "SpugPush" +msgstr "SpugPush" + +#: apprise/plugins/streamlabs.py:125 +msgid "Alert Type" +msgstr "Tipo de Alerta" + +#: apprise/plugins/streamlabs.py:131 +msgid "Image Link" +msgstr "Enlace de Imagen" + +#: apprise/plugins/streamlabs.py:136 +#, fuzzy +msgid "Sound Link" +msgstr "Enlace de Sonido" + +#: apprise/plugins/streamlabs.py:147 +msgid "Special Text Color" +msgstr "Color de Texto Especial" + +#: apprise/plugins/streamlabs.py:153 +msgid "Amount" +msgstr "Cantidad" + +#: apprise/plugins/streamlabs.py:159 +#, fuzzy +msgid "Currency" +msgstr "Moneda" + +#: apprise/plugins/streamlabs.py:165 +#, fuzzy +msgid "Name" +msgstr "Nombre" + +#: apprise/plugins/streamlabs.py:171 +msgid "Identifier" +msgstr "Identificador" + +#: apprise/plugins/synology.py:116 +msgid "Upload" +msgstr "Subir" + +#: apprise/plugins/syslog.py:167 +msgid "Log to STDERR" +msgstr "Registrar en STDERR" + +#: apprise/plugins/telegram.py:353 +msgid "Target Chat ID" +msgstr "ID del Chat de Destino" + +#: apprise/plugins/telegram.py:376 +msgid "Detect Bot Owner" +msgstr "Detectar Propietario del Bot" + +#: apprise/plugins/telegram.py:382 +msgid "Silent Notification" +msgstr "Notificacion Silenciosa" + +#: apprise/plugins/telegram.py:387 +msgid "Web Page Preview" +msgstr "Vista Previa de Pagina Web" + +#: apprise/plugins/telegram.py:392 +msgid "Topic Thread ID" +msgstr "ID de Hilo del Tema" + +#: apprise/plugins/telegram.py:399 +#, fuzzy +msgid "Markdown Version" +msgstr "Version de Markdown" + +#: apprise/plugins/telegram.py:408 +msgid "Content Placement" +msgstr "Ubicacion del Contenido" + +#: apprise/plugins/threema.py:85 +msgid "Gateway ID" +msgstr "ID de la Pasarela" + +#: apprise/plugins/threema.py:110 +#, fuzzy +msgid "Target Threema ID" +msgstr "ID Threema de Destino" + +#: apprise/plugins/twilio.py:200 +msgid "Notification Method: sms or call" +msgstr "Metodo de Notificacion: sms o llamada" + +#: apprise/plugins/twitter.py:163 +msgid "Consumer Key" +msgstr "Clave del Consumidor" + +#: apprise/plugins/twitter.py:169 +msgid "Consumer Secret" +msgstr "Secreto del Consumidor" + +#: apprise/plugins/twitter.py:181 +msgid "Access Secret" +msgstr "Secreto de Acceso" + +#: apprise/plugins/viber.py:49 +msgid "Viber" +msgstr "Viber" + +#: apprise/plugins/viber.py:81 +#, fuzzy +msgid "Authentication Token" +msgstr "Token de Autenticacion" + +#: apprise/plugins/viber.py:87 +msgid "Receiver IDs" +msgstr "IDs de Receptores" + +#: apprise/plugins/viber.py:105 +#, fuzzy +msgid "Bot Avatar URL" +msgstr "URL del Avatar del Bot" + +#: apprise/plugins/voipms.py:83 +#, fuzzy +msgid "User Email" +msgstr "Correo del Usuario" + +#: apprise/plugins/vapid/__init__.py:179 apprise/plugins/vonage.py:133 +msgid "ttl" +msgstr "ttl" + +#: apprise/plugins/webexteams.py:178 +#, fuzzy +msgid "Bot Access Token" +msgstr "Token de Acceso OAuth" + +#: apprise/plugins/webexteams.py:184 +#, fuzzy +msgid "Room IDs" +msgstr "ID de la Sala" + +#: apprise/plugins/wechat.py:174 +#, fuzzy +msgid "Corp ID" +msgstr "ID de Aplicacion" + +#: apprise/plugins/wechat.py:181 +#, fuzzy +msgid "App Secret" +msgstr "Secreto de API" + +#: apprise/plugins/wechat.py:189 +#, fuzzy +msgid "Agent ID" +msgstr "ID de Destino" + +#: apprise/plugins/wechat.py:205 +#, fuzzy +msgid "Target Department" +msgstr "Dispositivo de Destino" + +#: apprise/plugins/wechat.py:212 +#, fuzzy +msgid "Target Tag" +msgstr "Equipo de Destino" + +#: apprise/plugins/wecombot.py:99 +#, fuzzy +msgid "Bot Webhook Key" +msgstr "Clave Webhook del Bot" + +#: apprise/plugins/whatsapp.py:128 +msgid "Template Name" +msgstr "Nombre de Plantilla" + +#: apprise/plugins/whatsapp.py:134 +#, fuzzy +msgid "From Phone ID" +msgstr "ID del Telefono de Origen" + +#: apprise/plugins/windows.py:62 +msgid "A local Microsoft Windows environment is required." +msgstr "Se requiere un entorno local de Microsoft Windows." + +#: apprise/plugins/workflows.py:138 +#, fuzzy +msgid "Workflow ID" +msgstr "ID del Flujo de Trabajo" + +#: apprise/plugins/workflows.py:146 +msgid "Signature" +msgstr "Firma" + +#: apprise/plugins/workflows.py:169 +msgid "Use Power Automate URL" +msgstr "Usar URL de Power Automate" + +#: apprise/plugins/workflows.py:176 +msgid "Wrap Text" +msgstr "Ajustar Texto" + +#: apprise/plugins/workflows.py:191 +#, fuzzy +msgid "API Version" +msgstr "Version de API" + +#: apprise/plugins/wxpusher.py:121 +#, fuzzy +msgid "App Token" +msgstr "Token de Aplicacion" + +#: apprise/plugins/wxpusher.py:133 +#, fuzzy +msgid "Target User ID" +msgstr "ID de Usuario de Destino" + +#: apprise/plugins/zoom.py:145 +#, fuzzy +msgid "Verification Token" +msgstr "Token de Autenticacion" + +#: apprise/plugins/zulip.py:148 +#, fuzzy +msgid "Target Stream" +msgstr "Stream de Destino" + +#: apprise/plugins/email/base.py:184 +msgid "SMTP Server" +msgstr "Servidor SMTP" + +#: apprise/plugins/email/base.py:189 apprise/plugins/xmpp/base.py:149 +msgid "Secure Mode" +msgstr "Modo Seguro" + +#: apprise/plugins/email/base.py:201 +#, fuzzy +msgid "PGP Security Mode" +msgstr "Modo Seguro" + +#: apprise/plugins/email/base.py:208 +msgid "PGP Public Key Path" +msgstr "Ruta de Clave Publica PGP" + +#: apprise/plugins/email/base.py:221 +#, fuzzy +msgid "PGP Private Key Path" +msgstr "Ruta de Clave Publica PGP" + +#: apprise/plugins/email/base.py:229 +msgid "Web Key Directory" +msgstr "" + +#: apprise/plugins/email/base.py:235 +msgid "To Email" +msgstr "Para Correo" + +#: apprise/plugins/fcm/__init__.py:148 +msgid "OAuth2 KeyFile" +msgstr "Archivo de Clave OAuth2" + +#: apprise/plugins/fcm/__init__.py:193 +msgid "Custom Image URL" +msgstr "URL de Imagen Personalizada" + +#: apprise/plugins/fcm/__init__.py:205 +msgid "Notification Color" +msgstr "Color de Notificacion" + +#: apprise/plugins/fcm/__init__.py:215 +msgid "Data Entries" +msgstr "Entradas de Datos" + +#: apprise/plugins/irc/base.py:159 +#, fuzzy +msgid "Real Name" +msgstr "Nombre Real" + +#: apprise/plugins/irc/base.py:160 +#, fuzzy +msgid "Nickname" +msgstr "Apodo" + +#: apprise/plugins/irc/base.py:162 +#, fuzzy +msgid "Join Channels" +msgstr "Unirse a Canales" + +#: apprise/plugins/irc/base.py:167 +msgid "Auth Mode" +msgstr "" + +#: apprise/plugins/matrix/base.py:301 +msgid "Target Room Alias" +msgstr "Alias de Sala de Destino" + +#: apprise/plugins/matrix/base.py:324 +#, fuzzy +msgid "Server Discovery" +msgstr "Descubrimiento de Servidor" + +#: apprise/plugins/matrix/base.py:329 +msgid "Force Home Server on Room IDs" +msgstr "Forzar Servidor Home en IDs de Sala" + +#: apprise/plugins/matrix/base.py:340 +#, fuzzy +msgid "Webhook Path" +msgstr "Webhook" + +#: apprise/plugins/matrix/base.py:346 +msgid "Matrix API Verion" +msgstr "Version de API de Matrix" + +#: apprise/plugins/matrix/base.py:358 +#, fuzzy +msgid "End-to-End Encryption" +msgstr "Cifrado PGP" + +#: apprise/plugins/vapid/__init__.py:193 +msgid "PEM Private KeyFile" +msgstr "Archivo de Clave Privada PEM" + +#: apprise/plugins/vapid/__init__.py:199 +msgid "Subscripion File" +msgstr "Archivo de Suscripcion" + +#: apprise/plugins/xmpp/base.py:144 +#, fuzzy +msgid "XMPP Server" +msgstr "Servidor SMTP" + +#: apprise/plugins/xmpp/base.py:156 +#, fuzzy +msgid "Get Roster" +msgstr "Obtener Lista de Contactos" + +#: apprise/plugins/xmpp/base.py:161 +msgid "Use Subject" +msgstr "Usar Asunto" + +#: apprise/plugins/xmpp/base.py:166 +#, fuzzy +msgid "Keep Connection Alive" +msgstr "Mantener Conexion Activa" + +#: apprise/plugins/xmpp/base.py:172 +#, fuzzy +msgid "MUC Nickname" +msgstr "Apodo MUC" + +#: apprise/plugins/xmpp/base.py:176 +msgid "SCRAM-PLUS Channel Binding" +msgstr "" diff --git a/libs/apprise/i18n/pt_BR/LC_MESSAGES/apprise.mo b/libs/apprise/i18n/pt_BR/LC_MESSAGES/apprise.mo new file mode 100644 index 000000000..947d0b63c Binary files /dev/null and b/libs/apprise/i18n/pt_BR/LC_MESSAGES/apprise.mo differ diff --git a/libs/apprise/i18n/pt_BR/LC_MESSAGES/apprise.po b/libs/apprise/i18n/pt_BR/LC_MESSAGES/apprise.po new file mode 100644 index 000000000..71c0ccae1 --- /dev/null +++ b/libs/apprise/i18n/pt_BR/LC_MESSAGES/apprise.po @@ -0,0 +1,2162 @@ +# Portuguese (Brazilian) translations for apprise. +# Copyright (C) 2026 Chris Caron +# This file is distributed under the same license as the apprise project. +# Chris Caron , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: apprise 1.11.0\n" +"Report-Msgid-Bugs-To: lead2gold@gmail.com\n" +"POT-Creation-Date: 2026-05-29 13:49-0400\n" +"PO-Revision-Date: 2019-05-24 20:00-0400\n" +"Last-Translator: Chris Caron \n" +"Language: pt_BR\n" +"Language-Team: pt_BR \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: apprise/attachment/base.py:96 apprise/url.py:148 +msgid "Verify SSL" +msgstr "Verificar SSL" + +#: apprise/url.py:158 +msgid "Follow Redirects" +msgstr "" + +#: apprise/url.py:168 +#, fuzzy +msgid "Socket Read Timeout" +msgstr "Tempo Limite de Leitura do Socket" + +#: apprise/url.py:182 +#, fuzzy +msgid "Socket Connect Timeout" +msgstr "Tempo Limite de Conexao do Socket" + +#: apprise/attachment/base.py:82 +msgid "Cache Age" +msgstr "Idade do Cache" + +#: apprise/attachment/base.py:88 +msgid "Forced Mime Type" +msgstr "Tipo MIME Forcado" + +#: apprise/attachment/base.py:92 +msgid "Forced File Name" +msgstr "Nome de Arquivo Forcado" + +#: apprise/attachment/file.py:41 apprise/config/file.py:41 +msgid "Local File" +msgstr "Arquivo Local" + +#: apprise/attachment/http.py:46 apprise/config/http.py:54 +msgid "Web Based" +msgstr "Baseado na Web" + +#: apprise/attachment/memory.py:44 apprise/config/memory.py:37 +msgid "Memory" +msgstr "Memoria" + +#: apprise/plugins/__init__.py:278 +msgid "Schema" +msgstr "Esquema" + +#: apprise/plugins/__init__.py:398 +msgid "No dependencies." +msgstr "Sem dependencias." + +#: apprise/plugins/__init__.py:401 +msgid "Packages are required to function." +msgstr "Pacotes sao necessarios para funcionar." + +#: apprise/plugins/__init__.py:405 +msgid "Packages are recommended to improve functionality." +msgstr "Pacotes sao recomendados para melhorar a funcionalidade." + +#: apprise/plugins/africas_talking.py:132 +#, fuzzy +msgid "App User Name" +msgstr "Nome de Usuario do Aplicativo" + +#: apprise/plugins/africas_talking.py:138 apprise/plugins/brevo.py:157 +#: apprise/plugins/burstsms.py:104 apprise/plugins/clicksend.py:98 +#: apprise/plugins/dot.py:136 apprise/plugins/evolution.py:105 +#: apprise/plugins/exotel.py:213 apprise/plugins/fcm/__init__.py:143 +#: apprise/plugins/httpsms.py:77 apprise/plugins/jira.py:230 +#: apprise/plugins/join.py:140 apprise/plugins/kavenegar.py:115 +#: apprise/plugins/kumulos.py:87 apprise/plugins/mailgun.py:146 +#: apprise/plugins/messagebird.py:78 apprise/plugins/octopush.py:136 +#: apprise/plugins/one_signal.py:113 apprise/plugins/opsgenie.py:228 +#: apprise/plugins/pagerduty.py:131 apprise/plugins/popcorn_notify.py:71 +#: apprise/plugins/postmark.py:117 apprise/plugins/prowl.py:121 +#: apprise/plugins/resend.py:107 apprise/plugins/resend.py:145 +#: apprise/plugins/sendgrid.py:116 apprise/plugins/seven.py:75 +#: apprise/plugins/simplepush.py:101 apprise/plugins/smsmanager.py:106 +#: apprise/plugins/smtp2go.py:118 apprise/plugins/sparkpost.py:169 +#: apprise/plugins/splunk.py:165 apprise/plugins/techuluspush.py:97 +#: apprise/plugins/twilio.py:194 apprise/plugins/vapid/__init__.py:152 +#: apprise/plugins/vonage.py:80 +msgid "API Key" +msgstr "Chave de API" + +#: apprise/plugins/africas_talking.py:145 apprise/plugins/fortysixelks.py:106 +#, fuzzy +msgid "Target Phone" +msgstr "Telefone de Destino" + +#: apprise/plugins/africas_talking.py:150 apprise/plugins/aprs.py:187 +#: apprise/plugins/bark.py:159 apprise/plugins/brevo.py:174 +#: apprise/plugins/bulksms.py:137 apprise/plugins/bulkvs.py:110 +#: apprise/plugins/burstsms.py:131 apprise/plugins/clickatell.py:90 +#: apprise/plugins/clicksend.py:112 apprise/plugins/d7networks.py:110 +#: apprise/plugins/dapnet.py:138 apprise/plugins/dingtalk.py:111 +#: apprise/plugins/email/base.py:164 apprise/plugins/evolution.py:134 +#: apprise/plugins/exotel.py:190 apprise/plugins/fcm/__init__.py:168 +#: apprise/plugins/flock.py:125 apprise/plugins/fortysixelks.py:111 +#: apprise/plugins/home_assistant.py:153 apprise/plugins/httpsms.py:97 +#: apprise/plugins/humhub.py:149 apprise/plugins/irc/base.py:149 +#: apprise/plugins/join.py:172 apprise/plugins/kavenegar.py:135 +#: apprise/plugins/line.py:93 apprise/plugins/mailgun.py:157 +#: apprise/plugins/mastodon.py:202 apprise/plugins/matrix/base.py:307 +#: apprise/plugins/mattermost.py:189 apprise/plugins/messagebird.py:99 +#: apprise/plugins/mqtt.py:177 apprise/plugins/msg91.py:123 +#: apprise/plugins/nextcloud.py:148 apprise/plugins/nextcloudtalk.py:101 +#: apprise/plugins/notifiarr.py:103 apprise/plugins/notificationapi.py:192 +#: apprise/plugins/ntfy.py:241 apprise/plugins/octopush.py:153 +#: apprise/plugins/office365.py:287 apprise/plugins/one_signal.py:141 +#: apprise/plugins/plivo.py:114 apprise/plugins/popcorn_notify.py:89 +#: apprise/plugins/postmark.py:133 apprise/plugins/pushbullet.py:105 +#: apprise/plugins/pushed.py:107 apprise/plugins/pushover.py:252 +#: apprise/plugins/pushsafer.py:376 apprise/plugins/pushy.py:97 +#: apprise/plugins/reddit.py:170 apprise/plugins/resend.py:124 +#: apprise/plugins/revolt.py:112 apprise/plugins/ringcentral.py:219 +#: apprise/plugins/rocketchat.py:167 apprise/plugins/sendgrid.py:133 +#: apprise/plugins/sendpulse.py:139 apprise/plugins/seven.py:88 +#: apprise/plugins/sfr.py:113 apprise/plugins/signal_api.py:138 +#: apprise/plugins/sinch.py:140 apprise/plugins/slack.py:273 +#: apprise/plugins/smpp.py:128 apprise/plugins/smseagle.py:172 +#: apprise/plugins/smsmanager.py:119 apprise/plugins/sns.py:170 +#: apprise/plugins/telegram.py:359 apprise/plugins/threema.py:115 +#: apprise/plugins/twilio.py:174 apprise/plugins/twist.py:123 +#: apprise/plugins/twitter.py:193 apprise/plugins/vapid/__init__.py:158 +#: apprise/plugins/voipms.py:108 apprise/plugins/vonage.py:108 +#: apprise/plugins/wechat.py:218 apprise/plugins/whatsapp.py:166 +#: apprise/plugins/wxpusher.py:139 apprise/plugins/xmpp/base.py:134 +#: apprise/plugins/zulip.py:153 +msgid "Targets" +msgstr "Destinatarios" + +#: apprise/plugins/africas_talking.py:166 +#, fuzzy +msgid "From" +msgstr "De" + +#: apprise/plugins/africas_talking.py:172 +#, fuzzy +msgid "SMS Mode" +msgstr "Modo SMS" + +#: apprise/plugins/africas_talking.py:181 apprise/plugins/bulksms.py:170 +#: apprise/plugins/bulkvs.py:131 apprise/plugins/burstsms.py:166 +#: apprise/plugins/clicksend.py:130 apprise/plugins/d7networks.py:144 +#: apprise/plugins/dapnet.py:167 apprise/plugins/exotel.py:222 +#: apprise/plugins/home_assistant.py:170 apprise/plugins/jira.py:282 +#: apprise/plugins/mailgun.py:194 apprise/plugins/mastodon.py:247 +#: apprise/plugins/octopush.py:173 apprise/plugins/one_signal.py:185 +#: apprise/plugins/opsgenie.py:307 apprise/plugins/plivo.py:137 +#: apprise/plugins/popcorn_notify.py:104 apprise/plugins/signal_api.py:160 +#: apprise/plugins/smseagle.py:211 apprise/plugins/smsmanager.py:152 +#: apprise/plugins/smtp2go.py:151 apprise/plugins/sparkpost.py:209 +#: apprise/plugins/twitter.py:218 +#, fuzzy +msgid "Batch Mode" +msgstr "Modo em Lote" + +#: apprise/plugins/apprise_api.py:102 apprise/plugins/bark.py:134 +#: apprise/plugins/custom_form.py:130 apprise/plugins/custom_json.py:112 +#: apprise/plugins/custom_xml.py:112 apprise/plugins/emby.py:85 +#: apprise/plugins/enigma2.py:110 apprise/plugins/evolution.py:111 +#: apprise/plugins/fluxer.py:159 apprise/plugins/gotify.py:129 +#: apprise/plugins/growl.py:140 apprise/plugins/home_assistant.py:122 +#: apprise/plugins/humhub.py:138 apprise/plugins/irc/base.py:117 +#: apprise/plugins/kodi.py:96 apprise/plugins/lametric.py:460 +#: apprise/plugins/mastodon.py:180 apprise/plugins/matrix/base.py:262 +#: apprise/plugins/mattermost.py:155 apprise/plugins/misskey.py:117 +#: apprise/plugins/mqtt.py:149 apprise/plugins/nextcloud.py:116 +#: apprise/plugins/nextcloudtalk.py:74 apprise/plugins/notica.py:126 +#: apprise/plugins/notifico.py:166 apprise/plugins/ntfy.py:211 +#: apprise/plugins/parseplatform.py:90 apprise/plugins/pushdeer.py:75 +#: apprise/plugins/pushjet.py:71 apprise/plugins/rocketchat.py:120 +#: apprise/plugins/rsyslog.py:180 apprise/plugins/signal_api.py:97 +#: apprise/plugins/smseagle.py:135 apprise/plugins/sogs.py:207 +#: apprise/plugins/synology.py:83 apprise/plugins/workflows.py:126 +#: apprise/plugins/xmpp/base.py:101 +msgid "Hostname" +msgstr "Nome do Host" + +#: apprise/plugins/apprise_api.py:107 apprise/plugins/bark.py:139 +#: apprise/plugins/custom_form.py:135 apprise/plugins/custom_json.py:117 +#: apprise/plugins/custom_xml.py:117 apprise/plugins/email/base.py:153 +#: apprise/plugins/emby.py:90 apprise/plugins/enigma2.py:115 +#: apprise/plugins/evolution.py:116 apprise/plugins/fluxer.py:163 +#: apprise/plugins/gotify.py:140 apprise/plugins/growl.py:145 +#: apprise/plugins/home_assistant.py:127 apprise/plugins/humhub.py:143 +#: apprise/plugins/irc/base.py:122 apprise/plugins/kodi.py:101 +#: apprise/plugins/lametric.py:464 apprise/plugins/mastodon.py:190 +#: apprise/plugins/matrix/base.py:267 apprise/plugins/mattermost.py:171 +#: apprise/plugins/misskey.py:127 apprise/plugins/mqtt.py:154 +#: apprise/plugins/nextcloud.py:121 apprise/plugins/nextcloudtalk.py:79 +#: apprise/plugins/notica.py:130 apprise/plugins/notifico.py:170 +#: apprise/plugins/ntfy.py:215 apprise/plugins/parseplatform.py:95 +#: apprise/plugins/pushdeer.py:79 apprise/plugins/pushjet.py:76 +#: apprise/plugins/rocketchat.py:125 apprise/plugins/rsyslog.py:185 +#: apprise/plugins/signal_api.py:102 apprise/plugins/smpp.py:108 +#: apprise/plugins/smseagle.py:140 apprise/plugins/sogs.py:212 +#: apprise/plugins/synology.py:88 apprise/plugins/workflows.py:131 +#: apprise/plugins/xmpp/base.py:106 +msgid "Port" +msgstr "Porta" + +#: apprise/plugins/apprise_api.py:113 apprise/plugins/bark.py:145 +#: apprise/plugins/bluesky.py:119 apprise/plugins/custom_form.py:141 +#: apprise/plugins/custom_json.py:123 apprise/plugins/custom_xml.py:123 +#: apprise/plugins/emby.py:97 apprise/plugins/enigma2.py:121 +#: apprise/plugins/freemobile.py:78 apprise/plugins/home_assistant.py:133 +#: apprise/plugins/jira.py:236 apprise/plugins/kodi.py:107 +#: apprise/plugins/lametric.py:471 apprise/plugins/matrix/base.py:273 +#: apprise/plugins/nextcloud.py:127 apprise/plugins/nextcloudtalk.py:85 +#: apprise/plugins/notica.py:136 apprise/plugins/notifico.py:176 +#: apprise/plugins/ntfy.py:221 apprise/plugins/opsgenie.py:234 +#: apprise/plugins/pushjet.py:88 apprise/plugins/rocketchat.py:131 +#: apprise/plugins/signal_api.py:108 apprise/plugins/smpp.py:92 +#: apprise/plugins/synology.py:94 +msgid "Username" +msgstr "Nome de Usuario" + +#: apprise/plugins/apprise_api.py:117 apprise/plugins/aprs.py:172 +#: apprise/plugins/bark.py:149 apprise/plugins/bluesky.py:124 +#: apprise/plugins/bulksms.py:117 apprise/plugins/bulkvs.py:90 +#: apprise/plugins/custom_form.py:145 apprise/plugins/custom_json.py:127 +#: apprise/plugins/custom_xml.py:127 apprise/plugins/dapnet.py:123 +#: apprise/plugins/email/base.py:143 apprise/plugins/emby.py:101 +#: apprise/plugins/enigma2.py:125 apprise/plugins/freemobile.py:83 +#: apprise/plugins/growl.py:151 apprise/plugins/home_assistant.py:137 +#: apprise/plugins/humhub.py:133 apprise/plugins/irc/base.py:132 +#: apprise/plugins/kodi.py:111 apprise/plugins/matrix/base.py:277 +#: apprise/plugins/mqtt.py:165 apprise/plugins/nextcloud.py:131 +#: apprise/plugins/nextcloudtalk.py:90 apprise/plugins/notica.py:140 +#: apprise/plugins/notifico.py:180 apprise/plugins/ntfy.py:225 +#: apprise/plugins/pushjet.py:92 apprise/plugins/reddit.py:145 +#: apprise/plugins/rocketchat.py:135 apprise/plugins/signal_api.py:112 +#: apprise/plugins/simplepush.py:108 apprise/plugins/smpp.py:97 +#: apprise/plugins/synology.py:98 apprise/plugins/twist.py:101 +#: apprise/plugins/voipms.py:88 apprise/plugins/xmpp/base.py:117 +msgid "Password" +msgstr "Senha" + +#: apprise/plugins/apprise_api.py:122 apprise/plugins/chanify.py:74 +#: apprise/plugins/dingtalk.py:93 apprise/plugins/exotel.py:166 +#: apprise/plugins/feishu.py:80 apprise/plugins/gotify.py:123 +#: apprise/plugins/mattermost.py:161 apprise/plugins/notica.py:119 +#: apprise/plugins/notifiarr.py:91 apprise/plugins/ntfy.py:230 +#: apprise/plugins/pushme.py:62 apprise/plugins/ryver.py:99 +#: apprise/plugins/serverchan.py:70 apprise/plugins/slack.py:323 +#: apprise/plugins/synology.py:103 apprise/plugins/zulip.py:136 +msgid "Token" +msgstr "Token" + +#: apprise/plugins/apprise_api.py:136 apprise/plugins/jira.py:301 +#: apprise/plugins/ntfy.py:288 apprise/plugins/opsgenie.py:294 +#: apprise/plugins/pagertree.py:133 +#, fuzzy +msgid "Tags" +msgstr "Etiquetas" + +#: apprise/plugins/apprise_api.py:140 +msgid "Query Method" +msgstr "Metodo de Consulta" + +#: apprise/plugins/apprise_api.py:154 apprise/plugins/custom_form.py:174 +#: apprise/plugins/custom_json.py:150 apprise/plugins/custom_xml.py:150 +#: apprise/plugins/enigma2.py:153 apprise/plugins/nextcloud.py:181 +#: apprise/plugins/nextcloudtalk.py:122 apprise/plugins/notica.py:156 +#: apprise/plugins/pagertree.py:142 apprise/plugins/synology.py:128 +msgid "HTTP Header" +msgstr "Cabecalho HTTP" + +#: apprise/plugins/aprs.py:167 apprise/plugins/bulksms.py:112 +#: apprise/plugins/bulkvs.py:85 apprise/plugins/clicksend.py:93 +#: apprise/plugins/dapnet.py:118 apprise/plugins/email/base.py:139 +#: apprise/plugins/mailgun.py:136 apprise/plugins/mqtt.py:160 +#: apprise/plugins/reddit.py:140 apprise/plugins/sendpulse.py:110 +#: apprise/plugins/smtp2go.py:108 apprise/plugins/sparkpost.py:159 +msgid "User Name" +msgstr "Nome de Usuario" + +#: apprise/plugins/aprs.py:178 apprise/plugins/aprs.py:199 +#: apprise/plugins/dapnet.py:129 apprise/plugins/dapnet.py:162 +#, fuzzy +msgid "Target Callsign" +msgstr "Indicativo de Chamada de Destino" + +#: apprise/plugins/aprs.py:204 +msgid "Resend Delay" +msgstr "Atraso de Reenvio" + +#: apprise/plugins/aprs.py:211 +msgid "Locale" +msgstr "Localidade" + +#: apprise/plugins/bark.py:154 apprise/plugins/fcm/__init__.py:157 +#: apprise/plugins/home_assistant.py:148 apprise/plugins/pushbullet.py:89 +#: apprise/plugins/pushover.py:240 apprise/plugins/pushsafer.py:366 +#: apprise/plugins/pushy.py:85 +msgid "Target Device" +msgstr "Dispositivo de Destino" + +#: apprise/plugins/bark.py:171 apprise/plugins/lametric.py:516 +#: apprise/plugins/macosx.py:124 apprise/plugins/pushover.py:269 +#: apprise/plugins/pushsafer.py:392 apprise/plugins/pushy.py:110 +msgid "Sound" +msgstr "Som" + +#: apprise/plugins/bark.py:176 +msgid "Level" +msgstr "Nivel" + +#: apprise/plugins/bark.py:181 +msgid "Volume" +msgstr "Volume" + +#: apprise/plugins/bark.py:187 apprise/plugins/ntfy.py:270 +#: apprise/plugins/pagerduty.py:172 +msgid "Click" +msgstr "Clique" + +#: apprise/plugins/bark.py:191 apprise/plugins/pushy.py:114 +msgid "Badge" +msgstr "Emblema" + +#: apprise/plugins/bark.py:196 +msgid "Category" +msgstr "Categoria" + +#: apprise/plugins/bark.py:200 apprise/plugins/join.py:158 +#: apprise/plugins/pagerduty.py:163 +msgid "Group" +msgstr "Grupo" + +#: apprise/plugins/bark.py:204 apprise/plugins/dbus.py:228 +#: apprise/plugins/discord.py:196 apprise/plugins/fcm/__init__.py:197 +#: apprise/plugins/flock.py:136 apprise/plugins/fluxer.py:250 +#: apprise/plugins/glib.py:188 apprise/plugins/gnome.py:154 +#: apprise/plugins/growl.py:175 apprise/plugins/join.py:183 +#: apprise/plugins/kodi.py:129 apprise/plugins/line.py:108 +#: apprise/plugins/macosx.py:115 apprise/plugins/matrix/base.py:318 +#: apprise/plugins/mattermost.py:212 apprise/plugins/notifiarr.py:125 +#: apprise/plugins/ntfy.py:256 apprise/plugins/one_signal.py:164 +#: apprise/plugins/pagerduty.py:191 apprise/plugins/ryver.py:124 +#: apprise/plugins/slack.py:291 apprise/plugins/telegram.py:370 +#: apprise/plugins/vapid/__init__.py:204 apprise/plugins/windows.py:106 +#: apprise/plugins/workflows.py:163 +msgid "Include Image" +msgstr "Incluir Imagem" + +#: apprise/plugins/bark.py:210 apprise/plugins/mattermost.py:208 +#: apprise/plugins/revolt.py:129 +msgid "Icon URL" +msgstr "URL do Icone" + +#: apprise/plugins/bark.py:214 apprise/plugins/streamlabs.py:119 +msgid "Call" +msgstr "Chamada" + +#: apprise/plugins/base.py:305 +msgid "Overflow Mode" +msgstr "Modo de Estouro" + +#: apprise/plugins/base.py:320 +msgid "Notify Format" +msgstr "Formato de Notificacao" + +#: apprise/plugins/base.py:330 +#, fuzzy +msgid "Interpret Emojis" +msgstr "Interpretar Emojis" + +#: apprise/plugins/base.py:340 +msgid "Persistent Storage" +msgstr "Armazenamento Persistente" + +#: apprise/plugins/base.py:350 +#, fuzzy +msgid "Timezone" +msgstr "Fuso Horario" + +#: apprise/plugins/base.py:359 +#, fuzzy +msgid "Service Retry" +msgstr "Servico" + +#: apprise/plugins/base.py:367 +#, fuzzy +msgid "Inter-Retry Wait" +msgstr "Interpretar Emojis" + +#: apprise/plugins/base.py:375 +#, fuzzy +msgid "Optional Service" +msgstr "Servico" + +#: apprise/plugins/blink1.py:151 +#, fuzzy +msgid "blink(1)" +msgstr "Link" + +#: apprise/plugins/blink1.py:178 +#, fuzzy +msgid "Serial Number" +msgstr "Numero de Serie do Dispositivo" + +#: apprise/plugins/blink1.py:188 +#, fuzzy +msgid "Duration (ms)" +msgstr "Duracao" + +#: apprise/plugins/blink1.py:195 +msgid "Fade Time (ms)" +msgstr "" + +#: apprise/plugins/blink1.py:202 +msgid "LED Number" +msgstr "" + +#: apprise/plugins/brevo.py:164 apprise/plugins/postmark.py:123 +#: apprise/plugins/resend.py:114 apprise/plugins/sendgrid.py:123 +#, fuzzy +msgid "Source Email" +msgstr "E-mail de Origem" + +#: apprise/plugins/brevo.py:169 apprise/plugins/email/base.py:159 +#: apprise/plugins/mailgun.py:152 apprise/plugins/notificationapi.py:177 +#: apprise/plugins/office365.py:282 apprise/plugins/one_signal.py:124 +#: apprise/plugins/popcorn_notify.py:84 apprise/plugins/postmark.py:128 +#: apprise/plugins/pushbullet.py:100 apprise/plugins/pushsafer.py:371 +#: apprise/plugins/resend.py:119 apprise/plugins/sendgrid.py:128 +#: apprise/plugins/sendpulse.py:134 apprise/plugins/slack.py:256 +#: apprise/plugins/threema.py:105 +msgid "Target Email" +msgstr "E-mail de Destino" + +#: apprise/plugins/brevo.py:185 apprise/plugins/postmark.py:149 +#: apprise/plugins/ses.py:205 +#, fuzzy +msgid "Reply To Email" +msgstr "Responder para E-mail" + +#: apprise/plugins/brevo.py:193 apprise/plugins/email/base.py:240 +#: apprise/plugins/mailgun.py:186 apprise/plugins/notificationapi.py:243 +#: apprise/plugins/office365.py:307 apprise/plugins/postmark.py:157 +#: apprise/plugins/resend.py:158 apprise/plugins/sendgrid.py:153 +#: apprise/plugins/sendpulse.py:169 apprise/plugins/ses.py:232 +#: apprise/plugins/smtp2go.py:143 apprise/plugins/sparkpost.py:201 +msgid "Carbon Copy" +msgstr "Copia Carbono" + +#: apprise/plugins/brevo.py:197 apprise/plugins/email/base.py:244 +#: apprise/plugins/mailgun.py:190 apprise/plugins/notificationapi.py:247 +#: apprise/plugins/office365.py:311 apprise/plugins/postmark.py:161 +#: apprise/plugins/resend.py:162 apprise/plugins/sendgrid.py:157 +#: apprise/plugins/sendpulse.py:173 apprise/plugins/ses.py:236 +#: apprise/plugins/smtp2go.py:147 apprise/plugins/sparkpost.py:205 +msgid "Blind Carbon Copy" +msgstr "Copia Carbono Oculta" + +#: apprise/plugins/bulksms.py:123 apprise/plugins/bulkvs.py:103 +#: apprise/plugins/burstsms.py:124 apprise/plugins/clickatell.py:83 +#: apprise/plugins/clicksend.py:105 apprise/plugins/d7networks.py:103 +#: apprise/plugins/dingtalk.py:106 apprise/plugins/evolution.py:127 +#: apprise/plugins/exotel.py:183 apprise/plugins/httpsms.py:90 +#: apprise/plugins/kavenegar.py:128 apprise/plugins/messagebird.py:92 +#: apprise/plugins/msg91.py:116 apprise/plugins/octopush.py:146 +#: apprise/plugins/plivo.py:107 apprise/plugins/popcorn_notify.py:77 +#: apprise/plugins/ringcentral.py:213 apprise/plugins/seven.py:81 +#: apprise/plugins/signal_api.py:124 apprise/plugins/sinch.py:127 +#: apprise/plugins/smpp.py:121 apprise/plugins/smseagle.py:151 +#: apprise/plugins/smsmanager.py:112 apprise/plugins/sns.py:157 +#: apprise/plugins/threema.py:98 apprise/plugins/twilio.py:161 +#: apprise/plugins/voipms.py:101 apprise/plugins/vonage.py:101 +#: apprise/plugins/whatsapp.py:147 +msgid "Target Phone No" +msgstr "Numero de Telefone de Destino" + +#: apprise/plugins/bulksms.py:130 apprise/plugins/nextcloud.py:142 +#: apprise/plugins/pushover.py:246 +#, fuzzy +msgid "Target Group" +msgstr "Grupo de Destino" + +#: apprise/plugins/bulksms.py:149 apprise/plugins/bulkvs.py:96 +#: apprise/plugins/bulkvs.py:122 apprise/plugins/clickatell.py:78 +#: apprise/plugins/fortysixelks.py:100 apprise/plugins/httpsms.py:83 +#: apprise/plugins/httpsms.py:115 apprise/plugins/ringcentral.py:207 +#: apprise/plugins/signal_api.py:117 apprise/plugins/sinch.py:120 +#: apprise/plugins/smpp.py:114 apprise/plugins/smsmanager.py:134 +#: apprise/plugins/twilio.py:154 apprise/plugins/voipms.py:94 +#: apprise/plugins/vonage.py:94 +msgid "From Phone No" +msgstr "Numero de Telefone de Origem" + +#: apprise/plugins/bulksms.py:155 +#, fuzzy +msgid "Route Group" +msgstr "Grupo de Roteamento" + +#: apprise/plugins/bulksms.py:162 apprise/plugins/d7networks.py:136 +#: apprise/plugins/exotel.py:228 +msgid "Unicode Characters" +msgstr "Caracteres Unicode" + +#: apprise/plugins/burstsms.py:111 apprise/plugins/threema.py:92 +#: apprise/plugins/vonage.py:87 +#, fuzzy +msgid "API Secret" +msgstr "Segredo de API" + +#: apprise/plugins/burstsms.py:118 +#, fuzzy +msgid "Sender ID" +msgstr "ID do Remetente" + +#: apprise/plugins/burstsms.py:152 +msgid "Country" +msgstr "Pais" + +#: apprise/plugins/burstsms.py:161 +msgid "validity" +msgstr "validade" + +#: apprise/plugins/chanify.py:47 +msgid "Chanify" +msgstr "Chanify" + +#: apprise/plugins/chime.py:130 apprise/plugins/discord.py:130 +#: apprise/plugins/fluxer.py:174 apprise/plugins/ifttt.py:103 +#: apprise/plugins/zoom.py:139 +msgid "Webhook ID" +msgstr "ID do Webhook" + +#: apprise/plugins/chime.py:136 apprise/plugins/discord.py:136 +#: apprise/plugins/fluxer.py:181 apprise/plugins/google_chat.py:118 +#: apprise/plugins/webexteams.py:171 +msgid "Webhook Token" +msgstr "Token do Webhook" + +#: apprise/plugins/clickatell.py:45 +msgid "Clickatell" +msgstr "Clickatell" + +#: apprise/plugins/clickatell.py:72 apprise/plugins/rocketchat.py:140 +#, fuzzy +msgid "API Token" +msgstr "Token de API" + +#: apprise/plugins/custom_form.py:157 apprise/plugins/custom_json.py:139 +#: apprise/plugins/custom_xml.py:139 +msgid "Fetch Method" +msgstr "Metodo de Busca" + +#: apprise/plugins/custom_form.py:163 +msgid "Attach File As" +msgstr "Anexar Arquivo Como" + +#: apprise/plugins/custom_form.py:178 apprise/plugins/custom_json.py:154 +#: apprise/plugins/custom_xml.py:154 apprise/plugins/pagertree.py:146 +msgid "Payload Extras" +msgstr "Extras do Payload" + +#: apprise/plugins/custom_form.py:182 apprise/plugins/custom_json.py:158 +#: apprise/plugins/custom_xml.py:158 +msgid "GET Params" +msgstr "Parametros GET" + +#: apprise/plugins/d7networks.py:97 +#, fuzzy +msgid "API Access Token" +msgstr "Token de Acesso a API" + +#: apprise/plugins/d7networks.py:127 apprise/plugins/seven.py:105 +msgid "Originating Address" +msgstr "Endereco de Origem" + +#: apprise/plugins/dapnet.py:150 apprise/plugins/exotel.py:240 +#: apprise/plugins/gotify.py:153 apprise/plugins/growl.py:163 +#: apprise/plugins/jira.py:287 apprise/plugins/join.py:189 +#: apprise/plugins/lametric.py:494 apprise/plugins/ntfy.py:282 +#: apprise/plugins/opsgenie.py:280 apprise/plugins/prowl.py:141 +#: apprise/plugins/pushover.py:263 apprise/plugins/pushsafer.py:387 +#: apprise/plugins/smseagle.py:187 +msgid "Priority" +msgstr "Prioridade" + +#: apprise/plugins/dapnet.py:156 +msgid "Transmitter Groups" +msgstr "Grupos de Transmissores" + +#: apprise/plugins/dbus.py:156 +msgid "libdbus-1.so.x must be installed." +msgstr "libdbus-1.so.x deve estar instalado." + +#: apprise/plugins/dbus.py:160 +msgid "DBus Notification" +msgstr "Notificacao DBus" + +#: apprise/plugins/dbus.py:204 apprise/plugins/glib.py:165 +#: apprise/plugins/gnome.py:142 apprise/plugins/pagertree.py:128 +msgid "Urgency" +msgstr "Urgencia" + +#: apprise/plugins/dbus.py:216 apprise/plugins/glib.py:176 +msgid "X-Axis" +msgstr "Eixo X" + +#: apprise/plugins/dbus.py:222 apprise/plugins/glib.py:182 +msgid "Y-Axis" +msgstr "Eixo Y" + +#: apprise/plugins/dingtalk.py:100 apprise/plugins/signl4.py:76 +#, fuzzy +msgid "Secret" +msgstr "Segredo" + +#: apprise/plugins/discord.py:125 apprise/plugins/flock.py:106 +#: apprise/plugins/fluxer.py:169 apprise/plugins/ryver.py:106 +#: apprise/plugins/slack.py:208 apprise/plugins/viber.py:99 +#: apprise/plugins/zulip.py:124 +msgid "Bot Name" +msgstr "Nome do Bot" + +#: apprise/plugins/discord.py:149 apprise/plugins/fluxer.py:201 +msgid "Text To Speech" +msgstr "Texto para Fala" + +#: apprise/plugins/discord.py:154 apprise/plugins/fluxer.py:206 +msgid "Avatar Image" +msgstr "Imagem de Avatar" + +#: apprise/plugins/discord.py:159 apprise/plugins/fluxer.py:211 +#: apprise/plugins/ntfy.py:262 +#, fuzzy +msgid "Avatar URL" +msgstr "URL do Avatar" + +#: apprise/plugins/discord.py:163 apprise/plugins/fluxer.py:215 +#: apprise/plugins/pushover.py:275 +msgid "URL" +msgstr "URL" + +#: apprise/plugins/discord.py:172 apprise/plugins/fluxer.py:222 +msgid "Thread ID" +msgstr "ID da Thread" + +#: apprise/plugins/discord.py:176 apprise/plugins/fluxer.py:230 +msgid "Display Footer" +msgstr "Exibir Rodape" + +#: apprise/plugins/discord.py:181 apprise/plugins/fluxer.py:235 +msgid "Footer Logo" +msgstr "Logo do Rodape" + +#: apprise/plugins/discord.py:186 apprise/plugins/fluxer.py:240 +#, fuzzy +msgid "Use Fields" +msgstr "Usar Campos" + +#: apprise/plugins/discord.py:191 apprise/plugins/fluxer.py:245 +msgid "Discord Flags" +msgstr "Flags do Discord" + +#: apprise/plugins/discord.py:205 apprise/plugins/fluxer.py:256 +msgid "Ping Users/Roles" +msgstr "Mencionar Usuarios/Cargos" + +#: apprise/plugins/dot.py:142 +#, fuzzy +msgid "Device Serial Number" +msgstr "Numero de Serie do Dispositivo" + +#: apprise/plugins/dot.py:155 +#, fuzzy +msgid "API Mode" +msgstr "Modo de API" + +#: apprise/plugins/dot.py:161 +msgid "Refresh Now" +msgstr "Atualizar Agora" + +#: apprise/plugins/dot.py:167 +msgid "Text Signature" +msgstr "Assinatura de Texto" + +#: apprise/plugins/dot.py:171 +msgid "Icon Base64 (Text API)" +msgstr "Icone Base64 (API de Texto)" + +#: apprise/plugins/dot.py:175 +msgid "Image Base64 (Image API)" +msgstr "Imagem Base64 (API de Imagem)" + +#: apprise/plugins/dot.py:180 +msgid "Link" +msgstr "Link" + +#: apprise/plugins/dot.py:184 +#, fuzzy +msgid "Border" +msgstr "Borda" + +#: apprise/plugins/dot.py:191 +msgid "Dither Type" +msgstr "Tipo de Dithering" + +#: apprise/plugins/dot.py:197 +msgid "Dither Kernel" +msgstr "Kernel de Dithering" + +#: apprise/plugins/dot.py:203 +#, fuzzy +msgid "Task Key" +msgstr "Chave da Thread" + +#: apprise/plugins/emby.py:112 +msgid "Modal" +msgstr "Modal" + +#: apprise/plugins/enigma2.py:130 apprise/plugins/gotify.py:134 +#: apprise/plugins/mattermost.py:167 apprise/plugins/notica.py:145 +msgid "Path" +msgstr "Caminho" + +#: apprise/plugins/enigma2.py:140 +msgid "Server Timeout" +msgstr "Tempo Limite do Servidor" + +#: apprise/plugins/evolution.py:122 +#, fuzzy +msgid "Instance Name" +msgstr "Nome do Dispositivo" + +#: apprise/plugins/exotel.py:160 apprise/plugins/sinch.py:106 +#: apprise/plugins/twilio.py:140 +msgid "Account SID" +msgstr "SID da Conta" + +#: apprise/plugins/exotel.py:172 +#, fuzzy +msgid "From Phone No / Sender ID" +msgstr "Numero de Telefone de Origem" + +#: apprise/plugins/exotel.py:233 apprise/plugins/jira.py:275 +#: apprise/plugins/mailgun.py:176 apprise/plugins/notificationapi.py:212 +#: apprise/plugins/opsgenie.py:273 apprise/plugins/pagerduty.py:176 +#: apprise/plugins/sparkpost.py:191 +msgid "Region Name" +msgstr "Nome da Regiao" + +#: apprise/plugins/feishu.py:49 +msgid "Feishu" +msgstr "Feishu" + +#: apprise/plugins/flock.py:99 apprise/plugins/twitter.py:175 +msgid "Access Key" +msgstr "Chave de Acesso" + +#: apprise/plugins/flock.py:111 +msgid "To User ID" +msgstr "ID do Usuario de Destino" + +#: apprise/plugins/flock.py:118 +msgid "To Channel ID" +msgstr "ID do Canal de Destino" + +#: apprise/plugins/fcm/__init__.py:182 apprise/plugins/fcm/__init__.py:188 +#: apprise/plugins/fluxer.py:195 apprise/plugins/lametric.py:510 +#: apprise/plugins/mattermost.py:224 apprise/plugins/notificationapi.py:218 +#: apprise/plugins/ntfy.py:296 apprise/plugins/office365.py:319 +#: apprise/plugins/sns.py:202 apprise/plugins/vapid/__init__.py:169 +#: apprise/plugins/webexteams.py:197 apprise/plugins/zoom.py:158 +#, fuzzy +msgid "Mode" +msgstr "Modo" + +#: apprise/plugins/fluxer.py:226 +#, fuzzy +msgid "Thread Name" +msgstr "Nome da Thread" + +#: apprise/plugins/fortysixelks.py:58 +msgid "46elks" +msgstr "46elks" + +#: apprise/plugins/fortysixelks.py:89 +#, fuzzy +msgid "API Username" +msgstr "Nome de Usuario de API" + +#: apprise/plugins/fortysixelks.py:94 +#, fuzzy +msgid "API Password" +msgstr "Senha de API" + +#: apprise/plugins/freemobile.py:48 +msgid "Free-Mobile" +msgstr "Free-Mobile" + +#: apprise/plugins/glib.py:123 +msgid "libdbus-1.so.x or libdbus-2.so.x must be installed." +msgstr "libdbus-1.so.x ou libdbus-2.so.x deve estar instalado." + +#: apprise/plugins/glib.py:127 +#, fuzzy +msgid "GLib Notification" +msgstr "Notificacao DBus" + +#: apprise/plugins/gnome.py:100 +msgid "A local Gnome environment is required." +msgstr "Um ambiente Gnome local e necessario." + +#: apprise/plugins/gnome.py:104 +msgid "Gnome Notification" +msgstr "Notificacao Gnome" + +#: apprise/plugins/google_chat.py:106 +msgid "Workspace" +msgstr "Espaco de Trabalho" + +#: apprise/plugins/google_chat.py:112 +#, fuzzy +msgid "Webhook Key" +msgstr "Chave do Webhook" + +#: apprise/plugins/google_chat.py:124 +#, fuzzy +msgid "Thread Key" +msgstr "Chave da Thread" + +#: apprise/plugins/groupme.py:123 +#, fuzzy +msgid "Bot ID" +msgstr "ID do Webhook" + +#: apprise/plugins/groupme.py:130 apprise/plugins/line.py:82 +#: apprise/plugins/mastodon.py:185 apprise/plugins/matrix/base.py:282 +#: apprise/plugins/misskey.py:122 apprise/plugins/pushbullet.py:83 +#: apprise/plugins/pushover.py:234 apprise/plugins/smseagle.py:146 +#: apprise/plugins/spugpush.py:68 apprise/plugins/streamlabs.py:105 +#: apprise/plugins/whatsapp.py:121 +msgid "Access Token" +msgstr "Token de Acesso" + +#: apprise/plugins/growl.py:169 apprise/plugins/mqtt.py:195 +#: apprise/plugins/nextcloud.py:163 +msgid "Version" +msgstr "Versao" + +#: apprise/plugins/growl.py:181 +msgid "Sticky" +msgstr "Fixado" + +#: apprise/plugins/home_assistant.py:142 +#, fuzzy +msgid "Long-Lived Access Token" +msgstr "Token de Acesso de Longa Duracao" + +#: apprise/plugins/home_assistant.py:165 +msgid "Notification ID" +msgstr "ID de Notificacao" + +#: apprise/plugins/home_assistant.py:182 +#, fuzzy +msgid "Path Prefix" +msgstr "Prefixo" + +#: apprise/plugins/humhub.py:127 +#, fuzzy +msgid "Token or Username" +msgstr "Nome de Usuario" + +#: apprise/plugins/ifttt.py:109 +msgid "Events" +msgstr "Eventos" + +#: apprise/plugins/ifttt.py:129 +msgid "Add Tokens" +msgstr "Adicionar Tokens" + +#: apprise/plugins/ifttt.py:133 +msgid "Remove Tokens" +msgstr "Remover Tokens" + +#: apprise/plugins/jira.py:240 apprise/plugins/opsgenie.py:238 +#, fuzzy +msgid "Target Escalation" +msgstr "Escalada de Destino" + +#: apprise/plugins/jira.py:246 apprise/plugins/opsgenie.py:244 +#, fuzzy +msgid "Target Schedule" +msgstr "Agendamento de Destino" + +#: apprise/plugins/irc/base.py:137 apprise/plugins/jira.py:252 +#: apprise/plugins/line.py:88 apprise/plugins/mastodon.py:196 +#: apprise/plugins/matrix/base.py:289 apprise/plugins/nextcloud.py:136 +#: apprise/plugins/one_signal.py:129 apprise/plugins/opsgenie.py:250 +#: apprise/plugins/pushed.py:95 apprise/plugins/rocketchat.py:156 +#: apprise/plugins/slack.py:261 apprise/plugins/twitter.py:187 +#: apprise/plugins/wechat.py:198 apprise/plugins/xmpp/base.py:123 +#: apprise/plugins/zulip.py:143 +msgid "Target User" +msgstr "Usuario de Destino" + +#: apprise/plugins/jira.py:258 apprise/plugins/opsgenie.py:256 +#, fuzzy +msgid "Target Team" +msgstr "Equipe de Destino" + +#: apprise/plugins/jira.py:264 apprise/plugins/opsgenie.py:262 +#, fuzzy +msgid "Targets " +msgstr "Destinatarios" + +#: apprise/plugins/jira.py:293 apprise/plugins/opsgenie.py:286 +msgid "Entity" +msgstr "Entidade" + +#: apprise/plugins/jira.py:297 apprise/plugins/opsgenie.py:290 +msgid "Alias" +msgstr "Alias" + +#: apprise/plugins/jira.py:308 apprise/plugins/opsgenie.py:298 +#: apprise/plugins/pagertree.py:118 apprise/plugins/splunk.py:202 +#, fuzzy +msgid "Action" +msgstr "Acao" + +#: apprise/plugins/jira.py:319 apprise/plugins/opsgenie.py:317 +#, fuzzy +msgid "Details" +msgstr "Detalhes" + +#: apprise/plugins/jira.py:323 apprise/plugins/opsgenie.py:321 +#: apprise/plugins/splunk.py:213 +msgid "Action Mapping" +msgstr "Mapeamento de Acoes" + +#: apprise/plugins/join.py:147 +msgid "Device ID" +msgstr "ID do Dispositivo" + +#: apprise/plugins/join.py:153 +#, fuzzy +msgid "Device Name" +msgstr "Nome do Dispositivo" + +#: apprise/plugins/kavenegar.py:122 apprise/plugins/messagebird.py:85 +#: apprise/plugins/plivo.py:100 +#, fuzzy +msgid "Source Phone No" +msgstr "Numero de Telefone de Origem" + +#: apprise/plugins/kodi.py:123 apprise/plugins/streamlabs.py:141 +#: apprise/plugins/windows.py:100 +msgid "Duration" +msgstr "Duracao" + +#: apprise/plugins/kumulos.py:99 +#, fuzzy +msgid "Server Key" +msgstr "Chave do Servidor" + +#: apprise/plugins/lametric.py:436 +#, fuzzy +msgid "Device API Key" +msgstr "Chave de API do Dispositivo" + +#: apprise/plugins/lametric.py:442 apprise/plugins/one_signal.py:102 +#: apprise/plugins/parseplatform.py:101 +msgid "App ID" +msgstr "ID do Aplicativo" + +#: apprise/plugins/lametric.py:448 +#, fuzzy +msgid "App Version" +msgstr "Versao do Aplicativo" + +#: apprise/plugins/lametric.py:455 +#, fuzzy +msgid "App Access Token" +msgstr "Token de Acesso do Aplicativo" + +#: apprise/plugins/lametric.py:500 +msgid "Custom Icon" +msgstr "Icone Personalizado" + +#: apprise/plugins/lametric.py:504 +msgid "Icon Type" +msgstr "Tipo de Icone" + +#: apprise/plugins/lametric.py:521 +msgid "Cycles" +msgstr "Ciclos" + +#: apprise/plugins/lark.py:47 +msgid "Lark (Feishu)" +msgstr "Lark (Feishu)" + +#: apprise/plugins/lark.py:67 apprise/plugins/revolt.py:98 +#: apprise/plugins/telegram.py:344 +msgid "Bot Token" +msgstr "Token do Bot" + +#: apprise/plugins/macosx.py:65 +msgid "" +"Only works with Mac OS X 10.8 and higher. Additionally requires that /usr/" +"local/bin/terminal-notifier is locally accessible." +msgstr "" + +#: apprise/plugins/macosx.py:72 +msgid "MacOSX Notification" +msgstr "Notificacao MacOSX" + +#: apprise/plugins/macosx.py:128 +msgid "Open/Click URL" +msgstr "URL de Abertura/Clique" + +#: apprise/plugins/email/base.py:148 apprise/plugins/mailgun.py:141 +#: apprise/plugins/sendpulse.py:115 apprise/plugins/smtp2go.py:113 +#: apprise/plugins/sparkpost.py:164 +msgid "Domain" +msgstr "Dominio" + +#: apprise/plugins/email/base.py:179 apprise/plugins/mailgun.py:168 +#: apprise/plugins/postmark.py:144 apprise/plugins/resend.py:140 +#: apprise/plugins/ses.py:210 apprise/plugins/smtp2go.py:135 +#: apprise/plugins/sparkpost.py:186 +msgid "From Name" +msgstr "Nome de Origem" + +#: apprise/plugins/email/base.py:253 apprise/plugins/mailgun.py:204 +#: apprise/plugins/smtp2go.py:161 apprise/plugins/sparkpost.py:219 +#, fuzzy +msgid "Email Header" +msgstr "Cabecalho de E-mail" + +#: apprise/plugins/mailgun.py:208 apprise/plugins/notificationapi.py:256 +#: apprise/plugins/slack.py:340 apprise/plugins/sparkpost.py:223 +#: apprise/plugins/workflows.py:202 +#, fuzzy +msgid "Template Tokens" +msgstr "Tokens do Template" + +#: apprise/plugins/mastodon.py:216 apprise/plugins/misskey.py:143 +msgid "Visibility" +msgstr "Visibilidade" + +#: apprise/plugins/mastodon.py:222 +msgid "Spoiler Text" +msgstr "Texto de Spoiler" + +#: apprise/plugins/mastodon.py:226 +msgid "Idempotency-Key" +msgstr "Chave de Idempotencia" + +#: apprise/plugins/mastodon.py:230 +msgid "Language Code" +msgstr "Codigo de Idioma" + +#: apprise/plugins/mastodon.py:234 apprise/plugins/twitter.py:210 +msgid "Cache Results" +msgstr "Armazenar Resultados em Cache" + +#: apprise/plugins/mastodon.py:239 +msgid "Sensitive Attachments" +msgstr "Anexos Sensiveis" + +#: apprise/plugins/mastodon.py:255 +#, fuzzy +msgid "Ping Users/Tags" +msgstr "Mencionar Usuarios/Cargos" + +#: apprise/plugins/irc/base.py:128 apprise/plugins/mattermost.py:151 +#: apprise/plugins/xmpp/base.py:112 +#, fuzzy +msgid "User" +msgstr "Usuario" + +#: apprise/plugins/irc/base.py:143 apprise/plugins/mattermost.py:177 +#: apprise/plugins/notifiarr.py:97 apprise/plugins/pushbullet.py:94 +#: apprise/plugins/pushed.py:101 apprise/plugins/rocketchat.py:150 +#: apprise/plugins/slack.py:267 apprise/plugins/twist.py:112 +#: apprise/plugins/xmpp/base.py:128 +msgid "Target Channel" +msgstr "Canal de Destino" + +#: apprise/plugins/mattermost.py:183 apprise/plugins/twist.py:118 +#, fuzzy +msgid "Target Channel ID" +msgstr "ID do Canal de Destino" + +#: apprise/plugins/mqtt.py:171 +msgid "Target Queue" +msgstr "" + +#: apprise/plugins/mqtt.py:188 +msgid "QOS" +msgstr "QoS" + +#: apprise/plugins/mqtt.py:201 apprise/plugins/notificationapi.py:166 +#: apprise/plugins/office365.py:269 apprise/plugins/ringcentral.py:192 +#: apprise/plugins/sendpulse.py:120 +#, fuzzy +msgid "Client ID" +msgstr "ID do Cliente" + +#: apprise/plugins/mqtt.py:205 +msgid "Use Session" +msgstr "Usar Sessao" + +#: apprise/plugins/mqtt.py:210 +msgid "Retain Messages" +msgstr "Reter Mensagens" + +#: apprise/plugins/msg91.py:102 apprise/plugins/sendpulse.py:156 +msgid "Template ID" +msgstr "ID do Template" + +#: apprise/plugins/msg91.py:109 +#, fuzzy +msgid "Authentication Key" +msgstr "Chave de Autenticacao" + +#: apprise/plugins/msg91.py:138 +msgid "Short URL" +msgstr "URL Curta" + +#: apprise/plugins/msg91.py:148 apprise/plugins/whatsapp.py:203 +msgid "Template Mapping" +msgstr "Mapeamento de Template" + +#: apprise/plugins/nextcloud.py:169 apprise/plugins/nextcloudtalk.py:113 +msgid "URL Prefix" +msgstr "Prefixo de URL" + +#: apprise/plugins/nextcloudtalk.py:43 +msgid "Nextcloud Talk" +msgstr "Nextcloud Talk" + +#: apprise/plugins/nextcloudtalk.py:96 +#, fuzzy +msgid "Room ID" +msgstr "ID da Sala" + +#: apprise/plugins/notifiarr.py:121 +msgid "Discord Event ID" +msgstr "ID de Evento do Discord" + +#: apprise/plugins/notifiarr.py:131 apprise/plugins/pagerduty.py:145 +#, fuzzy +msgid "Source" +msgstr "Origem" + +#: apprise/plugins/matrix/base.py:352 apprise/plugins/notificationapi.py:159 +msgid "Message Type" +msgstr "Tipo de Mensagem" + +#: apprise/plugins/notificationapi.py:171 apprise/plugins/office365.py:276 +#: apprise/plugins/ringcentral.py:199 apprise/plugins/sendpulse.py:127 +#, fuzzy +msgid "Client Secret" +msgstr "Segredo do Cliente" + +#: apprise/plugins/notificationapi.py:182 +#, fuzzy +msgid "Target ID" +msgstr "ID de Destino" + +#: apprise/plugins/notificationapi.py:187 +#, fuzzy +msgid "Target SMS" +msgstr "SMS de Destino" + +#: apprise/plugins/notificationapi.py:207 +msgid "Channels" +msgstr "Canais" + +#: apprise/plugins/email/base.py:196 apprise/plugins/notificationapi.py:223 +#: apprise/plugins/office365.py:315 apprise/plugins/resend.py:150 +msgid "Reply To" +msgstr "Responder Para" + +#: apprise/plugins/email/base.py:174 apprise/plugins/notificationapi.py:228 +#: apprise/plugins/sendpulse.py:150 apprise/plugins/ses.py:163 +msgid "From Email" +msgstr "E-mail de Origem" + +#: apprise/plugins/fcm/__init__.py:153 apprise/plugins/notifico.py:187 +#, fuzzy +msgid "Project ID" +msgstr "ID do Projeto" + +#: apprise/plugins/notifico.py:196 +msgid "Message Hook" +msgstr "Hook de Mensagem" + +#: apprise/plugins/notifico.py:211 +msgid "IRC Colors" +msgstr "Cores IRC" + +#: apprise/plugins/notifico.py:217 +msgid "Prefix" +msgstr "Prefixo" + +#: apprise/plugins/ntfy.py:235 +msgid "Topic" +msgstr "Topico" + +#: apprise/plugins/ntfy.py:252 +msgid "Attach" +msgstr "Anexar" + +#: apprise/plugins/ntfy.py:266 +msgid "Attach Filename" +msgstr "Nome do Arquivo Anexado" + +#: apprise/plugins/ntfy.py:274 +msgid "Delay" +msgstr "Atraso" + +#: apprise/plugins/ntfy.py:278 apprise/plugins/twist.py:107 +#, fuzzy +msgid "Email" +msgstr "E-mail" + +#: apprise/plugins/ntfy.py:292 +#, fuzzy +msgid "Actions" +msgstr "Acoes" + +#: apprise/plugins/ntfy.py:305 +#, fuzzy +msgid "Authentication Type" +msgstr "Tipo de Autenticacao" + +#: apprise/plugins/octopush.py:130 +#, fuzzy +msgid "API Login" +msgstr "Token de API" + +#: apprise/plugins/octopush.py:142 +#, fuzzy +msgid "Sender" +msgstr "ID do Remetente" + +#: apprise/plugins/octopush.py:178 +#, fuzzy +msgid "Accept Replies" +msgstr "Enviar Respostas" + +#: apprise/plugins/octopush.py:183 +msgid "Purpose" +msgstr "" + +#: apprise/plugins/octopush.py:189 +#, fuzzy +msgid "Type" +msgstr "Tipo de Icone" + +#: apprise/plugins/office365.py:257 +#, fuzzy +msgid "Tenant Domain" +msgstr "Dominio do Locatario" + +#: apprise/plugins/office365.py:264 +msgid "Account Email or Object ID" +msgstr "E-mail da Conta ou ID do Objeto" + +#: apprise/plugins/office365.py:325 +#, fuzzy +msgid "Save to Sent Items" +msgstr "Entradas de Dados" + +#: apprise/plugins/one_signal.py:108 apprise/plugins/sendgrid.py:146 +msgid "Template" +msgstr "Template" + +#: apprise/plugins/one_signal.py:119 +#, fuzzy +msgid "Target Player ID" +msgstr "ID do Jogador de Destino" + +#: apprise/plugins/one_signal.py:135 +#, fuzzy +msgid "Include Segment" +msgstr "Incluir Segmento" + +#: apprise/plugins/one_signal.py:155 +msgid "Subtitle" +msgstr "Legenda" + +#: apprise/plugins/one_signal.py:159 apprise/plugins/sfr.py:125 +#: apprise/plugins/whatsapp.py:141 +msgid "Language" +msgstr "Idioma" + +#: apprise/plugins/one_signal.py:170 +msgid "Enable Contents" +msgstr "Habilitar Conteudo" + +#: apprise/plugins/one_signal.py:176 +msgid "Decode Template Args" +msgstr "Decodificar Argumentos do Template" + +#: apprise/plugins/one_signal.py:195 +msgid "Custom Data" +msgstr "Dados Personalizados" + +#: apprise/plugins/one_signal.py:199 +msgid "Postback Data" +msgstr "Dados de Postback" + +#: apprise/plugins/pagerduty.py:138 apprise/plugins/spike.py:68 +#, fuzzy +msgid "Integration Key" +msgstr "Chave de Integracao" + +#: apprise/plugins/pagerduty.py:151 +#, fuzzy +msgid "Component" +msgstr "Componente" + +#: apprise/plugins/pagerduty.py:167 +msgid "Class" +msgstr "Classe" + +#: apprise/plugins/pagerduty.py:185 +msgid "Severity" +msgstr "Severidade" + +#: apprise/plugins/pagerduty.py:202 +#, fuzzy +msgid "Custom Details" +msgstr "Detalhes Personalizados" + +#: apprise/plugins/pagertree.py:105 +msgid "Integration ID" +msgstr "ID de Integracao" + +#: apprise/plugins/pagertree.py:124 +msgid "Third Party ID" +msgstr "ID de Terceiros" + +#: apprise/plugins/pagertree.py:150 +msgid "Meta Extras" +msgstr "Extras de Meta" + +#: apprise/plugins/parseplatform.py:107 +#, fuzzy +msgid "Master Key" +msgstr "Chave Mestra" + +#: apprise/plugins/parseplatform.py:120 +#, fuzzy +msgid "Device" +msgstr "Dispositivo" + +#: apprise/plugins/plivo.py:88 +#, fuzzy +msgid "Auth ID" +msgstr "ID de Autenticacao" + +#: apprise/plugins/plivo.py:94 apprise/plugins/sinch.py:113 +#: apprise/plugins/twilio.py:147 +msgid "Auth Token" +msgstr "Token de Autenticacao" + +#: apprise/plugins/prowl.py:128 +msgid "Provider Key" +msgstr "Chave do Provedor" + +#: apprise/plugins/pushdeer.py:85 +#, fuzzy +msgid "Pushkey" +msgstr "Chave Push" + +#: apprise/plugins/pushed.py:83 +msgid "Application Key" +msgstr "Chave do Aplicativo" + +#: apprise/plugins/pushed.py:89 apprise/plugins/reddit.py:158 +msgid "Application Secret" +msgstr "Segredo do Aplicativo" + +#: apprise/plugins/pushjet.py:82 +msgid "Secret Key" +msgstr "Chave Secreta" + +#: apprise/plugins/pushme.py:81 apprise/plugins/signal_api.py:152 +#: apprise/plugins/smseagle.py:193 +msgid "Show Status" +msgstr "Mostrar Status" + +#: apprise/plugins/pushover.py:228 +msgid "User Key" +msgstr "Chave do Usuario" + +#: apprise/plugins/pushover.py:280 +msgid "URL Title" +msgstr "Titulo da URL" + +#: apprise/plugins/pushover.py:285 +msgid "Emergency Retry Interval" +msgstr "" + +#: apprise/plugins/pushover.py:291 +msgid "Expire" +msgstr "Expirar" + +#: apprise/plugins/pushover.py:301 +#, fuzzy +msgid "Encryption Key" +msgstr "Criptografia PGP" + +#: apprise/plugins/pushover.py:308 +msgid "E2EE" +msgstr "" + +#: apprise/plugins/pushplus.py:172 +msgid "Pushplus" +msgstr "Pushplus" + +#: apprise/plugins/pushplus.py:210 apprise/plugins/qq.py:66 +#, fuzzy +msgid "User Token" +msgstr "Token do Usuario" + +#: apprise/plugins/pushplus.py:219 +msgid "Group Topics" +msgstr "" + +#: apprise/plugins/pushplus.py:240 +#, fuzzy +msgid "Channel" +msgstr "Canais" + +#: apprise/plugins/pushplus.py:257 +#, fuzzy +msgid "Webhook Name" +msgstr "Modo Webhook" + +#: apprise/plugins/pushsafer.py:360 +#, fuzzy +msgid "Private Key" +msgstr "Chave Privada" + +#: apprise/plugins/pushsafer.py:397 +#, fuzzy +msgid "Vibration" +msgstr "Vibracao" + +#: apprise/plugins/pushy.py:79 +#, fuzzy +msgid "Secret API Key" +msgstr "Chave Secreta de API" + +#: apprise/plugins/fcm/__init__.py:162 apprise/plugins/pushy.py:91 +#: apprise/plugins/sns.py:163 apprise/plugins/wxpusher.py:128 +msgid "Target Topic" +msgstr "Topico de Destino" + +#: apprise/plugins/qq.py:46 +msgid "QQ Push" +msgstr "QQ Push" + +#: apprise/plugins/reddit.py:151 +#, fuzzy +msgid "Application ID" +msgstr "ID do Aplicativo" + +#: apprise/plugins/reddit.py:165 +#, fuzzy +msgid "Target Subreddit" +msgstr "Subreddit de Destino" + +#: apprise/plugins/reddit.py:182 +msgid "Kind" +msgstr "Tipo" + +#: apprise/plugins/reddit.py:188 +msgid "Flair ID" +msgstr "ID do Flair" + +#: apprise/plugins/reddit.py:193 +msgid "Flair Text" +msgstr "Texto do Flair" + +#: apprise/plugins/reddit.py:198 +msgid "NSFW" +msgstr "Conteudo Adulto" + +#: apprise/plugins/reddit.py:204 +msgid "Is Ad?" +msgstr "E Anuncio?" + +#: apprise/plugins/reddit.py:210 +msgid "Send Replies" +msgstr "Enviar Respostas" + +#: apprise/plugins/reddit.py:216 +msgid "Is Spoiler" +msgstr "E Spoiler" + +#: apprise/plugins/reddit.py:222 +msgid "Resubmit Flag" +msgstr "Flag de Reenvio" + +#: apprise/plugins/resend.py:135 +#, fuzzy +msgid "From Address" +msgstr "Nome de Origem" + +#: apprise/plugins/revolt.py:104 +#, fuzzy +msgid "Channel ID" +msgstr "ID do Canal" + +#: apprise/plugins/revolt.py:131 +msgid "Embed URL" +msgstr "URL Incorporada" + +#: apprise/plugins/ringcentral.py:186 +#, fuzzy +msgid "Token / Password" +msgstr "Senha" + +#: apprise/plugins/ringcentral.py:239 +#, fuzzy +msgid "Environment" +msgstr "Evento" + +#: apprise/plugins/ringcentral.py:252 +#, fuzzy +msgid "Authentication Mode" +msgstr "Token de Autenticacao" + +#: apprise/plugins/rocketchat.py:146 +msgid "Webhook" +msgstr "Webhook" + +#: apprise/plugins/matrix/base.py:295 apprise/plugins/rocketchat.py:162 +msgid "Target Room ID" +msgstr "ID da Sala de Destino" + +#: apprise/plugins/matrix/base.py:334 apprise/plugins/rocketchat.py:178 +#: apprise/plugins/ryver.py:118 +msgid "Webhook Mode" +msgstr "Modo Webhook" + +#: apprise/plugins/rocketchat.py:183 +msgid "Use Avatar" +msgstr "Usar Avatar" + +#: apprise/plugins/rsyslog.py:173 apprise/plugins/syslog.py:144 +msgid "Facility" +msgstr "Instalacao" + +#: apprise/plugins/rsyslog.py:203 apprise/plugins/syslog.py:161 +msgid "Log PID" +msgstr "PID do Log" + +#: apprise/plugins/ryver.py:93 apprise/plugins/zulip.py:130 +msgid "Organization" +msgstr "Organizacao" + +#: apprise/plugins/sendgrid.py:166 apprise/plugins/sendpulse.py:182 +msgid "Template Data" +msgstr "Dados do Template" + +#: apprise/plugins/ses.py:157 apprise/plugins/sns.py:132 +#, fuzzy +msgid "Session Token" +msgstr "Token de Acesso" + +#: apprise/plugins/ses.py:169 apprise/plugins/sns.py:138 +msgid "Access Key ID" +msgstr "ID da Chave de Acesso" + +#: apprise/plugins/ses.py:175 apprise/plugins/sns.py:144 +msgid "Secret Access Key" +msgstr "Chave de Acesso Secreta" + +#: apprise/plugins/ses.py:181 apprise/plugins/sinch.py:157 +#: apprise/plugins/sns.py:150 +msgid "Region" +msgstr "Regiao" + +#: apprise/plugins/ses.py:188 apprise/plugins/smtp2go.py:124 +#: apprise/plugins/sparkpost.py:175 +msgid "Target Emails" +msgstr "E-mails de Destino" + +#: apprise/plugins/seven.py:113 apprise/plugins/smseagle.py:203 +msgid "Flash" +msgstr "Flash" + +#: apprise/plugins/seven.py:117 +msgid "Label" +msgstr "Rotulo" + +#: apprise/plugins/sfr.py:58 +msgid "Société Française du Radiotéléphone" +msgstr "" + +#: apprise/plugins/sfr.py:90 +#, fuzzy +msgid "Service ID" +msgstr "ID do Servico" + +#: apprise/plugins/sfr.py:95 +#, fuzzy +msgid "Service Password" +msgstr "Senha do Servico" + +#: apprise/plugins/sfr.py:101 +#, fuzzy +msgid "Space ID" +msgstr "ID do Espaco" + +#: apprise/plugins/sfr.py:107 +msgid "Recipient Phone Number" +msgstr "Numero de Telefone do Destinatario" + +#: apprise/plugins/sfr.py:131 +#, fuzzy +msgid "Sender Name" +msgstr "Nome do Remetente" + +#: apprise/plugins/sfr.py:138 +msgid "Media Type" +msgstr "Tipo de Midia" + +#: apprise/plugins/sfr.py:145 +#, fuzzy +msgid "Timeout" +msgstr "Tempo Limite" + +#: apprise/plugins/sfr.py:151 +#, fuzzy +msgid "TTS Voice" +msgstr "Voz TTS" + +#: apprise/plugins/signal_api.py:131 apprise/plugins/smseagle.py:158 +#: apprise/plugins/whatsapp.py:159 +#, fuzzy +msgid "Target Group ID" +msgstr "ID do Grupo de Destino" + +#: apprise/plugins/signl4.py:89 +#, fuzzy +msgid "Service" +msgstr "Servico" + +#: apprise/plugins/signl4.py:93 +#, fuzzy +msgid "Location" +msgstr "Localizacao" + +#: apprise/plugins/signl4.py:97 +msgid "Alerting Scenario" +msgstr "Cenario de Alerta" + +#: apprise/plugins/signl4.py:101 +msgid "Filtering" +msgstr "Filtragem" + +#: apprise/plugins/signl4.py:106 +#, fuzzy +msgid "External ID" +msgstr "ID Externo" + +#: apprise/plugins/signl4.py:110 +#, fuzzy +msgid "Status" +msgstr "Status" + +#: apprise/plugins/simplepush.py:113 +msgid "Salt" +msgstr "Salt" + +#: apprise/plugins/simplepush.py:126 +#, fuzzy +msgid "Event" +msgstr "Evento" + +#: apprise/plugins/sinch.py:134 apprise/plugins/twilio.py:168 +msgid "Target Short Code" +msgstr "Codigo Curto de Destino" + +#: apprise/plugins/slack.py:216 +#, fuzzy +msgid "OAuth Access Token" +msgstr "Token de Acesso OAuth" + +#: apprise/plugins/slack.py:225 +msgid "Token A" +msgstr "Token A" + +#: apprise/plugins/slack.py:234 +msgid "Token B" +msgstr "Token B" + +#: apprise/plugins/slack.py:243 +msgid "Token C" +msgstr "Token C" + +#: apprise/plugins/slack.py:250 +msgid "Target Encoded ID" +msgstr "ID Codificado de Destino" + +#: apprise/plugins/slack.py:279 +#, fuzzy +msgid "Workflow Path" +msgstr "ID do Fluxo de Trabalho" + +#: apprise/plugins/slack.py:297 +#, fuzzy +msgid "Include Footer" +msgstr "Incluir Rodape" + +#: apprise/plugins/slack.py:305 +msgid "Use Blocks" +msgstr "Usar Blocos" + +#: apprise/plugins/slack.py:311 +#, fuzzy +msgid "Include Timestamp" +msgstr "Incluir Timestamp" + +#: apprise/plugins/slack.py:317 apprise/plugins/twitter.py:204 +#, fuzzy +msgid "Message Mode" +msgstr "Modo de Mensagem" + +#: apprise/plugins/slack.py:330 apprise/plugins/workflows.py:181 +msgid "Template Path" +msgstr "Caminho do Template" + +#: apprise/plugins/smpp.py:61 +msgid "SMPP" +msgstr "SMPP" + +#: apprise/plugins/smpp.py:103 +#, fuzzy +msgid "Host" +msgstr "Host" + +#: apprise/plugins/smseagle.py:165 +#, fuzzy +msgid "Target Contact" +msgstr "Contato de Destino" + +#: apprise/plugins/smseagle.py:198 +msgid "Test Only" +msgstr "Somente Teste" + +#: apprise/plugins/smsmanager.py:143 +msgid "Gateway" +msgstr "Gateway" + +#: apprise/plugins/sogs.py:219 +#, fuzzy +msgid "Public Key" +msgstr "Chave de API" + +#: apprise/plugins/sogs.py:226 +#, fuzzy +msgid "Seed" +msgstr "ID do Remetente" + +#: apprise/plugins/sogs.py:233 +#, fuzzy +msgid "Rooms" +msgstr "ID da Sala" + +#: apprise/plugins/spike.py:48 +msgid "Spike.sh" +msgstr "Spike.sh" + +#: apprise/plugins/splunk.py:117 +msgid "Splunk On-Call" +msgstr "Splunk On-Call" + +#: apprise/plugins/splunk.py:172 +#, fuzzy +msgid "Target Routing Key" +msgstr "Chave de Roteamento de Destino" + +#: apprise/plugins/splunk.py:179 +msgid "Entity ID" +msgstr "ID da Entidade" + +#: apprise/plugins/spugpush.py:48 +msgid "SpugPush" +msgstr "SpugPush" + +#: apprise/plugins/streamlabs.py:125 +msgid "Alert Type" +msgstr "Tipo de Alerta" + +#: apprise/plugins/streamlabs.py:131 +msgid "Image Link" +msgstr "Link da Imagem" + +#: apprise/plugins/streamlabs.py:136 +#, fuzzy +msgid "Sound Link" +msgstr "Link do Som" + +#: apprise/plugins/streamlabs.py:147 +msgid "Special Text Color" +msgstr "Cor de Texto Especial" + +#: apprise/plugins/streamlabs.py:153 +msgid "Amount" +msgstr "Quantidade" + +#: apprise/plugins/streamlabs.py:159 +#, fuzzy +msgid "Currency" +msgstr "Moeda" + +#: apprise/plugins/streamlabs.py:165 +#, fuzzy +msgid "Name" +msgstr "Nome" + +#: apprise/plugins/streamlabs.py:171 +msgid "Identifier" +msgstr "Identificador" + +#: apprise/plugins/synology.py:116 +msgid "Upload" +msgstr "Enviar" + +#: apprise/plugins/syslog.py:167 +msgid "Log to STDERR" +msgstr "Registrar no STDERR" + +#: apprise/plugins/telegram.py:353 +msgid "Target Chat ID" +msgstr "ID do Chat de Destino" + +#: apprise/plugins/telegram.py:376 +msgid "Detect Bot Owner" +msgstr "Detectar Proprietario do Bot" + +#: apprise/plugins/telegram.py:382 +msgid "Silent Notification" +msgstr "Notificacao Silenciosa" + +#: apprise/plugins/telegram.py:387 +msgid "Web Page Preview" +msgstr "Pre-visualizacao de Pagina Web" + +#: apprise/plugins/telegram.py:392 +msgid "Topic Thread ID" +msgstr "ID de Thread do Topico" + +#: apprise/plugins/telegram.py:399 +#, fuzzy +msgid "Markdown Version" +msgstr "Versao do Markdown" + +#: apprise/plugins/telegram.py:408 +msgid "Content Placement" +msgstr "Posicionamento de Conteudo" + +#: apprise/plugins/threema.py:85 +msgid "Gateway ID" +msgstr "ID do Gateway" + +#: apprise/plugins/threema.py:110 +#, fuzzy +msgid "Target Threema ID" +msgstr "ID Threema de Destino" + +#: apprise/plugins/twilio.py:200 +msgid "Notification Method: sms or call" +msgstr "Metodo de Notificacao: sms ou chamada" + +#: apprise/plugins/twitter.py:163 +msgid "Consumer Key" +msgstr "Chave do Consumidor" + +#: apprise/plugins/twitter.py:169 +msgid "Consumer Secret" +msgstr "Segredo do Consumidor" + +#: apprise/plugins/twitter.py:181 +msgid "Access Secret" +msgstr "Segredo de Acesso" + +#: apprise/plugins/viber.py:49 +msgid "Viber" +msgstr "Viber" + +#: apprise/plugins/viber.py:81 +#, fuzzy +msgid "Authentication Token" +msgstr "Token de Autenticacao" + +#: apprise/plugins/viber.py:87 +msgid "Receiver IDs" +msgstr "IDs dos Receptores" + +#: apprise/plugins/viber.py:105 +#, fuzzy +msgid "Bot Avatar URL" +msgstr "URL do Avatar do Bot" + +#: apprise/plugins/voipms.py:83 +#, fuzzy +msgid "User Email" +msgstr "E-mail do Usuario" + +#: apprise/plugins/vapid/__init__.py:179 apprise/plugins/vonage.py:133 +msgid "ttl" +msgstr "ttl" + +#: apprise/plugins/webexteams.py:178 +#, fuzzy +msgid "Bot Access Token" +msgstr "Token de Acesso OAuth" + +#: apprise/plugins/webexteams.py:184 +#, fuzzy +msgid "Room IDs" +msgstr "ID da Sala" + +#: apprise/plugins/wechat.py:174 +#, fuzzy +msgid "Corp ID" +msgstr "ID do Aplicativo" + +#: apprise/plugins/wechat.py:181 +#, fuzzy +msgid "App Secret" +msgstr "Segredo de API" + +#: apprise/plugins/wechat.py:189 +#, fuzzy +msgid "Agent ID" +msgstr "ID de Destino" + +#: apprise/plugins/wechat.py:205 +#, fuzzy +msgid "Target Department" +msgstr "Dispositivo de Destino" + +#: apprise/plugins/wechat.py:212 +#, fuzzy +msgid "Target Tag" +msgstr "Equipe de Destino" + +#: apprise/plugins/wecombot.py:99 +#, fuzzy +msgid "Bot Webhook Key" +msgstr "Chave Webhook do Bot" + +#: apprise/plugins/whatsapp.py:128 +msgid "Template Name" +msgstr "Nome do Template" + +#: apprise/plugins/whatsapp.py:134 +#, fuzzy +msgid "From Phone ID" +msgstr "ID do Telefone de Origem" + +#: apprise/plugins/windows.py:62 +msgid "A local Microsoft Windows environment is required." +msgstr "Um ambiente Microsoft Windows local e necessario." + +#: apprise/plugins/workflows.py:138 +#, fuzzy +msgid "Workflow ID" +msgstr "ID do Fluxo de Trabalho" + +#: apprise/plugins/workflows.py:146 +msgid "Signature" +msgstr "Assinatura" + +#: apprise/plugins/workflows.py:169 +msgid "Use Power Automate URL" +msgstr "Usar URL do Power Automate" + +#: apprise/plugins/workflows.py:176 +msgid "Wrap Text" +msgstr "Quebrar Texto" + +#: apprise/plugins/workflows.py:191 +#, fuzzy +msgid "API Version" +msgstr "Versao da API" + +#: apprise/plugins/wxpusher.py:121 +#, fuzzy +msgid "App Token" +msgstr "Token do Aplicativo" + +#: apprise/plugins/wxpusher.py:133 +#, fuzzy +msgid "Target User ID" +msgstr "ID do Usuario de Destino" + +#: apprise/plugins/zoom.py:145 +#, fuzzy +msgid "Verification Token" +msgstr "Token de Autenticacao" + +#: apprise/plugins/zulip.py:148 +#, fuzzy +msgid "Target Stream" +msgstr "Stream de Destino" + +#: apprise/plugins/email/base.py:184 +msgid "SMTP Server" +msgstr "Servidor SMTP" + +#: apprise/plugins/email/base.py:189 apprise/plugins/xmpp/base.py:149 +msgid "Secure Mode" +msgstr "Modo Seguro" + +#: apprise/plugins/email/base.py:201 +#, fuzzy +msgid "PGP Security Mode" +msgstr "Modo Seguro" + +#: apprise/plugins/email/base.py:208 +msgid "PGP Public Key Path" +msgstr "Caminho da Chave Publica PGP" + +#: apprise/plugins/email/base.py:221 +#, fuzzy +msgid "PGP Private Key Path" +msgstr "Caminho da Chave Publica PGP" + +#: apprise/plugins/email/base.py:229 +msgid "Web Key Directory" +msgstr "" + +#: apprise/plugins/email/base.py:235 +msgid "To Email" +msgstr "Para E-mail" + +#: apprise/plugins/fcm/__init__.py:148 +msgid "OAuth2 KeyFile" +msgstr "Arquivo de Chave OAuth2" + +#: apprise/plugins/fcm/__init__.py:193 +msgid "Custom Image URL" +msgstr "URL de Imagem Personalizada" + +#: apprise/plugins/fcm/__init__.py:205 +msgid "Notification Color" +msgstr "Cor da Notificacao" + +#: apprise/plugins/fcm/__init__.py:215 +msgid "Data Entries" +msgstr "Entradas de Dados" + +#: apprise/plugins/irc/base.py:159 +#, fuzzy +msgid "Real Name" +msgstr "Nome Real" + +#: apprise/plugins/irc/base.py:160 +#, fuzzy +msgid "Nickname" +msgstr "Apelido" + +#: apprise/plugins/irc/base.py:162 +#, fuzzy +msgid "Join Channels" +msgstr "Entrar em Canais" + +#: apprise/plugins/irc/base.py:167 +msgid "Auth Mode" +msgstr "" + +#: apprise/plugins/matrix/base.py:301 +msgid "Target Room Alias" +msgstr "Alias da Sala de Destino" + +#: apprise/plugins/matrix/base.py:324 +#, fuzzy +msgid "Server Discovery" +msgstr "Descoberta de Servidor" + +#: apprise/plugins/matrix/base.py:329 +msgid "Force Home Server on Room IDs" +msgstr "Forcar Servidor Home nos IDs de Sala" + +#: apprise/plugins/matrix/base.py:340 +#, fuzzy +msgid "Webhook Path" +msgstr "Webhook" + +#: apprise/plugins/matrix/base.py:346 +msgid "Matrix API Verion" +msgstr "Versao da API Matrix" + +#: apprise/plugins/matrix/base.py:358 +#, fuzzy +msgid "End-to-End Encryption" +msgstr "Criptografia PGP" + +#: apprise/plugins/vapid/__init__.py:193 +msgid "PEM Private KeyFile" +msgstr "Arquivo de Chave Privada PEM" + +#: apprise/plugins/vapid/__init__.py:199 +msgid "Subscripion File" +msgstr "Arquivo de Assinatura" + +#: apprise/plugins/xmpp/base.py:144 +#, fuzzy +msgid "XMPP Server" +msgstr "Servidor SMTP" + +#: apprise/plugins/xmpp/base.py:156 +#, fuzzy +msgid "Get Roster" +msgstr "Obter Lista de Contatos" + +#: apprise/plugins/xmpp/base.py:161 +msgid "Use Subject" +msgstr "Usar Assunto" + +#: apprise/plugins/xmpp/base.py:166 +#, fuzzy +msgid "Keep Connection Alive" +msgstr "Manter Conexao Ativa" + +#: apprise/plugins/xmpp/base.py:172 +#, fuzzy +msgid "MUC Nickname" +msgstr "Apelido MUC" + +#: apprise/plugins/xmpp/base.py:176 +msgid "SCRAM-PLUS Channel Binding" +msgstr "" diff --git a/libs/apprise/manager.py b/libs/apprise/manager.py index 5e8fdb5d1..f6da15b4c 100644 --- a/libs/apprise/manager.py +++ b/libs/apprise/manager.py @@ -101,6 +101,20 @@ class PluginManager(metaclass=Singleton): # Track manually disabled modules (by their schema) self._disabled = set() + # Reference counter for optional runtime libraries declared via + # runtime_deps(). Maps top-level package name -> count of currently + # *enabled* plugins that depend on it. When a plugin is disabled and + # its counter reaches zero the library *may* be evicted from + # sys.modules - but only when evict_on_disable is True. + self._dep_counter = {} + + # Controls whether libraries are evicted from sys.modules when their + # dep counter reaches zero. Defaults to False so that third-party + # projects embedding Apprise are not surprised by modules disappearing. + # The Apprise API sets this to True at startup to reclaim memory from + # optional libraries whose plugins are all disabled. + self.evict_on_disable = False + # Hash of all paths previously scanned so we don't waste # effort/overhead doing it again self._paths_previously_scanned = set() @@ -134,6 +148,10 @@ class PluginManager(metaclass=Singleton): self._schema_map[schema].enabled = True self._disabled.clear() + # Reset the library dependency counter (evict_on_disable is an + # intentional configuration choice and is NOT reset here) + self._dep_counter = {} + # Reset our variables self._schema_map = {} self._custom_module_map = {} @@ -275,8 +293,8 @@ class PluginManager(metaclass=Singleton): continue logger.trace( - f"{self.name} {module_name} loaded in" - f" {time.time() - tl_start:.6f}s" + f"{self.name} {module_name} loaded" + f" in {time.time() - tl_start:.6f}s" ) # Track the directory loaded so we never load it again @@ -288,6 +306,12 @@ class PluginManager(metaclass=Singleton): f" {time.time() - t_start:.4f}s" ) + # Build the runtime dependency reference counter so that + # disable() can evict libraries when their last user is + # disabled. This is done here (inside the lock) by iterating + # _module_map directly to avoid a recursive lock acquisition. + self._build_dep_counter() + def module_detection(self, paths, cache=True): """Leverage the @notify decorator and load all objects found matching this.""" @@ -330,7 +354,6 @@ class PluginManager(metaclass=Singleton): if module_pyname in self._custom_module_map: # First clear out existing entries for schema in self._custom_module_map[module_pyname]["notify"]: - # Remove any mapped modules to this file del self._schema_map[schema] @@ -347,8 +370,8 @@ class PluginManager(metaclass=Singleton): # Print our loaded modules if any if module_pyname in self._custom_module_map: logger.debug( - "Custom module %s - %d schema(s) (name=%s) " - "loaded in %.6fs", + "Custom module %s - %d schema(s)" + " (name=%s) loaded in %.6fs", path_, len(self._custom_module_map[module_pyname]["notify"]), module_name, @@ -366,7 +389,6 @@ class PluginManager(metaclass=Singleton): for schema, _meta in self._custom_module_map[module_pyname][ "notify" ].items(): - # For mapping purposes; map our element in our main list self._module_map[module_name]["plugin"].add( self._schema_map[schema] @@ -398,7 +420,6 @@ class PluginManager(metaclass=Singleton): if os.path.isdir(path) and not os.path.isfile( os.path.join(path, "__init__.py") ): - logger.debug("Scanning for custom plugins in: %s", path) for entry in os.listdir(path): re_match = module_re.match(entry) @@ -419,8 +440,7 @@ class PluginManager(metaclass=Singleton): continue if not cache or ( - cache - and new_path not in self._paths_previously_scanned + new_path not in self._paths_previously_scanned ): # Load our module _import_module(new_path) @@ -472,8 +492,8 @@ class PluginManager(metaclass=Singleton): if not schemas or not isinstance(schemas, (set, tuple, list)): # We're done logger.error( - "The schemas provided (type %s) is unsupported; " - "loaded from %s.", + "The schemas provided (type %s) is" + " unsupported; loaded from %s.", type(schemas), send_func.__name__ if send_func else plugin.__class__.__name__, ) @@ -495,7 +515,8 @@ class PluginManager(metaclass=Singleton): "force loaded; overriding %s%s.", ", ".join(conflict), "custom notify function " if send_func else "", - send_func.__name__ if send_func + send_func.__name__ + if send_func else plugin.__class__.__name__, ) self.remove(*conflict, unload=False) @@ -506,7 +527,8 @@ class PluginManager(metaclass=Singleton): "loaded from %s%s.", ", ".join(conflict), "custom notify function " if send_func else "", - send_func.__name__ if send_func + send_func.__name__ + if send_func else plugin.__class__.__name__, ) return False @@ -519,7 +541,8 @@ class PluginManager(metaclass=Singleton): "loaded from %s%s.", ", ".join(conflict), "custom notify function " if send_func else "", - send_func.__name__ if send_func + send_func.__name__ + if send_func else plugin.__class__.__name__, ) return False @@ -617,6 +640,87 @@ class PluginManager(metaclass=Singleton): else [s for s in self._schema_map if self._schema_map[s].enabled] ) + def _build_dep_counter(self): + """Build the runtime library reference counter from loaded plugins. + + Iterates `_module_map` directly (rather than calling + `self.plugins()`) so it is safe to call while `_lock` is held + inside `load_modules`. + + Only enabled plugins contribute to the counter - plugins already + disabled because their optional library is not installed are not + counted, as there is nothing in memory to evict for them. + """ + self._dep_counter = {} + if not self._module_map: + return + for module in self._module_map.values(): + for plugin in module["plugin"]: + # Guard: attachment and other non-notify plugin types do not + # carry `enabled` or `runtime_deps`; skip them safely. + if not getattr(plugin, "enabled", False): + continue + fn = getattr(plugin, "runtime_deps", None) + if not callable(fn): + continue + for lib in fn(): + self._dep_counter[lib] = self._dep_counter.get(lib, 0) + 1 + + def _evict_library(self, lib_name): + """Remove a library and all its submodules from `sys.modules`. + + Called when `evict_on_disable` is `True` and the reference counter + for *lib_name* reaches zero - meaning no enabled plugin requires it. + """ + to_remove = [ + k + for k in sys.modules + if k == lib_name or k.startswith(lib_name + ".") + ] + + evicted = 0 + for key in to_remove: + try: + del sys.modules[key] + evicted += 1 + except KeyError: + logger.trace("Eviction skipped: '%s' not in sys.modules", key) + + if evicted: + logger.debug( + "Evicted %d module(s) for '%s' from memory", + evicted, + lib_name, + ) + elif to_remove: + # Keys were found but all raised KeyError (race condition) + logger.trace( + "Eviction of '%s' found %d candidate(s) but removed none", + lib_name, + len(to_remove), + ) + + def _update_dep_counter(self, plugin, delta): + """Increment or decrement dep counters for *plugin* by *delta* (+1/-1). + + Evicts libraries from memory when `evict_on_disable` is `True` and the + counter reaches zero. + """ + fn = getattr(plugin, "runtime_deps", None) + if not callable(fn): + return + for lib in fn(): + count = self._dep_counter.get(lib, 0) + delta + self._dep_counter[lib] = max(0, count) + logger.trace( + "Dep counter '%s': %d -> %d", + lib, + count - delta, + self._dep_counter[lib], + ) + if self.evict_on_disable and count <= 0: + self._evict_library(lib) + def disable(self, *schemas): """Disables the modules associated with the specified schemas.""" if not self: @@ -627,12 +731,17 @@ class PluginManager(metaclass=Singleton): if schema not in self._schema_map: continue - if not self._schema_map[schema].enabled: + plugin = self._schema_map[schema] + if not plugin.enabled: continue - # Disable - self._schema_map[schema].enabled = False + # Disable via the plugin classmethod so subclasses can hook in + plugin.disable() self._disabled.add(schema) + logger.debug("Disabled %s plugin (%s://)", self.name, schema) + + # Decrement dep counters; evict if evict_on_disable and zero + self._update_dep_counter(plugin, -1) def enable_only(self, *schemas): """Disables the modules associated with the specified schemas.""" @@ -654,15 +763,31 @@ class PluginManager(metaclass=Singleton): # Disable it (only if previously enabled); this prevents us # from adjusting schemas that were disabled due to missing # libraries or other environment reasons - plugin.enabled = False + plugin.disable() self._disabled |= p_schemas + logger.debug( + "Disabled %s plugin (%s)", + self.name, + ", ".join(f"{s}://" for s in p_schemas), + ) + + # Decrement dep counters; evict at zero if evict_on_disable + self._update_dep_counter(plugin, -1) continue # If we reach here, our schema was flagged to be enabled if p_schemas & self._disabled: # Previously disabled; no worries, let's clear this up self._disabled -= p_schemas - plugin.enabled = True + plugin.enable() + logger.debug( + "Enabled %s plugin (%s)", + self.name, + ", ".join(f"{s}://" for s in p_schemas), + ) + + # Increment dep counters for the re-enabled plugin + self._update_dep_counter(plugin, +1) def __contains__(self, schema): """Checks if a schema exists.""" @@ -738,9 +863,8 @@ class PluginManager(metaclass=Singleton): not self._module_map[key]["native"] and module_pyname in self._custom_module_map ): - - del self._custom_module_map[module_pyname][ - "notify"][schema] + notify = self._custom_module_map[module_pyname]["notify"] + del notify[schema] if not self._custom_module_map[module_pyname]["notify"]: # diff --git a/libs/apprise/manager_config.py b/libs/apprise/manager_config.py index 0544a6500..01c3eebca 100644 --- a/libs/apprise/manager_config.py +++ b/libs/apprise/manager_config.py @@ -52,5 +52,5 @@ class ConfigurationManager(PluginManager): # For filtering our result set module_filter_re = re.compile( - r"^(?P" + fname_prefix + r"(?!Base)[A-Za-z0-9]+)$" + r"^(?P" + fname_prefix + r"(?!Base|Format)[A-Za-z0-9]+)$" ) diff --git a/libs/apprise/manager_plugins.py b/libs/apprise/manager_plugins.py index 00ac59511..7db68fddc 100644 --- a/libs/apprise/manager_plugins.py +++ b/libs/apprise/manager_plugins.py @@ -52,5 +52,7 @@ class NotificationManager(PluginManager): # For filtering our result set module_filter_re = re.compile( - r"^(?P" + fname_prefix + r"(?!Base|ImageSize|Type)[A-Za-z0-9]+)$" + r"^(?P" + + fname_prefix + + r"(?!Base|Format|ImageSize|Type)[A-Za-z0-9]+)$" ) diff --git a/libs/apprise/persistent_store.py b/libs/apprise/persistent_store.py index 926647af8..fdb5b3aa3 100644 --- a/libs/apprise/persistent_store.py +++ b/libs/apprise/persistent_store.py @@ -43,17 +43,17 @@ from typing import Any, Optional, Union import zlib from . import exception -from .common import PersistentStoreMode +from .common import ( + AWARE_DATE_ISO_FORMAT, + NAIVE_DATE_ISO_FORMAT, + PersistentStoreMode, +) from .logger import logger from .utils.disk import path_decode # Used for writing/reading time stored in cache file EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) -# isoformat is spelled out for compatibility with Python v3.6 -AWARE_DATE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z" -NAIVE_DATE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" - def _ntf_tidy(ntf): """Reusable NamedTemporaryFile Cleanup.""" @@ -79,7 +79,6 @@ def _ntf_tidy(ntf): class CacheObject: - hash_engine = hashlib.sha256 hash_length = 6 @@ -124,8 +123,9 @@ class CacheObject: if persistent is not None: self.__persistent = bool(persistent) - def set_expiry(self, expires: - Union[datetime, bool, float, int, None] = None) -> None: + def set_expiry( + self, expires: Union[datetime, bool, float, int, None] = None + ) -> None: """Sets a new expiry.""" if isinstance(expires, datetime): @@ -430,7 +430,8 @@ class PersistentStore: try: # Store our mode self.__mode = ( - mode if isinstance(mode, PersistentStoreMode) + mode + if isinstance(mode, PersistentStoreMode) else PersistentStoreMode(mode.lower()) ) @@ -826,7 +827,6 @@ class PersistentStore: and self.__mode != PersistentStoreMode.MEMORY and not self.__dirty ): - # ensure we renew our content self.__renew.add(self.cache_file) @@ -881,7 +881,6 @@ class PersistentStore: if args: for arg in args: - try: del self._cache[arg] @@ -966,7 +965,6 @@ class PersistentStore: AttributeError, EOFError, ): - # Let users known there was a problem logger.warning( "Corrupted access persistent cache content: %s", cache_file @@ -1618,8 +1616,10 @@ class PersistentStore: # Get a list of files (file paths) in the given directory try: - self.__cache_size = sum(os.stat(path).st_size - for path in self.files(exclude=exclude, lazy=lazy)) + self.__cache_size = sum( + os.stat(path).st_size + for path in self.files(exclude=exclude, lazy=lazy) + ) except OSError: # We can't access the directory or it does not exist @@ -1751,7 +1751,6 @@ class PersistentStore: self.__dirty = False for path in self.files(exclude=False): - # Some information we use to validate the actions of our clean() # call. This is so we don't remove anything we shouldn't base = os.path.dirname(path) diff --git a/libs/apprise/plugins/__init__.py b/libs/apprise/plugins/__init__.py index b486e9577..2a0485569 100644 --- a/libs/apprise/plugins/__init__.py +++ b/libs/apprise/plugins/__init__.py @@ -68,7 +68,6 @@ def _sanitize_token(tokens, default_delimiter): # Iterate over our tokens for key in tokens: - for element in tokens[key]: # Perform translations (if detected to do so) if isinstance(tokens[key][element], LazyTranslation): @@ -114,7 +113,6 @@ def _sanitize_token(tokens, default_delimiter): and "values" in tokens[key] and len(tokens[key]["values"]) == 1 ): - # If there is only one choice; then make it the default # - support dictionaries too tokens[key]["default"] = ( @@ -291,7 +289,6 @@ def details(plugin): # Argument/Option Handling for key in list(template_args.keys()): - if "alias_of" in template_args[key]: # Check if the mapped reference is a list; if it is, then # we need to store a different delimiter diff --git a/libs/apprise/plugins/africas_talking.py b/libs/apprise/plugins/africas_talking.py index 4a4efc637..eca10538c 100644 --- a/libs/apprise/plugins/africas_talking.py +++ b/libs/apprise/plugins/africas_talking.py @@ -149,6 +149,7 @@ class NotifyAfricasTalking(NotifyBase): "targets": { "name": _("Targets"), "type": "list:string", + "required": True, }, }, ) @@ -157,9 +158,6 @@ class NotifyAfricasTalking(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "apikey": { "alias_of": "apikey", }, @@ -170,17 +168,20 @@ class NotifyAfricasTalking(NotifyBase): "default": "AFRICASTKNG", "map_to": "sender", }, - "batch": { - "name": _("Batch Mode"), - "type": "bool", - "default": False, - }, "mode": { "name": _("SMS Mode"), "type": "choice:string", "values": AFRICAS_TALKING_SMS_MODES, "default": AFRICAS_TALKING_SMS_MODES[0], }, + "to": { + "alias_of": "targets", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, }, ) @@ -316,10 +317,11 @@ class NotifyAfricasTalking(NotifyBase): self.logger.debug(f"Africas Talking Payload: {payload!s}") # Printable target detail + _batch = self.targets[index : index + batch_size] p_target = ( self.targets[index] if batch_size == 1 - else f"{len(self.targets[index:index + batch_size])} target(s)" + else f"{len(_batch)} target(s)" ) # Always call throttle before any remote server i/o is made @@ -331,6 +333,7 @@ class NotifyAfricasTalking(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Sample response # { @@ -365,7 +368,8 @@ class NotifyAfricasTalking(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/apprise_api.py b/libs/apprise/plugins/apprise_api.py index a0bacd704..f25a89350 100644 --- a/libs/apprise/plugins/apprise_api.py +++ b/libs/apprise/plugins/apprise_api.py @@ -294,23 +294,27 @@ class NotifyAppriseAPI(NotifyBase): if self.method == AppriseAPIMethod.JSON: # Output must be in a DataURL format (that's what # PushSafer calls it): - attachments.append({ - "filename": filename, - "base64": attachment.base64(), - "mimetype": attachment.mimetype, - }) + attachments.append( + { + "filename": filename, + "base64": attachment.base64(), + "mimetype": attachment.mimetype, + } + ) else: # AppriseAPIMethod.FORM - files.append(( - f"file{no:02d}", + files.append( ( - filename, - # file handle is safely closed in `finally`; - # inline open is intentional - open(attachment.path, "rb"), # noqa: SIM115 - attachment.mimetype, - ), - )) + f"file{no:02d}", + ( + filename, + # file handle safely closed in + # `finally`; inline open intentional + open(attachment.path, "rb"), # noqa: SIM115 + attachment.mimetype, + ), + ) + ) except (TypeError, OSError, exception.AppriseException): # We could not access the attachment @@ -361,14 +365,16 @@ class NotifyAppriseAPI(NotifyBase): url += f"/notify/{self.token}" # Some entries can not be over-ridden - headers.update({ - # Our response to be in JSON format always - "Accept": "application/json", - # Pass our Source UUID4 Identifier - "X-Apprise-ID": self.asset._uid, - # Pass our current recursion count to our upstream server - "X-Apprise-Recursion-Count": str(self.asset._recursion + 1), - }) + headers.update( + { + # Our response to be in JSON format always + "Accept": "application/json", + # Pass our Source UUID4 Identifier + "X-Apprise-ID": self.asset._uid, + # Pass our current recursion count to our upstream server + "X-Apprise-Recursion-Count": str(self.asset._recursion + 1), + } + ) # Some Debug Logging if self.logger.isEnabledFor(logging.DEBUG): @@ -380,7 +386,8 @@ class NotifyAppriseAPI(NotifyBase): f" {url} (cert_verify={self.verify_certificate!r})" ) self.logger.debug( - "Apprise API Payload: %s", sanitize_payload(payload)) + "Apprise API Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -394,6 +401,7 @@ class NotifyAppriseAPI(NotifyBase): files=files if files else None, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -409,7 +417,8 @@ class NotifyAppriseAPI(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/aprs.py b/libs/apprise/plugins/aprs.py index 33d40fbe4..5318e58f7 100644 --- a/libs/apprise/plugins/aprs.py +++ b/libs/apprise/plugins/aprs.py @@ -350,8 +350,7 @@ class NotifyAprs(NotifyBase): return False except socket.timeout as e: - self.logger.debug( - "Socket Timeout Exception socket_open: %s", e) + self.logger.debug("Socket Timeout Exception socket_open: %s", e) self.sock = None return False diff --git a/libs/apprise/plugins/bark.py b/libs/apprise/plugins/bark.py index 6dad9f3db..10500df6b 100644 --- a/libs/apprise/plugins/bark.py +++ b/libs/apprise/plugins/bark.py @@ -167,9 +167,6 @@ class NotifyBark(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "sound": { "name": _("Sound"), "type": "choice:string", @@ -218,6 +215,9 @@ class NotifyBark(NotifyBase): "type": "bool", "default": False, }, + "to": { + "alias_of": "targets", + }, }, ) @@ -436,6 +436,7 @@ class NotifyBark(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -454,7 +455,8 @@ class NotifyBark(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -633,8 +635,6 @@ class NotifyBark(NotifyBase): ) # Call - results["call"] = parse_bool( - results["qsd"].get("call", False) - ) + results["call"] = parse_bool(results["qsd"].get("call", False)) return results diff --git a/libs/apprise/plugins/base.py b/libs/apprise/plugins/base.py index 444728bd0..0831b2077 100644 --- a/libs/apprise/plugins/base.py +++ b/libs/apprise/plugins/base.py @@ -29,12 +29,15 @@ import asyncio from collections.abc import Generator from datetime import tzinfo from functools import partial +import math import re from typing import Any, ClassVar, Optional, TypedDict, Union from zoneinfo import ZoneInfo from ..apprise_attachment import AppriseAttachment from ..common import ( + APPRISE_MAX_SERVICE_RETRY, + APPRISE_MAX_SERVICE_WAIT, NOTIFY_FORMATS, OVERFLOW_MODES, NotifyFormat, @@ -69,6 +72,56 @@ class NotifyBase(URLBase): # enabled. enabled = True + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + + The plugin manager uses this to maintain a reference counter per + library. When every plugin that declared a given library is disabled, + its counter reaches zero and the manager evicts the library from + `sys.modules`, releasing the associated Python objects from memory. + + Names must be the importable top-level namespace - the same string you + would pass to `import` - not the pip install name: + + ('paho',) # paho-mqtt installs as 'paho' + ('slixmpp',) + ('cryptography',) + + Submodules are handled automatically; declaring the top-level name is + sufficient. + + Override this in any plugin that conditionally imports a heavy optional + library. Return an empty tuple (the default) when the plugin has no + optional dependencies that are worth evicting. + """ + return () + + @classmethod + def enable(self): + """Mark this plugin as enabled. + + This is the counterpart to :meth:`disable`. Calling this restores the + plugin to an active state so it will be used for notifications again. + Note that if the plugin's runtime dependencies were evicted from memory + by the plugin manager, re-enabling will restore the flag but the + plugin may not function until the process is restarted. + """ + self.enabled = True + + @classmethod + def disable(self): + """Mark this plugin as disabled. + + The plugin will not be used for notifications. The plugin manager + calls this when honouring `APPRISE_DENY_SERVICES` / + `APPRISE_ALLOW_SERVICES` and uses the result of + :method:`runtime_deps` to decrement its per-library reference counters, + potentially evicting unused libraries from `sys.modules`. + """ + self.enabled = False + # The category allows for parent inheritance of this object to alter # this when it's function/use is intended to behave differently. The # following category types exist: @@ -162,6 +215,66 @@ class NotifyBase(URLBase): # Default Emoji Interpretation interpret_emojis = False + # Default retry count for failed notifications. Plugins may override + # via ?retry=N in their URL; the asset's default_service_retry is used + # when no per-plugin value is set. + # Always in [0, APPRISE_MAX_SERVICE_RETRY]. + service_retry = 0 + + # Default wait (seconds) between retry attempts. Plugins may override + # via ?wait=N in their URL; the asset's default_service_wait is used when + # no per-plugin value is set. Always in [0.0, APPRISE_MAX_SERVICE_WAIT]. + service_wait = 0.0 + + # When set to True, a delivery failure for this individual service is + # silently absorbed by all three dispatch paths (sequential, thread-pool, + # and asyncio coroutine). The overall notify() / async_notify() call + # still returns True even if this particular endpoint could not be + # reached, provided that every *required* (non-optional) service in + # the same batch succeeded. + # + # This flag is designed for "nice to have" services -- endpoints that + # you want to notify when they are available but whose unavailability + # must not be treated as a delivery failure by the calling application. + # + # Concrete example -- four Kodi home screens tagged "media": + # + # Configure every Kodi entry with optional=yes. When the application + # calls notify(tag="media"), Apprise attempts delivery to whichever + # screens are currently powered on and reachable. If none of the four + # respond (all are off or unreachable), the call still returns True + # because every failing service was optional. The application is + # never penalised for transient screen availability. + # + # Important behaviour details: + # - Retries are still performed up to the configured retry count + # before a service is considered to have failed. The optional flag + # only fires *after* all retry attempts have been exhausted without + # a successful delivery. In other words, optional does not bypass + # the retry loop -- it only changes the meaning of the final result. + # - Setting optional=True does NOT skip the service. Delivery is + # still attempted on every call. Only the *outcome* is affected: + # a failure is reported as True instead of False to the caller. + # - A batch that mixes optional and required services returns False + # only if one of the *required* (optional=False) services fails. + # Optional failures are never included in the aggregate result. + # - The flag is per-instance. Different URLs that share a tag can + # have different optional values, so you can mix required and + # optional endpoints within the same tag group. + # + # Ways to enable this flag: + # - URL query parameter : append ?optional=yes to the service URL + # - YAML configuration : add optional: yes under the URL entry + # - Python constructor : MyPlugin(host="...", optional=True) + # - Direct assignment : server.optional = True + # + # Accepted truthy values : "yes", "true", "on", "1", True + # Accepted falsy values : "no", "false", "off", "0", False + # + # Defaults to False -- every delivery failure is propagated to the + # caller unchanged. + optional = False + # Support Attachments; this defaults to being disabled. # Since apprise allows you to send attachments without a body or title # defined, by letting Apprise know the plugin won't support attachments @@ -242,6 +355,28 @@ class NotifyBase(URLBase): # runtime. "_lookup_default": "timezone", }, + "retry": { + "name": _("Service Retry"), + "type": "int", + "min": 0, + "max": APPRISE_MAX_SERVICE_RETRY, + "default": service_retry, + "_lookup_default": "service_retry", + }, + "wait": { + "name": _("Inter-Retry Wait"), + "type": "float", + "min": 0.0, + "max": APPRISE_MAX_SERVICE_WAIT, + "default": service_wait, + "_lookup_default": "service_wait", + }, + "optional": { + "name": _("Optional Service"), + "type": "bool", + "default": optional, + "_lookup_default": "optional", + }, }, ) @@ -299,6 +434,24 @@ class NotifyBase(URLBase): # automatically initialized by specifying ?tz= on the Apprise URLs __tzinfo = None + @classmethod + def __init_subclass__(cls, **kwargs): + """Automatically wrap any __len__ defined on a subclass so that it + multiplies its base target count by (retry + 1). + + This ensures every plugin's __len__ reflects the total number of + transmission attempts (targets * retry-factor) without requiring + each plugin to be updated individually. + """ + super().__init_subclass__(**kwargs) + if "__len__" in cls.__dict__: + _orig = cls.__dict__["__len__"] + + def _retry_aware_len(self, _orig=_orig): + return _orig(self) * (getattr(self, "retry", 0) + 1) + + cls.__len__ = _retry_aware_len + def __init__(self, **kwargs): """Initialize some general configuration that will keep things consistent when working with the notifiers that will inherit this @@ -306,6 +459,94 @@ class NotifyBase(URLBase): super().__init__(**kwargs) + # Initialize retry count from (in priority order): + # 1. kwargs["retry"] (from ?retry= URL param or YAML retry: key) + # 2. asset.default_service_retry + # Always clamped to [0, APPRISE_MAX_SERVICE_RETRY]. + _retry_raw = kwargs.get("retry") + if _retry_raw is not None: + try: + _retry_val = int(_retry_raw) + if _retry_val < 0: + msg = f"Service retry count must be >= 0; got {_retry_raw}" + self.logger.warning(msg) + raise TypeError(msg) + self.retry = min(_retry_val, APPRISE_MAX_SERVICE_RETRY) + except (TypeError, ValueError): + self.logger.warning( + "Invalid retry value specified (%s); using default", + _retry_raw, + ) + self.retry = min( + max(0, self.asset.default_service_retry), + APPRISE_MAX_SERVICE_RETRY, + ) + else: + self.retry = min( + max(0, self.asset.default_service_retry), + APPRISE_MAX_SERVICE_RETRY, + ) + + # Initialize wait (inter-retry delay) from (in priority order): + # 1. kwargs["wait"] (from ?wait= URL param or YAML wait: key) + # 2. asset.default_service_wait + # Only valid non-negative finite floats are accepted; integers are + # promoted to float. Invalid values fall back to the asset default. + # Negative values raise TypeError. + _wait_raw = kwargs.get("wait") + if _wait_raw is not None: + try: + _wait_val = float(_wait_raw) + if not math.isfinite(_wait_val) or _wait_val < 0.0: + msg = f"Service retry wait must be >= 0; got {_wait_raw}" + self.logger.warning(msg) + raise TypeError(msg) + self.wait = min(_wait_val, APPRISE_MAX_SERVICE_WAIT) + except (TypeError, ValueError): + self.logger.warning( + "Invalid wait value specified (%s); using default", + _wait_raw, + ) + self.wait = min( + max(0.0, self.asset.default_service_wait), + APPRISE_MAX_SERVICE_WAIT, + ) + else: + self.wait = min( + max(0.0, self.asset.default_service_wait), + APPRISE_MAX_SERVICE_WAIT, + ) + + # Initialize the optional= flag from kwargs if provided, otherwise + # fall back to the class-level attribute (default: False). + # + # Priority order (highest to lowest): + # 1. kwargs["optional"] -- supplied via ?optional=yes/no URL + # parameter, YAML optional: yes/no key, or a direct Python + # keyword argument to the constructor. + # 2. The class-level 'optional' attribute on the plugin class + # (or any of its superclasses). Plugin authors can ship + # with optional=True as the default simply by overriding + # the class attribute -- no extra __init__ boilerplate needed. + # + # parse_bool() is used for the string -> bool conversion so that + # all of the canonical truthy/falsy representations that Apprise + # accepts elsewhere are valid here too: "yes"/"no", "true"/"false", + # "on"/"off", "1"/"0", and native Python booleans. The function + # returns the 'default' argument (False) for unrecognised strings, + # so garbage input silently falls back to the safe behaviour + # (propagate failures) rather than raising an exception. + _optional_raw = kwargs.get("optional") + if _optional_raw is not None: + # An explicit value was passed in -- parse it and store on + # this instance, shadowing the class-level attribute only for + # this particular service object. + self.optional = parse_bool(_optional_raw) + # else: kwargs did not include "optional". Leave self.optional + # unset so that normal Python attribute lookup falls through to + # the class-level default (NotifyBase.optional = False, or the + # value set by a plugin subclass that overrides it). + # Store our interpret_emoji's setting # If asset emoji value is set to a default of True and the user # specifies it to be false, this is accepted and False over-rides. @@ -337,14 +578,15 @@ class NotifyBase(URLBase): value = kwargs["format"] try: self.notify_format = ( - value if isinstance(value, NotifyFormat) + value + if isinstance(value, NotifyFormat) else NotifyFormat(value.lower()) ) except (AttributeError, ValueError): err = ( - f"An invalid notification format ({value}) was " - "specified.") + f"An invalid notification format ({value}) was specified." + ) self.logger.warning(err) raise TypeError(err) from None @@ -353,8 +595,9 @@ class NotifyBase(URLBase): self.__tzinfo = zoneinfo(value) if not self.__tzinfo: err = ( - f"An invalid notification timezone ({value}) was " - "specified.") + "An invalid notification timezone " + f"({value}) was specified." + ) self.logger.warning(err) raise TypeError(err) from None @@ -362,7 +605,8 @@ class NotifyBase(URLBase): value = kwargs["overflow"] try: self.overflow_mode = ( - value if isinstance(value, OverflowMode) + value + if isinstance(value, OverflowMode) else OverflowMode(value.lower()) ) @@ -540,8 +784,8 @@ class NotifyBase(URLBase): # attachments, there is nothing further to do here, just move # along. msg = ( - f"{self.service_name} does not support attachments; " - " service skipped" + f"{self.service_name} does not support " + "attachments; service skipped" ) self.logger.warning(msg) raise TypeError(msg) @@ -566,7 +810,6 @@ class NotifyBase(URLBase): for chunk in self._apply_overflow( body=body, title=title, overflow=overflow, body_format=body_format ): - # Send notification yield { "body": chunk["body"], @@ -702,10 +945,12 @@ class NotifyBase(URLBase): # TRUNCATE mode: hard truncation (no smart-splitting) if overflow == OverflowMode.TRUNCATE: - response.append({ - "body": body[:body_maxlen].lstrip("\r\n\x0b\x0c").rstrip(), - "title": title, - }) + response.append( + { + "body": body[:body_maxlen].lstrip("\r\n\x0b\x0c").rstrip(), + "title": title, + } + ) return response # @@ -725,8 +970,7 @@ class NotifyBase(URLBase): # SPLIT mode with repeated title (with/without counter) if not overflow_display_title_once and not ( # edge case: amalgamated title but no body space - self.overflow_amalgamate_title - and body_maxlen <= 0 + self.overflow_amalgamate_title and body_maxlen <= 0 ): # Decide whether to show a counter (legacy condition) show_counter = ( @@ -735,20 +979,20 @@ class NotifyBase(URLBase): and ( ( self.overflow_amalgamate_title - and body_maxlen >= - self.overflow_display_count_threshold + and body_maxlen + >= self.overflow_display_count_threshold ) or ( not self.overflow_amalgamate_title - and title_maxlen > - self.overflow_display_count_threshold + and title_maxlen + > self.overflow_display_count_threshold ) ) and ( - title_maxlen > - (self.overflow_max_display_count_width + overflow_buffer) - and self.title_maxlen >= - self.overflow_display_count_threshold + title_maxlen + > (self.overflow_max_display_count_width + overflow_buffer) + and self.title_maxlen + >= self.overflow_display_count_threshold ) ) @@ -775,13 +1019,9 @@ class NotifyBase(URLBase): <= self.overflow_max_display_count_width ): # Truncate title further if needed to make room for counter - if ( - len(title) - > title_maxlen - overflow_display_count_width - ): - title = title[ - : title_maxlen - overflow_display_count_width - ] + t_max = title_maxlen - overflow_display_count_width + if len(title) > t_max: + title = title[:t_max] template = f" [{{:0{digits}d}}/{{:0{digits}d}}]" else: # Too many messages; fall back to repeated title without @@ -791,10 +1031,12 @@ class NotifyBase(URLBase): response = [] for idx, chunk_body in enumerate(chunks, start=1): suffix = template.format(idx, count) if show_counter else "" - response.append({ - "body": chunk_body.lstrip("\r\n\x0b\x0c").rstrip(), - "title": f"{title}{suffix}", - }) + response.append( + { + "body": chunk_body.lstrip("\r\n\x0b\x0c").rstrip(), + "title": f"{title}{suffix}", + } + ) else: # @@ -818,18 +1060,22 @@ class NotifyBase(URLBase): consumed = len(first_body) remainder = body[consumed:] - response.append({ - "body": first_body.lstrip("\r\n\x0b\x0c").rstrip(), - "title": title, - }) + response.append( + { + "body": first_body.lstrip("\r\n\x0b\x0c").rstrip(), + "title": title, + } + ) else: # body_maxlen <= 0 or no body; send title only, still honouring # body - response.append({ - "body": "", - "title": title, - }) + response.append( + { + "body": "", + "title": title, + } + ) # remainder stays as full body; will be split below # Remaining chunks: no title, use the full body_maxlen of the @@ -841,10 +1087,12 @@ class NotifyBase(URLBase): body_format, ) for chunk_body in more_chunks: - response.append({ - "body": chunk_body.lstrip("\r\n\x0b\x0c").rstrip(), - "title": "", - }) + response.append( + { + "body": chunk_body.lstrip("\r\n\x0b\x0c").rstrip(), + "title": "", + } + ) return response @@ -857,9 +1105,19 @@ class NotifyBase(URLBase): ) -> bool: """Should preform the actual notification itself.""" raise NotImplementedError( - "send() is not implimented by the child class." + "send() is not implemented by the child class." ) + def __len__(self): + """Returns the number of HTTP requests this instance will make, + factoring in the configured retry count. + + Subclasses that override this are automatically wrapped by + __init_subclass__ to apply the same retry multiplier, so they + should return their raw target count without worrying about retry. + """ + return self.retry + 1 + def url_parameters( self, *args: Any, @@ -884,6 +1142,26 @@ class NotifyBase(URLBase): if self.persistent_storage != NotifyBase.persistent_storage: params["store"] = "yes" if self.persistent_storage else "no" + # Always emit retry=, wait=, and optional= in the URL regardless + # of whether they hold their zero/False defaults. Emitting them + # unconditionally keeps the URL fully self-describing: a service + # URL that is round-tripped through parse_url() -> __init__() will + # reconstruct an identical object with the same settings. This + # property is important for configuration-file generators, UI + # tooling, and any code that serialises service objects to strings + # and needs to restore them later without silent data loss. + # + # retry= and wait= are emitted as strings because URL query strings + # are inherently text. The receiving __init__() will re-parse them + # with int() and float() respectively. + # + # optional= is emitted as the canonical lower-case pair "yes"/"no" + # that parse_bool() recognises on the receiving end, ensuring the + # round-trip is lossless even for the boolean case. + params["retry"] = str(self.retry) + params["wait"] = str(self.wait) + params["optional"] = "yes" if self.optional else "no" + params.update(super().url_parameters(*args, **kwargs)) # return default parameters @@ -899,6 +1177,14 @@ class NotifyBase(URLBase): This is very specific and customized for Apprise. + In addition to the fields extracted by URLBase.parse_url(), this + method extracts the NotifyBase-level query-string parameters: + ``format``, ``overflow``, ``emojis``, ``tz``, ``store``, ``retry``, + ``wait``, and ``optional``. The extracted values are placed + directly in the returned results dict under their respective keys, + ready to be consumed by NotifyBase.__init__() (or a subclass). + Child classes should call this method via ``super()`` and then + layer their own parameter extraction on top of the returned dict. Args: url (str): The URL you want to fully parse. @@ -925,8 +1211,8 @@ class NotifyBase(URLBase): results["format"] = results["qsd"].get("format", "").lower() if results["format"] not in NOTIFY_FORMATS: URLBase.logger.warning( - "Unsupported format specified " - f"{results['qsd']['format']!r}" + "Unsupported format specified %r", + results["qsd"]["format"], ) del results["format"] @@ -952,6 +1238,34 @@ class NotifyBase(URLBase): if "store" in results["qsd"]: results["store"] = results["qsd"]["store"] + # Global per-plugin retry count (?retry=N) + if "retry" in results["qsd"] and results["qsd"]["retry"]: + results["retry"] = results["qsd"]["retry"] + + # Global per-plugin inter-retry wait (?wait=N) + if "wait" in results["qsd"] and results["qsd"]["wait"]: + results["wait"] = results["qsd"]["wait"] + + # Global optional flag (?optional=yes/no). + # + # parse_bool() converts the raw query-string value to a native + # Python bool using Apprise's canonical truth table: "yes", "true", + # "on", "1" -> True; "no", "false", "off", "0" -> False. + # Unrecognised strings fall back to False (the safe default). + # + # The parsed bool is stored in results["optional"] so that + # NotifyBase.__init__() receives a value it can pass straight + # through parse_bool() again without loss (parse_bool(True) is + # True and parse_bool(False) is False). + # + # When ?optional= is absent from the URL entirely, this block is + # skipped and results["optional"] is never set. NotifyBase.__init__ + # will then leave self.optional at its class-level default (False), + # which means the absence of the parameter is indistinguishable from + # passing ?optional=no -- both result in failures being propagated. + if "optional" in results["qsd"]: + results["optional"] = parse_bool(results["qsd"]["optional"]) + return results @staticmethod diff --git a/libs/apprise/plugins/blink1.py b/libs/apprise/plugins/blink1.py new file mode 100644 index 000000000..607693e5e --- /dev/null +++ b/libs/apprise/plugins/blink1.py @@ -0,0 +1,396 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Sources: +# - https://blink1.thingm.com/ +# - https://github.com/todbot/blink1-python +# - https://github.com/todbot/blink1/blob/main/docs/blink1-hid-commands.md + +import time + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from .base import NotifyBase + +# Default our global support flag +NOTIFY_BLINK1_ENABLED = False + +try: + import hid as blink1_hid + + NOTIFY_BLINK1_ENABLED = True + +except ImportError: + # No problem -- hidapi is an optional dependency. Users who wish to + # use this plugin must install it: pip install hidapi + pass + +# +# blink(1) USB HID constants +# + +# USB vendor / product identifiers for all blink(1) revisions +BLINK1_VENDOR_ID = 0x27B8 +BLINK1_PRODUCT_ID = 0x01ED + +# HID report size (report ID byte + 8 payload bytes) +BLINK1_REPORT_ID = 0x01 +BLINK1_REPORT_SIZE = 9 + +# Command byte: 'c' == fade to RGB with a time argument +BLINK1_CMD_FADE = ord("c") + + +class Blink1LED: + """ + Defines the LED to focus on + """ + + ALL = 0 + FIRST = 1 + SECOND = 2 + + +BLINK1_LED_CHOICES = { + Blink1LED.ALL: "all", + Blink1LED.FIRST: "1", + Blink1LED.SECOND: "2", +} + +# Maps inbound strings to Blink1LED constants; unknown values fall back to ALL +BLINK1_LED_MAP = { + "0": Blink1LED.ALL, + "all": Blink1LED.ALL, + "a": Blink1LED.ALL, + "1": Blink1LED.FIRST, + "2": Blink1LED.SECOND, +} + +# How long (ms) to hold the LED colour after the fade completes +BLINK1_DEFAULT_DURATION_MS = 5000 +BLINK1_MIN_DURATION_MS = 0 +BLINK1_MAX_DURATION_MS = 300000 # 5 minutes + +# Fade transition time (ms); 0 = instant +BLINK1_DEFAULT_FADE_MS = 0 +BLINK1_MIN_FADE_MS = 0 +BLINK1_MAX_FADE_MS = 10000 # 10 seconds + + +def _blink1_fade_buf(red, green, blue, fade_ms, ledn): + """Build the 9-byte HID feature-report buffer for a fade-to-RGB command. + + The blink(1) wire format (all values unsigned 8-bit unless noted): + [0] REPORT_ID (0x01) + [1] command 'c' (0x63) + [2] red + [3] green + [4] blue + [5] fade_time high byte (fade_time = fade_ms // 10, 16-bit big-endian) + [6] fade_time low byte + [7] ledn (0=all, 1=LED1, 2=LED2) + [8] 0x00 (padding) + """ + fade_time = int(fade_ms) // 10 + th = (fade_time >> 8) & 0xFF + tl = fade_time & 0xFF + return [ + BLINK1_REPORT_ID, + BLINK1_CMD_FADE, + int(red), + int(green), + int(blue), + th, + tl, + int(ledn), + 0x00, + ] + + +class NotifyBlink1(NotifyBase): + """A wrapper for blink(1) USB LED notifications. + + Colors are driven by Apprise's notification-type color map + (info=blue, success=green, warning=yellow, failure=red). + No external blink1 library is required; the USB HID wire protocol + is implemented directly via the hidapi package. + """ + + # Set our global enabled flag + enabled = NOTIFY_BLINK1_ENABLED + + requirements = { + "packages_required": "hidapi", + } + + # The default descriptive name associated with the notification + service_name = _("blink(1)") + + # The services URL + service_url = "https://blink1.thingm.com/" + + # The default protocol + protocol = "blink1" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/blink1/" + + # blink(1) is a local USB device; a title field has no meaning here. + title_maxlen = 0 + + # No throttling needed for a local USB device + request_rate_per_sec = 0 + + # URL templates + templates = ( + "{schema}://", + "{schema}://{serial}/", + ) + + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "serial": { + "name": _("Serial Number"), + "type": "string", + }, + }, + ) + + template_args = dict( + NotifyBase.template_args, + **{ + "duration": { + "name": _("Duration (ms)"), + "type": "int", + "min": BLINK1_MIN_DURATION_MS, + "max": BLINK1_MAX_DURATION_MS, + "default": BLINK1_DEFAULT_DURATION_MS, + }, + "fade": { + "name": _("Fade Time (ms)"), + "type": "int", + "min": BLINK1_MIN_FADE_MS, + "max": BLINK1_MAX_FADE_MS, + "default": BLINK1_DEFAULT_FADE_MS, + }, + "ledn": { + "name": _("LED Number"), + "type": "choice:int", + "values": BLINK1_LED_CHOICES, + "default": Blink1LED.ALL, + }, + }, + ) + + def __init__( + self, + serial=None, + duration=None, + fade=None, + ledn=None, + **kwargs, + ): + """Initialize Blink1 Object.""" + + super().__init__(**kwargs) + + # Device serial number; None means "first available device". + # An underscore is accepted as a URL placeholder meaning "any". + serial = serial.strip() if isinstance(serial, str) else None + self.serial = serial if serial and serial != "_" else None + + # Duration (ms) to hold the LED colour before turning it off + try: + self.duration = int( + BLINK1_DEFAULT_DURATION_MS if duration is None else duration + ) + if not ( + BLINK1_MIN_DURATION_MS + <= self.duration + <= BLINK1_MAX_DURATION_MS + ): + raise ValueError("out of range") + + except (TypeError, ValueError): + msg = ( + f"blink(1) duration ({duration}) must be between" + f" {BLINK1_MIN_DURATION_MS} and" + f" {BLINK1_MAX_DURATION_MS} ms." + ) + self.logger.warning(msg) + raise TypeError(msg) from None + + # Fade transition time (ms) + try: + self.fade = int(BLINK1_DEFAULT_FADE_MS if fade is None else fade) + if not (BLINK1_MIN_FADE_MS <= self.fade <= BLINK1_MAX_FADE_MS): + raise ValueError("out of range") + + except (TypeError, ValueError): + msg = ( + f"blink(1) fade ({fade}) must be between" + f" {BLINK1_MIN_FADE_MS} and {BLINK1_MAX_FADE_MS} ms." + ) + self.logger.warning(msg) + raise TypeError(msg) from None + + # LED selector; unrecognised values silently fall back to ALL + self.ledn = ( + BLINK1_LED_MAP.get(str(ledn).lower(), Blink1LED.ALL) + if ledn is not None + else Blink1LED.ALL + ) + + def _open_device(self): + """Open and return a hidapi device handle for the blink(1). + + Returns None and logs a warning when the device cannot be found. + """ + try: + dev = blink1_hid.device() + dev.open( + BLINK1_VENDOR_ID, + BLINK1_PRODUCT_ID, + self.serial, + ) + return dev + + except OSError: + msg = "Failed to open blink(1) device" + if self.serial: + msg += f" (serial={self.serial})" + msg += "." + self.logger.warning(msg) + return None + + def _send_fade(self, dev, red, green, blue, fade_ms): + """Send a single fade-to-RGB HID feature report. + + Returns True on success, False if the report could not be sent. + """ + buf = _blink1_fade_buf(red, green, blue, fade_ms, self.ledn) + try: + rc = dev.send_feature_report(buf) + + except OSError as e: + self.logger.warning("blink(1) HID write failed: %s", e) + return False + + if rc != BLINK1_REPORT_SIZE: + self.logger.warning( + "blink(1) HID write returned %d (expected %d).", + rc, + BLINK1_REPORT_SIZE, + ) + return False + + return True + + @property + def url_identifier(self): + """Returns all fields that uniquely identify this connection.""" + return ( + self.protocol, + self.serial, + self.ledn, + ) + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform blink(1) Notification.""" + + # Resolve the RGB triple for this notification type + r, g, b = self.color(notify_type=notify_type, color_type=tuple) + + dev = self._open_device() + if dev is None: + return False + + try: + # Always throttle before any device I/O + self.throttle() + + if not self._send_fade(dev, r, g, b, self.fade): + return False + + # Hold for fade + duration, then switch off + time.sleep((self.fade + self.duration) / 1000.0) + + # Turn off: instant fade to black + if not self._send_fade(dev, 0, 0, 0, 0): + return False + + finally: + dev.close() + + self.logger.info("Sent blink(1) notification.") + return True + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + params = { + "duration": str(self.duration), + "fade": str(self.fade), + "ledn": str(self.ledn), + } + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + serial = self.serial if self.serial else "_" + return f"{self.protocol}://{serial}/?{NotifyBlink1.urlencode(params)}" + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments to re-instantiate.""" + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + return results + + # The hostname, when present, is the device serial number. + # An underscore or empty value means "first available device". + host = results.get("host", "") + if host and host != "_": + results["serial"] = NotifyBlink1.unquote(host) + + if results["qsd"].get("duration"): + results["duration"] = NotifyBlink1.unquote( + results["qsd"]["duration"] + ) + + if results["qsd"].get("fade"): + results["fade"] = NotifyBlink1.unquote(results["qsd"]["fade"]) + + if results["qsd"].get("ledn"): + results["ledn"] = NotifyBlink1.unquote(results["qsd"]["ledn"]) + + return results + + @staticmethod + def runtime_deps(): + """Return optional runtime dependency package names.""" + return ("hid",) diff --git a/libs/apprise/plugins/bluesky.py b/libs/apprise/plugins/bluesky.py index d52ac9e5e..2cb979131 100644 --- a/libs/apprise/plugins/bluesky.py +++ b/libs/apprise/plugins/bluesky.py @@ -180,7 +180,6 @@ class NotifyBlueSky(NotifyBase): # We need to upload our payload first so that we can source it # in remaining messages for no, attachment in enumerate(attach, start=1): - # Perform some simple error checking if not attachment: # We could not access the attachment @@ -261,10 +260,12 @@ class NotifyBlueSky(NotifyBase): payload_["record"]["text"] = f"{no:02d}/{len(blobs):02d}" payload_["record"]["embed"] = { - "images": [{ - "image": blob[0], - "alt": blob[1], - }], + "images": [ + { + "image": blob[0], + "alt": blob[1], + } + ], "$type": "app.bsky.embed.images", } payloads.append(payload_) @@ -567,6 +568,7 @@ class NotifyBlueSky(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Get our JSON content if it's possible @@ -611,7 +613,8 @@ class NotifyBlueSky(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure return (False, content) diff --git a/libs/apprise/plugins/brevo.py b/libs/apprise/plugins/brevo.py index bc24a02a6..f035a20ca 100644 --- a/libs/apprise/plugins/brevo.py +++ b/libs/apprise/plugins/brevo.py @@ -53,17 +53,62 @@ BREVO_HTTP_ERROR_MAP = { # Source: Brevo API Documentation & Transactional Attachment Guidelines BREVO_VALID_EXTENSIONS = ( # Documents & Text - "xlsx", "xls", "ods", "docx", "docm", "doc", "csv", "pdf", "txt", - "rtf", "msg", "pub", "mobi", "ppt", "pptx", "eps", "odt", "ics", - "xml", "css", "html", "htm", "shtml", + "xlsx", + "xls", + "ods", + "docx", + "docm", + "doc", + "csv", + "pdf", + "txt", + "rtf", + "msg", + "pub", + "mobi", + "ppt", + "pptx", + "eps", + "odt", + "ics", + "xml", + "css", + "html", + "htm", + "shtml", # Images - "gif", "jpg", "jpeg", "png", "tif", "tiff", "bmp", "cgm", + "gif", + "jpg", + "jpeg", + "png", + "tif", + "tiff", + "bmp", + "cgm", # Archives - "zip", "tar", "ez", "pkpass", + "zip", + "tar", + "ez", + "pkpass", # Audio - "mp3", "m4a", "m4v", "wma", "ogg", "flac", "wav", "aif", "aifc", "aiff", + "mp3", + "m4a", + "m4v", + "wma", + "ogg", + "flac", + "wav", + "aif", + "aifc", + "aiff", # Video - "mp4", "mov", "avi", "mkv", "mpeg", "mpg", "wmv" + "mp4", + "mov", + "avi", + "mkv", + "mpeg", + "mpg", + "wmv", ) @@ -136,6 +181,11 @@ class NotifyBrevo(NotifyBase): template_args = dict( NotifyBase.template_args, **{ + "reply": { + "name": _("Reply To Email"), + "type": "string", + "map_to": "reply_to", + }, "to": { "alias_of": "targets", }, @@ -147,11 +197,6 @@ class NotifyBrevo(NotifyBase): "name": _("Blind Carbon Copy"), "type": "list:string", }, - "reply": { - "name": _("Reply To Email"), - "type": "string", - "map_to": "reply_to", - }, }, ) @@ -192,14 +237,15 @@ class NotifyBrevo(NotifyBase): result = is_email(reply_to) if not result: msg = "An invalid Brevo Reply To ({}) was specified.".format( - f"{reply_to}") + f"{reply_to}" + ) self.logger.warning(msg) raise TypeError(msg) self.reply_to = ( - result["name"] if result["name"] else False, - result["full_email"], - ) + result["name"] if result["name"] else False, + result["full_email"], + ) # Acquire Targets (To Emails) self.targets = [] @@ -213,7 +259,6 @@ class NotifyBrevo(NotifyBase): # Validate recipients (to:) and drop bad ones: if targets: for recipient in parse_list(targets): - result = is_email(recipient) if result: self.targets.append(result["full_email"]) @@ -228,7 +273,6 @@ class NotifyBrevo(NotifyBase): # Validate recipients (cc:) and drop bad ones: for recipient in parse_list(cc): - result = is_email(recipient) if result: self.cc.add(result["full_email"]) @@ -240,7 +284,6 @@ class NotifyBrevo(NotifyBase): # Validate recipients (bcc:) and drop bad ones: for recipient in parse_list(bcc): - result = is_email(recipient) if result: self.bcc.add(result["full_email"]) @@ -322,7 +365,8 @@ class NotifyBrevo(NotifyBase): if not self.targets: # There is no one to email; we're done self.logger.warning( - "There are no Brevo email recipients to notify") + "There are no Brevo email recipients to notify" + ) return False headers = { @@ -384,21 +428,29 @@ class NotifyBrevo(NotifyBase): # Use the attachment name if available, otherwise default to a # generic name - raw_name = attachment.name \ - if attachment.name else f"file{no:03}.txt" + raw_name = ( + attachment.name if attachment.name else f"file{no:03}.txt" + ) # If the filename does NOT match a supported extension, append # .txt _, ext = splitext(raw_name) - safe_name = f"{raw_name}.txt" if ( - not ext or ext[1:].lower() - not in BREVO_VALID_EXTENSIONS) else raw_name + safe_name = ( + f"{raw_name}.txt" + if ( + not ext + or ext[1:].lower() not in BREVO_VALID_EXTENSIONS + ) + else raw_name + ) try: - attachments.append({ - "content": attachment.base64(), - "name": safe_name, - }) + attachments.append( + { + "content": attachment.base64(), + "name": safe_name, + } + ) except exception.AppriseException: # We could not access the attachment @@ -414,9 +466,11 @@ class NotifyBrevo(NotifyBase): ) # Append our attachments to the payload - payload_.update({ - "attachment": attachments, - }) + payload_.update( + { + "attachment": attachments, + } + ) if self.reply_to: payload_["replyTo"] = {"email": self.reply_to[1]} @@ -456,7 +510,8 @@ class NotifyBrevo(NotifyBase): f"(cert_verify={self.verify_certificate!r})" ) self.logger.debug( - "Brevo Payload: %s", sanitize_payload(payload)) + "Brevo Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -467,6 +522,7 @@ class NotifyBrevo(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -489,16 +545,15 @@ class NotifyBrevo(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True continue else: - self.logger.info( - f"Sent Brevo notification to {target}." - ) + self.logger.info(f"Sent Brevo notification to {target}.") except requests.RequestException as e: self.logger.warning( @@ -554,9 +609,7 @@ class NotifyBrevo(NotifyBase): # The 'to' makes it easier to use yaml configuration if "to" in results["qsd"] and len(results["qsd"]["to"]): - results["targets"] += NotifyBrevo.parse_list( - results["qsd"]["to"] - ) + results["targets"] += NotifyBrevo.parse_list(results["qsd"]["to"]) # Handle Carbon Copy Addresses if "cc" in results["qsd"] and len(results["qsd"]["cc"]): diff --git a/libs/apprise/plugins/bulksms.py b/libs/apprise/plugins/bulksms.py index 372c8fce6..296c86104 100644 --- a/libs/apprise/plugins/bulksms.py +++ b/libs/apprise/plugins/bulksms.py @@ -145,9 +145,6 @@ class NotifyBulkSMS(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "name": _("From Phone No"), "type": "string", @@ -166,6 +163,9 @@ class NotifyBulkSMS(NotifyBase): "type": "bool", "default": True, }, + "to": { + "alias_of": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -291,9 +291,11 @@ class NotifyBulkSMS(NotifyBase): } if self.source: - payload.update({ - "from": self.source, - }) + payload.update( + { + "from": self.source, + } + ) # Authentication auth = (self.user, self.password) @@ -343,6 +345,7 @@ class NotifyBulkSMS(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # The responsne might look like: @@ -388,7 +391,8 @@ class NotifyBulkSMS(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/bulkvs.py b/libs/apprise/plugins/bulkvs.py index 244f7164d..23b929262 100644 --- a/libs/apprise/plugins/bulkvs.py +++ b/libs/apprise/plugins/bulkvs.py @@ -118,15 +118,15 @@ class NotifyBulkVS(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "name": _("From Phone No"), "type": "string", "regex": (r"^\+?[0-9\s)(+-]+$", "i"), "map_to": "source", }, + "to": { + "alias_of": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -257,6 +257,7 @@ class NotifyBulkVS(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # A Response may look like: @@ -295,7 +296,8 @@ class NotifyBulkVS(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/burstsms.py b/libs/apprise/plugins/burstsms.py index 154366ffb..518d30d54 100644 --- a/libs/apprise/plugins/burstsms.py +++ b/libs/apprise/plugins/burstsms.py @@ -139,9 +139,6 @@ class NotifyBurstSMS(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "sender_id", }, @@ -162,6 +159,9 @@ class NotifyBurstSMS(NotifyBase): # If set to Zero (0); this is the default and sets the max validity # period "validity": {"name": _("validity"), "type": "int", "default": 0}, + "to": { + "alias_of": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -295,7 +295,6 @@ class NotifyBurstSMS(NotifyBase): targets = list(self.targets) for index in range(0, len(targets), batch_size): - # Prepare our user payload["to"] = ",".join(self.targets[index : index + batch_size]) @@ -317,6 +316,7 @@ class NotifyBurstSMS(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -336,7 +336,8 @@ class NotifyBurstSMS(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/chanify.py b/libs/apprise/plugins/chanify.py index 70d3c62b6..a14e87ca2 100644 --- a/libs/apprise/plugins/chanify.py +++ b/libs/apprise/plugins/chanify.py @@ -132,6 +132,7 @@ class NotifyChanify(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -147,7 +148,8 @@ class NotifyChanify(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/chime.py b/libs/apprise/plugins/chime.py new file mode 100644 index 000000000..491af4c97 --- /dev/null +++ b/libs/apprise/plugins/chime.py @@ -0,0 +1,342 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Amazon Chime Incoming Webhooks +# +# To create an incoming webhook for an Amazon Chime chat room: +# 1. Open Amazon Chime and navigate to the chat room you want to +# send notifications to. +# 2. Choose the gear icon in the top-right corner of the chat room. +# 3. Choose "Manage webhooks and bots". +# 4. Choose "Add webhook", give it a name, then choose "Create". +# 5. To copy the webhook URL, choose "Copy URL" next to your new +# webhook in the list. +# +# The webhook URL will look something like: +# https://hooks.chime.aws/incomingwebhooks/ +# xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?token=AaBbCcDd%3D%3D +# +# Where the path segment after /incomingwebhooks/ is the Webhook ID +# and the ?token= query parameter is the authentication token. +# +# Your Apprise URL would be assembled as: +# chime://{WebhookID}/{Token} +# +# For example: +# chime://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/AaBbCcDd%3D%3D +# +# The token value may include base64 padding characters (=). Apprise +# handles encoding automatically; you can paste the token exactly as +# it appears in the Chime webhook URL (already URL-decoded form) or +# as-is from the ?token= query parameter. +# +# References: +# - https://docs.aws.amazon.com/chime/latest/ug/webhooks.html +# - https://docs.aws.amazon.com/chime/latest/APIReference/\ +# API_CreateIncomingWebhook.html + +from json import dumps +import re + +import requests + +from ..common import NotifyFormat, NotifyType +from ..locale import gettext_lazy as _ +from ..utils.parse import validate_regex +from .base import NotifyBase + +# Extend HTTP Error Messages +CHIME_HTTP_ERROR_MAP = { + 401: "Unauthorized - Invalid Webhook Token.", + 403: "Forbidden - Webhook has been disabled or revoked.", + 404: "Not Found - Invalid Webhook ID.", + 429: "Too many requests; rate-limit exceeded.", +} + +# Amazon Chime Incoming Webhook base URL +CHIME_WEBHOOK_URL = ( + "https://hooks.chime.aws/incomingwebhooks/{webhook_id}?token={token}" +) + +# Validates a Chime webhook ID (UUID-like hex string with hyphens, +# but we keep this flexible to handle any future format changes) +IS_WEBHOOK_ID = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", re.I) + + +class NotifyChime(NotifyBase): + """A wrapper for Amazon Chime Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "Amazon Chime" + + # The services URL + service_url = "https://aws.amazon.com/chime/" + + # The default secure protocol + secure_protocol = "chime" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/chime/" + + # Amazon Chime Webhook URL (used in send()) + notify_url = CHIME_WEBHOOK_URL + + # Amazon Chime webhooks support Markdown in the Content field + notify_format = NotifyFormat.MARKDOWN + + # Chime webhooks have no native title field; title_maxlen = 0 + # causes the framework to prepend the title to the body automatically + title_maxlen = 0 + + # Maximum allowed characters in the Content field + body_maxlen = 4096 + + # Chime incoming webhooks do not support file attachments; + # attachments can only be sent through the Chime SDK/API + attachment_support = False + + # Define object URL templates + templates = ("{schema}://{webhook_id}/{token}",) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "webhook_id": { + "name": _("Webhook ID"), + "type": "string", + "private": True, + "required": True, + }, + "token": { + "name": _("Webhook Token"), + "type": "string", + "private": True, + "required": True, + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "webhook_id": { + "alias_of": "webhook_id", + }, + "token": { + "alias_of": "token", + }, + }, + ) + + def __init__(self, webhook_id, token, **kwargs): + """Initialize Amazon Chime Object.""" + super().__init__(**kwargs) + + # Validate the Webhook ID (any non-empty, non-whitespace string) + self.webhook_id = validate_regex(webhook_id) + if not self.webhook_id: + msg = ( + "An invalid Amazon Chime Webhook ID " + "({}) was specified.".format(webhook_id) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Validate the Webhook Token + self.token = validate_regex(token) + if not self.token: + msg = ( + "An invalid Amazon Chime Webhook Token " + "({}) was specified.".format(token) + ) + self.logger.warning(msg) + raise TypeError(msg) + + return + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform Amazon Chime Notification.""" + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json; charset=utf-8", + } + + # Prepare our payload; Chime webhooks accept a single Content field + payload = { + "Content": body, + } + + # Construct the full webhook URL; the token must be URL-encoded + # because it may contain base64 padding characters (=, +, /) + notify_url = self.notify_url.format( + webhook_id=self.webhook_id, + token=NotifyChime.quote(self.token, safe=""), + ) + + self.logger.debug( + "Amazon Chime POST URL: %s (cert_verify=%s)", + notify_url, + self.verify_certificate, + ) + self.logger.debug("Amazon Chime Payload: %s", str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = NotifyChime.http_response_code_lookup( + r.status_code, CHIME_HTTP_ERROR_MAP + ) + + self.logger.warning( + "Failed to send Amazon Chime notification: " + "{}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + + self.logger.debug( + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) + + # Return; we're done + return False + + else: + self.logger.info("Sent Amazon Chime notification.") + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred posting to Amazon Chime." + ) + self.logger.debug("Socket Exception: %s", str(e)) + return False + + return True + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another simliar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol, + self.webhook_id, + self.token, + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # Acquire any global URL parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + return "{schema}://{webhook_id}/{token}/?{params}".format( + schema=self.secure_protocol, + webhook_id=self.pprint(self.webhook_id, privacy, safe=""), + token=self.pprint(self.token, privacy, safe=""), + params=NotifyChime.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow + us to re-instantiate this object.""" + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Webhook ID is in the host position + results["webhook_id"] = NotifyChime.unquote(results["host"]) + + # The token is the first entry in the URL path + tokens = NotifyChime.split_path(results["fullpath"]) + results["token"] = tokens.pop(0) if tokens else None + + # Allow ?token= to override the path-supplied token + if "token" in results["qsd"] and results["qsd"]["token"]: + results["token"] = NotifyChime.unquote(results["qsd"]["token"]) + + # Allow ?webhook_id= to override the host-supplied webhook ID + if "webhook_id" in results["qsd"] and results["qsd"]["webhook_id"]: + results["webhook_id"] = NotifyChime.unquote( + results["qsd"]["webhook_id"] + ) + + return results + + @staticmethod + def parse_native_url(url): + """Support native Amazon Chime webhook URLs. + + For example: + https://hooks.chime.aws/incomingwebhooks/ + xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?token=AaBbCcDd%3D%3D + """ + result = re.match( + r"^https?://hooks\.chime\.aws/incomingwebhooks/" + r"(?P[a-z0-9][a-z0-9-]*[a-z0-9])/?" + r"(\?.*token=(?P[^&#]+).*)?$", + url, + re.I, + ) + if result: + # Unquote the token from the native URL's query string so + # parse_url() receives the decoded form and can store it + token = result.group("token") or "" + return NotifyChime.parse_url( + "{schema}://{webhook_id}/{token}".format( + schema=NotifyChime.secure_protocol, + webhook_id=result.group("webhook_id"), + # Re-encode so parse_url / split_path decode it once + token=NotifyChime.quote( + NotifyChime.unquote(token), safe="" + ), + ) + ) + + return None diff --git a/libs/apprise/plugins/clickatell.py b/libs/apprise/plugins/clickatell.py index 8233854a0..b189605dc 100644 --- a/libs/apprise/plugins/clickatell.py +++ b/libs/apprise/plugins/clickatell.py @@ -170,10 +170,12 @@ class NotifyClickatell(NotifyBase): schema=self.secure_protocol, source=f"{self.source}@" if self.source else "", apikey=self.pprint(self.apikey, privacy, safe="="), - targets="/".join([ - NotifyClickatell.quote(t, safe="") - for t in chain(self.targets, self._invalid_targets) - ]), + targets="/".join( + [ + NotifyClickatell.quote(t, safe="") + for t in chain(self.targets, self._invalid_targets) + ] + ), params=self.urlencode(params), ) @@ -228,6 +230,7 @@ class NotifyClickatell(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if ( @@ -247,7 +250,8 @@ class NotifyClickatell(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/clicksend.py b/libs/apprise/plugins/clicksend.py index cada8e690..32ecc3701 100644 --- a/libs/apprise/plugins/clicksend.py +++ b/libs/apprise/plugins/clicksend.py @@ -120,12 +120,12 @@ class NotifyClickSend(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "key": { "alias_of": "apikey", }, + "to": { + "alias_of": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -209,6 +209,7 @@ class NotifyClickSend(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -232,7 +233,8 @@ class NotifyClickSend(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/custom_form.py b/libs/apprise/plugins/custom_form.py index 5cae8c8f2..01845c4da 100644 --- a/libs/apprise/plugins/custom_form.py +++ b/libs/apprise/plugins/custom_form.py @@ -45,7 +45,16 @@ class FORMPayloadField: # Defines the method to send the notification -METHODS = ("POST", "GET", "DELETE", "PUT", "HEAD", "PATCH") +METHODS = ( + "POST", + "GET", + "DELETE", + "PUT", + "HEAD", + "PATCH", + "UPDATE", + "OPTIONS", +) class NotifyForm(NotifyBase): @@ -306,24 +315,26 @@ class NotifyForm(NotifyBase): return False try: - files.append(( - ( - self.attach_as.format(no) - if self.attach_multi_support - else self.attach_as - ), + files.append( ( ( - attachment.name - if attachment.name - else f"file{no:03}.dat" + self.attach_as.format(no) + if self.attach_multi_support + else self.attach_as ), - # file handle is safely closed in `finally`; inline - # open is intentional - open(attachment.path, "rb"), # noqa: SIM115 - attachment.mimetype, - ), - )) + ( + ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + # file handle safely closed in + # `finally`; inline open intentional + open(attachment.path, "rb"), # noqa: SIM115 + attachment.mimetype, + ), + ) + ) except OSError as e: self.logger.warning( @@ -349,7 +360,6 @@ class NotifyForm(NotifyBase): (FORMPayloadField.MESSAGE, body), (FORMPayloadField.MESSAGETYPE, notify_type.value), ): - if not self.payload_map[key]: # Do not store element in payload response continue @@ -380,27 +390,14 @@ class NotifyForm(NotifyBase): # Always call throttle before any remote server i/o is made self.throttle() + # For GET the payload becomes URL query parameters; for all other + # methods it is sent as the request body. if self.method == "GET": - method = requests.get payload.update(self.params) - elif self.method == "PUT": - method = requests.put - - elif self.method == "PATCH": - method = requests.patch - - elif self.method == "DELETE": - method = requests.delete - - elif self.method == "HEAD": - method = requests.head - - else: # POST - method = requests.post - try: - r = method( + r = requests.request( + self.method, url, files=files if files else None, data=payload if self.method != "GET" else None, @@ -409,6 +406,7 @@ class NotifyForm(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code < 200 or r.status_code >= 300: # We had a problem @@ -425,7 +423,8 @@ class NotifyForm(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/custom_json.py b/libs/apprise/plugins/custom_json.py index 3a7c8dc16..957a3c647 100644 --- a/libs/apprise/plugins/custom_json.py +++ b/libs/apprise/plugins/custom_json.py @@ -49,7 +49,16 @@ class JSONPayloadField: # Defines the method to send the notification -METHODS = ("POST", "GET", "DELETE", "PUT", "HEAD", "PATCH") +METHODS = ( + "POST", + "GET", + "DELETE", + "PUT", + "HEAD", + "PATCH", + "UPDATE", + "OPTIONS", +) class NotifyJSON(NotifyBase): @@ -226,15 +235,17 @@ class NotifyJSON(NotifyBase): return False try: - attachments.append({ - "filename": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - "base64": attachment.base64(), - "mimetype": attachment.mimetype, - }) + attachments.append( + { + "filename": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + "base64": attachment.base64(), + "mimetype": attachment.mimetype, + } + ) except exception.AppriseException: # We could not access the attachment @@ -259,7 +270,6 @@ class NotifyJSON(NotifyBase): } for key, value in self.payload_extras.items(): - if key in payload: if not value: # Do not store element in payload response @@ -301,26 +311,9 @@ class NotifyJSON(NotifyBase): # Always call throttle before any remote server i/o is made self.throttle() - if self.method == "GET": - method = requests.get - - elif self.method == "PUT": - method = requests.put - - elif self.method == "PATCH": - method = requests.patch - - elif self.method == "DELETE": - method = requests.delete - - elif self.method == "HEAD": - method = requests.head - - else: # POST - method = requests.post - try: - r = method( + r = requests.request( + self.method, url, data=dumps(payload), params=self.params, @@ -328,6 +321,7 @@ class NotifyJSON(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code < 200 or r.status_code >= 300: # We had a problem @@ -344,7 +338,8 @@ class NotifyJSON(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/custom_xml.py b/libs/apprise/plugins/custom_xml.py index 9cbcbcc36..41f2d8c23 100644 --- a/libs/apprise/plugins/custom_xml.py +++ b/libs/apprise/plugins/custom_xml.py @@ -48,7 +48,16 @@ class XMLPayloadField: # Defines the method to send the notification -METHODS = ("POST", "GET", "DELETE", "PUT", "HEAD", "PATCH") +METHODS = ( + "POST", + "GET", + "DELETE", + "PUT", + "HEAD", + "PATCH", + "UPDATE", + "OPTIONS", +) class NotifyXML(NotifyBase): @@ -283,17 +292,18 @@ class NotifyXML(NotifyBase): NotifyXML.escape_html(notify_type.value, whitespace=False), ), ): - if not self.payload_map[key]: # Do not store element in payload response continue payload_base[self.payload_map[key]] = value # Apply our payload extras - payload_base.update({ - k: NotifyXML.escape_html(v, whitespace=False) - for k, v in self.payload_extras.items() - }) + payload_base.update( + { + k: NotifyXML.escape_html(v, whitespace=False) + for k, v in self.payload_extras.items() + } + ) # Base Entres xml_base = "".join( @@ -388,38 +398,21 @@ class NotifyXML(NotifyBase): f"XML POST URL: {url} " f"(cert_verify={self.verify_certificate!r})" ) - self.logger.debug( - "XML Payload: %s", sanitize_payload(payload)) + self.logger.debug("XML Payload: %s", sanitize_payload(payload)) # Always call throttle before any remote server i/o is made self.throttle() - if self.method == "GET": - method = requests.get - - elif self.method == "PUT": - method = requests.put - - elif self.method == "PATCH": - method = requests.patch - - elif self.method == "DELETE": - method = requests.delete - - elif self.method == "HEAD": - method = requests.head - - else: # POST - method = requests.post - try: - r = method( + r = requests.request( + self.method, url, data=payload, headers=headers, auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code < 200 or r.status_code >= 300: # We had a problem @@ -434,7 +427,8 @@ class NotifyXML(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/d7networks.py b/libs/apprise/plugins/d7networks.py index 3d4b6a4d5..3361b5bb6 100644 --- a/libs/apprise/plugins/d7networks.py +++ b/libs/apprise/plugins/d7networks.py @@ -118,20 +118,6 @@ class NotifyD7Networks(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "unicode": { - # Unicode characters (default is 'auto') - "name": _("Unicode Characters"), - "type": "bool", - "default": False, - }, - "batch": { - "name": _("Batch Mode"), - "type": "bool", - "default": False, - }, - "to": { - "alias_of": "targets", - }, "source": { # Originating address,In cases where the rewriting of the # sender's address is supported or permitted by the SMS-C. @@ -145,6 +131,20 @@ class NotifyD7Networks(NotifyBase): "from": { "alias_of": "source", }, + "unicode": { + # Unicode characters (default is 'auto') + "name": _("Unicode Characters"), + "type": "bool", + "default": False, + }, + "to": { + "alias_of": "targets", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, }, ) @@ -220,15 +220,17 @@ class NotifyD7Networks(NotifyBase): "message_globals": { "channel": "sms", }, - "messages": [{ - # Populated later on - "recipients": None, - "content": body, - "data_coding": - # auto is a better substitute over 'text' as text is easier to - # detect from a post than `unicode` is. - "auto" if not self.unicode else "unicode", - }], + "messages": [ + { + # Populated later on + "recipients": None, + "content": body, + "data_coding": + # auto is a better substitute over 'text' as text + # is easier to detect from a post than `unicode`. + "auto" if not self.unicode else "unicode", + } + ], } # use the list directly @@ -239,7 +241,6 @@ class NotifyD7Networks(NotifyBase): target = None while len(targets): - if self.batch: # Prepare our payload payload["messages"][0]["recipients"] = self.targets @@ -272,6 +273,7 @@ class NotifyD7Networks(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -308,14 +310,14 @@ class NotifyD7Networks(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True continue else: - if self.batch: self.logger.info( "Sent D7 Networks batch SMS notification to " @@ -331,9 +333,10 @@ class NotifyD7Networks(NotifyBase): except requests.RequestException as e: self.logger.warning( - "A Connection error occurred sending D7 Networks:{} " - .format(", ".join(self.targets)) - + "notification." + "A Connection error occurred sending" + " D7 Networks:{} notification.".format( + ", ".join(self.targets) + ) ) self.logger.debug(f"Socket Exception: {e!s}") # Mark our failure diff --git a/libs/apprise/plugins/dapnet.py b/libs/apprise/plugins/dapnet.py index 3325bc19e..e692f23c9 100644 --- a/libs/apprise/plugins/dapnet.py +++ b/libs/apprise/plugins/dapnet.py @@ -146,11 +146,6 @@ class NotifyDapnet(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "name": _("Target Callsign"), - "type": "string", - "map_to": "targets", - }, "priority": { "name": _("Priority"), "type": "choice:int", @@ -163,6 +158,11 @@ class NotifyDapnet(NotifyBase): "default": "dl-all", "private": True, }, + "to": { + "name": _("Target Callsign"), + "type": "string", + "map_to": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -249,7 +249,6 @@ class NotifyDapnet(NotifyBase): targets = list(self.targets) for index in range(0, len(targets), batch_size): - # prepare JSON payload payload = { "text": body, @@ -273,6 +272,7 @@ class NotifyDapnet(NotifyBase): ), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.created: # We had a problem @@ -287,7 +287,8 @@ class NotifyDapnet(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/dbus.py b/libs/apprise/plugins/dbus.py index 94c11248f..c63fe29db 100644 --- a/libs/apprise/plugins/dbus.py +++ b/libs/apprise/plugins/dbus.py @@ -54,6 +54,7 @@ try: # glib/dbus try: from dbus.mainloop.glib import DBusGMainLoop + LOOP_GLIB = DBusGMainLoop() except ImportError: # pragma: no cover @@ -63,6 +64,7 @@ try: # qt try: from dbus.mainloop.qt import DBusQtMainLoop + LOOP_QT = DBusQtMainLoop(set_as_default=True) except ImportError: @@ -82,8 +84,10 @@ try: try: # The following is required for Image/Icon loading only import gi + gi.require_version("GdkPixbuf", "2.0") from gi.repository import GdkPixbuf + NOTIFY_DBUS_IMAGE_SUPPORT = True except (ImportError, ValueError, AttributeError): @@ -134,7 +138,6 @@ DBUS_URGENCY_MAP = { "h": DBusUrgency.HIGH, # Maps against string 'emergency' "e": DBusUrgency.HIGH, - # Entries to additionally support (so more like DBus's API) "0": DBusUrgency.LOW, "1": DBusUrgency.NORMAL, diff --git a/libs/apprise/plugins/dingtalk.py b/libs/apprise/plugins/dingtalk.py index 0c4bdfb74..716e0abd1 100644 --- a/libs/apprise/plugins/dingtalk.py +++ b/libs/apprise/plugins/dingtalk.py @@ -247,6 +247,7 @@ class NotifyDingTalk(NotifyBase): params=params, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -263,7 +264,8 @@ class NotifyDingTalk(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False diff --git a/libs/apprise/plugins/discord.py b/libs/apprise/plugins/discord.py index 3b05f1232..0a630e340 100644 --- a/libs/apprise/plugins/discord.py +++ b/libs/apprise/plugins/discord.py @@ -232,8 +232,8 @@ class NotifyDiscord(NotifyBase): self.webhook_id = validate_regex(webhook_id) if not self.webhook_id: msg = ( - f"An invalid Discord Webhook ID ({webhook_id}) was " - "specified.") + f"An invalid Discord Webhook ID ({webhook_id}) was specified." + ) self.logger.warning(msg) raise TypeError(msg) @@ -286,7 +286,8 @@ class NotifyDiscord(NotifyBase): except (TypeError, ValueError): msg = ( f"An invalid Discord flags setting ({flags}) was " - "specified.") + "specified." + ) self.logger.warning(msg) raise TypeError(msg) from None else: @@ -358,16 +359,18 @@ class NotifyDiscord(NotifyBase): if self.notify_format == NotifyFormat.MARKDOWN: # Use embeds for payload - payload["embeds"] = [{ - "author": { - "name": self.app_id, - "url": self.app_url, - }, - "title": title, - "description": body, - # Our color associated with our notification - "color": self.color(notify_type, int), - }] + payload["embeds"] = [ + { + "author": { + "name": self.app_id, + "url": self.app_url, + }, + "title": title, + "description": body, + # Our color associated with our notification + "color": self.color(notify_type, int), + } + ] if self.href: payload["embeds"][0]["url"] = self.href @@ -432,12 +435,14 @@ class NotifyDiscord(NotifyBase): if attach and self.attachment_support: # Update our payload; the idea is to preserve it's other detected # and assigned values for re-use here too - payload.update({ - # Text-To-Speech - "tts": False, - # Wait until the upload has posted itself before continuing - "wait": True, - }) + payload.update( + { + # Text-To-Speech + "tts": False, + # Wait until the upload has posted itself before continuing + "wait": True, + } + ) # # Remove our text/title based content for attachment use @@ -523,15 +528,15 @@ class NotifyDiscord(NotifyBase): # Our attachment path (if specified) files = None try: - # Open our attachment path if required: if attach: files = { "file": ( attach.name, # file handle is safely closed in `finally`; inline - # open is intentional - open(attach.path, "rb"), # noqa: SIM115 + # open is intentional; attach.open() dispatches to + # BytesIO for memory attachments + attach.open(), ) } else: @@ -545,6 +550,7 @@ class NotifyDiscord(NotifyBase): files=files, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Handle rate limiting (if specified) @@ -566,7 +572,6 @@ class NotifyDiscord(NotifyBase): requests.codes.ok, requests.codes.no_content, ): - # We had a problem status_str = NotifyBase.http_response_code_lookup( r.status_code @@ -576,7 +581,6 @@ class NotifyDiscord(NotifyBase): r.status_code == requests.codes.too_many_requests and rate_limit > 0 ): - # handle rate limiting self.logger.warning( "Discord rate limiting in effect; " @@ -604,7 +608,8 @@ class NotifyDiscord(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -682,7 +687,8 @@ class NotifyDiscord(NotifyBase): bname=botname, webhook_id=self.pprint(self.webhook_id, privacy, safe=""), webhook_token=self.pprint( - self.webhook_token, privacy, safe=""), + self.webhook_token, privacy, safe="" + ), params=NotifyDiscord.urlencode(params), ) ) @@ -867,7 +873,8 @@ class NotifyDiscord(NotifyBase): @staticmethod def extract_markdown_sections( - markdown: str) -> tuple[str, list[dict[str, str]]]: + markdown: str, + ) -> tuple[str, list[dict[str, str]]]: """Extract headers and their corresponding sections into embed fields.""" @@ -896,12 +903,18 @@ class NotifyDiscord(NotifyBase): for el in common: d = el.groupdict() - fields.append({ - "name": d.get("name", "").strip("#`* \r\n\t\v"), - "value": "```{}\n{}```".format( - "md" if d.get("value") else "", - (d.get("value").strip() + "\n" if d.get("value") else ""), - ), - }) + fields.append( + { + "name": d.get("name", "").strip("#`* \r\n\t\v"), + "value": "```{}\n{}```".format( + "md" if d.get("value") else "", + ( + d.get("value").strip() + "\n" + if d.get("value") + else "" + ), + ), + } + ) return description, fields diff --git a/libs/apprise/plugins/dot.py b/libs/apprise/plugins/dot.py index 5b386059f..d84980141 100644 --- a/libs/apprise/plugins/dot.py +++ b/libs/apprise/plugins/dot.py @@ -26,27 +26,45 @@ # POSSIBILITY OF SUCH DAMAGE. # -# API: https://dot.mindreset.tech/docs/service/studio/api/text_api -# https://dot.mindreset.tech/docs/service/studio/api/image_api +# API: https://dot.mindreset.tech/docs/service/open/text_api +# https://dot.mindreset.tech/docs/service/open/image_api +# +# New API endpoints (v2): +# - Text API: /api/authV2/open/device/:deviceId/text +# - Image API: /api/authV2/open/device/:deviceId/image +# +# Note: Old endpoints (/api/open/text and /api/open/image) are deprecated +# and will be removed in the future. Requests are automatically forwarded +# to the new endpoints. # # Text API Fields: # - refreshNow (bool, optional, default true): controls display timing. -# - deviceId (string, required): unique device serial. # - title (string, optional): title text shown on screen. # - message (string, optional): body text shown on screen. # - signature (string, optional): footer/signature text. # - icon (string, optional): base64 PNG icon (40px x 40px). # - link (string, optional): tap-to-interact target URL. +# - taskKey (string, optional): specify which text API content to update. # # Image API Fields: # - refreshNow (bool, optional, default true): controls display timing. -# - deviceId (string, required): unique device serial. # - image (string, required): base64 PNG image (296px x 152px). # - link (string, optional): tap-to-interact target URL. # - border (number, optional, default 0): 0=white, 1=black frame. # - ditherType (string, optional, default DIFFUSION): dithering mode. # - ditherKernel (string, optional, default FLOYD_STEINBERG): # dithering kernel. +# - taskKey (string, optional): specify which image API content to update. +# +# Mode selection: +# - text (default): smart dual-send mode. Body/title go to the Text API; +# image= param or an attachment goes to the Image API. When both are +# present, text is dispatched first, then image. If only one is +# available, only that API is called. +# - image: only the Image API is called; body and title are ignored. +# +# Providing image= in the URL without an explicit mode= leaves the mode +# as the default (text), which will send the image alongside any text. from contextlib import suppress import json @@ -82,13 +100,15 @@ DOT_DITHER_KERNELS = ( "DIFFUSION_2D", ) +# Supported API modes; first entry is the default +DOT_MODES = ("text", "image") + class NotifyDot(NotifyBase): """A wrapper for Dot. Notifications.""" # The default descriptive name associated with the Notification service_name = "Dot." - # Alias: devices marketed as "Quote/0" remain discoverable. # The services URL service_url = "https://dot.mindreset.tech" @@ -105,15 +125,10 @@ class NotifyDot(NotifyBase): # Support Attachments attachment_support = True - # Supported API modes - SUPPORTED_MODES = ("text", "image") - - DEFAULT_MODE = "text" - # Define object templates - templates = ("{schema}://{apikey}@{device_id}/{mode}/",) + templates = ("{schema}://{apikey}@{device_id}/",) - # Define our template arguments + # Define our template tokens template_tokens = dict( NotifyBase.template_tokens, **{ @@ -129,13 +144,6 @@ class NotifyDot(NotifyBase): "required": True, "map_to": "device_id", }, - "mode": { - "name": _("API Mode"), - "type": "choice:string", - "values": SUPPORTED_MODES, - "default": DEFAULT_MODE, - "map_to": "mode", - }, }, ) @@ -143,6 +151,12 @@ class NotifyDot(NotifyBase): template_args = dict( NotifyBase.template_args, **{ + "mode": { + "name": _("API Mode"), + "type": "choice:string", + "values": DOT_MODES, + "default": DOT_MODES[0], + }, "refresh": { "name": _("Refresh Now"), "type": "bool", @@ -185,22 +199,19 @@ class NotifyDot(NotifyBase): "values": DOT_DITHER_KERNELS, "default": "FLOYD_STEINBERG", }, + "task_key": { + "name": _("Task Key"), + "type": "string", + }, }, ) - # Note: - # - icon (Text API): base64 PNG icon (40px x 40px) in lower-left corner. - # Can be provided via `icon` parameter or first attachment. - # - image (Image API): base64 PNG image (296px x 152px) supplied via - # configuration `image` parameter or first attachment. - # - Only the first attachment is used; multiple attachments trigger a - # warning. def __init__( self, apikey=None, device_id=None, - mode=DEFAULT_MODE, - refresh_now=True, + mode=DOT_MODES[0], + refresh_now=None, signature=None, icon=None, link=None, @@ -208,6 +219,7 @@ class NotifyDot(NotifyBase): dither_type=None, dither_kernel=None, image_data=None, + task_key=None, **kwargs, ): """Initialize Notify Dot Object.""" @@ -220,18 +232,22 @@ class NotifyDot(NotifyBase): self.device_id = device_id # Refresh Now flag: True shows content immediately (default). - self.refresh_now = parse_bool(refresh_now, default=True) + self.refresh_now = ( + parse_bool( + refresh_now, + self.template_args["refresh"]["default"], + ) + if refresh_now is not None + else self.template_args["refresh"]["default"] + ) - # API mode ("text" or "image") + # API mode self.mode = ( mode.lower() - if isinstance(mode, str) and mode.lower() in self.SUPPORTED_MODES - else self.DEFAULT_MODE + if isinstance(mode, str) and mode.lower() in DOT_MODES + else DOT_MODES[0] ) - if ( - not isinstance(mode, str) - or mode.lower() not in self.SUPPORTED_MODES - ): + if not isinstance(mode, str) or mode.lower() not in DOT_MODES: self.logger.warning( "Unsupported Dot mode (%s) specified; defaulting to '%s'.", mode, @@ -242,17 +258,12 @@ class NotifyDot(NotifyBase): self.signature = signature if isinstance(signature, str) else None # Icon for the Text API (base64 PNG 40x40, lower-left corner). - # Note: distinct from the Image API "image" field. self.icon = icon if isinstance(icon, str) else None # Image payload for the Image API (base64 PNG 296x152). - self.image_data = image_data if isinstance(image_data, str) else None - if self.mode == "text" and self.image_data: - self.logger.warning( - "Image data provided in text mode; ignoring configurable" - " image payload." - ) - self.image_data = None + self.image_data = ( + image_data if image_data and isinstance(image_data, str) else None + ) # Link for tap-to-interact navigation. self.link = link if isinstance(link, str) else None @@ -266,14 +277,133 @@ class NotifyDot(NotifyBase): # Dither kernel for the Image API self.dither_kernel = dither_kernel - # Text API endpoint - self.text_api_url = "https://dot.mindreset.tech/api/open/text" + # Task Key for specifying which content to update + self.task_key = ( + task_key if task_key and isinstance(task_key, str) else None + ) - # Image API endpoint - self.image_api_url = "https://dot.mindreset.tech/api/open/image" + # Text API endpoint (v2) + self.text_api_url = ( + "https://dot.mindreset.tech/api/authV2/open/device/" + f"{self.device_id}/text" + ) + + # Image API endpoint (v2) + self.image_api_url = ( + "https://dot.mindreset.tech/api/authV2/open/device/" + f"{self.device_id}/image" + ) return + def _post(self, api_url, payload, headers): + """POST payload to api_url; return True on success, False otherwise.""" + + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug( + "Dot POST URL: %s (cert_verify=%r)", + api_url, + self.verify_certificate, + ) + self.logger.debug("Dot Payload %s", sanitize_payload(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + api_url, + data=json.dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code == requests.codes.ok: + self.logger.info( + "Sent Dot notification to %s.", self.device_id + ) + return True + + status_str = NotifyDot.http_response_code_lookup(r.status_code) + self.logger.warning( + "Failed to send Dot notification to %s: %s%serror=%d.", + self.device_id, + status_str, + ", " if status_str else "", + r.status_code, + ) + self.logger.debug( + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) + return False + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Dot notification to %s.", + self.device_id, + ) + self.logger.debug("Socket Exception: %s", str(e)) + return False + + def _send_text(self, body, title, icon_data, headers): + """Send body/title via the Text API.""" + payload = {"refreshNow": self.refresh_now} + + if title: + payload["title"] = title + if body: + payload["message"] = body + if self.signature: + payload["signature"] = self.signature + if icon_data: + payload["icon"] = icon_data + if self.link: + payload["link"] = self.link + if self.task_key is not None: + payload["taskKey"] = self.task_key + + return self._post(self.text_api_url, payload, headers) + + def _send_image(self, image_data, headers): + """Send image_data via the Image API.""" + payload = { + "image": image_data, + "refreshNow": self.refresh_now, + } + + if self.link: + payload["link"] = self.link + if self.border is not None: + payload["border"] = self.border + if self.dither_type is not None: + payload["ditherType"] = self.dither_type + if self.dither_kernel is not None: + payload["ditherKernel"] = self.dither_kernel + if self.task_key is not None: + payload["taskKey"] = self.task_key + + return self._post(self.image_api_url, payload, headers) + + def _resolve_attachment(self, attach, warn_label): + """Return base64 string from the first usable attachment, or None.""" + if not (attach and self.attachment_support): + return None + if len(attach) > 1: + self.logger.warning( + "Multiple attachments provided; only the first" + " one will be used as %s.", + warn_label, + ) + try: + attachment = attach[0] + if attachment: + return attachment.base64() + except Exception as e: + self.logger.warning("Failed to process attachment: %s", str(e)) + return None + def send( self, body, @@ -292,7 +422,6 @@ class NotifyDot(NotifyBase): self.logger.warning("No device ID was specified") return False - # Prepare our headers headers = { "Authorization": f"Bearer {self.apikey}", "Content-Type": "application/json", @@ -300,172 +429,48 @@ class NotifyDot(NotifyBase): } if self.mode == "image": + # Image-only mode: body and title are ignored. if title or body: self.logger.warning( - "Title and body are not supported in image mode " - "and will be ignored." + "Title and body are not supported in image mode" + " and will be ignored." ) - image_data = ( - self.image_data if isinstance(self.image_data, str) else None + self.image_data + if isinstance(self.image_data, str) + else self._resolve_attachment(attach, "image") ) - - # Use first attachment as image if no image_data provided - # attachment.base64() returns base64-encoded string for API - if not image_data and attach and self.attachment_support: - if len(attach) > 1: - self.logger.warning( - "Multiple attachments provided; only the first " - "one will be used as image." - ) - try: - attachment = attach[0] - if attachment: - # Convert attachment to base64-encoded string - image_data = attachment.base64() - except Exception as e: - self.logger.warning(f"Failed to process attachment: {e!s}") - if not image_data: self.logger.warning( - "Image API mode selected but no image data was provided." + "Image mode selected but no image data was provided." ) return False + return self._send_image(image_data, headers) - # Use Image API - # Image API payload: - # refreshNow: display timing control. - # deviceId: Dot device serial (required). - # image: base64 PNG 296x152 (required). - # link: optional tap target. - # border: optional frame color. - # ditherType: optional dithering mode. - # ditherKernel: optional dithering kernel. - payload = { - "refreshNow": self.refresh_now, - "deviceId": self.device_id, - "image": image_data, # Image payload shown on screen - } - - if self.link: - payload["link"] = self.link - - if self.border is not None: - payload["border"] = self.border - - if self.dither_type is not None: - payload["ditherType"] = self.dither_type - - if self.dither_kernel is not None: - payload["ditherKernel"] = self.dither_kernel - - api_url = self.image_api_url - - else: - # Use Text API - # Text API payload: - # refreshNow: display timing control. - # deviceId: Dot device serial (required). - # title: optional title on screen. - # message: optional body on screen. - # signature: optional footer text. - # icon: optional base64 PNG icon (40x40). - # link: optional tap target. - payload = { - "refreshNow": self.refresh_now, - "deviceId": self.device_id, - } - - if title: - payload["title"] = title - - if body: - payload["message"] = body - - if self.signature: - payload["signature"] = ( - self.signature - ) # Footer/signature displayed on screen - - # Use first attachment as icon if no icon provided - # attachment.base64() returns base64-encoded string for API - icon_data = self.icon - if not icon_data and attach and self.attachment_support: - if len(attach) > 1: - self.logger.warning( - "Multiple attachments provided; only the first " - "one will be used as icon." - ) - try: - attachment = attach[0] - if attachment: - # Convert attachment to base64-encoded string - icon_data = attachment.base64() - except Exception as e: - self.logger.warning(f"Failed to process attachment: {e!s}") - - if icon_data: - # Text API icon payload - payload["icon"] = icon_data - - if self.link: - payload["link"] = self.link - - api_url = self.text_api_url - - # Some Debug Logging - if self.logger.isEnabledFor(logging.DEBUG): - # Due to attachments; output can be quite heavy and io intensive - # To accommodate this, we only show our debug payload information - # if required. - self.logger.debug( - "Dot POST URL:" - f" {api_url} (cert_verify={self.verify_certificate!r})" - ) - self.logger.debug("Dot Payload %s", sanitize_payload(payload)) - - # Always call throttle before any remote server i/o is made - self.throttle() - - try: - r = requests.post( - api_url, - data=json.dumps(payload), - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - - if r.status_code == requests.codes.ok: - self.logger.info(f"Sent Dot notification to {self.device_id}.") - return True - - # We had a problem - status_str = NotifyDot.http_response_code_lookup(r.status_code) + # Text mode (default): smart dual-send. + # Body/title go to the Text API; image data or attachment goes + # to the Image API. Text is always dispatched before image. + image_data = ( + self.image_data + if isinstance(self.image_data, str) + else self._resolve_attachment(attach, "image") + ) + has_text = bool(body or title) + has_image = bool(image_data) + if not has_text and not has_image: self.logger.warning( - "Failed to send Dot notification to {}: " - "{}{}error={}.".format( - self.device_id, - status_str, - ", " if status_str else "", - r.status_code, - ) + "Nothing to send to Dot. device %s.", self.device_id ) - - self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) - return False - except requests.RequestException as e: - self.logger.warning( - "A Connection error occurred sending Dot " - f"notification to {self.device_id}." - ) - self.logger.debug(f"Socket Exception: {e!s}") + has_error = False + if has_text and not self._send_text(body, title, self.icon, headers): + has_error = True - return False + if has_image and not self._send_image(image_data, headers): + has_error = True + return not has_error @property def url_identifier(self): @@ -482,49 +487,38 @@ class NotifyDot(NotifyBase): def url(self, privacy=False, *args, **kwargs): """Returns the URL built dynamically based on specified arguments.""" - # Define any URL parameters - params = { - "refresh": "yes" if self.refresh_now else "no", - } + params = {} + params["refresh"] = "yes" if self.refresh_now else "no" - if self.mode == "text": - if self.signature: - params["signature"] = self.signature + # Include mode only when non-default + if self.mode != DOT_MODES[0]: + params["mode"] = self.mode - if self.icon: - params["icon"] = self.icon + if self.signature: + params["signature"] = self.signature + if self.icon: + params["icon"] = self.icon + if self.image_data: + params["image"] = self.image_data + if self.border is not None: + params["border"] = str(self.border) + if self.dither_type and self.dither_type != "DIFFUSION": + params["dither_type"] = self.dither_type + if self.dither_kernel and self.dither_kernel != "FLOYD_STEINBERG": + params["dither_kernel"] = self.dither_kernel + if self.link: + params["link"] = self.link + if self.task_key: + params["task_key"] = self.task_key - if self.link: - params["link"] = self.link - - else: # image mode - if self.image_data: - params["image"] = self.image_data - - if self.link: - params["link"] = self.link - - if self.border is not None: - params["border"] = str(self.border) - - if self.dither_type and self.dither_type != "DIFFUSION": - params["dither_type"] = self.dither_type - - if self.dither_kernel and self.dither_kernel != "FLOYD_STEINBERG": - params["dither_kernel"] = self.dither_kernel - - # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - mode_segment = f"/{self.mode}/" - - return "{schema}://{apikey}@{device_id}{mode}?{params}".format( + return "{schema}://{apikey}@{device_id}/?{params}".format( schema=self.secure_protocol, apikey=self.pprint( self.apikey, privacy, mode=PrivacyMode.Secret, safe="" ), device_id=NotifyDot.quote(self.device_id, safe=""), - mode=mode_segment, params=NotifyDot.urlencode(params), ) @@ -534,41 +528,47 @@ class NotifyDot(NotifyBase): @staticmethod def parse_url(url): - """Parses the URL and returns enough arguments that can allow us to re- - instantiate this object.""" + """Parses the URL and returns enough arguments that can allow us to + re-instantiate this object.""" results = NotifyBase.parse_url(url) if not results: - # We're done early as we couldn't load the results return results - # Determine API mode from path (default to text) - mode = NotifyDot.DEFAULT_MODE - path_tokens = NotifyDot.split_path(results.get("fullpath")) - if path_tokens: - candidate = path_tokens.pop(0).lower() - if candidate in NotifyDot.SUPPORTED_MODES: + # Determine mode: explicit ?mode= wins; path provides backward compat. + mode = DOT_MODES[0] + if results["qsd"].get("mode"): + candidate = results["qsd"]["mode"].lower().strip() + if candidate in DOT_MODES: mode = candidate else: NotifyDot.logger.warning( - "Unsupported Dot mode (%s) detected; defaulting to '%s'.", + "Unsupported Dot mode (%s) specified; defaulting to '%s'.", candidate, - NotifyDot.DEFAULT_MODE, + DOT_MODES[0], ) + else: + path_tokens = NotifyDot.split_path(results.get("fullpath")) + if path_tokens: + candidate = path_tokens[0].lower() + if candidate in DOT_MODES: + mode = candidate + else: + NotifyDot.logger.warning( + "Unsupported Dot mode (%s) specified;" + " defaulting to '%s'.", + candidate, + DOT_MODES[0], + ) results["mode"] = mode - remaining_path = "/".join(path_tokens) - results["fullpath"] = "/" + remaining_path if remaining_path else "/" - results["path"] = remaining_path # Extract API key from user - user = results.get("user") - if user: - results["apikey"] = NotifyDot.unquote(user) + if results.get("user"): + results["apikey"] = NotifyDot.unquote(results["user"]) # Extract device ID from hostname - host = results.get("host") - if host: - results["device_id"] = NotifyDot.unquote(host) + if results.get("host"): + results["device_id"] = NotifyDot.unquote(results["host"]) # Refresh Now refresh_value = results["qsd"].get("refresh") @@ -576,43 +576,46 @@ class NotifyDot(NotifyBase): results["refresh_now"] = parse_bool(refresh_value.strip()) # Signature - signature_value = results["qsd"].get("signature") - if signature_value: - results["signature"] = NotifyDot.unquote(signature_value.strip()) + if results["qsd"].get("signature"): + results["signature"] = NotifyDot.unquote( + results["qsd"]["signature"].strip() + ) # Icon - icon_value = results["qsd"].get("icon") - if icon_value: - results["icon"] = NotifyDot.unquote(icon_value.strip()) + if results["qsd"].get("icon"): + results["icon"] = NotifyDot.unquote(results["qsd"]["icon"].strip()) # Link - link_value = results["qsd"].get("link") - if link_value: - results["link"] = NotifyDot.unquote(link_value.strip()) + if results["qsd"].get("link"): + results["link"] = NotifyDot.unquote(results["qsd"]["link"].strip()) # Border - border_value = results["qsd"].get("border") - if border_value: + if results["qsd"].get("border"): with suppress(TypeError, ValueError): - results["border"] = int(border_value.strip()) + results["border"] = int(results["qsd"]["border"].strip()) # Dither Type - dither_type_value = results["qsd"].get("dither_type") - if dither_type_value: + if results["qsd"].get("dither_type"): results["dither_type"] = NotifyDot.unquote( - dither_type_value.strip() + results["qsd"]["dither_type"].strip() ) # Dither Kernel - dither_kernel_value = results["qsd"].get("dither_kernel") - if dither_kernel_value: + if results["qsd"].get("dither_kernel"): results["dither_kernel"] = NotifyDot.unquote( - dither_kernel_value.strip() + results["qsd"]["dither_kernel"].strip() ) # Image (Image API) - image_value = results["qsd"].get("image") - if image_value: - results["image_data"] = NotifyDot.unquote(image_value.strip()) + if results["qsd"].get("image"): + results["image_data"] = NotifyDot.unquote( + results["qsd"]["image"].strip() + ) + + # Task Key + if results["qsd"].get("task_key"): + results["task_key"] = NotifyDot.unquote( + results["qsd"]["task_key"].strip() + ) return results diff --git a/libs/apprise/plugins/email/base.py b/libs/apprise/plugins/email/base.py index 2aa00d3c5..d46b51809 100644 --- a/libs/apprise/plugins/email/base.py +++ b/libs/apprise/plugins/email/base.py @@ -41,7 +41,7 @@ from ...conversion import convert_between from ...locale import gettext_lazy as _ from ...logger import logger from ...url import PrivacyMode -from ...utils import pgp as _pgp +from ...utils import pgp as _pgp, wkd as _wkd from ...utils.parse import ( is_email, is_hostname, @@ -60,6 +60,31 @@ from .common import ( ) +class PGPMode: + """PGP security mode for outbound email.""" + + # No PGP applied (default) + NONE = "no" + + # Sign using the sender's private key; encrypt opportunistically when + # a recipient public key is available (via WKD or pgppub=) + SIGN = "sign" + + # Encrypt using the recipient's public key (fails if no key found) + ENCRYPT = "encrypt" + + +# Ordered tuple of all valid PGP modes; prefix matching uses this order +PGP_MODES = ( + PGPMode.NONE, + PGPMode.SIGN, + PGPMode.ENCRYPT, +) + +# The mode used when ?pgp= is absent or empty +PGP_MODE_DEFAULT = PGPMode.NONE + + class NotifyEmail(NotifyBase): """ A wrapper to Email Notifications @@ -94,16 +119,15 @@ class NotifyEmail(NotifyBase): # Define object templates templates = ( "{schema}://{host}", - "{schema}://{host}:{port}", "{schema}://{host}/{targets}", - "{schema}://{host}:{port}/{targets}", "{schema}://{user}@{host}", + "{schema}://{user}@{host}/{targets}", "{schema}://{user}@{host}:{port}", "{schema}://{user}@{host}/{targets}", "{schema}://{user}@{host}:{port}/{targets}", "{schema}://{user}:{password}@{host}", - "{schema}://{user}:{password}@{host}:{port}", "{schema}://{user}:{password}@{host}/{targets}", + "{schema}://{user}:{password}@{host}:{port}", "{schema}://{user}:{password}@{host}:{port}/{targets}", ) @@ -146,11 +170,6 @@ class NotifyEmail(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "name": _("To Email"), - "type": "string", - "map_to": "targets", - }, "from": { "name": _("From Email"), "type": "string", @@ -161,14 +180,6 @@ class NotifyEmail(NotifyBase): "type": "string", "map_to": "from_addr", }, - "cc": { - "name": _("Carbon Copy"), - "type": "list:string", - }, - "bcc": { - "name": _("Blind Carbon Copy"), - "type": "list:string", - }, "smtp": { "name": _("SMTP Server"), "type": "string", @@ -187,12 +198,13 @@ class NotifyEmail(NotifyBase): "map_to": "reply_to", }, "pgp": { - "name": _("PGP Encryption"), - "type": "bool", - "map_to": "use_pgp", - "default": False, + "name": _("PGP Security Mode"), + "type": "choice:string", + "values": PGP_MODES, + "default": PGP_MODE_DEFAULT, + "map_to": "pgp_mode", }, - "pgpkey": { + "pgppub": { "name": _("PGP Public Key Path"), "type": "string", "private": True, @@ -200,6 +212,38 @@ class NotifyEmail(NotifyBase): "default": "", "map_to": "pgp_key", }, + # Backward-compat alias; deprecated in favour of pgppub= + # Handled in parse_url() with a deprecation warning. + "pgpkey": { + "alias_of": "pgppub", + }, + "pgpprv": { + "name": _("PGP Private Key Path"), + "type": "string", + "private": True, + # By default persistent storage is referenced + "default": "", + "map_to": "pgp_privkey", + }, + "wkd": { + "name": _("Web Key Directory"), + "type": "bool", + "default": False, + "map_to": "use_wkd", + }, + "to": { + "name": _("To Email"), + "type": "string", + "map_to": "targets", + }, + "cc": { + "name": _("Carbon Copy"), + "type": "list:string", + }, + "bcc": { + "name": _("Blind Carbon Copy"), + "type": "list:string", + }, }, ) @@ -221,8 +265,10 @@ class NotifyEmail(NotifyBase): bcc=None, reply_to=None, headers=None, - use_pgp=None, + pgp_mode=None, pgp_key=None, + pgp_privkey=None, + use_wkd=False, **kwargs, ): """ @@ -385,10 +431,12 @@ class NotifyEmail(NotifyBase): for recipient in parse_emails(targets): result = is_email(recipient) if result: - self.targets.append(( - result["name"] if result["name"] else False, - result["full_email"], - )) + self.targets.append( + ( + result["name"] if result["name"] else False, + result["full_email"], + ) + ) continue self.logger.warning( @@ -414,22 +462,70 @@ class NotifyEmail(NotifyBase): if not self.smtp_host: self.smtp_host = self.host + # Track whether pgp_mode was explicitly provided so wkd= implication + # does not override a deliberate pgp=none choice + pgp_mode_explicit = pgp_mode is not None + + # Handle deprecated use_pgp boolean for backward compatibility; + # kept in **kwargs rather than as a named parameter so the template + # verification test does not flag it as an unmapped __init__ argument + use_pgp = kwargs.pop("use_pgp", None) + if use_pgp is not None: + self.logger.warning( + "Email use_pgp= is deprecated; use pgp_mode='encrypt' instead" + ) + # Only override pgp_mode when the caller did not also pass it + if pgp_mode is None: + pgp_mode = PGPMode.ENCRYPT if use_pgp else PGPMode.NONE + # use_pgp counts as an explicit setting + pgp_mode_explicit = True + + # Resolve PGP mode via prefix match (allows 'e', 'en', 'encrypt') + if not pgp_mode: + self.pgp_mode = PGP_MODE_DEFAULT + else: + self.pgp_mode = next( + (m for m in PGP_MODES if m.startswith(str(pgp_mode).lower())), + PGP_MODE_DEFAULT, + ) + + # WKD flag -- use parse_bool() so string values like 'no'/'false' + # are treated as disabled (bool('no') would incorrectly return True) + self.use_wkd = parse_bool(use_wkd) + + # wkd=yes implies pgp=encrypt when pgp= was not explicitly set + if self.use_wkd and not pgp_mode_explicit: + self.pgp_mode = PGPMode.ENCRYPT + + # Build a WKD controller when WKD key discovery is requested + wkd_ctrl = ( + _wkd.AppriseWKDController( + asset=self.asset, + verify_certificate=self.verify_certificate, + request_timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + if self.use_wkd + else None + ) + # Prepare our Pretty Good Privacy Object self.pgp = _pgp.ApprisePGPController( path=self.store.path, pub_keyfile=pgp_key, + prv_keyfile=pgp_privkey, email=self.from_addr[1], asset=self.asset, + wkd=wkd_ctrl, ) # We store so we can generate a URL later on self.pgp_key = pgp_key - self.use_pgp = ( - use_pgp if not None else self.template_args["pgp"]["default"] - ) + # Store the private key path so it can be round-tripped via url() + self.pgp_privkey = pgp_privkey - if self.use_pgp and not _pgp.PGP_SUPPORT: + if self.pgp_mode != PGP_MODE_DEFAULT and not _pgp.PGP_SUPPORT: self.logger.warning( "PGP Support is not available on this installation; " "ask admin to install PGPy" @@ -463,12 +559,15 @@ class NotifyEmail(NotifyBase): for i in range(len(templates.EMAIL_TEMPLATES)): # pragma: no branch self.logger.trace( "Scanning %s against %s", - from_addr, templates.EMAIL_TEMPLATES[i][0]) + from_addr, + templates.EMAIL_TEMPLATES[i][0], + ) match = templates.EMAIL_TEMPLATES[i][1].match(from_addr) if match: self.logger.info( - f"Applying {templates.EMAIL_TEMPLATES[i][0]} Defaults") + f"Applying {templates.EMAIL_TEMPLATES[i][0]} Defaults" + ) # the secure flag can not be altered if defined in the template self.secure = templates.EMAIL_TEMPLATES[i][2].get( @@ -521,7 +620,6 @@ class NotifyEmail(NotifyBase): "from_user" in templates.EMAIL_TEMPLATES[i][2] and not self.from_addr[1] ): - # Update our from address if defined self.from_addr[1] = "{}@{}".format( templates.EMAIL_TEMPLATES[i][2]["from_user"], self.host @@ -598,7 +696,8 @@ class NotifyEmail(NotifyBase): attach=attach, headers=headers, names=self.names, - pgp=self.pgp if self.use_pgp else None, + pgp=self.pgp, + pgp_mode=self.pgp_mode, tzinfo=self.tzinfo, ): try: @@ -649,13 +748,37 @@ class NotifyEmail(NotifyBase): """ # Define an URL parameters - params = { - "pgp": "yes" if self.use_pgp else "no", - } + params = {} - # Store our public key back into your URL + # Always emit pgp= when wkd=yes is set, even when the mode is the + # default 'none'. Without this, a URL like ?pgp=none&wkd=yes would + # round-trip as ?wkd=yes and the wkd=yes→pgp=encrypt implication + # would silently re-enable encryption on re-parse. + if self.pgp_mode != PGP_MODE_DEFAULT or self.use_wkd: + params["pgp"] = self.pgp_mode + + # Store our public key back into the URL when one was supplied; + # mask the value when building a privacy-safe URL because the key + # path can reveal local filesystem layout or remote key server URLs if self.pgp_key is not None: - params["pgp_key"] = NotifyEmail.quote(self.pgp_key, safe=":\\/") + params["pgppub"] = ( + "****" + if privacy + else NotifyEmail.quote(self.pgp_key, safe=":\\/") + ) + + # Store the private key path in the URL in the same way; masked + # in privacy mode because private key paths are sensitive + if self.pgp_privkey is not None: + params["pgpprv"] = ( + "****" + if privacy + else NotifyEmail.quote(self.pgp_privkey, safe=":\\/") + ) + + # Include wkd= when Web Key Directory lookup is enabled + if self.use_wkd: + params["wkd"] = "yes" # Append our headers into our parameters params.update({"+{}".format(k): v for k, v in self.headers.items()}) @@ -696,39 +819,45 @@ class NotifyEmail(NotifyBase): if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8", - ).replace(",", "%2C") - for e in self.cc - ]) + params["cc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.cc + ] + ) if self.bcc: # Handle our Blind Carbon Copy Addresses - params["bcc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8", - ).replace(",", "%2C") - for e in self.bcc - ]) + params["bcc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.bcc + ] + ) if self.reply_to: # Handle our Reply-To Addresses - params["reply"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for its escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8", - ).replace(",", "%2C") - for e in self.reply_to - ]) + params["reply"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.reply_to + ] + ) # pull email suffix from username (if present) user = None if not self.user else self.user.split("@")[0] @@ -770,17 +899,19 @@ class NotifyEmail(NotifyBase): targets=( "" if not has_targets - else "/".join([ - NotifyEmail.quote( - "{}{}".format( - "" if not e[0] else "{}:".format(e[0]), e[1] - ), - safe="", - ) - for e in self.targets - ]) + else "/".join( + [ + NotifyEmail.quote( + "{}{}".format( + "" if not e[0] else "{}:".format(e[0]), e[1] + ), + safe="", + ) + for e in self.targets + ] + ) ), - params=NotifyEmail.urlencode(params), + params=NotifyEmail.urlencode(params, safe="*"), ) @property @@ -831,7 +962,6 @@ class NotifyEmail(NotifyBase): elif not is_hostname( results["host"], ipv4=False, ipv6=False, underscore=False ): - if is_email(NotifyEmail.unquote(results["host"])): # Don't lose defined email addresses results["targets"].append(NotifyEmail.unquote(results["host"])) @@ -840,16 +970,58 @@ class NotifyEmail(NotifyBase): # value if invalid; we'll attempt to figure this out later on results["host"] = "" - # Get PGP Flag - results["use_pgp"] = parse_bool( - results["qsd"].get( - "pgp", NotifyEmail.template_args["pgp"]["default"] + # Get PGP mode -- accept the new choice strings (none, encrypt) + # with prefix matching, and fall back to legacy boolean parsing + # for backward compatibility with existing ?pgp=yes/no URLs. + pgp_raw = results["qsd"].get("pgp", "") + if pgp_raw: + pgp_mode = next( + (m for m in PGP_MODES if m.startswith(str(pgp_raw).lower())), + None, ) - ) + if pgp_mode is None: + # Not a recognised mode string; try legacy boolean + if parse_bool(pgp_raw): + logger.warning( + "Email ?pgp=%s is deprecated;" + " use ?pgp=encrypt instead", + pgp_raw, + ) + pgp_mode = PGPMode.ENCRYPT + else: + pgp_mode = PGP_MODE_DEFAULT + results["pgp_mode"] = pgp_mode - # Get PGP Public Key Override + # Get Web Key Directory flag + if "wkd" in results["qsd"] and results["qsd"]["wkd"]: + results["use_wkd"] = parse_bool(results["qsd"]["wkd"]) + + # wkd=yes implies pgp=encrypt when pgp= was not present in the URL + if results.get("use_wkd") and "pgp_mode" not in results: + results["pgp_mode"] = PGPMode.ENCRYPT + + # Get PGP Public Key Override (canonical name: pgppub=) + if "pgppub" in results["qsd"] and results["qsd"]["pgppub"]: + results["pgp_key"] = NotifyEmail.unquote(results["qsd"]["pgppub"]) + + # Backward-compat: pgpkey= is the old name for pgppub=; emit a + # deprecation warning and map it through so existing URLs still work. if "pgpkey" in results["qsd"] and results["qsd"]["pgpkey"]: - results["pgp_key"] = NotifyEmail.unquote(results["qsd"]["pgpkey"]) + logger.warning( + "Email ?pgpkey= is deprecated; use ?pgppub= instead." + " Support for pgpkey= will be removed in a future release." + ) + # Only override pgp_key when pgppub= was not also present + if "pgp_key" not in results: + results["pgp_key"] = NotifyEmail.unquote( + results["qsd"]["pgpkey"] + ) + + # Get PGP Private Key Override (canonical name: pgpprv=) + if "pgpprv" in results["qsd"] and results["qsd"]["pgpprv"]: + results["pgp_privkey"] = NotifyEmail.unquote( + results["qsd"]["pgpprv"] + ) # The From address is a must; either through the use of templates # from= entry and/or merging the user and hostname together, this @@ -948,6 +1120,8 @@ class NotifyEmail(NotifyBase): # Pretty Good Privacy Support; Pass in an # ApprisePGPController if you wish to use it pgp=None, + # PGP mode string (PGPMode.NONE / PGPMode.SIGN / PGPMode.ENCRYPT) + pgp_mode=PGP_MODE_DEFAULT, # Define our timezone; if one isn't provided, then we use # the system time instead tzinfo=None, @@ -969,15 +1143,13 @@ class NotifyEmail(NotifyBase): names: This is a dictionary of email addresses as keys and the Names to associate with them when sending the email. This is cross referenced for the cc and bcc lists - pgp: Encrypting the message using Pretty Good Privacy support - This requires that the pgp_path provided exists and - keys can be referenced here to perform the encryption - with. If a key isn't found, one will be generated. - - pgp support requires the 'PGPy' Python library to be - available. - - Pass in an ApprisePGPController() if you wish to use this + pgp: ApprisePGPController for signing and/or encrypting + email via Pretty Good Privacy. Pass None to skip. + pgp_mode: PGPMode string controlling what PGP operation to + perform. PGPMode.ENCRYPT encrypts (existing + behaviour). PGPMode.SIGN signs with the sender's + private key and opportunistically encrypts when a + recipient public key is available. """ if not to: # There is no one to email; we're done @@ -985,7 +1157,7 @@ class NotifyEmail(NotifyBase): logger.warning(msg) raise AppriseEmailException(msg) from None - elif pgp and not _pgp.PGP_SUPPORT: + elif pgp and pgp_mode != PGP_MODE_DEFAULT and not _pgp.PGP_SUPPORT: msg = "PGP Support unavailable; install PGPy library" logger.warning(msg) raise AppriseEmailException(msg) from None @@ -1120,7 +1292,96 @@ class NotifyEmail(NotifyBase): mixed.attach(app) base = mixed - if pgp: + if pgp and pgp_mode == PGPMode.SIGN: + logger.debug("Securing Email with PGP Signature") + # RFC 3156 requires the signature to be computed over the CRLF + # to guarantee all recipients can verify against it. + sig_result = pgp.sign( + re.sub(r"(?:\r\n|\n|\r(?!\n))", "\r\n", base.as_string()) + ) + + if not sig_result: + # Signing was requested but could not be performed; + # this is a hard failure for the sign mode. Include the + # plugin-specific hint here (pgp.py logs only generic + # debug detail so it stays reusable across plugins). + msg = ( + "Unable to sign email via PGP; supply a private key" + " via pgpprv= or place one in the cache directory" + ) + logger.warning(msg) + raise AppriseEmailException(msg) + + # Unpack the signature and its hash algorithm label + sig_str, micalg = sig_result + + # Build the RFC 3156 multipart/signed container + signed = MIMEMultipart( + "signed", + micalg=micalg, + protocol="application/pgp-signature", + ) + + # First part: the original message body (must not be + # modified after this point as the signature covers it) + signed.attach(base) + + # Second part: the detached PGP signature block + sig_part = MIMEBase("application", "pgp-signature") + sig_part.set_payload(sig_str) + sig_part.add_header( + "Content-Disposition", + "attachment", + filename="signature.asc", + ) + signed.attach(sig_part) + + # Replace base with the signed container so the rest of + # the function continues to work unchanged + base = signed + + # Opportunistic encryption: try to find the recipient's + # public key and encrypt the signed payload if found. + # autogen=False prevents a fake key from being auto-created + # for the recipient -- only pre-existing keys (WKD lookups + # or explicit pgppub= files) qualify for encryption. + # A missing key is NOT an error in sign mode -- we simply + # send the signed (unencrypted) email instead. + enc_content = pgp.encrypt( + base.as_string(), to_addr, autogen=False + ) + if enc_content: + logger.debug("PGP public key found; adding encryption") + + # Wrap the signed body in a multipart/encrypted + # container (the signed payload becomes the ciphertext) + enc = MIMEMultipart( + "encrypted", + protocol="application/pgp-encrypted", + ) + + # Autocrypt header for DeltaChat / compatible clients + enc.add_header( + "Autocrypt", + "addr={}; prefer-encrypt=mutual".format( + formataddr((False, to_addr), charset="utf-8") + ), + ) + + # Version identifier part (required by RFC 3156) + ver_part = MIMEText("Version: 1", "plain") + ver_part.set_type("application/pgp-encrypted") + enc.attach(ver_part) + + # Encrypted data part + data_part = MIMEBase("application", "octet-stream") + data_part.set_payload(enc_content) + enc.attach(data_part) + + # Replace base with the fully encrypted container + base = enc + + elif pgp and pgp_mode == PGPMode.ENCRYPT: logger.debug("Securing Email with PGP Encryption") # Set our header information to include in the encryption base["From"] = formataddr( @@ -1135,8 +1396,13 @@ class NotifyEmail(NotifyBase): encrypted_content = pgp.encrypt(base.as_string(), to_addr) if not encrypted_content: - # Unable to send notification - msg = "Unable to encrypt email via PGP" + # Unable to send notification. Include the plugin-specific + # hint here (pgp.py logs only generic debug detail so it + # stays reusable across plugins). + msg = ( + "Unable to encrypt email via PGP; supply a public key" + " via pgppub= or place one in the cache directory" + ) logger.warning(msg) raise AppriseEmailException(msg) @@ -1149,7 +1415,7 @@ class NotifyEmail(NotifyBase): base.add_header( "Autocrypt", f"addr={formataddr((False, to_addr), charset='utf-8')}; " - "prefer-encrypt=mutual" + "prefer-encrypt=mutual", ) # Set Encryption Info Part @@ -1184,3 +1450,10 @@ class NotifyEmail(NotifyBase): to_addrs=[to_addr, *list(cc_), *list(bcc_)], body=base.as_string(), ) + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("pgpy",) diff --git a/libs/apprise/plugins/email/templates.py b/libs/apprise/plugins/email/templates.py index 777de3f78..e500719bc 100644 --- a/libs/apprise/plugins/email/templates.py +++ b/libs/apprise/plugins/email/templates.py @@ -122,7 +122,7 @@ EMAIL_TEMPLATES = ( "port": 465, "smtp_host": "smtp.mail.yahoo.com", "secure": True, - "secure_mode": SecureMailMode.STARTTLS, + "secure_mode": SecureMailMode.SSL, "login_type": (WebBaseLogin.EMAIL,), }, ), diff --git a/libs/apprise/plugins/emby.py b/libs/apprise/plugins/emby.py index 6064d9de0..3d8c94d42 100644 --- a/libs/apprise/plugins/emby.py +++ b/libs/apprise/plugins/emby.py @@ -207,6 +207,7 @@ class NotifyEmby(NotifyBase): data=dumps(payload), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -226,7 +227,8 @@ class NotifyEmby(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -373,6 +375,7 @@ class NotifyEmby(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -455,6 +458,7 @@ class NotifyEmby(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -464,7 +468,6 @@ class NotifyEmby(NotifyBase): requests.codes.ok, requests.codes.no_content, ): - # We had a problem status_str = NotifyEmby.http_response_code_lookup( r.status_code @@ -560,6 +563,7 @@ class NotifyEmby(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, diff --git a/libs/apprise/plugins/enigma2.py b/libs/apprise/plugins/enigma2.py index 8ee1a8b0a..51489462b 100644 --- a/libs/apprise/plugins/enigma2.py +++ b/libs/apprise/plugins/enigma2.py @@ -291,6 +291,7 @@ class NotifyEnigma2(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -307,7 +308,8 @@ class NotifyEnigma2(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/evolution.py b/libs/apprise/plugins/evolution.py new file mode 100644 index 000000000..4cbaddfb6 --- /dev/null +++ b/libs/apprise/plugins/evolution.py @@ -0,0 +1,365 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Evolution API is a self-hosted WhatsApp integration layer. +# Project: https://github.com/EvolutionAPI/evolution-api +# API Docs: https://doc.evolution-api.com/ + +# Steps: +# 1. Deploy Evolution API on your server (Docker recommended). +# 2. Create an instance via the Evolution API dashboard or API. +# 3. Connect the instance by scanning the QR code via WhatsApp. +# 4. Use the API key shown in your instance settings. + +# URL syntax (HTTP): +# evolution://apikey@host/instance/5511999999999 +# evolution://apikey@host:port/instance/5511999999999 +# evolution://apikey@host/instance/number1/number2/... +# +# URL syntax (HTTPS): +# evolutions://apikey@host/instance/5511999999999 +# evolutions://apikey@host:port/instance/5511999999999 +# +# Phone numbers must be in international format without the leading '+', +# e.g. 5511999999999 for a Brazilian mobile number. + +from json import dumps + +import requests + +from ..common import NotifyFormat, NotifyType +from ..locale import gettext_lazy as _ +from ..utils.parse import ( + is_phone_no, + parse_phone_no, + validate_regex, +) +from .base import NotifyBase + + +class NotifyEvolution(NotifyBase): + """A wrapper for Evolution API (WhatsApp) Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "Evolution API" + + # The services URL + service_url = "https://github.com/EvolutionAPI/evolution-api" + + # The default protocol (plain HTTP) + protocol = "evolution" + + # The default secure protocol (HTTPS) + secure_protocol = "evolutions" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/evolution/" + + # Disable throttle rate + request_rate_per_sec = 0 + + # Evolution API / WhatsApp uses Markdown-like formatting; + # setting this causes Apprise to convert HTML bodies to Markdown + # before calling send(). + notify_format = NotifyFormat.MARKDOWN + + # Evolution API has no separate title field; Apprise will merge the + # title into the body before calling send(). + title_maxlen = 0 + + # Define object URL templates + templates = ( + "{schema}://{apikey}@{host}/{instance}/{targets}", + "{schema}://{apikey}@{host}:{port}/{instance}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "apikey": { + "name": _("API Key"), + "type": "string", + "private": True, + "required": True, + }, + "host": { + "name": _("Hostname"), + "type": "string", + "required": True, + }, + "port": { + "name": _("Port"), + "type": "int", + "min": 1, + "max": 65535, + }, + "instance": { + "name": _("Instance Name"), + "type": "string", + "required": True, + }, + "target_phone": { + "name": _("Target Phone No"), + "type": "string", + "prefix": "+", + "regex": (r"^[0-9\s)(+-]+$", "i"), + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "to": { + "alias_of": "targets", + }, + }, + ) + + def __init__(self, apikey, instance, targets=None, **kwargs): + """Initialize Evolution API Object.""" + super().__init__(**kwargs) + + # API Key + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = f"An invalid Evolution API key ({apikey}) was specified." + self.logger.warning(msg) + raise TypeError(msg) + + # Instance name + self.instance = validate_regex(instance) + if not self.instance: + msg = ( + "An invalid Evolution API instance name " + f"({instance}) was specified." + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Parse and validate recipient phone numbers + self.phone = [] + self.invalid_targets = [] + + for target in parse_phone_no(targets): + result = is_phone_no(target) + if not result: + self.logger.warning( + "Dropped invalid Evolution API phone # " + f"({target}) specified." + ) + self.invalid_targets.append(target) + continue + # Store digits only — Evolution API expects no leading '+' + self.phone.append(result["full"]) + + if not self.phone: + msg = "No valid Evolution API phone numbers were specified." + self.logger.warning(msg) + raise TypeError(msg) + + return + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform Evolution API Notification.""" + + # Build the base URL + schema = "https" if self.secure else "http" + default_port = 443 if self.secure else 80 + + base_url = "{}://{}{}".format( + schema, + self.host, + ( + "" + if not self.port or self.port == default_port + else f":{self.port}" + ), + ) + + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + "apikey": self.apikey, + } + + has_error = False + for number in self.phone: + url = f"{base_url}/message/sendText/{self.instance}" + payload = { + "number": number, + "text": body, + } + + self.logger.debug( + "Evolution API POST URL: {} (cert_verify={!r})".format( + url, self.verify_certificate + ) + ) + self.logger.debug(f"Evolution API Payload: {payload!s}") + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code not in ( + requests.codes.ok, + requests.codes.created, + ): + status_str = NotifyEvolution.http_response_code_lookup( + r.status_code + ) + self.logger.warning( + "Failed to send Evolution API notification to " + "{}: {}{}error={}.".format( + number, + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + has_error = True + continue + + else: + self.logger.info( + f"Sent Evolution API notification to {number}." + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Evolution API " + f"notification to {number}." + ) + self.logger.debug(f"Socket Exception: {e!s}") + has_error = True + continue + + return not has_error + + def __len__(self): + """Returns the number of targets associated with this notification.""" + return max(1, len(self.phone)) + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another similar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol if self.secure else self.protocol, + self.apikey, + self.host, + self.port, + self.instance, + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + default_port = 443 if self.secure else 80 + + targets = self.phone if self.phone else self.invalid_targets + + return ( + "{schema}://{apikey}@{host}{port}/{instance}/{targets}?{params}" + ).format( + schema=self.secure_protocol if self.secure else self.protocol, + apikey=( + self.pprint(self.apikey, "key", safe="") + if privacy + else NotifyEvolution.quote(self.apikey, safe="") + ), + host=self.host, + port=( + "" + if not self.port or self.port == default_port + else f":{self.port}" + ), + instance=NotifyEvolution.quote(self.instance, safe=""), + targets="/".join( + [NotifyEvolution.quote(t, safe="+") for t in targets] + ), + params=NotifyEvolution.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow us to + re-instantiate this object.""" + + results = NotifyBase.parse_url(url, verify_host=True) + if not results: + return results + + # The API key is placed in the user field of the URL + if results.get("user"): + results["apikey"] = NotifyEvolution.unquote(results["user"]) + else: + results["apikey"] = None + + # Path tokens: first is instance name, rest are phone numbers + entries = NotifyEvolution.split_path(results["fullpath"]) + + try: + results["instance"] = NotifyEvolution.unquote(entries.pop(0)) + except IndexError: + results["instance"] = None + + results["targets"] = entries + + # Also accept ?to= query param for additional targets + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyEvolution.parse_phone_no( + results["qsd"]["to"] + ) + + return results diff --git a/libs/apprise/plugins/exotel.py b/libs/apprise/plugins/exotel.py new file mode 100644 index 000000000..73025c72e --- /dev/null +++ b/libs/apprise/plugins/exotel.py @@ -0,0 +1,677 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from itertools import chain +import re + +import requests + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from ..url import PrivacyMode +from ..utils.parse import ( + is_phone_no, + parse_bool, + parse_phone_no, + validate_regex, +) +from .base import NotifyBase + + +class ExotelPriority: + """ + Priorities + """ + + NORMAL = "normal" + HIGH = "high" + + +EXOTEL_PRIORITIES = ( + ExotelPriority.NORMAL, + ExotelPriority.HIGH, +) + +EXOTEL_PRIORITY_MAP = { + # short for 'normal' + "normal": ExotelPriority.NORMAL, + # short for 'high' + "+": ExotelPriority.HIGH, + "high": ExotelPriority.HIGH, +} + + +class ExotelEncoding: + """ + The different encodings supported + """ + + TEXT = "plain" + UNICODE = "unicode" + + +class ExotelRegion: + """ + Regions + """ + + US = "us" + + # India + IN = "in" + + +# Exotel APIs +EXOTEL_API_LOOKUP = { + ExotelRegion.US: "https://api.exotel.com/v1/Accounts/{sid}/Sms/send", + ExotelRegion.IN: "https://api.in.exotel.com/v1/Accounts/{sid}/Sms/send", +} + +# A List of our regions we can use for verification +EXOTEL_REGIONS = ( + ExotelRegion.US, + ExotelRegion.IN, +) + +EXOTEL_SOURCE_RE = re.compile(r"^[A-Z0-9][A-Z0-9.-]{2,15}$", re.I) + + +def parse_exotel_source(source): + """Parse an Exotel source as an ExoPhone or approved Sender ID.""" + source = validate_regex(source) + if not source: + return None + + result = is_phone_no(source, min_len=9) + if result: + return result["full"] + + if EXOTEL_SOURCE_RE.match(source) and ( + not source.isdigit() or len(source) == 6 + ): + return source + + return None + + +class NotifyExotel(NotifyBase): + """ + A wrapper for Exotel Notifications + """ + + # Exotel supports static bulk SMS by accepting an array of To values on + # the same /Sms/send endpoint. + default_batch_size = 100 + + # The default descriptive name associated with the Notification + service_name = "Exotel" + + # The services URL + service_url = "https://exotel.com/" + + # The default protocol + secure_protocol = "exotel" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/exotel/" + + # The maximum length of the body + body_maxlen = 2000 + + # A title can not be used for SMS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Define object templates + templates = ( + "{schema}://{sid}:{token}@{from_phone}", + "{schema}://{sid}:{token}@{from_phone}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "sid": { + "name": _("Account SID"), + "type": "string", + "required": True, + "private": True, + }, + "token": { + "name": _("Token"), + "type": "string", + "private": True, + "required": True, + }, + "from_phone": { + "name": _("From Phone No / Sender ID"), + "type": "string", + "required": True, + "regex": ( + r"^(?:(?=.{3,16}$)(?=.*[A-Z.-])[A-Z0-9][A-Z0-9.-]*" + r"|[0-9]{6}|\+?[0-9\s)(+-]{9,})$", + "i", + ), + "map_to": "source", + }, + "target_phone": { + "name": _("Target Phone No"), + "type": "string", + "prefix": "+", + "regex": (r"^[0-9\s)(+-]+$", "i"), + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "to": { + "alias_of": "targets", + }, + "from": { + "alias_of": "from_phone", + }, + "sid": { + "alias_of": "sid", + }, + "token": { + "alias_of": "token", + }, + "apikey": { + "name": _("API Key"), + "type": "string", + "private": True, + "map_to": "apikey", + }, + "key": { + "alias_of": "apikey", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, + "unicode": { + # Unicode characters + "name": _("Unicode Characters"), + "type": "bool", + "default": True, + }, + "region": { + "name": _("Region Name"), + "type": "choice:string", + "values": EXOTEL_REGIONS, + "default": ExotelRegion.US, + "map_to": "region_name", + }, + "priority": { + "name": _("Priority"), + "type": "choice:string", + "values": EXOTEL_PRIORITIES, + "default": ExotelPriority.NORMAL, + }, + }, + ) + + def __init__( + self, + sid, + token, + source, + targets=None, + apikey=None, + batch=None, + unicode=None, + priority=None, + region_name=None, + **kwargs, + ): + """ + Initialize Exotel Object + """ + super().__init__(**kwargs) + + # Account SID + self.sid = validate_regex(sid) + if not self.sid: + msg = "An invalid Exotel SID ({}) was specified.".format(sid) + self.logger.warning(msg) + raise TypeError(msg) + + # API Token (associated with account) + self.token = validate_regex(token) + if not self.token: + msg = "An invalid Exotel API Token ({}) was specified.".format( + token + ) + self.logger.warning(msg) + raise TypeError(msg) + + # API Key used as the HTTP Basic Auth username. Older URLs did not + # carry this separately, so default it to the account SID. + self.apikey = validate_regex(apikey if apikey else sid) + if not self.apikey: + msg = "An invalid Exotel API Key ({}) was specified.".format( + apikey + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Used for URL generation afterwards only + self.invalid_targets = [] + + # Store our region + self.region_name = ( + self.template_args["region"]["default"] + if region_name is None + else validate_regex(region_name) + ) + if not self.region_name: + self.region_name = "" + else: + self.region_name = self.region_name.lower() + + if self.region_name not in EXOTEL_REGIONS: + # Invalid region specified + msg = "The Exotel region specified ({}) is invalid.".format( + region_name + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Define whether or not we should set the unicode flag + self.unicode = ( + self.template_args["unicode"]["default"] + if unicode is None + else bool(unicode) + ) + + # Define whether or not we should operate in batch mode + self.batch = ( + self.template_args["batch"]["default"] + if batch is None + else bool(batch) + ) + + # + # Priority + # + if priority is None: + # Default + self.priority = self.template_args["priority"]["default"] + + else: + # Input is a string; attempt to get the lookup from our + # priority mapping + self.priority = priority.lower().strip() + + # Allow partial matching against supported priorities: + # normal, norma, norm, nor, no, n (for normal) + # high, hig, hi, h (for high) + result = ( + next( + ( + key + for key in EXOTEL_PRIORITY_MAP + if key.startswith(self.priority) + ), + None, + ) + if self.priority + else None + ) + + # Now test to see if we got a match + if not result: + msg = "An invalid Exotel priority ({}) was specified.".format( + priority + ) + self.logger.warning(msg) + raise TypeError(msg) + + # store our successfully looked up priority + self.priority = EXOTEL_PRIORITY_MAP[result] + + # The Source Phone # + self.source = source + + result = parse_exotel_source(source) + if not result: + msg = ( + "The Account (From) Phone # / Sender ID specified " + "({}) is invalid.".format(source) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store our parsed value + self.source = result + + # Parse our targets + self.targets = [] + + for target in parse_phone_no(targets): + # Validate targets and drop bad ones: + result = is_phone_no(target, min_len=9) + if not result: + self.logger.warning( + "Dropped invalid phone # ({}) specified.".format(target), + ) + self.invalid_targets.append(target) + continue + + # store valid phone number + self.targets.append(result["full"]) + + if len(self.targets) == 0 and not self.invalid_targets: + # No targets specified, use our own source. + self.targets.append(self.source) + + return + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """ + Perform Exotel Notification + """ + + if not self.targets: + # There were no endpoints to notify + self.logger.warning("There were no Exotel targets to notify.") + return False + + # error tracking (used for function return) + has_error = False + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/x-www-form-urlencoded", + } + + # Our authentication + auth = (self.apikey, self.token) + + # Prepare our payload + payload = { + "From": self.source, + "Body": body, + "EncodingType": ExotelEncoding.UNICODE + if self.unicode + else ExotelEncoding.TEXT, + "Priority": self.priority, + # The to gets populated in the loop below + "To": None, + } + + # Prepare our targets + batch_size = 1 if not self.batch else self.default_batch_size + targets = ( + list(self.targets) + if batch_size == 1 + else [ + self.targets[index : index + batch_size] + for index in range(0, len(self.targets), batch_size) + ] + ) + + # Prepare our notify_url + notify_url = EXOTEL_API_LOOKUP[self.region_name].format(sid=self.sid) + + while len(targets): + # Get our target to notify + target = targets.pop(0) + + # Prepare our user(s) + payload["To"] = target + + p_target = ( + "{} targets".format(len(target)) + if isinstance(target, list) + else target + ) + + # Some Debug Logging + self.logger.debug( + "Exotel POST URL: {} (cert_verify={})".format( + notify_url, self.verify_certificate + ) + ) + self.logger.debug("Exotel Payload: {}".format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + notify_url, + auth=auth, + data=payload.copy(), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = NotifyExotel.http_response_code_lookup( + r.status_code + ) + + self.logger.warning( + "Failed to send Exotel notification to {}: " + "{}{}error={}.".format( + p_target, + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + + self.logger.debug( + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) + + # Mark our failure + has_error = True + continue + + else: + self.logger.info( + "Sent Exotel notification to %s.", p_target + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Exotel:%s " + "notification.", + p_target, + ) + self.logger.debug("Socket Exception: %s", str(e)) + + # Mark our failure + has_error = True + continue + + return not has_error + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another similar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol, + self.apikey, + self.sid, + self.token, + self.source, + self.region_name, + ) + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = { + "batch": "yes" if self.batch else "no", + "unicode": "yes" if self.unicode else "no", + "region": self.region_name, + "priority": self.priority, + } + if self.apikey != self.sid: + params["apikey"] = self.pprint(self.apikey, privacy, safe="") + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + return "{schema}://{sid}:{token}@{source}/{targets}/?{params}".format( + schema=self.secure_protocol, + sid=self.pprint( + self.sid, privacy, mode=PrivacyMode.Secret, safe="" + ), + token=self.pprint(self.token, privacy, safe=""), + source=NotifyExotel.quote(self.source, safe=""), + targets="/".join( + [ + NotifyExotel.quote(x, safe="") + for x in chain(self.targets, self.invalid_targets) + ] + ), + params=NotifyExotel.urlencode(params), + ) + + def __len__(self): + """Returns the number of targets associated with this notification.""" + batch_size = 1 if not self.batch else self.default_batch_size + targets = len(self.targets) + if not targets: + return 1 + + if batch_size > 1: + targets = int(targets / batch_size) + ( + 1 if targets % batch_size else 0 + ) + + return targets + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Get our entries; split_path() looks after unquoting content for us + # by default + results["targets"] = NotifyExotel.split_path(results["fullpath"]) + + # The hostname is our source number + results["source"] = NotifyExotel.unquote(results["host"]) + + # Get our account_sid and token from the user/pass config + results["sid"] = NotifyExotel.unquote(results["user"]) + results["token"] = NotifyExotel.unquote(results["password"]) + + # Get region + if "region" in results["qsd"] and len(results["qsd"]["region"]): + results["region_name"] = NotifyExotel.unquote( + results["qsd"]["region"] + ) + + # API Token + if "token" in results["qsd"] and len(results["qsd"]["token"]): + # Extract the Token from an argument + results["token"] = NotifyExotel.unquote(results["qsd"]["token"]) + + # API SID + if "sid" in results["qsd"] and len(results["qsd"]["sid"]): + # Extract the API SID from an argument + results["sid"] = NotifyExotel.unquote(results["qsd"]["sid"]) + + # API Key + if "apikey" in results["qsd"] and len(results["qsd"]["apikey"]): + # Extract the API Key from an argument + results["apikey"] = NotifyExotel.unquote(results["qsd"]["apikey"]) + + elif "key" in results["qsd"] and len(results["qsd"]["key"]): + # Extract the API Key from an argument + results["apikey"] = NotifyExotel.unquote(results["qsd"]["key"]) + + # Support the 'from' and 'source' variable so that we can support + # targets this way too. + # The 'from' makes it easier to use yaml configuration + if "from" in results["qsd"] and len(results["qsd"]["from"]): + results["source"] = NotifyExotel.unquote(results["qsd"]["from"]) + if "source" in results["qsd"] and len(results["qsd"]["source"]): + results["source"] = NotifyExotel.unquote(results["qsd"]["source"]) + + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if "to" in results["qsd"] and len(results["qsd"]["to"]): + results["targets"] += NotifyExotel.parse_phone_no( + results["qsd"]["to"] + ) + + # Get priority + if "priority" in results["qsd"] and len(results["qsd"]["priority"]): + results["priority"] = NotifyExotel.unquote( + results["qsd"]["priority"] + ) + + # Get Batch Mode Flag + results["batch"] = parse_bool( + results["qsd"].get( + "batch", NotifyExotel.template_args["batch"]["default"] + ) + ) + + # Unicode Characters + results["unicode"] = parse_bool( + results["qsd"].get( + "unicode", NotifyExotel.template_args["unicode"]["default"] + ) + ) + + return results diff --git a/libs/apprise/plugins/fcm/__init__.py b/libs/apprise/plugins/fcm/__init__.py index 143b35d7e..000b71a13 100644 --- a/libs/apprise/plugins/fcm/__init__.py +++ b/libs/apprise/plugins/fcm/__init__.py @@ -265,6 +265,7 @@ class NotifyFCM(NotifyBase): self.oauth = GoogleOAuth( user_agent=self.app_id, timeout=self.request_timeout, + redirects=self.redirects, verify_certificate=self.verify_certificate, ) @@ -289,7 +290,6 @@ class NotifyFCM(NotifyBase): self.keyfile[0].max_file_size = self.max_fcm_keyfile_size else: # Legacy Mode - # The apikey associated with the account self.apikey = validate_regex(apikey) if not self.apikey: @@ -490,6 +490,7 @@ class NotifyFCM(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -511,7 +512,8 @@ class NotifyFCM(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) has_error = True @@ -648,3 +650,10 @@ class NotifyFCM(NotifyBase): results["data_kwargs"] = results["qsd+"] return results + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("cryptography",) diff --git a/libs/apprise/plugins/fcm/oauth.py b/libs/apprise/plugins/fcm/oauth.py index 04b103633..ea003af7c 100644 --- a/libs/apprise/plugins/fcm/oauth.py +++ b/libs/apprise/plugins/fcm/oauth.py @@ -64,13 +64,20 @@ class GoogleOAuth: clock_skew = timedelta(seconds=10) def __init__( - self, user_agent=None, timeout=(5, 4), verify_certificate=True + self, + user_agent=None, + timeout=(5, 4), + verify_certificate=True, + redirects=True, ): """Initialize our OAuth object.""" # Wether or not to verify ssl self.verify_certificate = verify_certificate + # Whether to follow HTTP 3xx redirects + self.redirects = redirects + # Our (connect, read) timeout self.request_timeout = timeout @@ -255,10 +262,12 @@ class GoogleOAuth: segments.append(base64.urlsafe_b64encode(signature).rstrip(b"=")) assertion = b".".join(segments) - http_payload = _urlencode({ - "assertion": assertion, - "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", - }) + http_payload = _urlencode( + { + "assertion": assertion, + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + } + ) http_headers = { "Content-Type": "application/x-www-form-urlencoded", @@ -274,6 +283,7 @@ class GoogleOAuth: headers=http_headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -282,7 +292,8 @@ class GoogleOAuth: ) logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return None except requests.RequestException as e: diff --git a/libs/apprise/plugins/fcm/priority.py b/libs/apprise/plugins/fcm/priority.py index 037801bc0..b645d385d 100644 --- a/libs/apprise/plugins/fcm/priority.py +++ b/libs/apprise/plugins/fcm/priority.py @@ -132,7 +132,12 @@ class FCMPriorityManager: FCMMode.OAuth2: { "message": { "android": {"priority": NotificationPriority.HIGH}, - "apns": {"headers": {"apns-priority": "10"}}, + "apns": { + "headers": { + "apns-priority": "10", + "apns-push-type": "alert", + } + }, "webpush": {"headers": {"Urgency": "high"}}, } }, @@ -144,7 +149,12 @@ class FCMPriorityManager: FCMMode.OAuth2: { "message": { "android": {"priority": NotificationPriority.HIGH}, - "apns": {"headers": {"apns-priority": "10"}}, + "apns": { + "headers": { + "apns-priority": "10", + "apns-push-type": "alert", + } + }, "webpush": {"headers": {"Urgency": "high"}}, } }, diff --git a/libs/apprise/plugins/feishu.py b/libs/apprise/plugins/feishu.py index 8f74a2e52..71fda5a56 100644 --- a/libs/apprise/plugins/feishu.py +++ b/libs/apprise/plugins/feishu.py @@ -143,6 +143,7 @@ class NotifyFeishu(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # @@ -174,7 +175,8 @@ class NotifyFeishu(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/flock.py b/libs/apprise/plugins/flock.py index ed6973281..389dcc371 100644 --- a/libs/apprise/plugins/flock.py +++ b/libs/apprise/plugins/flock.py @@ -273,6 +273,7 @@ class NotifyFlock(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -287,7 +288,8 @@ class NotifyFlock(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/fluxer.py b/libs/apprise/plugins/fluxer.py index b868fa198..7f0169ba6 100644 --- a/libs/apprise/plugins/fluxer.py +++ b/libs/apprise/plugins/fluxer.py @@ -324,8 +324,9 @@ class NotifyFluxer(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - if self.mode == FluxerMode.PRIVATE and \ - self.__auto_cloud_host.search(self.host): + if self.mode == FluxerMode.PRIVATE and self.__auto_cloud_host.search( + self.host + ): # Is a Fluxer Cloud API self.mode = FluxerMode.CLOUD self.logger.warning( @@ -336,30 +337,50 @@ class NotifyFluxer(NotifyBase): ) # Text To Speech - self.tts = tts if isinstance(tts, bool) \ + self.tts = ( + tts + if isinstance(tts, bool) else parse_bool(tts, self.template_args["tts"]["default"]) + ) # Avatar - self.avatar = avatar if isinstance(avatar, bool) \ + self.avatar = ( + avatar + if isinstance(avatar, bool) else parse_bool(avatar, self.template_args["avatar"]["default"]) + ) # Footer - self.footer = footer if isinstance(footer, bool) \ + self.footer = ( + footer + if isinstance(footer, bool) else parse_bool(footer, self.template_args["footer"]["default"]) + ) # Footer Logo - self.footer_logo = footer_logo if isinstance(footer_logo, bool) \ + self.footer_logo = ( + footer_logo + if isinstance(footer_logo, bool) else parse_bool( - footer_logo, self.template_args["footer_logo"]["default"]) + footer_logo, self.template_args["footer_logo"]["default"] + ) + ) # Include Image - self.include_image = include_image if isinstance(include_image, bool) \ + self.include_image = ( + include_image + if isinstance(include_image, bool) else parse_bool( - include_image, self.template_args["image"]["default"]) + include_image, self.template_args["image"]["default"] + ) + ) # Fields - self.fields = fields if isinstance(fields, bool) \ + self.fields = ( + fields + if isinstance(fields, bool) else parse_bool(fields, self.template_args["fields"]["default"]) + ) self.thread_id = thread self.thread_name = thread_name @@ -395,7 +416,6 @@ class NotifyFluxer(NotifyBase): attach=None, **kwargs: Any, ) -> bool: - """Perform Fluxer Notification.""" # Prepare our headers: @@ -508,15 +528,16 @@ class NotifyFluxer(NotifyBase): return False if attach and self.attachment_support: - # Update our payload; the idea is to preserve it's other detected # and assigned values for re-use here too - payload.update({ - # Text-To-Speech can be off so we don't read the filename - "tts": False, - # no tts; no need to wait - "wait": False, - }) + payload.update( + { + # Text-To-Speech can be off so we don't read the filename + "tts": False, + # no tts; no need to wait + "wait": False, + } + ) # # Remove our text/title based content for attachment use @@ -617,25 +638,29 @@ class NotifyFluxer(NotifyBase): files = None data: dict[str, Any] | str try: - # Open our attachment path if required: if attach: # # Fluxer requires content to be provided # - payload.update({ - "content": attach.name, - "attachments": [{ - "id": 0, - "filename": attach.name, - }], - }) + payload.update( + { + "content": attach.name, + "attachments": [ + { + "id": 0, + "filename": attach.name, + } + ], + } + ) files = { "files[0]": ( attach.name, # file handle is safely closed in `finally`; inline - # open is intentional - open(attach.path, "rb"), # noqa: SIM115 + # open is intentional; attach.open() dispatches to + # BytesIO for memory attachments + attach.open(), # Explicitly declare the file type so the server # doesn't hang attach.mimetype, @@ -656,6 +681,7 @@ class NotifyFluxer(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code == requests.codes.too_many_requests: @@ -727,8 +753,9 @@ class NotifyFluxer(NotifyBase): # Reset Rate Limiting (a bit of a hacky approach for now) # TODO: Learn more about how ratelimiting works with Fluxer - self.ratelimit_reset = \ - datetime.now(timezone.utc).replace(tzinfo=None) + self.ratelimit_reset = datetime.now(timezone.utc).replace( + tzinfo=None + ) self.ratelimit_remaining = self.default_delay_sec except requests.RequestException as e: @@ -740,7 +767,8 @@ class NotifyFluxer(NotifyBase): except OSError as e: self.logger.warning( - "An I/O error occurred while reading attachment(s)") + "An I/O error occurred while reading attachment(s)" + ) self.logger.debug(f"I/O Exception: {e!s}") return False @@ -837,7 +865,8 @@ class NotifyFluxer(NotifyBase): ), self.host if self.mode == FluxerMode.PRIVATE else "", ( - "" if self.mode == FluxerMode.CLOUD + "" + if self.mode == FluxerMode.CLOUD else (self.port if self.port else (443 if self.secure else 80)) ), self.webhook_id, @@ -866,8 +895,11 @@ class NotifyFluxer(NotifyBase): ] # Text To Speech - results["tts"] = parse_bool(results["qsd"].get( - "tts", NotifyFluxer.template_args["tts"]["default"])) + results["tts"] = parse_bool( + results["qsd"].get( + "tts", NotifyFluxer.template_args["tts"]["default"] + ) + ) # Mode override if "mode" in results["qsd"] and results["qsd"]["mode"]: @@ -901,7 +933,7 @@ class NotifyFluxer(NotifyBase): results["footer_logo"] = parse_bool( results["qsd"].get( "footer_logo", - NotifyFluxer.template_args["footer_logo"]["default"] + NotifyFluxer.template_args["footer_logo"]["default"], ) ) results["fields"] = parse_bool( @@ -938,8 +970,11 @@ class NotifyFluxer(NotifyBase): results["format"] = NotifyFormat.MARKDOWN # Update Avatar Icon - results["avatar"] = parse_bool(results["qsd"].get( - "avatar", NotifyFluxer.template_args["avatar"]["default"])) + results["avatar"] = parse_bool( + results["qsd"].get( + "avatar", NotifyFluxer.template_args["avatar"]["default"] + ) + ) if "thread" in results["qsd"]: results["thread"] = NotifyFluxer.unquote(results["qsd"]["thread"]) @@ -1040,7 +1075,8 @@ class NotifyFluxer(NotifyBase): @staticmethod def extract_markdown_sections( - markdown: str) -> tuple[str, list[dict[str, str]]]: + markdown: str, + ) -> tuple[str, list[dict[str, str]]]: """Extract headers and their corresponding sections into embed fields.""" @@ -1069,12 +1105,18 @@ class NotifyFluxer(NotifyBase): for el in common: d = el.groupdict() - fields.append({ - "name": d.get("name", "").strip("#`* \r\n\t\v"), - "value": "```{}\n{}```".format( - "md" if d.get("value") else "", - (d.get("value").strip() + "\n" if d.get("value") else ""), - ), - }) + fields.append( + { + "name": d.get("name", "").strip("#`* \r\n\t\v"), + "value": "```{}\n{}```".format( + "md" if d.get("value") else "", + ( + d.get("value").strip() + "\n" + if d.get("value") + else "" + ), + ), + } + ) return description, fields diff --git a/libs/apprise/plugins/fortysixelks.py b/libs/apprise/plugins/fortysixelks.py index cb1579e0c..5b01a326c 100644 --- a/libs/apprise/plugins/fortysixelks.py +++ b/libs/apprise/plugins/fortysixelks.py @@ -188,9 +188,7 @@ class Notify46Elks(NotifyBase): if not self.targets: # There is no one to email; we're done - self.logger.warning( - "There are no 46elks recipients to notify" - ) + self.logger.warning("There are no 46elks recipients to notify") return False headers = { @@ -227,13 +225,12 @@ class Notify46Elks(NotifyBase): auth=(self.user, self.password), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem - status_str = ( - Notify46Elks.http_response_code_lookup( - r.status_code - ) + status_str = Notify46Elks.http_response_code_lookup( + r.status_code ) self.logger.warning( @@ -247,16 +244,15 @@ class Notify46Elks(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True continue else: - self.logger.info( - f"Sent 46elks notification to {target}." - ) + self.logger.info(f"Sent 46elks notification to {target}.") except requests.RequestException as e: self.logger.warning( @@ -289,15 +285,18 @@ class Notify46Elks(NotifyBase): # Apprise URL can be condensed and target can be eliminated if its # our source phone no targets = ( - [] if len(self.targets) == 1 and - self.source in self.targets else self.targets) + [] + if len(self.targets) == 1 and self.source in self.targets + else self.targets + ) return "{schema}://{user}:{pw}@{source}/{targets}?{params}".format( schema=self.secure_protocol[0], user=self.quote(self.user, safe=""), source=self.source if self.source else "", pw=self.pprint( - self.password, privacy, mode=PrivacyMode.Secret, safe=""), + self.password, privacy, mode=PrivacyMode.Secret, safe="" + ), targets="/".join( [Notify46Elks.quote(x, safe="+") for x in targets] ), @@ -348,17 +347,13 @@ class Notify46Elks(NotifyBase): # The 'from' makes it easier to use yaml configuration if "from" in results["qsd"] and len(results["qsd"]["from"]): - results["source"] = Notify46Elks.unquote( - results["qsd"]["from"] - ) + results["source"] = Notify46Elks.unquote(results["qsd"]["from"]) elif results["host"]: results["source"] = Notify46Elks.unquote(results["host"]) # Store our remaining targets found on path - results["targets"].extend( - Notify46Elks.split_path(results["fullpath"]) - ) + results["targets"].extend(Notify46Elks.split_path(results["fullpath"])) # Support the 'to' variable so that we can support targets this way too # The 'to' makes it easier to use yaml configuration diff --git a/libs/apprise/plugins/freemobile.py b/libs/apprise/plugins/freemobile.py index 606735b5d..181f257b6 100644 --- a/libs/apprise/plugins/freemobile.py +++ b/libs/apprise/plugins/freemobile.py @@ -151,6 +151,7 @@ class NotifyFreeMobile(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -166,7 +167,8 @@ class NotifyFreeMobile(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/glib.py b/libs/apprise/plugins/glib.py index b05179808..8bdf0ee2f 100644 --- a/libs/apprise/plugins/glib.py +++ b/libs/apprise/plugins/glib.py @@ -42,6 +42,7 @@ NOTIFY_GLIB_IMAGE_SUPPORT = False try: # glib essentials import gi + gi.require_version("Gio", "2.0") gi.require_version("GLib", "2.0") from gi.repository import Gio, GLib @@ -60,6 +61,7 @@ try: # The following is required for Image/Icon loading only gi.require_version("GdkPixbuf", "2.0") from gi.repository import GdkPixbuf + NOTIFY_GLIB_IMAGE_SUPPORT = True except (ImportError, ValueError, AttributeError): @@ -101,7 +103,6 @@ GLIB_URGENCY_MAP = { "h": GLibUrgency.HIGH, # Maps against string 'emergency' "e": GLibUrgency.HIGH, - # Entries to additionally support (so more like DBus's API) "0": GLibUrgency.LOW, "1": GLibUrgency.NORMAL, @@ -123,11 +124,12 @@ class NotifyGLib(NotifyBase): } # The default descriptive name associated with the Notification - service_name = _("DBus Notification") + service_name = _("GLib Notification") # The services URL - service_url = \ + service_url = ( "https://lazka.github.io/pgi-docs/Gio-2.0/classes/DBusProxy.html" + ) # The default protocols protocol = ("glib", "gio") @@ -153,46 +155,52 @@ class NotifyGLib(NotifyBase): glib_setting_location = "/org/freedesktop/Notifications" # Define object templates - templates = ( - "{schema}://", - ) + templates = ("{schema}://",) # Define our template arguments - template_args = dict(NotifyBase.template_args, **{ - "urgency": { - "name": _("Urgency"), - "type": "choice:int", - "values": GLIB_URGENCIES, - "default": GLibUrgency.NORMAL, + template_args = dict( + NotifyBase.template_args, + **{ + "urgency": { + "name": _("Urgency"), + "type": "choice:int", + "values": GLIB_URGENCIES, + "default": GLibUrgency.NORMAL, + }, + "priority": { + # Apprise uses 'priority' everywhere; it's just a + # nice consistent feel. Map the value back to urgency. + "alias_of": "urgency", + }, + "x": { + "name": _("X-Axis"), + "type": "int", + "min": 0, + "map_to": "x_axis", + }, + "y": { + "name": _("Y-Axis"), + "type": "int", + "min": 0, + "map_to": "y_axis", + }, + "image": { + "name": _("Include Image"), + "type": "bool", + "default": True, + "map_to": "include_image", + }, }, - "priority": { - # Apprise uses 'priority' everywhere; it's just a nice consistent - # feel to be able to use it here as well. Just map the - # value back to 'priority' - "alias_of": "urgency", - }, - "x": { - "name": _("X-Axis"), - "type": "int", - "min": 0, - "map_to": "x_axis", - }, - "y": { - "name": _("Y-Axis"), - "type": "int", - "min": 0, - "map_to": "y_axis", - }, - "image": { - "name": _("Include Image"), - "type": "bool", - "default": True, - "map_to": "include_image", - }, - }) + ) - def __init__(self, urgency=None, x_axis=None, y_axis=None, - include_image=True, **kwargs): + def __init__( + self, + urgency=None, + x_axis=None, + y_axis=None, + include_image=True, + **kwargs, + ): """ Initialize DBus Object """ @@ -205,11 +213,16 @@ class NotifyGLib(NotifyBase): # The urgency of the message self.urgency = int( NotifyGLib.template_args["urgency"]["default"] - if urgency is None else - next(( - v for k, v in GLIB_URGENCY_MAP.items() - if str(urgency).lower().startswith(k)), - NotifyGLib.template_args["urgency"]["default"])) + if urgency is None + else next( + ( + v + for k, v in GLIB_URGENCY_MAP.items() + if str(urgency).lower().startswith(k) + ), + NotifyGLib.template_args["urgency"]["default"], + ) + ) # Our x/y axis settings if x_axis or y_axis: @@ -219,8 +232,10 @@ class NotifyGLib(NotifyBase): except (TypeError, ValueError): # Invalid x/y values specified - msg = "The x,y coordinates specified ({},{}) are invalid."\ - .format(x_axis, y_axis) + msg = ( + "The x,y coordinates specified" + f" ({x_axis},{y_axis}) are invalid." + ) self.logger.warning(msg) raise TypeError(msg) from None else: @@ -259,8 +274,11 @@ class NotifyGLib(NotifyBase): body = "" # image path - icon_path = None if not self.include_image \ + icon_path = ( + None + if not self.include_image else self.image_path(notify_type, extension=".ico") + ) # Our meta payload meta_payload = { @@ -293,7 +311,8 @@ class NotifyGLib(NotifyBase): except Exception as e: self.logger.warning( - "Could not load notification icon (%s).", icon_path) + "Could not load notification icon (%s).", icon_path + ) self.logger.debug(f"GLib/Gio Exception: {e}") try: @@ -337,10 +356,9 @@ class NotifyGLib(NotifyBase): # Define any URL parameters params = { "image": "yes" if self.include_image else "no", - "urgency": - GLIB_URGENCIES[self.template_args["urgency"]["default"]] - if self.urgency not in GLIB_URGENCIES - else GLIB_URGENCIES[self.urgency], + "urgency": GLIB_URGENCIES[self.template_args["urgency"]["default"]] + if self.urgency not in GLIB_URGENCIES + else GLIB_URGENCIES[self.urgency], } # Extend our parameters @@ -369,19 +387,18 @@ class NotifyGLib(NotifyBase): results = NotifyBase.parse_url(url, verify_host=False) # Include images with our message - results["include_image"] = \ - parse_bool(results["qsd"].get("image", True)) + results["include_image"] = parse_bool( + results["qsd"].get("image", True) + ) # GLib/Gio supports urgency, but we we also support the keyword # priority so that it is consistent with some of the other plugins if "priority" in results["qsd"] and len(results["qsd"]["priority"]): # We intentionally store the priority in the urgency section - results["urgency"] = \ - NotifyGLib.unquote(results["qsd"]["priority"]) + results["urgency"] = NotifyGLib.unquote(results["qsd"]["priority"]) if "urgency" in results["qsd"] and len(results["qsd"]["urgency"]): - results["urgency"] = \ - NotifyGLib.unquote(results["qsd"]["urgency"]) + results["urgency"] = NotifyGLib.unquote(results["qsd"]["urgency"]) # handle x,y coordinates if "x" in results["qsd"] and len(results["qsd"]["x"]): diff --git a/libs/apprise/plugins/google_chat.py b/libs/apprise/plugins/google_chat.py index ee1ba9ceb..431be8485 100644 --- a/libs/apprise/plugins/google_chat.py +++ b/libs/apprise/plugins/google_chat.py @@ -223,15 +223,21 @@ class NotifyGoogleChat(NotifyBase): } if self.thread_key: - params.update({ - "messageReplyOption": "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD", - }) - - payload.update({ - "thread": { - "thread_key": self.thread_key, + params.update( + { + "messageReplyOption": ( + "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" + ), } - }) + ) + + payload.update( + { + "thread": { + "thread_key": self.thread_key, + } + } + ) self.logger.debug( "Google Chat POST URL:" @@ -250,12 +256,12 @@ class NotifyGoogleChat(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, requests.codes.no_content, ): - # We had a problem status_str = NotifyBase.http_response_code_lookup( r.status_code @@ -269,7 +275,8 @@ class NotifyGoogleChat(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/gotify.py b/libs/apprise/plugins/gotify.py index ef99a4a96..c0056fd11 100644 --- a/libs/apprise/plugins/gotify.py +++ b/libs/apprise/plugins/gotify.py @@ -238,6 +238,7 @@ class NotifyGotify(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -252,7 +253,8 @@ class NotifyGotify(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure return False diff --git a/libs/apprise/plugins/groupme.py b/libs/apprise/plugins/groupme.py new file mode 100644 index 000000000..8a6564df8 --- /dev/null +++ b/libs/apprise/plugins/groupme.py @@ -0,0 +1,447 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# GroupMe Bot API Notifications +# +# To use this plugin you first need a GroupMe Bot: +# 1. Sign in at https://dev.groupme.com/bots using your GroupMe account. +# 2. Click "Create Bot", choose a group, give your bot a name, and submit. +# 3. Copy the bot_id shown in the bot list -- it looks like a hexadecimal +# string (e.g. 68ca900a7d17f9b9891a73af2a). +# +# Basic Apprise URL (text messages only): +# groupme://{bot_id} +# +# To also send image attachments you need your personal access token: +# 1. Visit https://dev.groupme.com/ and log in. +# 2. Click on "Access Token" in the top-right corner. +# 3. Copy the displayed token. +# +# Apprise URL with attachment support: +# groupme://{bot_id}/{access_token} +# +# The access token may also be supplied as a query parameter: +# groupme://{bot_id}?token={access_token} +# +# API References: +# https://dev.groupme.com/tutorials/bots +# https://dev.groupme.com/docs/image_service + +from json import dumps, loads + +import requests + +from ..common import NotifyFormat, NotifyType +from ..locale import gettext_lazy as _ +from ..utils.parse import validate_regex +from .base import NotifyBase + +# GroupMe Bot post endpoint +GROUPME_BOT_URL = "https://api.groupme.com/v3/bots/post" + +# GroupMe image-service upload endpoint +GROUPME_IMAGE_URL = "https://image.groupme.com/pictures" + +# HTTP error messages specific to GroupMe +GROUPME_HTTP_ERROR_MAP = { + 401: "Unauthorized - Invalid or expired access token.", + 400: "Bad Request - Malformed payload or missing required fields.", + 404: "Not Found - The specified bot_id does not exist.", + 429: "Too many requests; rate-limit exceeded.", +} + + +class NotifyGroupMe(NotifyBase): + """A wrapper for GroupMe Bot Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "GroupMe" + + # The services URL + service_url = "https://groupme.com/" + + # The default secure protocol + secure_protocol = "groupme" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/groupme/" + + # GroupMe Bot API post endpoint + notify_url = GROUPME_BOT_URL + + # GroupMe image-service upload endpoint + groupme_image_url = GROUPME_IMAGE_URL + + # GroupMe does not have a native title field + title_maxlen = 0 + + # GroupMe bot messages are limited to 1000 characters + body_maxlen = 1000 + + # GroupMe image attachments are supported when an access token is + # provided (required for the image-service upload step) + attachment_support = True + + # GroupMe renders plain text; no markdown support in bot messages + notify_format = NotifyFormat.TEXT + + # Define object URL templates + templates = ( + "{schema}://{bot_id}", + "{schema}://{bot_id}/{token}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "bot_id": { + "name": _("Bot ID"), + "type": "string", + "private": True, + "required": True, + }, + # Access token lives in the URL path; also accepted as ?token= + "token": { + "name": _("Access Token"), + "type": "string", + "private": True, + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + # Allow bot_id to also be supplied as a query parameter + "bot_id": { + "alias_of": "bot_id", + }, + # ?token= is accepted as a query-parameter alias for the + # path-positioned token + "token": { + "alias_of": "token", + }, + }, + ) + + def __init__(self, bot_id, token=None, **kwargs): + """Initialize GroupMe Object.""" + super().__init__(**kwargs) + + # Validate the Bot ID + self.bot_id = validate_regex(bot_id, r"^[a-z0-9]+$", "i") + if not self.bot_id: + msg = "A GroupMe bot_id must be specified ({}).".format(bot_id) + self.logger.warning(msg) + raise TypeError(msg) + + # Store the optional access token (needed for image uploads) + self.token = validate_regex(token) if token else None + + return + + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): + """Perform GroupMe Bot Notification.""" + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json; charset=utf-8", + } + + # Prepare our base payload + payload = { + "bot_id": self.bot_id, + "text": body, + } + + # Handle image attachments + groupme_attachments = [] + if attach and self.attachment_support: + if not self.token: + # Warn but do not abort -- text still gets sent + self.logger.warning( + "GroupMe image attachments require an access token;" + " skipping attachments." + ) + + else: + # Upload each attachment and collect image URLs + for attachment in attach: + # Only image MIME types are accepted by GroupMe's image + # service; skip non-image files with a warning. + # Note: mimetype is None for inaccessible files, so we + # fall through to _upload_image() which handles that. + mimetype = attachment.mimetype or "" + if mimetype and not mimetype.startswith("image/"): + self.logger.warning( + "GroupMe image service only supports images;" + " skipping %s (%s).", + attachment.name or "attachment", + mimetype, + ) + continue + + image_url = self._upload_image(attachment) + if image_url is None: + # Upload failed -- abort the entire send + return False + + groupme_attachments.append( + { + "type": "image", + "url": image_url, + } + ) + + if groupme_attachments: + # Include the uploaded image URLs in the payload + payload["attachments"] = groupme_attachments + + self.logger.debug( + "GroupMe POST URL: %s (cert_verify=%s)", + self.notify_url, + self.verify_certificate, + ) + self.logger.debug("GroupMe Payload: %s", str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + self.notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code not in ( + requests.codes.ok, + requests.codes.accepted, + ): + # We had a failure + status_str = NotifyGroupMe.http_response_code_lookup( + r.status_code, GROUPME_HTTP_ERROR_MAP + ) + + self.logger.warning( + "Failed to send GroupMe notification:" + " {}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) + + # Return; we're done + return False + + else: + self.logger.info("Sent GroupMe notification.") + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred posting to GroupMe." + ) + self.logger.debug("Socket Exception: %s", str(e)) + return False + + return True + + def _upload_image(self, attachment): + """Upload an image attachment to GroupMe's image service. + + Returns the hosted image URL on success, or None on failure. + """ + + # Guard 1: verify the attachment is accessible before opening + if not attachment: + self.logger.warning( + "Could not access GroupMe attachment %s.", + attachment.url(privacy=True), + ) + return None + + # Prepare the upload headers + headers = { + "User-Agent": self.app_id, + "Content-Type": attachment.mimetype, + "X-Access-Token": self.token, + } + + self.logger.debug( + "GroupMe image upload URL: %s (cert_verify=%s)", + self.groupme_image_url, + self.verify_certificate, + ) + + # Always call throttle before any remote server i/o is made + self.throttle() + + fh = None + try: + # Guard 2: OSError is caught by the except below + fh = attachment.open() + + r = requests.post( + self.groupme_image_url, + data=fh, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # Upload was rejected by the image service + status_str = NotifyGroupMe.http_response_code_lookup( + r.status_code, GROUPME_HTTP_ERROR_MAP + ) + self.logger.warning( + "Failed to upload GroupMe image: {}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) + return None + + # Parse the response to extract the hosted image URL + try: + content = loads(r.content) + + except (AttributeError, TypeError, ValueError): + content = {} + + # The image URL is nested under payload.url + image_url = ( + content.get("payload", {}).get("url") if content else None + ) + if not image_url: + self.logger.warning( + "GroupMe image service returned no URL for %s.", + attachment.name or "attachment", + ) + return None + + # Return the hosted image URL + return image_url + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred uploading GroupMe image." + ) + self.logger.debug("Socket Exception: %s", str(e)) + return None + + except OSError as e: + self.logger.warning( + "An I/O error occurred reading GroupMe attachment %s.", + attachment.name or "attachment", + ) + self.logger.debug("I/O Exception: %s", str(e)) + return None + + finally: + # Guard 3: always close the file handle + if fh is not None: + fh.close() + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another simliar one. + + Targets or end points should never be identified here. + """ + return (self.secure_protocol, self.bot_id) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # Acquire any global URL parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + if self.token: + # Emit the access token in the URL path (canonical form) + return "{schema}://{bot_id}/{token}/?{params}".format( + schema=self.secure_protocol, + bot_id=self.pprint(self.bot_id, privacy, safe=""), + token=self.pprint(self.token, privacy, safe=""), + params=NotifyGroupMe.urlencode(params), + ) + + return "{schema}://{bot_id}/?{params}".format( + schema=self.secure_protocol, + bot_id=self.pprint(self.bot_id, privacy, safe=""), + params=NotifyGroupMe.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow + us to re-instantiate this object.""" + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # bot_id is in the host position + results["bot_id"] = NotifyGroupMe.unquote(results["host"]) + + # Allow ?bot_id= to override the host-supplied bot ID + if "bot_id" in results["qsd"] and results["qsd"]["bot_id"]: + results["bot_id"] = NotifyGroupMe.unquote(results["qsd"]["bot_id"]) + + # Token may appear as the first path segment + entries = NotifyGroupMe.split_path(results["fullpath"]) + if entries: + results["token"] = entries.pop(0) + + # ?token= query parameter overrides path-supplied token + if "token" in results["qsd"] and results["qsd"]["token"]: + results["token"] = NotifyGroupMe.unquote(results["qsd"]["token"]) + + return results diff --git a/libs/apprise/plugins/growl.py b/libs/apprise/plugins/growl.py index d9eb2d1cf..e379567b9 100644 --- a/libs/apprise/plugins/growl.py +++ b/libs/apprise/plugins/growl.py @@ -472,3 +472,10 @@ class NotifyGrowl(NotifyBase): results["version"] = version return results + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("gntp",) diff --git a/libs/apprise/plugins/home_assistant.py b/libs/apprise/plugins/home_assistant.py index ed45bc8c3..d9b6de657 100644 --- a/libs/apprise/plugins/home_assistant.py +++ b/libs/apprise/plugins/home_assistant.py @@ -27,8 +27,10 @@ # You must generate a "Long-Lived Access Token". This can be done from your # Home Assistant Profile page. - +from itertools import chain from json import dumps +import math +import re from uuid import uuid4 import requests @@ -36,12 +38,41 @@ import requests from ..common import NotifyType from ..locale import gettext_lazy as _ from ..url import PrivacyMode -from ..utils.parse import validate_regex +from ..utils.parse import ( + is_domain_service_target, + parse_bool, + parse_domain_service_targets, + validate_regex, +) from .base import NotifyBase +# This regex matches exactly 8 hex digits, +# a dot, then exactly 64 hex digits. it can also be a JWT +# token in which case it will be 180 characters+ +RE_IS_LONG_LIVED_TOKEN = re.compile( + r"^([0-9a-f]{8}\.[0-9a-f]{64}|[a-z0-9_-]+\.[a-z0-9_-]+\.[a-z0-9_-]+)$", + re.I, +) + +# Define our supported device notification formats: +# - service +# - default domain is always 'notify' if one isn't detected +# - service:target +# - service:target1,target2,target3 +# - domain.service +# - domain.service:target +# - domain.service:target1,target2,target3 +# - - targets can be comma/space separated if more hten one +# - service:target1,target2,target3 + +# Define a persistent entry (used for handling message delivery +PERSISTENT_ENTRY = (None, None, []) + class NotifyHomeAssistant(NotifyBase): - """A wrapper for Home Assistant Notifications.""" + """ + A wrapper for Home Assistant Notifications + """ # The default descriptive name associated with the Notification service_name = "HomeAssistant" @@ -58,17 +89,29 @@ class NotifyHomeAssistant(NotifyBase): # Default to Home Assistant Default Insecure port of 8123 instead of 80 default_insecure_port = 8123 + # The maximum amount of services that can be notified in a single batch + default_batch_size = 10 + + # The default ha notification domain if one isn't detected + default_domain = "notify" + # A URL that takes you to the setup/help of the specific protocol setup_url = "https://appriseit.com/services/homeassistant/" # Define object templates templates = ( "{schema}://{host}/{accesstoken}", + "{schema}://{host}/{accesstoken}/{targets}", "{schema}://{host}:{port}/{accesstoken}", + "{schema}://{host}:{port}/{accesstoken}/{targets}", "{schema}://{user}@{host}/{accesstoken}", + "{schema}://{user}@{host}/{accesstoken}/{targets}", "{schema}://{user}@{host}:{port}/{accesstoken}", + "{schema}://{user}@{host}:{port}/{accesstoken}/{targets}", "{schema}://{user}:{password}@{host}/{accesstoken}", + "{schema}://{user}:{password}@{host}/{accesstoken}/{targets}", "{schema}://{user}:{password}@{host}:{port}/{accesstoken}", + "{schema}://{user}:{password}@{host}:{port}/{accesstoken}/{targets}", ) # Define our template tokens @@ -101,6 +144,15 @@ class NotifyHomeAssistant(NotifyBase): "private": True, "required": True, }, + "target_device": { + "name": _("Target Device"), + "type": "string", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, }, ) @@ -114,14 +166,39 @@ class NotifyHomeAssistant(NotifyBase): "type": "string", "regex": (r"^[a-z0-9_-]+$", "i"), }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, + "to": { + "alias_of": "targets", + }, + "token": { + # Shorthand alias for accesstoken in query string + "alias_of": "accesstoken", + }, + "prefix": { + "name": _("Path Prefix"), + "type": "string", + }, }, ) - def __init__(self, accesstoken, nid=None, **kwargs): + def __init__( + self, + accesstoken, + nid=None, + targets=None, + batch=None, + prefix=None, + **kwargs, + ): """Initialize Home Assistant Object.""" super().__init__(**kwargs) self.fullpath = kwargs.get("fullpath", "") + self.prefix = prefix if prefix else "" if not (self.secure or self.port): # Use default insecure port @@ -149,18 +226,50 @@ class NotifyHomeAssistant(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # Prepare Batch Mode Flag + self.batch = ( + self.template_args["batch"]["default"] if batch is None else batch + ) + + # Store our targets + self.targets = [] + + # Track our invalid targets + self._invalid_targets = [] + + if targets: + for target in parse_domain_service_targets(targets): + result = is_domain_service_target( + target, domain=self.default_domain + ) + if result: + self.targets.append( + ( + result["domain"], + result["service"], + result["targets"], + ) + ) + continue + + self.logger.warning( + "Dropped invalid [domain.]service[:target] entry " + "({}) specified.".format(target), + ) + self._invalid_targets.append(target) + else: + self.targets = [PERSISTENT_ENTRY] + return def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): """Sends Message.""" - # Prepare our persistent_notification.create payload + # Base payload; notification_id is only added for persistent + # notification calls — other service domains reject it. payload = { "title": title, "message": body, - # Use a unique ID so we don't over-write the last message - # we posted. Otherwise use the notification id specified - "notification_id": self.nid if self.nid else str(uuid4()), } # Prepare our headers @@ -181,20 +290,84 @@ class NotifyHomeAssistant(NotifyBase): if isinstance(self.port, int): url += f":{self.port}" - url += ( + # Determine if we're doing it the old way (using persistent notices) + # or the new (supporting device targets) + has_targets = bool( + not self.targets or self.targets[0] is not PERSISTENT_ENTRY + ) + + if has_targets and not self.targets: + self.logger.warning( + "No valid Home Assistant service targets to notify." + ) + return False + + base_url = url + ( self.fullpath.rstrip("/") + "/api/services/persistent_notification/create" ) + # Send in batches if identified to do so + batch_size = 1 if not self.batch else self.default_batch_size + + for target in self.targets: + # Use a unique ID so we don't over-write the last message we + # posted. Otherwise use the notification id specified + if has_targets: + # Base target details + domain = target[0] + service = target[1] + + # Prepare our URL + base_url = ( + url + + self.prefix.rstrip("/") + + f"/api/services/{domain}/{service}" + ) + + # Possibly prepare batches + if target[2]: + _payload = payload.copy() + for index in range(0, len(target[2]), batch_size): + _payload["targets"] = target[2][ + index : index + batch_size + ] + if not self._ha_post( + base_url, _payload, headers, auth + ): + return False + + continue + + if not self._ha_post( + base_url, + payload, + headers, + auth, + persistent=not has_targets, + ): + return False + + return True + + def _ha_post(self, url, payload, headers, auth=None, persistent=False): + """ + Wrapper to single upstream server post + """ + # notification_id is only meaningful for persistent_notification; + # other HA service domains reject it with a 400. + if persistent: + payload["notification_id"] = self.nid if self.nid else str(uuid4()) + self.logger.debug( - "Home Assistant POST URL:" - f" {url} (cert_verify={self.verify_certificate!r})" + "Home Assistant POST URL: {} (cert_verify={!r})".format( + url, self.verify_certificate + ) ) - self.logger.debug(f"Home Assistant Payload: {payload!s}") + self.logger.debug("Home Assistant Payload: {}".format(str(payload))) # Always call throttle before any remote server i/o is made self.throttle() - try: r = requests.post( url, @@ -203,6 +376,7 @@ class NotifyHomeAssistant(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -218,7 +392,8 @@ class NotifyHomeAssistant(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -250,11 +425,7 @@ class NotifyHomeAssistant(NotifyBase): self.user, self.password, self.host, - ( - self.port - if self.port - else (443 if self.secure else self.default_insecure_port) - ), + self.port, self.fullpath.rstrip("/"), self.accesstoken, ) @@ -263,7 +434,17 @@ class NotifyHomeAssistant(NotifyBase): """Returns the URL built dynamically based on specified arguments.""" # Define any URL parameters - params = {} + params = { + "batch": "yes" if self.batch else "no", + } + + if self.prefix not in ("", "/"): + params["prefix"] = ( + "/" + if not self.prefix + else "/{}/".format(self.prefix.strip("/")) + ) + if self.nid: params["nid"] = self.nid @@ -288,7 +469,13 @@ class NotifyHomeAssistant(NotifyBase): url = ( "{schema}://{auth}{hostname}{port}{fullpath}" - "{accesstoken}/?{params}" + "{accesstoken}/{targets}?{params}" + ) + + # Determine if we're doing it the old way (using persistent notices) + # or the new (supporting device targets) + has_targets = bool( + not self.targets or self.targets[0] is not PERSISTENT_ENTRY ) return url.format( @@ -311,9 +498,54 @@ class NotifyHomeAssistant(NotifyBase): ) ), accesstoken=self.pprint(self.accesstoken, privacy, safe=""), + targets="" + if not has_targets + else "/".join( + chain( + [ + NotifyHomeAssistant.quote( + "{}.{}{}".format( + x[0], + x[1], + "" if not x[2] else ":" + ",".join(x[2]), + ), + safe="", + ) + for x in self.targets + ], + [ + NotifyHomeAssistant.quote(x, safe="") + for x in self._invalid_targets + ], + ) + ), params=NotifyHomeAssistant.urlencode(params), ) + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + # + # Factor batch into calculation + # + + # Determine if we're doing it the old way (using persistent notices) + # or the new (supporting device targets) + has_targets = bool( + not self.targets or self.targets[0] is not PERSISTENT_ENTRY + ) + + if not has_targets: + return 1 + + # Handle targets + batch_size = 1 if not self.batch else self.default_batch_size + return sum( + math.ceil(len(identities) / batch_size) if identities else 1 + for _, _, identities in self.targets + ) + @staticmethod def parse_url(url): """Parses the URL and returns enough arguments that can allow us to re- @@ -324,23 +556,82 @@ class NotifyHomeAssistant(NotifyBase): # We're done early as we couldn't load the results return results - # Get our Long-Lived Access Token - if "accesstoken" in results["qsd"] and len( - results["qsd"]["accesstoken"] - ): + # Initialize targets + results["targets"] = [] + + # Get our Long-Lived Access Token — check query string first + # (?accesstoken= or the shorthand ?token=) + if "accesstoken" in results["qsd"] and results["qsd"]["accesstoken"]: results["accesstoken"] = NotifyHomeAssistant.unquote( results["qsd"]["accesstoken"] ) + # Remaining path elements become targets + results["targets"] = NotifyHomeAssistant.split_path( + results["fullpath"] + ) + results["fullpath"] = "" + + elif "token" in results["qsd"] and results["qsd"]["token"]: + results["accesstoken"] = NotifyHomeAssistant.unquote( + results["qsd"]["token"] + ) + # Remaining path elements become targets + results["targets"] = NotifyHomeAssistant.split_path( + results["fullpath"] + ) + results["fullpath"] = "" else: - # Acquire our full path - fullpath = NotifyHomeAssistant.split_path(results["fullpath"]) + # Scan path forward; the first element matching the token + # regex is the access token. Everything before it is the + # prefix path; everything after it (reversed for consistent + # call ordering) is treated as service targets. + tokens = NotifyHomeAssistant.split_path(results["fullpath"]) + token_idx = None + for idx, t in enumerate(tokens): + if RE_IS_LONG_LIVED_TOKEN.match(t): + token_idx = idx + results["accesstoken"] = t + break - # Otherwise pop the last element from our path to be it - results["accesstoken"] = fullpath.pop() if fullpath else None + if token_idx is not None: + # Elements after the token are service targets (reversed + # so the last URL segment is called first). + post_targets = list(reversed(tokens[token_idx + 1 :])) + # Elements before the token are also treated as service + # targets (reversed to maintain consistent ordering). + pre_targets = list(reversed(tokens[:token_idx])) + results["targets"] = post_targets + pre_targets + results["fullpath"] = "" - # Re-assemble our full path - results["fullpath"] = "/" + "/".join(fullpath) if fullpath else "" + elif tokens: + # No regex match — treat the last path element as the + # access token (plain token like "accesstoken" in tests). + results["accesstoken"] = tokens[-1] + results["fullpath"] = "" + + else: + results["accesstoken"] = None + + # Support ?to= for additional targets + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyHomeAssistant.split_path( + NotifyHomeAssistant.unquote(results["qsd"]["to"]) + ) + + # Support ?prefix= path-prefix override + if "prefix" in results["qsd"] and results["qsd"]["prefix"]: + results["prefix"] = NotifyHomeAssistant.unquote( + results["qsd"]["prefix"] + ) + + # Get Batch Mode Flag + results["batch"] = parse_bool( + results["qsd"].get( + "batch", + NotifyHomeAssistant.template_args["batch"]["default"], + ) + ) # Allow the specification of a unique notification_id so that # it will always replace the last one sent. diff --git a/libs/apprise/plugins/httpsms.py b/libs/apprise/plugins/httpsms.py index 91a4c6610..6045a3ab2 100644 --- a/libs/apprise/plugins/httpsms.py +++ b/libs/apprise/plugins/httpsms.py @@ -214,6 +214,7 @@ class NotifyHttpSMS(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -236,7 +237,8 @@ class NotifyHttpSMS(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/humhub.py b/libs/apprise/plugins/humhub.py new file mode 100644 index 000000000..7ca474a40 --- /dev/null +++ b/libs/apprise/plugins/humhub.py @@ -0,0 +1,474 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# HumHub is a self-hosted social network for teams. +# +# To post notifications to HumHub you need: +# 1. A running HumHub instance with the REST API module enabled. +# Visit: https://marketplace.humhub.com/module/rest +# 2. Authentication: either a Bearer token or a username and password +# (Basic Authentication). +# 3. The numeric ID of the container (space) you want to post to. +# +# To enable and configure Bearer Authentication in HumHub: +# 1. Log in to your HumHub instance as an administrator. +# 2. Navigate to Administration > Modules and ensure the REST API +# module is installed and active. +# 3. Go to Administration > Authentication > REST API > Bearer Auth. +# 4. Enable Bearer token authentication and create a new token. +# 5. Copy the generated token -- this is your {token}. +# +# To find a container (space) ID: +# 1. Navigate to the space in your HumHub instance. +# 2. The container ID appears in URLs such as: +# https://yourhost/s/my-space-1 (the trailing number) +# 3. Alternatively, query the REST API: +# GET /api/v1/space to list all spaces with their IDs. +# +# Apprise URLs: +# Bearer token over HTTPS (recommended): +# humhubs://{token}@{hostname}/{container_id} +# Multiple containers in one URL: +# humhubs://{token}@{hostname}/{id1}/{id2}/{id3} +# Basic authentication over HTTPS: +# humhubs://{user}:{password}@{hostname}/{container_id} +# Insecure HTTP -- not recommended for production: +# humhub://{token}@{hostname}/{container_id} +# Targets via ?to= parameter: +# humhubs://{token}@{hostname}/?to={id1},{id2} +# +# API Reference: +# https://marketplace.humhub.com/module/rest/docs/html/post.html + +from itertools import chain +from json import dumps, loads + +import requests + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from ..url import PrivacyMode +from ..utils.parse import parse_list +from .base import NotifyBase + + +class NotifyHumHub(NotifyBase): + """A wrapper for HumHub Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "HumHub" + + # The services URL + service_url = "https://www.humhub.com/" + + # The default protocol + protocol = "humhub" + + # The default secure protocol (HTTPS) + secure_protocol = "humhubs" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/humhub/" + + # HumHub posts have no native title field; the framework prepends + # a non-empty title to the body automatically when title_maxlen <= 0 + title_maxlen = 0 + + # Maximum post length (characters) + body_maxlen = 4000 + + # Self-hosted service; relax the default throttle slightly + request_rate_per_sec = 0.02 + + # HumHub supports file attachments via a two-step API: + # POST /api/v1/post/container/{id} to create the post (returns post ID), + # then POST /api/v1/post/{post_id}/upload-files to attach each file + attachment_support = True + + # Define object URL templates + templates = ( + # Bearer token -- user field only, no password + "{schema}://{user}@{host}/{targets}", + "{schema}://{user}@{host}:{port}/{targets}", + # Basic auth -- user + password + "{schema}://{user}:{password}@{host}/{targets}", + "{schema}://{user}:{password}@{host}:{port}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "user": { + "name": _("Token or Username"), + "type": "string", + "required": True, + "private": True, + }, + "password": { + "name": _("Password"), + "type": "string", + "private": True, + }, + "host": { + "name": _("Hostname"), + "type": "string", + "required": True, + }, + "port": { + "name": _("Port"), + "type": "int", + "min": 1, + "max": 65535, + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "to": { + "alias_of": "targets", + }, + }, + ) + + def __init__(self, targets=None, **kwargs): + """Initialize HumHub Object.""" + super().__init__(**kwargs) + + # Determine our schema (http vs https) + self.schema = "https" if self.secure else "http" + + # Require authentication credentials + if not self.user: + msg = "A HumHub bearer token or username must be specified." + self.logger.warning(msg) + raise TypeError(msg) + + # Accumulate invalid targets for lossless URL round-tripping + self._invalid_targets = [] + + # Parse and validate container IDs (must be positive integers) + self.targets = [] + for target in parse_list(targets): + try: + cid = int(str(target).strip()) + if cid > 0: + # Valid positive integer container ID + self.targets.append(str(cid)) + continue + except (ValueError, TypeError): + pass + + # Invalid container ID -- track it so the URL round-trips + self.logger.warning( + "Dropping invalid HumHub container ID: %s", target + ) + self._invalid_targets.append(str(target)) + + # Require at least one valid container ID to proceed + if not self.targets: + msg = "No valid HumHub container ID(s) were specified." + self.logger.warning(msg) + raise TypeError(msg) + + return + + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): + """Perform HumHub Notification.""" + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + } + + # Determine authentication method + if self.password: + # Basic authentication -- pass credentials via auth= argument + auth = (self.user, self.password) + else: + # Bearer token authentication via Authorization header + auth = None + headers["Authorization"] = "Bearer {}".format(self.user) + + # Prepare our payload (HumHub wraps the message in a data object) + payload = {"data": {"message": body}} + + # Prepare port reference + port = "" if self.port is None else ":{}".format(self.port) + + # Track overall success across all container posts + has_error = False + + # Post to each container in turn + for container_id in self.targets: + # Build the HumHub post creation URL for this container + url = "{}://{}{}/api/v1/post/container/{}".format( + self.schema, self.host, port, container_id + ) + + # Create the post + ok, content = self._send(url, dumps(payload), headers, auth) + if not ok: + # Mark our failure + has_error = True + continue + + self.logger.info( + "Sent HumHub notification to container %s.", + container_id, + ) + + # Skip attachment handling if no attachments were provided + if not attach: + continue + + # Parse the post ID from the creation response so we can + # attach files to the newly created post + try: + response = loads(content) + post_id = response.get("id") + except (AttributeError, TypeError, ValueError): + post_id = None + + if not post_id: + self.logger.warning( + "Failed to parse HumHub post ID from response;" + " attachments will not be sent to container %s.", + container_id, + ) + # Mark our failure + has_error = True + continue + + # Build the attachment upload URL for this post + attach_url = "{}://{}{}/api/v1/post/{}/upload-files".format( + self.schema, self.host, port, post_id + ) + + # Upload each attachment to the newly created post + for attachment in attach: + # Verify the attachment is accessible before uploading + if not attachment: + self.logger.warning( + "Could not access HumHub attachment %s.", + attachment.url(privacy=True), + ) + # Mark our failure + has_error = True + continue + + # Upload the attachment + ok, _ = self._send( + attach_url, + None, + headers, + auth, + attach=attachment, + ) + if not ok: + # Mark our failure + has_error = True + + return not has_error + + def _send(self, url, payload=None, headers=None, auth=None, attach=None): + """Wrapper to the requests (post) object. + + Returns (ok, content) where content is the raw response bytes + on success, or None on failure. + """ + + # Track any open file handle for cleanup in finally + files = None + + self.logger.debug( + "HumHub POST URL: %s (cert_verify=%r)", + url, + self.verify_certificate, + ) + self.logger.debug("HumHub Payload: %s", str(payload)) + + # Always call throttle before any network request + self.throttle() + + try: + # Open our attachment and build multipart payload if provided; + # strip Content-Type so requests sets multipart/form-data + if attach: + _headers = { + k: v + for k, v in (headers or {}).items() + if k != "Content-Type" + } + files = {"files[]": (attach.name, attach.open())} + else: + _headers = headers or {} + + r = requests.post( + url, + data=payload, + headers=_headers, + files=files, + auth=auth, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # We had a failure + status_str = NotifyHumHub.http_response_code_lookup( + r.status_code + ) + self.logger.warning( + "Failed to send HumHub %s: %s%serror=%s.", + "attachment" if attach else "notification", + status_str, + ", " if status_str else "", + r.status_code, + ) + self.logger.debug("Response Details:\r\n%s", r.content) + # Return failure with no content + return (False, None) + + # Return success with raw response content + return (True, r.content) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending HumHub %s.", + "attachment" if attach else "notification", + ) + self.logger.debug("Socket Exception: %s", str(e)) + # Return failure with no content + return (False, None) + + except OSError as e: + self.logger.warning( + "An I/O error occurred while reading %s.", + attach.name if attach else "attachment", + ) + self.logger.debug("I/O Exception: %s", str(e)) + # Return failure with no content + return (False, None) + + finally: + # Close our file handle (if it was opened) + if files: + files["files[]"][1].close() + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another similar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol if self.secure else self.protocol, + self.user, + self.password, + self.host, + self.port if self.port else (443 if self.secure else 80), + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # Prepare our parameters + params = {} + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Default port reference + default_port = 443 if self.secure else 80 + + # Build the auth portion of the URL + if self.password: + # Basic auth: user:password@ + auth = "{user}:{password}@".format( + user=self.pprint(self.user, privacy, safe=""), + password=self.pprint( + self.password, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + ) + else: + # Bearer token: token@ + auth = "{token}@".format( + token=self.pprint(self.user, privacy, safe=""), + ) + + return "{schema}://{auth}{host}{port}/{targets}/?{params}".format( + schema=(self.secure_protocol if self.secure else self.protocol), + auth=auth, + host=self.host, + port=( + "" + if self.port is None or self.port == default_port + else ":{}".format(self.port) + ), + targets="/".join( + NotifyHumHub.quote(t, safe="") + for t in chain(self.targets, self._invalid_targets) + ), + params=NotifyHumHub.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow us + to re-instantiate this object.""" + results = NotifyBase.parse_url(url) + if not results: + # We're done early + return results + + # Acquire container IDs from the URL path + results["targets"] = NotifyHumHub.split_path(results["fullpath"]) + + # Support ?to= as a comma-separated list of additional container IDs + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyHumHub.parse_list(results["qsd"]["to"]) + + return results diff --git a/libs/apprise/plugins/ifttt.py b/libs/apprise/plugins/ifttt.py index 940386a35..91a23c57f 100644 --- a/libs/apprise/plugins/ifttt.py +++ b/libs/apprise/plugins/ifttt.py @@ -221,7 +221,6 @@ class NotifyIFTTT(NotifyBase): events = list(self.events) while len(events): - # Retrive an entry off of our event list event = events.pop(0) @@ -247,6 +246,7 @@ class NotifyIFTTT(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) self.logger.debug( f"IFTTT HTTP response headers: {r.headers!r}" @@ -270,7 +270,8 @@ class NotifyIFTTT(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/irc/base.py b/libs/apprise/plugins/irc/base.py index 0fee6da72..f7d170501 100644 --- a/libs/apprise/plugins/irc/base.py +++ b/libs/apprise/plugins/irc/base.py @@ -148,6 +148,7 @@ class NotifyIRC(NotifyBase): "targets": { "name": _("Targets"), "type": "list:string", + "required": True, }, }, ) @@ -155,7 +156,6 @@ class NotifyIRC(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": {"alias_of": "targets"}, "name": {"name": _("Real Name"), "type": "string"}, "nick": {"name": _("Nickname"), "type": "string"}, "join": { @@ -169,6 +169,7 @@ class NotifyIRC(NotifyBase): "values": IRC_AUTH_MODES, "default": IRCAuthMode.SERVER, }, + "to": {"alias_of": "targets"}, }, ) @@ -202,8 +203,8 @@ class NotifyIRC(NotifyBase): if isinstance(mode, str) and mode.strip(): self.auth_mode = mode.strip().lower() self.auth_mode = next( - (a for a in IRC_AUTH_MODES - if a.startswith(self.auth_mode)), None + (a for a in IRC_AUTH_MODES if a.startswith(self.auth_mode)), + None, ) if self.auth_mode not in IRC_AUTH_MODES: msg = f"The IRC auth mode specified ({mode}) is invalid." @@ -252,25 +253,31 @@ class NotifyIRC(NotifyBase): for i in range(len(templates.IRC_TEMPLATES)): # pragma: no branch self.logger.trace( "Scanning %s against %s", - self.host, templates.IRC_TEMPLATES[i][0]) + self.host, + templates.IRC_TEMPLATES[i][0], + ) match = templates.IRC_TEMPLATES[i][1].match(self.host) if match: self.logger.info( - f"Applying {templates.IRC_TEMPLATES[i][0]} Defaults") + f"Applying {templates.IRC_TEMPLATES[i][0]} Defaults" + ) # the secure flag can not be altered if defined in the template self.secure = templates.IRC_TEMPLATES[i][2].get( - "secure", self.secure, + "secure", + self.secure, ) # store default port self.port = templates.IRC_TEMPLATES[i][2].get( - "port", self.port, + "port", + self.port, ) self.auth_mode = templates.IRC_TEMPLATES[i][2].get( - "mode", self.auth_mode, + "mode", + self.auth_mode, ) break @@ -306,7 +313,8 @@ class NotifyIRC(NotifyBase): # In ZNC mode, authentication is performed against the bouncer # itself. ZNC configurations expect the PASS line to include the # username. - password=self.password if self.auth_mode != IRCAuthMode.ZNC + password=self.password + if self.auth_mode != IRCAuthMode.ZNC else f"{self.user}:{self.password}", auth_mode=self.auth_mode, nick_generator=IRCClient.nick_generation, @@ -321,9 +329,12 @@ class NotifyIRC(NotifyBase): # ZNC operates as a bouncer, so perform a quick sanity check that # the connection is alive before issuing commands. - if self.auth_mode == IRCAuthMode.ZNC \ - and not client.check_connection( - timeout=min(5.0, float(self.send_timeout or 5.0))): + if ( + self.auth_mode == IRCAuthMode.ZNC + and not client.check_connection( + timeout=min(5.0, float(self.send_timeout or 5.0)) + ) + ): raise AppriseSocketError("ZNC connection check failed") message = body if not title else f"{title} {body}".strip() @@ -379,7 +390,8 @@ class NotifyIRC(NotifyBase): @property def url_identifier( - self) -> tuple[str, Optional[str], Optional[str], Optional[str]]: + self, + ) -> tuple[str, Optional[str], Optional[str], Optional[str]]: """Return the pieces that uniquely identify this configuration.""" return ( self.secure_protocol if self.secure else self.protocol, @@ -420,12 +432,20 @@ class NotifyIRC(NotifyBase): elif self.user: auth = "{user}@".format(user=self.quote(self.user, safe="")) - default_port = IRCClient.default_secure_port \ - if self.secure else IRCClient.default_insecure_port - - port = self.port if isinstance(self.port, int) else ( + default_port = ( IRCClient.default_secure_port - if self.secure else IRCClient.default_insecure_port + if self.secure + else IRCClient.default_insecure_port + ) + + port = ( + self.port + if isinstance(self.port, int) + else ( + IRCClient.default_secure_port + if self.secure + else IRCClient.default_insecure_port + ) ) port = "" if port == default_port else f":{port}" @@ -436,18 +456,28 @@ class NotifyIRC(NotifyBase): auth=auth, host=self.host, port=port, - targets="/".join(chain( - [self.quote(f"#{c}" if not k else "#{}:{}".format( - c, - self.pprint( - k, - privacy, - mode=PrivacyMode.Secret, - safe="")), - safe="#") for c, k in self.channels.items()], - [self.quote(f"@{u}", safe="@") - for u in self.users], - )), + targets="/".join( + chain( + [ + self.quote( + f"#{c}" + if not k + else "#{}:{}".format( + c, + self.pprint( + k, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + ), + safe="#", + ) + for c, k in self.channels.items() + ], + [self.quote(f"@{u}", safe="@") for u in self.users], + ) + ), params=self.urlencode(params), ) diff --git a/libs/apprise/plugins/irc/client.py b/libs/apprise/plugins/irc/client.py index f00a08ed4..39243f666 100644 --- a/libs/apprise/plugins/irc/client.py +++ b/libs/apprise/plugins/irc/client.py @@ -83,9 +83,14 @@ class IRCClient: ) -> None: # Detect port if not set - port = port if port is not None else ( - self.default_secure_port - if secure else self.default_insecure_port + port = ( + port + if port is not None + else ( + self.default_secure_port + if secure + else self.default_insecure_port + ) ) self.transport = SocketTransport( @@ -98,8 +103,7 @@ class IRCClient: self._nick_generator = nick_generator self._nick_length = ( - int(nick_length) - if nick_length else int(self.nickname_max_length) + int(nick_length) if nick_length else int(self.nickname_max_length) ) self._nick_collision = 0 @@ -139,13 +143,19 @@ class IRCClient: if remaining <= 0.0: raise TimeoutError("timeout while writing IRC commands") - payload = (line + "\r\n").encode("utf-8", errors="replace") \ - if isinstance(line, str) else line + payload = ( + (line + "\r\n").encode("utf-8", errors="replace") + if isinstance(line, str) + else line + ) self.transport.write(payload, flush=True, timeout=remaining) if logger.isEnabledFor(logging.TRACE): logger.trace( - "IRC write: %s", payload.rstrip(b"\r") - .decode("utf-8", errors="replace").rstrip()) + "IRC write: %s", + payload.rstrip(b"\r") + .decode("utf-8", errors="replace") + .rstrip(), + ) def _flush(self, deadline: float) -> None: """Flush all queued information to the IRC server.""" @@ -167,8 +177,7 @@ class IRCClient: remaining = max(0.0, deadline - time.monotonic()) if remaining <= 0.0: - logger.trace( - "IRC read timeout - deadline=%.2fs", deadline) + logger.trace("IRC read timeout - deadline=%.2fs", deadline) return None chunk = self.transport.read(4096, blocking=True, timeout=remaining) @@ -327,9 +336,7 @@ class IRCClient: deadline = time.monotonic() + float(timeout) self._queue( - "PRIVMSG NickServ :IDENTIFY {}".format( - self.sm.ctx.password - ) + "PRIVMSG NickServ :IDENTIFY {}".format(self.sm.ctx.password) ) self._flush(deadline) self._handshake( @@ -346,14 +353,14 @@ class IRCClient: @staticmethod def nick_generation( - prefix: str, length: Optional[int] = None, - collision: int = 0) -> str: + prefix: str, length: Optional[int] = None, collision: int = 0 + ) -> str: """Generate a nickname suitable for retry after collision.""" if length is None: # Default Assignment length = IRCClient.nickname_max_length - base = "{}".format(prefix)[:length - 3].strip().lower() + base = "{}".format(prefix)[: length - 3].strip().lower() charset = string.ascii_lowercase + string.digits + "_" suffix = "".join(random.choice(charset) for _ in range(max(1, length))) nick = "{}{}".format(base, suffix) diff --git a/libs/apprise/plugins/irc/protocol.py b/libs/apprise/plugins/irc/protocol.py index f12fc4482..660e95b08 100644 --- a/libs/apprise/plugins/irc/protocol.py +++ b/libs/apprise/plugins/irc/protocol.py @@ -164,14 +164,19 @@ def parse_irc_line(line: str) -> IRCMessage: s = s.strip() if not s: return IRCMessage( - raw=raw, prefix=prefix, command="", params=(), trailing=trailing) + raw=raw, prefix=prefix, command="", params=(), trailing=trailing + ) bits = s.split() command = bits[0] params = tuple(bits[1:]) if len(bits) > 1 else () return IRCMessage( - raw=raw, prefix=prefix, command=command, params=params, - trailing=trailing) + raw=raw, + prefix=prefix, + command=command, + params=params, + trailing=trailing, + ) def is_ping(msg: IRCMessage) -> bool: diff --git a/libs/apprise/plugins/irc/state.py b/libs/apprise/plugins/irc/state.py index db4c1e3c3..5b3b8bf55 100644 --- a/libs/apprise/plugins/irc/state.py +++ b/libs/apprise/plugins/irc/state.py @@ -126,10 +126,12 @@ class IRCStateMachine: self.state = IRCState.REGISTERING out: list[IRCAction] = [] if self.ctx.password: - out.append(IRCAction( - IRCActionKind.SEND, line=f"PASS {self.ctx.password}")) - out.append(IRCAction( - IRCActionKind.SEND, line=f"NICK {self.ctx.desired_nick}")) + out.append( + IRCAction(IRCActionKind.SEND, line=f"PASS {self.ctx.password}") + ) + out.append( + IRCAction(IRCActionKind.SEND, line=f"NICK {self.ctx.desired_nick}") + ) out.append( IRCAction( IRCActionKind.SEND, @@ -150,12 +152,17 @@ class IRCStateMachine: if n in REGISTER_ERRORS: self.ctx.last_error = f"{REGISTER_ERRORS[n]}: {_err(msg)}" self.state = IRCState.ERROR - return [IRCAction( - IRCActionKind.FAIL, reason=self.ctx.last_error)] + return [ + IRCAction(IRCActionKind.FAIL, reason=self.ctx.last_error) + ] if n in (432, 433): - actions.append(IRCAction( - IRCActionKind.SEND, line=f"NICK {self.ctx.desired_nick}")) + actions.append( + IRCAction( + IRCActionKind.SEND, + line=f"NICK {self.ctx.desired_nick}", + ) + ) return actions if n == 1: @@ -176,8 +183,9 @@ class IRCStateMachine: if n in JOIN_ERRORS: self.ctx.last_error = f"{JOIN_ERRORS[n]}: {_err(msg)}" self.state = IRCState.ERROR - return [IRCAction( - IRCActionKind.FAIL, reason=self.ctx.last_error)] + return [ + IRCAction(IRCActionKind.FAIL, reason=self.ctx.last_error) + ] if n == 443 and len(msg.params) >= 2: chan = msg.params[1] @@ -201,13 +209,15 @@ class IRCStateMachine: return actions def request_join( - self, channel: str, key: Optional[str] = None) -> list[IRCAction]: + self, channel: str, key: Optional[str] = None + ) -> list[IRCAction]: """Request a channel join and enter JOINING state.""" self.state = IRCState.JOINING if key: - return [IRCAction( - IRCActionKind.SEND, line=f"JOIN {channel} {key}")] + return [ + IRCAction(IRCActionKind.SEND, line=f"JOIN {channel} {key}") + ] return [IRCAction(IRCActionKind.SEND, line=f"JOIN {channel}")] def request_quit(self, message: str) -> list[IRCAction]: diff --git a/libs/apprise/plugins/irc/templates.py b/libs/apprise/plugins/irc/templates.py index 81eb96637..760eefdb3 100644 --- a/libs/apprise/plugins/irc/templates.py +++ b/libs/apprise/plugins/irc/templates.py @@ -57,7 +57,6 @@ IRC_TEMPLATES = ( "mode": IRCAuthMode.NICKSERV, }, ), - # Libera.Chat: TLS default 6697, NickServ common (SASL also common but # not implemented here) ( @@ -69,7 +68,6 @@ IRC_TEMPLATES = ( "mode": IRCAuthMode.NICKSERV, }, ), - # EFnet: traditionally plain 6667, auth varies widely ( "EFnet", diff --git a/libs/apprise/plugins/jira.py b/libs/apprise/plugins/jira.py new file mode 100644 index 000000000..9014c855e --- /dev/null +++ b/libs/apprise/plugins/jira.py @@ -0,0 +1,912 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Knowing this, you can build your Jira URL as follows: +# jira://{apikey}/ +# jira://{apikey}/@{user} +# jira://{apikey}/*{schedule} +# jira://{apikey}/^{escalation} +# jira://{apikey}/#{team} +# +# You can mix and match what you want to notify freely +# jira://{apikey}/@{user}/#{team}/*{schedule}/^{escalation} +# +# If no target prefix is specified, then it is assumed to be a user. +# +# API Documentation: \ +# https://developer.atlassian.com/cloud/jira/ \ +# service-desk-ops/rest/v1/api-group-integration-events/ + +import hashlib +from json import dumps, loads + +import requests + +from ..common import NotifyType, PersistentStoreMode +from ..locale import gettext_lazy as _ +from ..utils.parse import is_uuid, parse_bool, parse_list, validate_regex +from .base import NotifyBase + + +class JiraCategory(NotifyBase): + """ + We define the different category types that we can notify + """ + + USER = "user" + SCHEDULE = "schedule" + ESCALATION = "escalation" + TEAM = "team" + + +JIRA_CATEGORIES = ( + JiraCategory.USER, + JiraCategory.SCHEDULE, + JiraCategory.ESCALATION, + JiraCategory.TEAM, +) + + +class JiraAlertAction: + """ + Defines the supported actions + """ + + # Use mapping (specify :key=arg to over-ride) + MAP = "map" + + # Create new alert (default) + NEW = "new" + + # Close Alert + CLOSE = "close" + + # Delete Alert + DELETE = "delete" + + # Acknowledge Alert + ACKNOWLEDGE = "acknowledge" + + # Add note to alert + NOTE = "note" + + +JIRA_ACTIONS = ( + JiraAlertAction.MAP, + JiraAlertAction.NEW, + JiraAlertAction.CLOSE, + JiraAlertAction.DELETE, + JiraAlertAction.ACKNOWLEDGE, + JiraAlertAction.NOTE, +) + + +# Regions +class JiraRegion: + US = "us" + EU = "eu" + + +# Jira APIs (OpsGenie port - so keep us/en support for easy transition) +JIRA_API_LOOKUP = { + JiraRegion.US: "https://api.atlassian.com/jsm/ops/integration/v2/alerts", + JiraRegion.EU: "https://api.atlassian.com/jsm/ops/integration/v2/alerts", +} + +# A List of our regions we can use for verification +JIRA_REGIONS = ( + JiraRegion.US, + JiraRegion.EU, +) + + +# Priorities +class JiraPriority: + LOW = 1 + MODERATE = 2 + NORMAL = 3 + HIGH = 4 + EMERGENCY = 5 + + +JIRA_PRIORITIES = { + # Note: This also acts as a reverse lookup mapping + JiraPriority.LOW: "low", + JiraPriority.MODERATE: "moderate", + JiraPriority.NORMAL: "normal", + JiraPriority.HIGH: "high", + JiraPriority.EMERGENCY: "emergency", +} + +JIRA_PRIORITY_MAP = { + # Maps against string 'low' + "l": JiraPriority.LOW, + # Maps against string 'moderate' + "m": JiraPriority.MODERATE, + # Maps against string 'normal' + "n": JiraPriority.NORMAL, + # Maps against string 'high' + "h": JiraPriority.HIGH, + # Maps against string 'emergency' + "e": JiraPriority.EMERGENCY, + # Entries to additionally support (so more like Jira's API) + "1": JiraPriority.LOW, + "2": JiraPriority.MODERATE, + "3": JiraPriority.NORMAL, + "4": JiraPriority.HIGH, + "5": JiraPriority.EMERGENCY, + # Support p-prefix + "p1": JiraPriority.LOW, + "p2": JiraPriority.MODERATE, + "p3": JiraPriority.NORMAL, + "p4": JiraPriority.HIGH, + "p5": JiraPriority.EMERGENCY, +} + + +class NotifyJira(NotifyBase): + """ + A wrapper for Jira Notifications + """ + + # The default descriptive name associated with the Notification + service_name = "Jira" + + # The services URL + service_url = "https://atlassian.com/" + + # All notification requests are secure + secure_protocol = "jira" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://github.com/caronc/apprise/wiki/Notify_jira" + + # The maximum length of the body + body_maxlen = 15000 + + # Our default is to no not use persistent storage beyond in-memory + # reference + storage_mode = PersistentStoreMode.AUTO + + # If we don't have the specified min length, then we don't bother using + # the body directive + jira_body_minlen = 130 + + # The default region to use if one isn't otherwise specified + jira_default_region = JiraRegion.US + + # The maximum allowable targets within a notification + default_batch_size = 50 + + # Defines our default message mapping + jira_message_map = { + # Add a note to an existing alert + NotifyType.INFO: JiraAlertAction.NOTE, + # Close existing alert + NotifyType.SUCCESS: JiraAlertAction.CLOSE, + # Create notice + NotifyType.WARNING: JiraAlertAction.NEW, + # Create notice + NotifyType.FAILURE: JiraAlertAction.NEW, + } + + # Define object templates + templates = ( + "{schema}://{apikey}", + "{schema}://{user}@{apikey}", + "{schema}://{apikey}/{targets}", + "{schema}://{user}@{apikey}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "apikey": { + "name": _("API Key"), + "type": "string", + "private": True, + "required": True, + }, + "user": { + "name": _("Username"), + "type": "string", + }, + "target_escalation": { + "name": _("Target Escalation"), + "prefix": "^", + "type": "string", + "map_to": "targets", + }, + "target_schedule": { + "name": _("Target Schedule"), + "type": "string", + "prefix": "*", + "map_to": "targets", + }, + "target_user": { + "name": _("Target User"), + "type": "string", + "prefix": "@", + "map_to": "targets", + }, + "target_team": { + "name": _("Target Team"), + "type": "string", + "prefix": "#", + "map_to": "targets", + }, + "targets": { + "name": _("Targets "), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "region": { + "name": _("Region Name"), + "type": "choice:string", + "values": JIRA_REGIONS, + "default": JiraRegion.US, + "map_to": "region_name", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, + "priority": { + "name": _("Priority"), + "type": "choice:int", + "values": JIRA_PRIORITIES, + "default": JiraPriority.NORMAL, + }, + "entity": { + "name": _("Entity"), + "type": "string", + }, + "alias": { + "name": _("Alias"), + "type": "string", + }, + "tags": { + "name": _("Tags"), + "type": "string", + }, + "to": { + "alias_of": "targets", + }, + "action": { + "name": _("Action"), + "type": "choice:string", + "values": JIRA_ACTIONS, + "default": JIRA_ACTIONS[0], + }, + }, + ) + + # Map of key-value pairs to use as custom properties of the alert. + template_kwargs = { + "details": { + "name": _("Details"), + "prefix": "+", + }, + "mapping": { + "name": _("Action Mapping"), + "prefix": ":", + }, + } + + def __init__( + self, + apikey, + targets, + region_name=None, + details=None, + priority=None, + alias=None, + entity=None, + batch=False, + tags=None, + action=None, + mapping=None, + **kwargs, + ): + """ + Initialize Jira Object + """ + super().__init__(**kwargs) + + # API Key (associated with project) + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = "An invalid Jira API Key ({}) was specified.".format(apikey) + self.logger.warning(msg) + raise TypeError(msg) + + # The Priority of the message + self.priority = ( + NotifyJira.template_args["priority"]["default"] + if not priority + else next( + ( + v + for k, v in JIRA_PRIORITY_MAP.items() + if str(priority).lower().startswith(k) + ), + NotifyJira.template_args["priority"]["default"], + ) + ) + + # Store our region + try: + self.region_name = ( + self.jira_default_region + if region_name is None + else region_name.lower() + ) + + if self.region_name not in JIRA_REGIONS: + # allow the outer except to handle this common response + raise + except: + # Invalid region specified + msg = "The Jira region specified ({}) is invalid.".format( + region_name + ) + self.logger.warning(msg) + raise TypeError(msg) from None + + if action and isinstance(action, str): + self.action = next( + (a for a in JIRA_ACTIONS if a.startswith(action)), None + ) + if self.action not in JIRA_ACTIONS: + msg = "The Jira action specified ({}) is invalid.".format( + action + ) + self.logger.warning(msg) + raise TypeError(msg) + else: + self.action = self.template_args["action"]["default"] + + # Store our mappings + self.mapping = self.jira_message_map.copy() + if mapping and isinstance(mapping, dict): + for _k, _v in mapping.items(): + # Get our mapping + k = next((t for t in NotifyType if t.startswith(_k)), None) + if not k: + msg = ( + "The Jira mapping key specified ({}) " + "is invalid.".format(_k) + ) + self.logger.warning(msg) + raise TypeError(msg) + + _v_lower = _v.lower() + v = next( + (v for v in JIRA_ACTIONS[1:] if v.startswith(_v_lower)), + None, + ) + if not v: + msg = ( + "The Jira mapping value (assigned to {}) " + "specified ({}) is invalid.".format(k, _v) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Update our mapping + self.mapping[k] = v + + self.details = {} + if details: + # Store our extra details + self.details.update(details) + + # Prepare Batch Mode Flag + self.batch_size = self.default_batch_size if batch else 1 + + # Assign our tags (if defined) + self.__tags = parse_list(tags) + + # Assign our entity (if defined) + self.entity = entity + + # Assign our alias (if defined) + self.alias = alias + + # Initialize our Targets + self.targets = [] + + # Sort our targets + for _target in parse_list(targets): + target = _target.strip() + if len(target) < 2: + self.logger.debug("Ignoring Jira Entry: {}".format(target)) + continue + + if target.startswith( + NotifyJira.template_tokens["target_team"]["prefix"] + ): + self.targets.append( + {"type": JiraCategory.TEAM, "id": target[1:]} + if is_uuid(target[1:]) + else {"type": JiraCategory.TEAM, "name": target[1:]} + ) + + elif target.startswith( + NotifyJira.template_tokens["target_schedule"]["prefix"] + ): + self.targets.append( + {"type": JiraCategory.SCHEDULE, "id": target[1:]} + if is_uuid(target[1:]) + else {"type": JiraCategory.SCHEDULE, "name": target[1:]} + ) + + elif target.startswith( + NotifyJira.template_tokens["target_escalation"]["prefix"] + ): + self.targets.append( + {"type": JiraCategory.ESCALATION, "id": target[1:]} + if is_uuid(target[1:]) + else {"type": JiraCategory.ESCALATION, "name": target[1:]} + ) + + elif target.startswith( + NotifyJira.template_tokens["target_user"]["prefix"] + ): + self.targets.append( + {"type": JiraCategory.USER, "id": target[1:]} + if is_uuid(target[1:]) + else {"type": JiraCategory.USER, "username": target[1:]} + ) + + else: + # Ambiguious entry; treat it as a user but not before + # displaying a warning to the end user first: + self.logger.debug( + "Treating ambigious Jira target %s as a user", target + ) + self.targets.append( + {"type": JiraCategory.USER, "id": target} + if is_uuid(target) + else {"type": JiraCategory.USER, "username": target} + ) + + def _fetch(self, method, url, payload, params=None): + """ + Performs server retrieval/update and returns JSON Response + """ + headers = { + "User-Agent": self.app_id, + "Accepts": "application/json", + "Content-Type": "application/json", + "Authorization": "GenieKey {}".format(self.apikey), + } + + # Some Debug Logging + self.logger.debug( + "Jira POST URL: {} (cert_verify={})".format( + url, self.verify_certificate + ) + ) + self.logger.debug("Jira Payload: {}".format(payload)) + + # Initialize our response object + content = {} + + # Always call throttle before any remote server i/o is made + self.throttle() + try: + r = method( + url, + data=dumps(payload), + params=params, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + # A Response might look like: + # { + # "result": "Request will be processed", + # "took": 0.302, + # "requestId": "43a29c5c-3dbf-4fa4-9c26-f4f71023e120" + # } + + try: + # Update our response object + content = loads(r.content) + + except (AttributeError, TypeError, ValueError): + # ValueError = r.content is Unparsable + # TypeError = r.content is None + # AttributeError = r is None + content = {} + + if r.status_code not in ( + requests.codes.accepted, + requests.codes.ok, + ): + status_str = NotifyBase.http_response_code_lookup( + r.status_code + ) + + self.logger.warning( + "Failed to send Jira notification:{}{}error={}.".format( + status_str, ", " if status_str else "", r.status_code + ) + ) + + self.logger.debug("Response Details:\r\n{}".format(r.content)) + + return (False, content.get("requestId")) + + # If we reach here; the message was sent + self.logger.info("Sent Jira notification") + self.logger.debug("Response Details:\r\n{}".format(r.content)) + + return (True, content.get("requestId")) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Jira notification." + ) + self.logger.debug("Socket Exception: {}".format(str(e))) + + return (False, content.get("requestId")) + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """ + Perform Jira Notification + """ + + # Get our Jira Action + action = ( + self.mapping[notify_type] + if self.action == JiraAlertAction.MAP + else self.action + ) + + # Prepare our URL as it's based on our hostname + notify_url = JIRA_API_LOOKUP[self.region_name] + + # Initialize our has_error flag + has_error = False + + # Default method is to post + method = requests.post + + # For indexing in persistent store + key = hashlib.sha1( + ( + self.entity + if self.entity + else ( + self.alias + if self.alias + else (title if title else self.app_id) + ) + ).encode("utf-8") + ).hexdigest()[0:10] + + # Get our Jira Request IDs + request_ids = self.store.get(key, []) + if not isinstance(request_ids, list): + request_ids = [] + + if action == JiraAlertAction.NEW: + # Create a copy ouf our details object + details = self.details.copy() + if "type" not in details: + details["type"] = notify_type + + # Use body if title not set + title_body = title if title else body + + # Prepare our payload + payload = { + "source": self.app_desc, + "message": title_body, + "description": body, + "details": details, + "priority": "P{}".format(self.priority), + } + + # Use our body directive if we exceed the minimum message + # limitation + if len(payload["message"]) > self.jira_body_minlen: + payload["message"] = "{}...".format( + title_body[: self.jira_body_minlen - 3] + ) + + if self.__tags: + payload["tags"] = self.__tags + + if self.entity: + payload["entity"] = self.entity + + if self.alias: + payload["alias"] = self.alias + + if self.user: + payload["user"] = self.user + + # reset our request IDs - we will re-populate them + request_ids = [] + + length = len(self.targets) if self.targets else 1 + for index in range(0, length, self.batch_size): + if self.targets: + # If there were no targets identified, then we simply + # just iterate once without the responders set + payload["responders"] = self.targets[ + index : index + self.batch_size + ] + + # Perform our post + success, request_id = self._fetch(method, notify_url, payload) + + if success and request_id: + # Save our response + request_ids.append(request_id) + + else: + has_error = True + + # Store our entries for a maximum of 60 days + self.store.set(key, request_ids, expires=60 * 60 * 24 * 60) + + elif request_ids: + # Prepare our payload + payload = { + "source": self.app_desc, + "note": body, + } + + if self.user: + payload["user"] = self.user + + # Prepare our Identifier type + params = { + "identifierType": "id", + } + + for request_id in request_ids: + if action == JiraAlertAction.DELETE: + # Update our URL + url = f"{notify_url}/{request_id}" + method = requests.delete + + elif action == JiraAlertAction.ACKNOWLEDGE: + url = f"{notify_url}/{request_id}/acknowledge" + + elif action == JiraAlertAction.CLOSE: + url = f"{notify_url}/{request_id}/close" + + else: # action == JiraAlertAction.NOTE: + url = f"{notify_url}/{request_id}/notes" + + # Perform our post + success, _ = self._fetch(method, url, payload, params) + + if not success: + has_error = True + + if not has_error and action == JiraAlertAction.DELETE: + # Remove cached entry + self.store.clear(key) + + else: + self.logger.info( + "No Jira notification sent due to (nothing to %s) condition", + self.action, + ) + + return not has_error + + @property + def url_identifier(self): + """ + Returns all of the identifiers that make this URL unique from + another simliar one. Targets or end points should never be identified + here. + """ + return (self.secure_protocol, self.region_name, self.apikey) + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = { + "action": self.action, + "region": self.region_name, + "priority": JIRA_PRIORITIES[ + self.template_args["priority"]["default"] + ] + if self.priority not in JIRA_PRIORITIES + else JIRA_PRIORITIES[self.priority], + "batch": "yes" if self.batch_size > 1 else "no", + } + + # Assign our entity value (if defined) + if self.entity: + params["entity"] = self.entity + + # Assign our alias value (if defined) + if self.alias: + params["alias"] = self.alias + + # Assign our tags (if specifed) + if self.__tags: + params["tags"] = ",".join(self.__tags) + + # Append our details into our parameters + params.update({"+{}".format(k): v for k, v in self.details.items()}) + + # Append our assignment extra's into our parameters + params.update( + {":{}".format(k.value): v for k, v in self.mapping.items()} + ) + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # A map allows us to map our target types so they can be correctly + # placed back into your URL below. Hence map the 'user' -> '@' + __map = { + JiraCategory.USER: NotifyJira.template_tokens["target_user"][ + "prefix" + ], + JiraCategory.SCHEDULE: NotifyJira.template_tokens[ + "target_schedule" + ]["prefix"], + JiraCategory.ESCALATION: NotifyJira.template_tokens[ + "target_escalation" + ]["prefix"], + JiraCategory.TEAM: NotifyJira.template_tokens["target_team"][ + "prefix" + ], + } + + return "{schema}://{user}{apikey}/{targets}/?{params}".format( + schema=self.secure_protocol, + user="{}@".format(self.user) if self.user else "", + apikey=self.pprint(self.apikey, privacy, safe=""), + targets="/".join( + [ + NotifyJira.quote( + "{}{}".format( + __map[x["type"]], + x.get("id", x.get("name", x.get("username"))), + ) + ) + for x in self.targets + ] + ), + params=NotifyJira.urlencode(params, safe=":"), + ) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + # + # Factor batch into calculation + # + targets = len(self.targets) + if self.batch_size > 1: + targets = int(targets / self.batch_size) + ( + 1 if targets % self.batch_size else 0 + ) + + return targets if targets > 0 else 1 + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # The API Key is stored in the hostname + results["apikey"] = NotifyJira.unquote(results["host"]) + + # Get our Targets + results["targets"] = NotifyJira.split_path(results["fullpath"]) + + # Add our Meta Detail keys + results["details"] = { + NotifyBase.unquote(x): NotifyBase.unquote(y) + for x, y in results["qsd+"].items() + } + + # Set our priority + if "priority" in results["qsd"] and len(results["qsd"]["priority"]): + results["priority"] = NotifyJira.unquote( + results["qsd"]["priority"] + ) + + # Get Batch Boolean (if set) + results["batch"] = parse_bool( + results["qsd"].get( + "batch", NotifyJira.template_args["batch"]["default"] + ) + ) + + if "apikey" in results["qsd"] and len(results["qsd"]["apikey"]): + results["apikey"] = NotifyJira.unquote(results["qsd"]["apikey"]) + + if "tags" in results["qsd"] and len(results["qsd"]["tags"]): + # Extract our tags + results["tags"] = parse_list( + NotifyJira.unquote(results["qsd"]["tags"]) + ) + + if "region" in results["qsd"] and len(results["qsd"]["region"]): + # Extract our region + results["region_name"] = NotifyJira.unquote( + results["qsd"]["region"] + ) + + if "entity" in results["qsd"] and len(results["qsd"]["entity"]): + # Extract optional entity field + results["entity"] = NotifyJira.unquote(results["qsd"]["entity"]) + + if "alias" in results["qsd"] and len(results["qsd"]["alias"]): + # Extract optional alias field + results["alias"] = NotifyJira.unquote(results["qsd"]["alias"]) + + # Handle 'to' email address + if "to" in results["qsd"] and len(results["qsd"]["to"]): + results["targets"].append(results["qsd"]["to"]) + + # Store our action (if defined) + if "action" in results["qsd"] and len(results["qsd"]["action"]): + results["action"] = NotifyJira.unquote(results["qsd"]["action"]) + + # store any custom mapping defined + results["mapping"] = { + NotifyJira.unquote(x): NotifyJira.unquote(y) + for x, y in results["qsd:"].items() + } + + return results diff --git a/libs/apprise/plugins/join.py b/libs/apprise/plugins/join.py index 3790e2d7e..a3420de64 100644 --- a/libs/apprise/plugins/join.py +++ b/libs/apprise/plugins/join.py @@ -171,6 +171,7 @@ class NotifyJoin(NotifyBase): "targets": { "name": _("Targets"), "type": "list:string", + "required": True, }, }, ) @@ -316,6 +317,7 @@ class NotifyJoin(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -335,7 +337,8 @@ class NotifyJoin(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/kavenegar.py b/libs/apprise/plugins/kavenegar.py index 838e9863b..a8a7e20f3 100644 --- a/libs/apprise/plugins/kavenegar.py +++ b/libs/apprise/plugins/kavenegar.py @@ -246,6 +246,7 @@ class NotifyKavenegar(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -282,7 +283,8 @@ class NotifyKavenegar(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/xbmc.py b/libs/apprise/plugins/kodi.py similarity index 98% rename from libs/apprise/plugins/xbmc.py rename to libs/apprise/plugins/kodi.py index 20a9a7203..28db2df33 100644 --- a/libs/apprise/plugins/xbmc.py +++ b/libs/apprise/plugins/kodi.py @@ -52,10 +52,10 @@ class NotifyXBMC(NotifyBase): kodi_secure_protocol = "kodis" # The default protocols - protocol = (xbmc_protocol, kodi_protocol) + protocol = (kodi_protocol, xbmc_protocol) # The default secure protocols - secure_protocol = (xbmc_secure_protocol, kodi_secure_protocol) + secure_protocol = (kodi_secure_protocol, xbmc_secure_protocol) # A URL that takes you to the setup/help of the specific protocol setup_url = "https://appriseit.com/services/kodi/" @@ -274,6 +274,7 @@ class NotifyXBMC(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -289,7 +290,8 @@ class NotifyXBMC(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/kumulos.py b/libs/apprise/plugins/kumulos.py index f11b4a23b..0ac963b0e 100644 --- a/libs/apprise/plugins/kumulos.py +++ b/libs/apprise/plugins/kumulos.py @@ -166,6 +166,7 @@ class NotifyKumulos(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -181,7 +182,8 @@ class NotifyKumulos(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False diff --git a/libs/apprise/plugins/lametric.py b/libs/apprise/plugins/lametric.py index 4f2ddbd28..51e570962 100644 --- a/libs/apprise/plugins/lametric.py +++ b/libs/apprise/plugins/lametric.py @@ -678,9 +678,11 @@ class NotifyLametric(NotifyBase): """Return URL and payload for cloud directed requests.""" # Update header entries - headers.update({ - "X-Access-Token": self.lametric_apikey, - }) + headers.update( + { + "X-Access-Token": self.lametric_apikey, + } + ) if self.sound: self.logger.warning( @@ -714,11 +716,13 @@ class NotifyLametric(NotifyBase): # Cloud Notifications don't have as much functionality # You can not set priority and/or sound payload = { - "frames": [{ - "icon": icon, - "text": body, - "index": 0, - }] + "frames": [ + { + "icon": icon, + "text": body, + "index": 0, + } + ] } # Prepare our Cloud Notify URL @@ -756,10 +760,12 @@ class NotifyLametric(NotifyBase): # cycles is set to 0, notification will stay on the screen # until user dismisses it manually. By default it is set to 1. "cycles": self.cycles, - "frames": [{ - "icon": icon, - "text": body, - }], + "frames": [ + { + "icon": icon, + "text": body, + } + ], }, } @@ -832,6 +838,7 @@ class NotifyLametric(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # An ideal response would be: # { @@ -857,7 +864,8 @@ class NotifyLametric(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -1032,13 +1040,11 @@ class NotifyLametric(NotifyBase): # App ID if "app" in results["qsd"] and results["qsd"]["app"]: - # Extract the App ID from an argument results["app_id"] = NotifyLametric.unquote(results["qsd"]["app"]) # App Version if "app_ver" in results["qsd"] and results["qsd"]["app_ver"]: - # Extract the App ID from an argument results["app_ver"] = NotifyLametric.unquote( results["qsd"]["app_ver"] diff --git a/libs/apprise/plugins/lark.py b/libs/apprise/plugins/lark.py index 4681e5392..53b3b587f 100644 --- a/libs/apprise/plugins/lark.py +++ b/libs/apprise/plugins/lark.py @@ -128,6 +128,7 @@ class NotifyLark(NotifyBase): data=json.dumps(payload), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: self.logger.warning( diff --git a/libs/apprise/plugins/line.py b/libs/apprise/plugins/line.py index 76ceffac5..81d583957 100644 --- a/libs/apprise/plugins/line.py +++ b/libs/apprise/plugins/line.py @@ -156,13 +156,15 @@ class NotifyLine(NotifyBase): # Prepare our persistent_notification.create payload payload = { "to": None, - "messages": [{ - "type": "text", - "text": body, - "sender": { - "name": self.app_id, - }, - }], + "messages": [ + { + "type": "text", + "text": body, + "sender": { + "name": self.app_id, + }, + } + ], } # Acquire our image url if configured to do so @@ -195,6 +197,7 @@ class NotifyLine(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -213,7 +216,8 @@ class NotifyLine(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/mailgun.py b/libs/apprise/plugins/mailgun.py index 9186c0b8a..30ceeb04f 100644 --- a/libs/apprise/plugins/mailgun.py +++ b/libs/apprise/plugins/mailgun.py @@ -124,8 +124,8 @@ class NotifyMailgun(NotifyBase): # Define object templates templates = ( - "{schema}://{user}@{host}:{apikey}/", - "{schema}://{user}@{host}:{apikey}/{targets}", + "{schema}://{user}@{host}/{apikey}/", + "{schema}://{user}@{host}/{apikey}/{targets}", ) # Define our template tokens @@ -305,10 +305,12 @@ class NotifyMailgun(NotifyBase): for recipient in parse_emails(targets): result = is_email(recipient) if result: - self.targets.append(( - result["name"] if result["name"] else False, - result["full_email"], - )) + self.targets.append( + ( + result["name"] if result["name"] else False, + result["full_email"], + ) + ) continue self.logger.warning( @@ -504,13 +506,15 @@ class NotifyMailgun(NotifyBase): if cc: # Format our cc addresses to support the Name field - payload["cc"] = ",".join([ - formataddr( - (self.names.get(addr, False), addr), - charset="utf-8", - ) - for addr in cc - ]) + payload["cc"] = ",".join( + [ + formataddr( + (self.names.get(addr, False), addr), + charset="utf-8", + ) + for addr in cc + ] + ) # Format our bcc addresses to support the Name field if bcc: @@ -535,14 +539,11 @@ class NotifyMailgun(NotifyBase): # For logging output of success and errors; we get a head count # of our outbound details: + _batch = self.targets[index : index + batch_size] verbose_dest = ( - ", ".join( - [x[1] for x in self.targets[index : index + batch_size]] - ) - if len(self.targets[index : index + batch_size]) <= 3 - else ( - f"{len(self.targets[index:index + batch_size])} recipients" - ) + ", ".join([x[1] for x in _batch]) + if len(_batch) <= 3 + else f"{len(_batch)} recipients" ) # Always call throttle before any remote server i/o is made @@ -556,6 +557,7 @@ class NotifyMailgun(NotifyBase): files=files if files else None, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -575,7 +577,8 @@ class NotifyMailgun(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -589,8 +592,7 @@ class NotifyMailgun(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending" - f" Mailgun:{verbose_dest} " - + "notification." + f" Mailgun:{verbose_dest} " + "notification." ) self.logger.debug(f"Socket Exception: {e!s}") @@ -653,13 +655,15 @@ class NotifyMailgun(NotifyBase): if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - "{}{}".format( - "" if not e not in self.names else f"{self.names[e]}:", - e, - ) - for e in self.cc - ]) + params["cc"] = ",".join( + [ + "{}{}".format( + "" if not e not in self.names else f"{self.names[e]}:", + e, + ) + for e in self.cc + ] + ) if self.bcc: # Handle our Blind Carbon Copy Addresses @@ -679,13 +683,17 @@ class NotifyMailgun(NotifyBase): targets=( "" if not has_targets - else "/".join([ - NotifyMailgun.quote( - "{}{}".format("" if not e[0] else f"{e[0]}:", e[1]), - safe="", - ) - for e in self.targets - ]) + else "/".join( + [ + NotifyMailgun.quote( + "{}{}".format( + "" if not e[0] else f"{e[0]}:", e[1] + ), + safe="", + ) + for e in self.targets + ] + ) ), params=NotifyMailgun.urlencode(params), ) diff --git a/libs/apprise/plugins/mastodon.py b/libs/apprise/plugins/mastodon.py index 161326bf3..a7d61520a 100644 --- a/libs/apprise/plugins/mastodon.py +++ b/libs/apprise/plugins/mastodon.py @@ -53,6 +53,18 @@ USER_DETECTION_RE = re.compile( r"(@[A-Z0-9_]+(?:@[A-Z0-9_.-]+)?)(?=$|[\s,.&()\[\]]+)", re.I ) +MENTION_DETECTION_RE = re.compile( + r"(?]+)", + re.I, +) + +HASHTAG_DETECTION_RE = re.compile( + r"(? -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# Great sources -# - https://github.com/matrix-org/matrix-python-sdk -# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst -# -from json import dumps, loads -import re -from time import time -import uuid - -from markdown import markdown -import requests - -from ..common import ( - NotifyFormat, - NotifyImageSize, - NotifyType, - PersistentStoreMode, -) -from ..exception import AppriseException -from ..locale import gettext_lazy as _ -from ..url import PrivacyMode -from ..utils.parse import is_hostname, parse_bool, parse_list, validate_regex -from .base import NotifyBase - -# Define default path -MATRIX_V1_WEBHOOK_PATH = "/api/v1/matrix/hook" -MATRIX_V2_API_PATH = "/_matrix/client/r0" -MATRIX_V3_API_PATH = "/_matrix/client/v3" -MATRIX_V3_MEDIA_PATH = "/_matrix/media/v3" -MATRIX_V2_MEDIA_PATH = "/_matrix/media/r0" - - -class MatrixDiscoveryException(AppriseException): - """Apprise Matrix Exception Class.""" - - -# Extend HTTP Error Messages -MATRIX_HTTP_ERROR_MAP = { - 403: "Unauthorized - Invalid Token.", - 429: "Rate limit imposed; wait 2s and try again", -} - -# Matrix Room Syntax -IS_ROOM_ALIAS = re.compile( - r"^\s*(#|%23)?(?P[A-Za-z0-9._=-]+)((:|%3A)" - r"(?P[A-Za-z0-9.-]+))?\s*$", - re.I, -) - -# Room ID MUST start with an exclamation to avoid ambiguity -IS_ROOM_ID = re.compile( - r"^\s*(!|!|%21)(?P[A-Za-z0-9._=-]+)((:|%3A)" - r"(?P[A-Za-z0-9.-]+))?\s*$", - re.I, -) - - -# Matrix is_image check -IS_IMAGE = re.compile(r"^image/.*", re.I) - - -class MatrixMessageType: - """The Matrix Message types.""" - - TEXT = "text" - NOTICE = "notice" - - -# matrix message types are placed into this list for validation purposes -MATRIX_MESSAGE_TYPES = ( - MatrixMessageType.TEXT, - MatrixMessageType.NOTICE, -) - - -class MatrixVersion: - # Version 2 - V2 = "2" - - # Version 3 - V3 = "3" - - -# webhook modes are placed into this list for validation purposes -MATRIX_VERSIONS = ( - MatrixVersion.V2, - MatrixVersion.V3, -) - - -class MatrixWebhookMode: - # Webhook Mode is disabled - DISABLED = "off" - - # The default webhook mode is to just be set to Matrix - MATRIX = "matrix" - - # Support the slack webhook plugin - SLACK = "slack" - - # Support the t2bot webhook plugin - T2BOT = "t2bot" - - -# webhook modes are placed into this list for validation purposes -MATRIX_WEBHOOK_MODES = ( - MatrixWebhookMode.DISABLED, - MatrixWebhookMode.MATRIX, - MatrixWebhookMode.SLACK, - MatrixWebhookMode.T2BOT, -) - - -class NotifyMatrix(NotifyBase): - """A wrapper for Matrix Notifications.""" - - # The default descriptive name associated with the Notification - service_name = "Matrix" - - # The services URL - service_url = "https://matrix.org/" - - # The default protocol - protocol = "matrix" - - # The default secure protocol - secure_protocol = "matrixs" - - # Support Attachments - attachment_support = True - - # A URL that takes you to the setup/help of the specific protocol - setup_url = "https://appriseit.com/services/matrix/" - - # Allows the user to specify the NotifyImageSize object - image_size = NotifyImageSize.XY_32 - - # The maximum allowable characters allowed in the body per message - # https://spec.matrix.org/v1.6/client-server-api/#size-limits - # The complete event MUST NOT be larger than 65536 bytes, when formatted - # with the federation event format, including any signatures, and encoded - # as Canonical JSON. - # - # To gracefully allow for some overhead' we'll define a max body length - # of just slighty lower then the limit of the full message itself. - body_maxlen = 65000 - - # Throttle a wee-bit to avoid thrashing - request_rate_per_sec = 0.5 - - # How many retry attempts we'll make in the event the server asks us to - # throttle back. - default_retries = 2 - - # The number of micro seconds to wait if we get a 429 error code and - # the server doesn't remind us how long we should wait for - default_wait_ms = 1000 - - # Our default is to no not use persistent storage beyond in-memory - # reference - storage_mode = PersistentStoreMode.AUTO - - # Keep our cache for 20 days - default_cache_expiry_sec = 60 * 60 * 24 * 20 - - # Used for server discovery - discovery_base_key = "__discovery_base" - discovery_identity_key = "__discovery_identity" - - # Defines how long we cache our discovery for - discovery_cache_length_sec = 86400 - - # Define object templates - templates = ( - # Targets are ignored when using t2bot mode; only a token is required - "{schema}://{token}", - "{schema}://{user}@{token}", - # Matrix Server - "{schema}://{user}:{password}@{host}/{targets}", - "{schema}://{user}:{password}@{host}:{port}/{targets}", - "{schema}://{token}@{host}/{targets}", - "{schema}://{token}@{host}:{port}/{targets}", - # Webhook mode - "{schema}://{user}:{token}@{host}/{targets}", - "{schema}://{user}:{token}@{host}:{port}/{targets}", - ) - - # Define our template tokens - template_tokens = dict( - NotifyBase.template_tokens, - **{ - "host": { - "name": _("Hostname"), - "type": "string", - }, - "port": { - "name": _("Port"), - "type": "int", - "min": 1, - "max": 65535, - }, - "user": { - "name": _("Username"), - "type": "string", - }, - "password": { - "name": _("Password"), - "type": "string", - "private": True, - }, - "token": { - "name": _("Access Token"), - "private": True, - "map_to": "password", - }, - "target_user": { - "name": _("Target User"), - "type": "string", - "prefix": "@", - "map_to": "targets", - }, - "target_room_id": { - "name": _("Target Room ID"), - "type": "string", - "prefix": "!", - "map_to": "targets", - }, - "target_room_alias": { - "name": _("Target Room Alias"), - "type": "string", - "prefix": "#", - "map_to": "targets", - }, - "targets": { - "name": _("Targets"), - "type": "list:string", - }, - }, - ) - - # Define our template arguments - template_args = dict( - NotifyBase.template_args, - **{ - "image": { - "name": _("Include Image"), - "type": "bool", - "default": False, - "map_to": "include_image", - }, - "discovery": { - "name": _("Server Discovery"), - "type": "bool", - "default": True, - }, - "hsreq": { - "name": _("Force Home Server on Room IDs"), - "type": "bool", - "default": True, - }, - "mode": { - "name": _("Webhook Mode"), - "type": "choice:string", - "values": MATRIX_WEBHOOK_MODES, - "default": MatrixWebhookMode.DISABLED, - }, - "version": { - "name": _("Matrix API Verion"), - "type": "choice:string", - "values": MATRIX_VERSIONS, - "default": MatrixVersion.V3, - }, - "msgtype": { - "name": _("Message Type"), - "type": "choice:string", - "values": MATRIX_MESSAGE_TYPES, - "default": MatrixMessageType.TEXT, - }, - "to": { - "alias_of": "targets", - }, - "token": { - "alias_of": "token", - }, - }, - ) - - def __init__( - self, - targets=None, - mode=None, - msgtype=None, - version=None, - include_image=None, - discovery=None, - hsreq=None, - **kwargs, - ): - """Initialize Matrix Object.""" - super().__init__(**kwargs) - - # Prepare a list of rooms to connect and notify - self.rooms = parse_list(targets) - - # our home server gets populated after a login/registration - self.home_server = None - - # our user_id gets populated after a login/registration - self.user_id = None - - # This gets initialized after a login/registration - self.access_token = None - - # This gets incremented for each request made against the v3 API - self.transaction_id = 0 - - # Place an image inline with the message body - self.include_image = ( - self.template_args["image"]["default"] - if include_image is None - else include_image - ) - - # Prepare Delegate Server Lookup Check - self.discovery = ( - self.template_args["discovery"]["default"] - if discovery is None - else discovery - ) - - # When enabled, room IDs missing a ':homeserver' segment will - # be treated as legacy identifiers and automatically suffixed - # with the authenticated homeserver. - self.hsreq = ( - self.template_args["hsreq"]["default"] - if hsreq is None - else hsreq - ) - - # Setup our mode - self.mode = ( - self.template_args["mode"]["default"] - if not isinstance(mode, str) - else mode.lower() - ) - if self.mode and self.mode not in MATRIX_WEBHOOK_MODES: - msg = f"The mode specified ({mode}) is invalid." - self.logger.warning(msg) - raise TypeError(msg) - - # Setup our version - self.version = ( - self.template_args["version"]["default"] - if not isinstance(version, str) - else version - ) - if self.version not in MATRIX_VERSIONS: - msg = f"The version specified ({version}) is invalid." - self.logger.warning(msg) - raise TypeError(msg) - - # Setup our message type - self.msgtype = ( - self.template_args["msgtype"]["default"] - if not isinstance(msgtype, str) - else msgtype.lower() - ) - if self.msgtype and self.msgtype not in MATRIX_MESSAGE_TYPES: - msg = f"The msgtype specified ({msgtype}) is invalid." - self.logger.warning(msg) - raise TypeError(msg) - - if self.mode == MatrixWebhookMode.T2BOT: - # t2bot configuration requires that a webhook id is specified - self.access_token = validate_regex( - self.password, r"^[a-z0-9]{64}$", "i" - ) - if not self.access_token: - msg = ( - "An invalid T2Bot/Matrix Webhook ID " - f"({self.password}) was specified." - ) - self.logger.warning(msg) - raise TypeError(msg) - - elif not is_hostname(self.host): - msg = f"An invalid Matrix Hostname ({self.host}) was specified" - self.logger.warning(msg) - raise TypeError(msg) - - else: - # Verify port if specified - if self.port is not None and not ( - isinstance(self.port, int) - and self.port >= self.template_tokens["port"]["min"] - and self.port <= self.template_tokens["port"]["max"] - ): - msg = f"An invalid Matrix Port ({self.port}) was specified" - self.logger.warning(msg) - raise TypeError(msg) - - if self.mode != MatrixWebhookMode.DISABLED: - # Discovery only works when we're not using webhooks - self.discovery = False - - # - # Initialize from cache if present - # - if self.mode != MatrixWebhookMode.T2BOT: - # our home server gets populated after a login/registration - self.home_server = self.store.get("home_server") - - # our user_id gets populated after a login/registration - self.user_id = self.store.get("user_id") - - # This gets initialized after a login/registration - self.access_token = self.store.get("access_token") - - # This gets incremented for each request made against the v3 API - self.transaction_id = ( - 0 if not self.access_token else self.store.get("transaction_id", 0) - ) - - def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): - """Perform Matrix Notification.""" - - # Call the _send_ function applicable to whatever mode we're in - # - calls _send_webhook_notification if the mode variable is set - # - calls _send_server_notification if the mode variable is not set - return getattr( - self, - "_send_{}_notification".format( - "webhook" - if self.mode != MatrixWebhookMode.DISABLED - else "server" - ), - )(body=body, title=title, notify_type=notify_type, **kwargs) - - def _send_webhook_notification( - self, body, title="", notify_type=NotifyType.INFO, **kwargs - ): - """Perform Matrix Notification as a webhook.""" - - headers = { - "User-Agent": self.app_id, - "Content-Type": "application/json", - } - - if self.mode != MatrixWebhookMode.T2BOT: - # Acquire our access token from our URL - access_token = self.password if self.password else self.user - - # Prepare our URL - url = "{schema}://{hostname}{port}{webhook_path}/{token}".format( - schema="https" if self.secure else "http", - hostname=self.host, - port=("" if not self.port else f":{self.port}"), - webhook_path=MATRIX_V1_WEBHOOK_PATH, - token=access_token, - ) - - else: - # - # t2bot Setup - # - - # Prepare our URL - url = ( - "https://webhooks.t2bot.io/api/v1/matrix/hook/" - f"{self.access_token}" - ) - - # Retrieve our payload - payload = getattr(self, f"_{self.mode}_webhook_payload")( - body=body, title=title, notify_type=notify_type, **kwargs - ) - - self.logger.debug( - f"Matrix POST URL: {url} (cert_verify={self.verify_certificate!r})" - ) - self.logger.debug(f"Matrix Payload: {payload!s}") - - # Always call throttle before any remote server i/o is made - self.throttle() - - try: - r = requests.post( - url, - data=dumps(payload), - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - if r.status_code != requests.codes.ok: - # We had a problem - status_str = NotifyMatrix.http_response_code_lookup( - r.status_code, MATRIX_HTTP_ERROR_MAP - ) - - self.logger.warning( - "Failed to send Matrix notification: {}{}error={}.".format( - status_str, ", " if status_str else "", r.status_code - ) - ) - - self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) - - # Return; we're done - return False - - else: - self.logger.info("Sent Matrix notification.") - - except requests.RequestException as e: - self.logger.warning( - "A Connection error occurred sending Matrix notification." - ) - self.logger.debug(f"Socket Exception: {e!s}") - # Return; we're done - return False - - return True - - def _slack_webhook_payload( - self, body, title="", notify_type=NotifyType.INFO, **kwargs - ): - """Format the payload for a Slack based message.""" - - if not hasattr(self, "_re_slack_formatting_rules"): - # Prepare some one-time slack formatting variables - - self._re_slack_formatting_map = { - # New lines must become the string version - r"\r\*\n": "\\n", - # Escape other special characters - r"&": "&", - r"<": "<", - r">": ">", - } - - # Iterate over above list and store content accordingly - self._re_slack_formatting_rules = re.compile( - r"(" + "|".join(self._re_slack_formatting_map.keys()) + r")", - re.IGNORECASE, - ) - - # Perform Formatting - title = self._re_slack_formatting_rules.sub( # pragma: no branch - lambda x: self._re_slack_formatting_map[x.group()], - title, - ) - - body = self._re_slack_formatting_rules.sub( # pragma: no branch - lambda x: self._re_slack_formatting_map[x.group()], - body, - ) - - # prepare JSON Object - payload = { - "username": self.user if self.user else self.app_id, - # Use Markdown language - "mrkdwn": self.notify_format == NotifyFormat.MARKDOWN, - "attachments": [{ - "title": title, - "text": body, - "color": self.color(notify_type), - "ts": time(), - "footer": self.app_id, - }], - } - - return payload - - def _matrix_webhook_payload( - self, body, title="", notify_type=NotifyType.INFO, **kwargs - ): - """Format the payload for a Matrix based message.""" - - payload = { - "displayName": self.user if self.user else self.app_id, - "format": ( - "plain" if self.notify_format == NotifyFormat.TEXT else "html" - ), - "text": "", - } - - if self.notify_format == NotifyFormat.HTML: - payload["text"] = "{title}{body}".format( - title=( - "" - if not title - else f"

{NotifyMatrix.escape_html(title)}

" - ), - body=body, - ) - - elif self.notify_format == NotifyFormat.MARKDOWN: - payload["text"] = "{title}{body}".format( - title=( - "" - if not title - else f"

{NotifyMatrix.escape_html(title)}

" - ), - body=markdown(body), - ) - - else: # NotifyFormat.TEXT - payload["text"] = body if not title else f"{title}\r\n{body}" - - return payload - - def _t2bot_webhook_payload( - self, body, title="", notify_type=NotifyType.INFO, **kwargs - ): - """Format the payload for a T2Bot Matrix based messages.""" - - # Retrieve our payload - payload = self._matrix_webhook_payload( - body=body, title=title, notify_type=notify_type, **kwargs - ) - - # Acquire our image url if we're configured to do so - image_url = ( - None if not self.include_image else self.image_url(notify_type) - ) - - if image_url: - # t2bot can take an avatarUrl Entry - payload["avatarUrl"] = image_url - - return payload - - def _send_server_notification( - self, - body, - title="", - notify_type=NotifyType.INFO, - attach=None, - **kwargs, - ): - """Perform Direct Matrix Server Notification (no webhook)""" - - if self.access_token is None and self.password and not self.user: - self.access_token = self.password - self.transaction_id = uuid.uuid4() - - if self.access_token is None and not self._login() \ - and not self._register(): - # We need to register - return False - - if len(self.rooms) == 0: - # Attempt to retrieve a list of already joined channels - self.rooms = self._joined_rooms() - - if len(self.rooms) == 0: - # Nothing to notify - self.logger.warning( - "There were no Matrix rooms specified to notify." - ) - return False - - # Create a copy of our rooms to join and message - rooms = list(self.rooms) - - # Initiaize our error tracking - has_error = False - - attachments = None - if attach and self.attachment_support: - attachments = self._send_attachments(attach) - if attachments is False: - # take an early exit - return False - - while len(rooms) > 0: - - # Get our room - room = rooms.pop(0) - - # Set method according to MatrixVersion - method = "PUT" if self.version == MatrixVersion.V3 else "POST" - - # Get our room_id from our response - room_id = self._room_join(room) - if not room_id: - # Notify our user about our failure - self.logger.warning(f"Could not join Matrix room {room}.") - - # Mark our failure - has_error = True - continue - - # Acquire our image url if we're configured to do so - image_url = ( - None if not self.include_image else self.image_url(notify_type) - ) - - # Build our path - if self.version == MatrixVersion.V3: - path = f"/rooms/{NotifyMatrix.quote(room_id)}" \ - f"/send/m.room.message/{self.transaction_id}" - - else: - path = ( - f"/rooms/{NotifyMatrix.quote(room_id)}/send/m.room.message" - ) - - if image_url and self.version == MatrixVersion.V2: - # Define our payload - image_payload = { - "msgtype": "m.image", - "url": image_url, - "body": f"{title if title else notify_type}", - } - - # Post our content - postokay, _, _ = self._fetch( - path, payload=image_payload) - if not postokay: - # Mark our failure - has_error = True - continue - - if attachments: - for attachment in attachments: - attachment["room_id"] = room_id - attachment["type"] = "m.room.message" - - postokay, _, _ = self._fetch( - path, payload=attachment, method=method) - - # Increment the transaction ID to avoid future messages - # being recognized as retransmissions and ignored - if self.version == MatrixVersion.V3 \ - and self.access_token != self.password: - self.transaction_id += 1 - self.store.set( - "transaction_id", self.transaction_id, - expires=self.default_cache_expiry_sec) - path = "/rooms/{}/send/m.room.message/{}".format( - NotifyMatrix.quote(room_id), - self.transaction_id, - ) - - if not postokay: - # Mark our failure - has_error = True - continue - - # Define our payload - payload = { - "msgtype": f"m.{self.msgtype}", - "body": "{title}{body}".format( - title="" if not title else f"# {title}\r\n", body=body - ), - } - - # Update our payload advance formatting for the services that - # support them. - if self.notify_format == NotifyFormat.HTML: - payload.update({ - "format": "org.matrix.custom.html", - "formatted_body": "{title}{body}".format( - title="" if not title else f"

{title}

", - body=body, - ), - }) - - elif self.notify_format == NotifyFormat.MARKDOWN: - title_ = ( - "" - if not title - else ( - "

" - f"{NotifyMatrix.escape_html(title, whitespace=False)}" - "

" - ) - ) - - payload.update({ - "format": "org.matrix.custom.html", - "formatted_body": "{title}{body}".format( - title=title_, - body=markdown(body), - ), - }) - - # Post our content - postokay, _, _ = self._fetch( - path, payload=payload, method=method - ) - - # Increment the transaction ID to avoid future messages being - # recognized as retransmissions and ignored - if ( - self.version == MatrixVersion.V3 - and self.access_token != self.password - ): - self.transaction_id += 1 - self.store.set( - "transaction_id", - self.transaction_id, - expires=self.default_cache_expiry_sec, - ) - - if not postokay: - # Notify our user - self.logger.warning( - f"Could not send notification Matrix room {room}." - ) - - # Mark our failure - has_error = True - continue - - return not has_error - - def _send_attachments(self, attach): - """Posts all of the provided attachments.""" - - payloads = [] - - for attachment in attach: - if not attachment: - # invalid attachment (bad file) - return False - - if not IS_IMAGE.match(attachment.mimetype) \ - and self.version == MatrixVersion.V2: - # unsuppored at this time - continue - - postokay, response, _ = \ - self._fetch("/upload", attachment=attachment) - if not (postokay and isinstance(response, dict)): - # Failed to perform upload - return False - - # If we get here, we'll have a response that looks like: - # { - # "content_uri": "mxc://example.com/a-unique-key" - # } - - if self.version == MatrixVersion.V3: - # Prepare our payload - is_image = IS_IMAGE.match(attachment.mimetype) - payloads.append({ - "body": attachment.name, - "info": { - "mimetype": attachment.mimetype, - "size": len(attachment), - }, - "msgtype": "m.image" if is_image else "m.file", - "url": response.get("content_uri"), - }) - if not is_image: - # Setup `m.file' - payloads[-1]["filename"] = attachment.name - - else: - # Prepare our payload - payloads.append({ - "info": { - "mimetype": attachment.mimetype, - }, - "msgtype": "m.image", - "body": "tta.webp", - "url": response.get("content_uri"), - }) - - return payloads - - def _register(self): - """Register with the service if possible.""" - - # Prepare our Registration Payload. This will only work if registration - # is enabled for the public - payload = { - "kind": "user", - "auth": {"type": "m.login.dummy"}, - } - - # parameters - params = { - "kind": "user", - } - - # If a user is not specified, one will be randomly generated for you. - # If you do not specify a password, you will be unable to login to the - # account if you forget the access_token. - if self.user: - payload["username"] = self.user - - if self.password: - payload["password"] = self.password - - # Register - postokay, response, _ = self._fetch( - "/register", payload=payload, params=params - ) - if not (postokay and isinstance(response, dict)): - # Failed to register - return False - - # Pull the response details - self.access_token = response.get("access_token") - self.home_server = response.get("home_server") - self.user_id = response.get("user_id") - - self.store.set( - "access_token", - self.access_token, - expires=self.default_cache_expiry_sec, - ) - self.store.set( - "home_server", - self.home_server, - expires=self.default_cache_expiry_sec, - ) - self.store.set( - "user_id", self.user_id, expires=self.default_cache_expiry_sec - ) - - if self.access_token is not None: - # Store our token into our store - self.logger.debug("Registered successfully with Matrix server.") - return True - - return False - - def _login(self): - """Acquires the matrix token required for making future requests. - - If we fail we return False, otherwise we return True - """ - - if self.access_token: - # Login not required; silently skip-over - return True - - if self.user and self.password: - # Prepare our Authentication Payload - if self.version == MatrixVersion.V3: - payload = { - "type": "m.login.password", - "identifier": { - "type": "m.id.user", - "user": self.user, - }, - "password": self.password, - } - - else: - payload = { - "type": "m.login.password", - "user": self.user, - "password": self.password, - } - - else: - # It's not possible to register since we need these 2 values to - # make the action possible. - self.logger.warning( - "Failed to login to Matrix server: " - "token or user/pass combo is missing." - ) - return False - - # Build our URL - postokay, response, _ = self._fetch("/login", payload=payload) - if not (postokay and isinstance(response, dict)): - # Failed to login - return False - - # Pull the response details - self.access_token = response.get("access_token") - self.home_server = response.get("home_server") - self.user_id = response.get("user_id") - - if not self.access_token: - return False - - self.logger.debug("Authenticated successfully with Matrix server.") - - # Store our token into our store - self.store.set( - "access_token", - self.access_token, - expires=self.default_cache_expiry_sec, - ) - self.store.set( - "home_server", - self.home_server, - expires=self.default_cache_expiry_sec, - ) - self.store.set( - "user_id", self.user_id, expires=self.default_cache_expiry_sec - ) - - return True - - def _logout(self): - """Relinquishes token from remote server.""" - - if not self.access_token: - # Login not required; silently skip-over - return True - - # Prepare our Registration Payload - payload = {} - - # Expire our token - postokay, response, _ = self._fetch("/logout", payload=payload) - if not postokay and response.get("errcode") != "M_UNKNOWN_TOKEN": - # If we get here, the token was declared as having already - # been expired. The response looks like this: - # { - # u'errcode': u'M_UNKNOWN_TOKEN', - # u'error': u'Access Token unknown or expired', - # } - # - # In this case it's okay to safely return True because - # we're logged out in this case. - return False - - # else: The response object looks like this if we were successful: - # {} - - # Pull the response details - self.access_token = None - self.home_server = None - self.user_id = None - - # clear our tokens - self.store.clear( - "access_token", "home_server", "user_id", "transaction_id" - ) - - self.logger.debug("Unauthenticated successfully with Matrix server.") - - return True - - def _room_join(self, room): - """Joins a matrix room if we're not already in it. - - Otherwise it attempts to create it if it doesn't exist and always - returns the room_id if it was successful, otherwise it returns None - """ - - if not self.access_token: - # We can't join a room if we're not logged in - return None - - if not isinstance(room, str): - # Not a supported string - return None - - # Prepare our Join Payload - payload = {} - - # Check if it's a room id... - result = IS_ROOM_ID.match(room) - if result: - room_token = result.group("room") - explicit_home_server = result.group("home_server") - - # Determine the homeserver context (used for cache metadata) - home_server = ( - explicit_home_server - if explicit_home_server else self.home_server - ) - - # When hsreq is enabled (legacy behaviour), we always require a - # ':homeserver' segment on room IDs. Otherwise, we honour exactly - # what the caller provided and do not synthesise a homeserver when - # it was not specified. - cache_key = f"!{room_token}:{home_server}" - if explicit_home_server or self.hsreq: - room_id = cache_key - else: - room_id = f"!{room_token}" - - # Check our cache for speed: - try: - return self.store[cache_key]["id"] - - except KeyError: - pass - - # Build our URL - path = f"/join/{NotifyMatrix.quote(room_id)}" - - # Attempt to join the channel - postokay, response, _status_code = \ - self._fetch(path, payload=payload) - if not postokay: - return None - - # Prefer the server-provided room_id if one was returned, - # otherwise fall back to whatever we joined with. - joined_id = ( - response.get("room_id") if isinstance(response, dict) else None - ) or room_id - - # Cache mapping for faster future lookups. - self.store.set( - cache_key, - { - "id": joined_id, - "home_server": home_server, - }, - ) - - return joined_id - - # Try to see if it's an alias then... - result = IS_ROOM_ALIAS.match(room) - if not result: - # There is nothing else it could be - self.logger.warning( - f"Ignoring illegally formed room {room} " - "from Matrix server list." - ) - return None - - # If we reach here, we're dealing with a channel alias - home_server = ( - self.home_server - if not result.group("home_server") - else result.group("home_server") - ) - - # tidy our room (alias) identifier - room = "#{}:{}".format(result.group("room"), home_server) - - # Check our cache for speed: - try: - # We're done as we've already joined the channel - return self.store[room]["id"] - - except KeyError: - # No worries, we'll try to acquire the info - pass - - # If we reach here, we need to join the channel - - # Build our URL - path = f"/join/{NotifyMatrix.quote(room)}" - - # Attempt to join the channel - postokay, response, status_code = self._fetch(path, payload=payload) - if postokay: - # Cache our entry for fast access later - self.store.set( - room, - { - "id": response.get("room_id"), - "home_server": home_server, - }, - ) - - return response.get("room_id") - - # Only attempt to create a room when the server clearly indicates - # the alias does not exist. A join can fail for many reasons, such as - # invite required, auth failure, or permissions, and in those cases - # auto-creating is both noisy and incorrect. - if (status_code == requests.codes.not_found - or response.get("errcode") == "M_NOT_FOUND"): - return self._room_create(room) - - self.logger.warning( - "Could not join Matrix room alias %s (error=%s). " - "If this is a private room, ensure the user is invited or " - "already joined, or specify the room_id (!...).", - room, - status_code, - ) - return None - - def _room_create(self, room): - """Creates a matrix room and return it's room_id if successful - otherwise None is returned.""" - if not self.access_token: - # We can't create a room if we're not logged in - return None - - if not isinstance(room, str): - # Not a supported string - return None - - # Build our room if we have to: - result = IS_ROOM_ALIAS.match(room) - if not result: - # Illegally formed room - return None - - # Our home_server - home_server = ( - result.group("home_server") - if result.group("home_server") - else self.home_server - ) - - # update our room details - room = "#{}:{}".format(result.group("room"), home_server) - - # Prepare our Create Payload - payload = { - "room_alias_name": result.group("room"), - # Set our channel name - "name": "#{} - {}".format(result.group("room"), self.app_desc), - # hide the room by default; let the user open it up if they wish - # to others. - "visibility": "private", - "preset": "trusted_private_chat", - } - - postokay, response, _ = \ - self._fetch("/createRoom", payload=payload) - if not postokay: - # Failed to create channel - # Typical responses: - # - {u'errcode': u'M_ROOM_IN_USE', - # u'error': u'Room alias already taken'} - # - {u'errcode': u'M_UNKNOWN', - # u'error': u'Internal server error'} - if response and response.get("errcode") == "M_ROOM_IN_USE": - return self._room_id(room) - return None - - # Cache our entry for fast access later - self.store.set( - response.get("room_alias"), - { - "id": response.get("room_id"), - "home_server": home_server, - }, - ) - - return response.get("room_id") - - def _joined_rooms(self): - """Returns a list of the current rooms the logged in user is a part - of.""" - - if not self.access_token: - # No list is possible - return [] - - postokay, response, _ = self._fetch( - "/joined_rooms", payload=None, method="GET" - ) - if not postokay: - # Failed to retrieve listings - return [] - - # Return our list of rooms - return response.get("joined_rooms", []) - - def _room_id(self, room): - """Get room id from its alias. - Args: - room (str): The room alias name. - - Returns: - returns the room id if it can, otherwise it returns None - """ - - if not self.access_token: - # We can't get a room id if we're not logged in - return None - - if not isinstance(room, str): - # Not a supported string - return None - - # Build our room if we have to: - result = IS_ROOM_ALIAS.match(room) - if not result: - # Illegally formed room - return None - - # Our home_server - home_server = ( - result.group("home_server") - if result.group("home_server") - else self.home_server - ) - - # update our room details - room = "#{}:{}".format(result.group("room"), home_server) - - # Make our request - postokay, response, _ = self._fetch( - f"/directory/room/{NotifyMatrix.quote(room)}", - payload=None, - method="GET", - ) - - if postokay: - return response.get("room_id") - - return None - - def _fetch( - self, - path, - payload=None, - params=None, - attachment=None, - method="POST", - url_override=None, - ): - """Wrapper to request.post() to manage it's response better and make - the send() function cleaner and easier to maintain. - - This function always returns a 3-tuple: - (success, response, status_code) - - The response is a dict when JSON is parseable, otherwise an empty dict. - The status_code defaults to 500 on local failures. - """ - - # Define our headers - if params is None: - params = {} - headers = { - "User-Agent": self.app_id, - "Content-Type": "application/json", - "Accept": "application/json", - } - - if self.access_token is not None: - headers["Authorization"] = f"Bearer {self.access_token}" - - # Server Discovery / Well-known URI - if url_override: - url = url_override - - else: - try: - url = self.base_url - - except MatrixDiscoveryException: - # Discovery failed; we're done - return (False, {}, requests.codes.internal_server_error) - - # Default return status code - status_code = requests.codes.internal_server_error - - if path == "/upload": - if self.version == MatrixVersion.V3: - url += MATRIX_V3_MEDIA_PATH + path - - else: - url += MATRIX_V2_MEDIA_PATH + path - - params.update({"filename": attachment.name}) - with open(attachment.path, "rb") as fp: - payload = fp.read() - - # Update our content type - headers["Content-Type"] = attachment.mimetype - - elif not url_override: - if self.version == MatrixVersion.V3: - url += MATRIX_V3_API_PATH + path - - else: - url += MATRIX_V2_API_PATH + path - - # Our response object - response = {} - - # fetch function - fn = ( - requests.post - if method == "POST" - else (requests.put if method == "PUT" else requests.get) - ) - - # Always call throttle before any remote server i/o is made - self.throttle() - - # Define how many attempts we'll make if we get caught in a throttle - # event - retries = self.default_retries if self.default_retries > 0 else 1 - while retries > 0: - - # Decrement our throttle retry count - retries -= 1 - - self.logger.debug( - "Matrix {} URL: {} (cert_verify={!r})".format( - ( - "POST" - if method == "POST" - else ("PUT" if method == "PUT" else "GET") - ), - url, - self.verify_certificate, - ) - ) - self.logger.debug(f"Matrix Payload: {payload!s}") - - # Initialize our response object - r = None - - try: - r = fn( - url, - data=dumps(payload) if not attachment else payload, - params=params if params else None, - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - - # Store status code - status_code = r.status_code - - self.logger.debug( - f"Matrix Response: code={r.status_code}, {r.content!s}" - ) - response = loads(r.content) - - if r.status_code == requests.codes.too_many_requests: - wait_ms = self.default_wait_ms - try: - wait_ms = response["retry_after_ms"] - - except KeyError: - try: - errordata = response["error"] - wait_ms = errordata["retry_after_ms"] - except KeyError: - pass - - self.logger.warning( - "Matrix server requested we throttle back " - f"{wait_ms}ms; retries left {retries}." - ) - self.logger.debug(f"Response Details:\r\n{r.content}") - - # Throttle for specified wait - self.throttle(wait=wait_ms / 1000) - - # Try again - continue - - elif r.status_code != requests.codes.ok: - # We had a problem - status_str = NotifyMatrix.http_response_code_lookup( - r.status_code, MATRIX_HTTP_ERROR_MAP - ) - - self.logger.warning( - "Failed to handshake with Matrix server: " - "{}{}error={}.".format( - status_str, - ", " if status_str else "", - r.status_code, - ) - ) - - self.logger.debug(f"Response Details:\r\n{r.content}") - - # Return; we're done - return (False, response, status_code) - - except (AttributeError, TypeError, ValueError): - # This gets thrown if we can't parse our JSON Response - # - ValueError = r.content is Unparsable - # - TypeError = r.content is None - # - AttributeError = r is None - self.logger.warning("Invalid response from Matrix server.") - self.logger.debug( - "Response Details:\r\n%r", - b"" if not r else (r.content or b"")) - return (False, {}, status_code) - - except (requests.TooManyRedirects, requests.RequestException) as e: - self.logger.warning( - "A Connection error occurred while registering with Matrix" - " server." - ) - self.logger.debug("Socket Exception: %s", e) - # Return; we're done - return (False, response, status_code) - - except OSError as e: - self.logger.warning( - "An I/O error occurred while reading {}.".format( - attachment.name if attachment else "unknown file" - ) - ) - self.logger.debug("I/O Exception: %s", e) - return (False, {}, status_code) - - return (True, response, status_code) - - # If we get here, we ran out of retries - return (False, {}, status_code) - - def __del__(self): - """Ensure we relinquish our token.""" - if self.mode == MatrixWebhookMode.T2BOT: - # nothing to do - return - - if self.store.mode != PersistentStoreMode.MEMORY: - # We no longer have to log out as we have persistant storage to - # re-use our credentials with - return - - if ( - self.access_token is not None - and self.access_token == self.password - and not self.user - ): - return - - self._logout() - - @property - def url_identifier(self): - """Returns all of the identifiers that make this URL unique from - another simliar one. - - Targets or end points should never be identified here. - """ - return ( - self.secure_protocol if self.secure else self.protocol, - ( - self.host - if self.mode != MatrixWebhookMode.T2BOT - else self.access_token - ), - self.port if self.port else (443 if self.secure else 80), - self.user if self.mode != MatrixWebhookMode.T2BOT else None, - self.password if self.mode != MatrixWebhookMode.T2BOT else None, - ) - - def url(self, privacy=False, *args, **kwargs): - """Returns the URL built dynamically based on specified arguments.""" - - # Define any URL parameters - params = { - "image": "yes" if self.include_image else "no", - "mode": self.mode, - "version": self.version, - "msgtype": self.msgtype, - "discovery": "yes" if self.discovery else "no", - "hsreq": "yes" if self.hsreq else "no", - } - - # Extend our parameters - params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - - auth = "" - if self.mode != MatrixWebhookMode.T2BOT: - # Determine Authentication - if self.user and self.password: - auth = "{user}:{password}@".format( - user=NotifyMatrix.quote(self.user, safe=""), - password=self.pprint( - self.password, - privacy, - mode=PrivacyMode.Secret, - safe="", - ), - ) - - elif self.user or self.password: - auth = "{value}@".format( - value=NotifyMatrix.quote( - self.user if self.user else self.password, safe="" - ), - ) - - return "{schema}://{auth}{hostname}{port}/{rooms}?{params}".format( - schema=self.secure_protocol if self.secure else self.protocol, - auth=auth, - hostname=( - NotifyMatrix.quote(self.host, safe="") - if self.mode != MatrixWebhookMode.T2BOT - else self.pprint(self.access_token, privacy, safe="") - ), - port=("" if not self.port else f":{self.port}"), - rooms=NotifyMatrix.quote("/".join(self.rooms)), - params=NotifyMatrix.urlencode(params), - ) - - def __len__(self): - """Returns the number of targets associated with this notification.""" - targets = len(self.rooms) - return targets if targets > 0 else 1 - - @staticmethod - def parse_url(url): - """Parses the URL and returns enough arguments that can allow us to re- - instantiate this object.""" - results = NotifyBase.parse_url(url, verify_host=False) - if not results: - # We're done early as we couldn't load the results - return results - - if not results.get("host"): - return None - - # Get our rooms - results["targets"] = NotifyMatrix.split_path(results["fullpath"]) - - # Support the 'to' variable so that we can support rooms this way too - # The 'to' makes it easier to use yaml configuration - if "to" in results["qsd"] and len(results["qsd"]["to"]): - results["targets"] += NotifyMatrix.parse_list(results["qsd"]["to"]) - - # Boolean to include an image or not - results["include_image"] = parse_bool( - results["qsd"].get( - "image", NotifyMatrix.template_args["image"]["default"] - ) - ) - - # Boolean to perform a server discovery - results["discovery"] = parse_bool( - results["qsd"].get( - "discovery", NotifyMatrix.template_args["discovery"]["default"] - ) - ) - - # Boolean to enforce ':homeserver' on room IDs when missing - results["hsreq"] = parse_bool( - results["qsd"].get( - "hsreq", NotifyMatrix.template_args["hsreq"]["default"] - ) - ) - - # Get our mode - results["mode"] = results["qsd"].get("mode") - - # t2bot detection... look for just a hostname, and/or just a user/host - # if we match this; we can go ahead and set the mode (but only if - # it was otherwise not set) - if ( - results["mode"] is None - and not results["password"] - and not results["targets"] - ): - - # Default mode to t2bot - results["mode"] = MatrixWebhookMode.T2BOT - - if ( - results["mode"] - and results["mode"].lower() == MatrixWebhookMode.T2BOT - ): - # unquote our hostname and pass it in as the password/token - results["password"] = NotifyMatrix.unquote(results["host"]) - - # Support the message type keyword - if "msgtype" in results["qsd"] and len(results["qsd"]["msgtype"]): - results["msgtype"] = NotifyMatrix.unquote( - results["qsd"]["msgtype"] - ) - - # Support the use of the token= keyword - if "token" in results["qsd"] and len(results["qsd"]["token"]): - results["password"] = NotifyMatrix.unquote(results["qsd"]["token"]) - - elif not results["password"] and results["user"]: - # swap - results["password"] = results["user"] - results["user"] = None - - # Support the use of the version= or v= keyword - if "version" in results["qsd"] and len(results["qsd"]["version"]): - results["version"] = NotifyMatrix.unquote( - results["qsd"]["version"] - ) - - elif "v" in results["qsd"] and len(results["qsd"]["v"]): - results["version"] = NotifyMatrix.unquote(results["qsd"]["v"]) - - return results - - @staticmethod - def parse_native_url(url): - """ - Support https://webhooks.t2bot.io/api/v1/matrix/hook/WEBHOOK_TOKEN/ - """ - - result = re.match( - r"^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/" - r"(?P[A-Z0-9_-]+)/?" - r"(?P\?.+)?$", - url, - re.I, - ) - - if result: - mode = f"mode={MatrixWebhookMode.T2BOT}" - - return NotifyMatrix.parse_url( - "{schema}://{webhook_token}/{params}".format( - schema=NotifyMatrix.secure_protocol, - webhook_token=result.group("webhook_token"), - params=( - f"?{mode}" - if not result.group("params") - else "{}&{}".format(result.group("params"), mode) - ), - ) - ) - - return None - - def server_discovery(self): - """ - Home Server Discovery as documented here: - https://spec.matrix.org/v1.11/client-server-api/#well-known-uri - """ - - if not (self.discovery and self.secure): - # Nothing further to do with insecure server setups - return "" - - # Get our content from cache - base_url, identity_url = ( - self.store.get(self.discovery_base_key), - self.store.get(self.discovery_identity_key), - ) - - if not (base_url is None and identity_url is None): - # We can use our cached value and return early - return base_url - - # the Matrix ID at the first colon. - verify_url = \ - "{schema}://{hostname}{port}/.well-known/matrix/client".format( - schema="https" if self.secure else "http", - hostname=self.host, - port=("" if not self.port else f":{self.port}"), - ) - - _, response, status_code = self._fetch( - None, method="GET", url_override=verify_url - ) - - # Output may look as follows: - # { - # "m.homeserver": { - # "base_url": "https://matrix.example.com" - # }, - # "m.identity_server": { - # "base_url": "https://nuxref.com" - # } - # } - - if status_code == requests.codes.not_found: - # This is an acceptable response; we're done - self.logger.debug( - "Matrix Well-Known Base URI not found at %s", verify_url - ) - - # Set our keys out for fast recall later on - self.store.set( - self.discovery_base_key, - "", - expires=self.discovery_cache_length_sec, - ) - self.store.set( - self.discovery_identity_key, - "", - expires=self.discovery_cache_length_sec, - ) - return "" - - elif status_code != requests.codes.ok: - # We're done early as we couldn't load the results - msg = "Matrix Well-Known Base URI Discovery Failed" - self.logger.warning( - "%s - %s returned error code: %d", - msg, verify_url, status_code, - ) - raise MatrixDiscoveryException(msg, error_code=status_code) - - if not response: - # This is an acceptable response; we simply do nothing - self.logger.debug( - "Matrix Well-Known Base URI not defined %s", verify_url - ) - - # Set our keys out for fast recall later on - self.store.set( - self.discovery_base_key, - "", - expires=self.discovery_cache_length_sec, - ) - self.store.set( - self.discovery_identity_key, - "", - expires=self.discovery_cache_length_sec, - ) - return "" - - # - # Parse our m.homeserver information - # - try: - base_url = response["m.homeserver"]["base_url"].rstrip("/") - results = NotifyBase.parse_url(base_url, verify_host=True) - - except (AttributeError, TypeError, KeyError): - # AttributeError: result wasn't a string (rstrip failed) - # TypeError : response wasn't a dictionary - # KeyError : response not to standards - results = None - - if not results: - msg = "Matrix Well-Known Base URI Discovery Failed" - self.logger.warning( - "%s - m.homeserver payload is missing or invalid: %s", - msg, - response, - ) - raise MatrixDiscoveryException(msg) - - # - # Our .well-known extraction was successful; now we need to verify - # that the version information resolves. - # - verify_url = f"{base_url}/_matrix/client/versions" - # Post our content - _, _, status_code = self._fetch( - None, method="GET", url_override=verify_url - ) - if status_code != requests.codes.ok: - # We're done early as we couldn't load the results - msg = "Matrix Well-Known Base URI Discovery Verification Failed" - self.logger.warning( - "%s - %s returned error code: %d", - msg, verify_url, status_code, - ) - raise MatrixDiscoveryException(msg, error_code=status_code) - - # - # Phase 2: Handle m.identity_server IF defined - # - if "m.identity_server" in response: - try: - identity_url = response["m.identity_server"][ - "base_url" - ].rstrip("/") - results = NotifyBase.parse_url(identity_url, verify_host=True) - - except (AttributeError, TypeError, KeyError): - # AttributeError: result wasn't a string (rstrip failed) - # TypeError : response wasn't a dictionary - # KeyError : response not to standards - results = None - - if not results: - msg = "Matrix Well-Known Identity URI Discovery Failed" - self.logger.warning( - "%s - m.identity_server payload is missing or invalid: %s", - msg, - response, - ) - raise MatrixDiscoveryException(msg) - - # - # Verify identity server found - # - verify_url = f"{identity_url}/_matrix/identity/v2" - - # Post our content - _postokay, _, status_code = self._fetch( - None, method="GET", url_override=verify_url - ) - if status_code != requests.codes.ok: - # We're done early as we couldn't load the results - msg = "Matrix Well-Known Identity URI Discovery Failed" - self.logger.warning( - "%s - %s returned error code: %d", - msg, verify_url, status_code, - ) - raise MatrixDiscoveryException(msg, error_code=status_code) - - # Update our cache - self.store.set( - self.discovery_identity_key, - identity_url, - # Add 2 seconds to prevent this key from expiring before base - expires=self.discovery_cache_length_sec + 2, - ) - else: - # No identity server - self.store.set( - self.discovery_identity_key, - "", - # Add 2 seconds to prevent this key from expiring before base - expires=self.discovery_cache_length_sec + 2, - ) - - # Update our cache - self.store.set( - self.discovery_base_key, - base_url, - expires=self.discovery_cache_length_sec, - ) - - return base_url - - @property - def base_url(self): - """Returns the base_url if known.""" - try: - base_url = self.server_discovery() - if base_url: - # We can use our cached value and return early - return base_url - - except MatrixDiscoveryException: - self.store.clear( - self.discovery_base_key, self.discovery_identity_key - ) - raise - - # If we get hear, we need to build our URL dynamically based on what - # was provided to us during the plugins initialization - return "{schema}://{hostname}{port}".format( - schema="https" if self.secure else "http", - hostname=self.host, - port=("" if not self.port else f":{self.port}"), - ) - - @property - def identity_url(self): - """Returns the identity_url if known.""" - base_url = self.base_url - identity_url = self.store.get(self.discovery_identity_key) - return identity_url if identity_url else base_url diff --git a/libs/apprise/plugins/matrix/__init__.py b/libs/apprise/plugins/matrix/__init__.py new file mode 100644 index 000000000..22dc2d48d --- /dev/null +++ b/libs/apprise/plugins/matrix/__init__.py @@ -0,0 +1,36 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Matrix Notifications.""" + +from .base import IS_ROOM_ID, MatrixDiscoveryException, NotifyMatrix + +__all__ = [ + "IS_ROOM_ID", + "MatrixDiscoveryException", + "NotifyMatrix", +] diff --git a/libs/apprise/plugins/matrix/base.py b/libs/apprise/plugins/matrix/base.py new file mode 100644 index 000000000..22fd61ff2 --- /dev/null +++ b/libs/apprise/plugins/matrix/base.py @@ -0,0 +1,3513 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Great sources +# - https://github.com/matrix-org/matrix-python-sdk +# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst +# +# End-to-End Encryption references: +# - https://spec.matrix.org/v1.11/client-server-api/ +# #end-to-end-encryption +# - https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md +# - https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md +# +import contextlib +from json import dumps, loads +import re +from time import time +import uuid + +from markdown import markdown +import requests + +from ...common import ( + NotifyFormat, + NotifyImageSize, + NotifyType, + PersistentStoreMode, +) +from ...exception import AppriseException +from ...locale import gettext_lazy as _ +from ...url import PrivacyMode +from ...utils.parse import ( + is_hostname, + parse_bool, + parse_list, + validate_regex, +) +from ..base import NotifyBase +from .e2ee import ( + MATRIX_E2EE_SUPPORT, + MatrixMegOlmSession, + MatrixOlmAccount, + encrypt_attachment, + verify_device_keys, + verify_signed_otk, +) + +# Define default path +MATRIX_V1_WEBHOOK_PATH = "/api/v1/matrix/hook" +MATRIX_V2_API_PATH = "/_matrix/client/r0" +MATRIX_V3_API_PATH = "/_matrix/client/v3" +MATRIX_V3_MEDIA_PATH = "/_matrix/media/v3" +MATRIX_V2_MEDIA_PATH = "/_matrix/media/r0" + + +class MatrixDiscoveryException(AppriseException): + """Apprise Matrix Exception Class.""" + + +# Extend HTTP Error Messages +MATRIX_HTTP_ERROR_MAP = { + 403: "Unauthorized - Invalid Token.", + 429: "Rate limit imposed; wait 2s and try again", +} + +# Matrix Room Syntax +IS_ROOM_ALIAS = re.compile( + r"^\s*(#|%23)?(?P[A-Za-z0-9._=-]+)((:|%3A)" + r"(?P[A-Za-z0-9.-]+))?\s*$", + re.I, +) + +# Room ID MUST start with an exclamation to avoid ambiguity +IS_ROOM_ID = re.compile( + r"^\s*(!|!|%21)(?P[A-Za-z0-9._=-]+)((:|%3A)" + r"(?P[A-Za-z0-9.-]+))?\s*$", + re.I, +) + +# Matrix User ID (for DM targets); must start with @ +IS_USER = re.compile( + r"^\s*(@|%40)(?P[A-Za-z0-9._=+/-]+)((:|%3A)" + r"(?P[A-Za-z0-9.-]+))?\s*$", + re.I, +) + + +# Matrix is_image check +IS_IMAGE = re.compile(r"^image/.*", re.I) + + +class MatrixMessageType: + """The Matrix Message types.""" + + TEXT = "text" + NOTICE = "notice" + + +# matrix message types are placed into this list for validation purposes +MATRIX_MESSAGE_TYPES = ( + MatrixMessageType.TEXT, + MatrixMessageType.NOTICE, +) + + +class MatrixVersion: + # Version 2 + V2 = "2" + + # Version 3 + V3 = "3" + + +# webhook modes are placed into this list for validation purposes +MATRIX_VERSIONS = ( + MatrixVersion.V2, + MatrixVersion.V3, +) + + +class MatrixWebhookMode: + # Webhook Mode is disabled + DISABLED = "off" + + # The default webhook mode is to just be set to Matrix + MATRIX = "matrix" + + # Support the slack webhook plugin + SLACK = "slack" + + # Support the t2bot webhook plugin + T2BOT = "t2bot" + + # Support matrix-hookshot generic webhooks + HOOKSHOT = "hookshot" + + +# webhook modes are placed into this list for validation purposes +MATRIX_WEBHOOK_MODES = ( + MatrixWebhookMode.DISABLED, + MatrixWebhookMode.MATRIX, + MatrixWebhookMode.SLACK, + MatrixWebhookMode.T2BOT, + MatrixWebhookMode.HOOKSHOT, +) + + +class NotifyMatrix(NotifyBase): + """A wrapper for Matrix Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "Matrix" + + # The services URL + service_url = "https://matrix.org/" + + # The default protocol + protocol = "matrix" + + # The default secure protocol + secure_protocol = "matrixs" + + # Support Attachments + attachment_support = True + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/matrix/" + + # Allows the user to specify the NotifyImageSize object + image_size = NotifyImageSize.XY_32 + + # The maximum allowable characters allowed in the body per message + # https://spec.matrix.org/v1.6/client-server-api/#size-limits + # The complete event MUST NOT be larger than 65536 bytes, when formatted + # with the federation event format, including any signatures, and encoded + # as Canonical JSON. + # + # To gracefully allow for some overhead' we'll define a max body length + # of just slighty lower then the limit of the full message itself. + body_maxlen = 65000 + + # Throttle a wee-bit to avoid thrashing + request_rate_per_sec = 0.5 + + # How many retry attempts we'll make in the event the server asks us to + # throttle back. + default_retries = 2 + + # The number of micro seconds to wait if we get a 429 error code and + # the server doesn't remind us how long we should wait for + default_wait_ms = 1000 + + # Our default is to no not use persistent storage beyond in-memory + # reference + storage_mode = PersistentStoreMode.AUTO + + # Keep our cache for 20 days + default_cache_expiry_sec = 60 * 60 * 24 * 20 + + # Number of signed_curve25519 one-time keys to generate and upload + # per batch (both on initial device registration and replenishment). + default_e2ee_otk_count = 10 + + # Replenish the server-side OTK pool when the estimated remaining + # count drops below this value. /keys/claim consumes one OTK per + # device; without replenishment the pool runs dry and subsequent + # key-shares skip devices that have no OTK available. + default_e2ee_otk_replenish_threshold = 5 + + # Used for server discovery + discovery_base_key = "__discovery_base" + discovery_identity_key = "__discovery_identity" + + # Defines how long we cache our discovery for + discovery_cache_length_sec = 86400 + + # Define object templates + templates = ( + # Targets are ignored when using t2bot/hookshot mode; only a token is + # required + "{schema}://{token}", + "{schema}://{user}@{token}", + # Matrix Server + "{schema}://{user}:{password}@{host}/{targets}", + "{schema}://{user}:{password}@{host}:{port}/{targets}", + "{schema}://{token}@{host}/{targets}", + "{schema}://{token}@{host}:{port}/{targets}", + # Webhook mode + "{schema}://{user}:{token}@{host}/{targets}", + "{schema}://{user}:{token}@{host}:{port}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "host": { + "name": _("Hostname"), + "type": "string", + "required": True, + }, + "port": { + "name": _("Port"), + "type": "int", + "min": 1, + "max": 65535, + }, + "user": { + "name": _("Username"), + "type": "string", + }, + "password": { + "name": _("Password"), + "type": "string", + "private": True, + }, + "token": { + "name": _("Access Token"), + "type": "string", + "private": True, + "map_to": "password", + "required": True, + }, + "target_user": { + "name": _("Target User"), + "type": "string", + "prefix": "@", + "map_to": "targets", + }, + "target_room_id": { + "name": _("Target Room ID"), + "type": "string", + "prefix": "!", + "map_to": "targets", + }, + "target_room_alias": { + "name": _("Target Room Alias"), + "type": "string", + "prefix": "#", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "image": { + "name": _("Include Image"), + "type": "bool", + "default": False, + "map_to": "include_image", + }, + "discovery": { + "name": _("Server Discovery"), + "type": "bool", + "default": True, + }, + "hsreq": { + "name": _("Force Home Server on Room IDs"), + "type": "bool", + "default": True, + }, + "mode": { + "name": _("Webhook Mode"), + "type": "choice:string", + "values": MATRIX_WEBHOOK_MODES, + "default": MatrixWebhookMode.DISABLED, + }, + "path": { + "name": _("Webhook Path"), + "type": "string", + "map_to": "webhook_path", + "default": "/webhook", + }, + "version": { + "name": _("Matrix API Verion"), + "type": "choice:string", + "values": MATRIX_VERSIONS, + "default": MatrixVersion.V3, + }, + "msgtype": { + "name": _("Message Type"), + "type": "choice:string", + "values": MATRIX_MESSAGE_TYPES, + "default": MatrixMessageType.TEXT, + }, + "e2ee": { + "name": _("End-to-End Encryption"), + "type": "bool", + "default": True, + }, + "token": { + "alias_of": "token", + }, + "to": { + "alias_of": "targets", + }, + }, + ) + + def __init__( + self, + targets=None, + mode=None, + msgtype=None, + version=None, + include_image=None, + discovery=None, + hsreq=None, + webhook_path=None, + e2ee=None, + **kwargs, + ): + """Initialize Matrix Object.""" + super().__init__(**kwargs) + + # Prepare a list of rooms to connect and notify; separate + # @user DM targets from room identifiers. + self.rooms = [] + self.users = [] + for _target in parse_list(targets): + if IS_USER.match(_target): + self.users.append(_target) + else: + self.rooms.append(_target) + + # our home server gets populated after a login/registration + self.home_server = None + + # our user_id gets populated after a login/registration + self.user_id = None + + # This gets initialized after a login/registration + self.access_token = None + + # Our device ID assigned by the Matrix server during login + self.device_id = None + + # This gets incremented for each request made against the v3 API + self.transaction_id = 0 + + # Lazy-initialized E2EE account (MatrixOlmAccount or None) + self._e2ee_account = None + + # Place an image inline with the message body + self.include_image = ( + self.template_args["image"]["default"] + if include_image is None + else include_image + ) + + # Prepare Delegate Server Lookup Check + self.discovery = ( + self.template_args["discovery"]["default"] + if discovery is None + else discovery + ) + + # When enabled, room IDs missing a ':homeserver' segment will + # be treated as legacy identifiers and automatically suffixed + # with the authenticated homeserver. + self.hsreq = ( + self.template_args["hsreq"]["default"] if hsreq is None else hsreq + ) + + # Public webhook path used by matrix-hookshot + self.webhook_path = ( + self.template_args["path"]["default"] + if not isinstance(webhook_path, str) or not webhook_path.strip() + else webhook_path.strip() + ) + if not self.webhook_path.startswith("/"): + self.webhook_path = f"/{self.webhook_path}" + self.webhook_path = self.webhook_path.rstrip("/") or "/" + + # End-to-end encryption (server mode only; requires cryptography) + self.e2ee = ( + self.template_args["e2ee"]["default"] + if e2ee is None + else parse_bool(e2ee) + ) + + # Setup our mode + self.mode = ( + self.template_args["mode"]["default"] + if not isinstance(mode, str) + else mode.lower() + ) + if self.mode and self.mode not in MATRIX_WEBHOOK_MODES: + msg = f"The mode specified ({mode}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) + + # Setup our version + self.version = ( + self.template_args["version"]["default"] + if not isinstance(version, str) + else version + ) + if self.version not in MATRIX_VERSIONS: + msg = f"The version specified ({version}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) + + # Setup our message type + self.msgtype = ( + self.template_args["msgtype"]["default"] + if not isinstance(msgtype, str) + else msgtype.lower() + ) + if self.msgtype and self.msgtype not in MATRIX_MESSAGE_TYPES: + msg = f"The msgtype specified ({msgtype}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) + + if self.mode == MatrixWebhookMode.T2BOT: + # t2bot configuration requires that a webhook id is specified + self.access_token = validate_regex( + self.password, r"^[a-z0-9]{64}$", "i" + ) + if not self.access_token: + msg = ( + "An invalid T2Bot/Matrix Webhook ID " + f"({self.password}) was specified." + ) + self.logger.warning(msg) + raise TypeError(msg) + + elif not is_hostname(self.host): + msg = f"An invalid Matrix Hostname ({self.host}) was specified" + self.logger.warning(msg) + raise TypeError(msg) + + else: + # Verify port if specified + if self.port is not None and not ( + isinstance(self.port, int) + and self.port >= self.template_tokens["port"]["min"] + and self.port <= self.template_tokens["port"]["max"] + ): + msg = f"An invalid Matrix Port ({self.port}) was specified" + self.logger.warning(msg) + raise TypeError(msg) + + if self.mode != MatrixWebhookMode.DISABLED: + # Discovery only works when we're not using webhooks + self.discovery = False + + # + # Initialize from cache if present + # + if self.mode != MatrixWebhookMode.T2BOT: + # our home server gets populated after a login/registration + self.home_server = self.store.get("home_server") + + # our user_id gets populated after a login/registration + self.user_id = self.store.get("user_id") + + # This gets initialized after a login/registration + self.access_token = self.store.get("access_token") + + # Device ID assigned by server + self.device_id = self.store.get("device_id") + + # Older cache entries may have user_id/access_token persisted + # without home_server. Recover it from @user:homeserver so room + # aliases do not degrade into '#room:None'. + if not self.home_server and self.user_id: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + self.home_server = parts[1] + + # This gets incremented for each request made against the v3 API + self.transaction_id = ( + 0 if not self.access_token else self.store.get("transaction_id", 0) + ) + + # Restore E2EE account from store if available + if self.e2ee and MATRIX_E2EE_SUPPORT: + acct_data = self.store.get("e2ee_account") + if acct_data: + with contextlib.suppress(Exception): + self._e2ee_account = MatrixOlmAccount.from_dict(acct_data) + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform Matrix Notification.""" + + # Call the _send_ function applicable to whatever mode we're in + # - calls _send_webhook_notification if the mode variable is set + # - calls _send_server_notification if the mode variable is not set + return getattr( + self, + "_send_{}_notification".format( + "webhook" + if self.mode != MatrixWebhookMode.DISABLED + else "server" + ), + )(body=body, title=title, notify_type=notify_type, **kwargs) + + def _send_webhook_notification( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): + """Perform Matrix Notification as a webhook.""" + + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + } + + if self.mode == MatrixWebhookMode.T2BOT: + # + # t2bot Setup + # + + # Prepare our URL + url = ( + "https://webhooks.t2bot.io/api/v1/matrix/hook/" + f"{self.access_token}" + ) + + elif self.mode == MatrixWebhookMode.HOOKSHOT: + # Acquire our access token from our URL + access_token = self.password if self.password else self.user + + # Prepare our public hookshot URL + url = "{schema}://{hostname}{port}{webhook_path}/{token}".format( + schema="https" if self.secure else "http", + hostname=self.host, + port=("" if not self.port else f":{self.port}"), + webhook_path=self.webhook_path.rstrip("/"), + token=access_token, + ) + + else: + # Acquire our access token from our URL + access_token = self.password if self.password else self.user + + # Prepare our URL + url = "{schema}://{hostname}{port}{webhook_path}/{token}".format( + schema="https" if self.secure else "http", + hostname=self.host, + port=("" if not self.port else f":{self.port}"), + webhook_path=MATRIX_V1_WEBHOOK_PATH, + token=access_token, + ) + + # Retrieve our payload + payload = getattr(self, f"_{self.mode}_webhook_payload")( + body=body, title=title, notify_type=notify_type, **kwargs + ) + + self.logger.debug( + "Matrix POST URL: {} (cert_verify={!r})".format( + url, self.verify_certificate + ) + ) + self.logger.debug(f"Matrix Payload: {payload!s}") + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = NotifyMatrix.http_response_code_lookup( + r.status_code, MATRIX_HTTP_ERROR_MAP + ) + + self.logger.warning( + "Failed to send Matrix notification: {}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + + # Return; we're done + return False + + else: + self.logger.info("Sent Matrix notification.") + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Matrix notification." + ) + self.logger.debug(f"Socket Exception: {e!s}") + # Return; we're done + return False + + return True + + def _slack_webhook_payload( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): + """Format the payload for a Slack based message.""" + + if not hasattr(self, "_re_slack_formatting_rules"): + # Prepare some one-time slack formatting variables + + self._re_slack_formatting_map = { + # New lines must become the string version + r"\r\*\n": "\\n", + # Escape other special characters + r"&": "&", + r"<": "<", + r">": ">", + } + + # Iterate over above list and store content accordingly + self._re_slack_formatting_rules = re.compile( + r"(" + "|".join(self._re_slack_formatting_map.keys()) + r")", + re.IGNORECASE, + ) + + # Perform Formatting + title = self._re_slack_formatting_rules.sub( # pragma: no branch + lambda x: self._re_slack_formatting_map[x.group()], + title, + ) + + body = self._re_slack_formatting_rules.sub( # pragma: no branch + lambda x: self._re_slack_formatting_map[x.group()], + body, + ) + + # prepare JSON Object + payload = { + "username": self.user if self.user else self.app_id, + # Use Markdown language + "mrkdwn": self.notify_format == NotifyFormat.MARKDOWN, + "attachments": [ + { + "title": title, + "text": body, + "color": self.color(notify_type), + "ts": time(), + "footer": self.app_id, + } + ], + } + + return payload + + def _matrix_webhook_payload( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): + """Format the payload for a Matrix based message.""" + + payload = { + "displayName": self.user if self.user else self.app_id, + "format": ( + "plain" if self.notify_format == NotifyFormat.TEXT else "html" + ), + "text": "", + } + + if self.notify_format == NotifyFormat.HTML: + payload["text"] = "{title}{body}".format( + title=( + "" + if not title + else f"

{NotifyMatrix.escape_html(title)}

" + ), + body=body, + ) + + elif self.notify_format == NotifyFormat.MARKDOWN: + payload["text"] = "{title}{body}".format( + title=( + "" + if not title + else f"

{NotifyMatrix.escape_html(title)}

" + ), + body=markdown(body), + ) + + else: # NotifyFormat.TEXT + payload["text"] = body if not title else f"{title}\r\n{body}" + + return payload + + def _t2bot_webhook_payload( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): + """Format the payload for a T2Bot Matrix based messages.""" + + # Retrieve our payload + payload = self._matrix_webhook_payload( + body=body, title=title, notify_type=notify_type, **kwargs + ) + + # Acquire our image url if we're configured to do so + image_url = ( + None if not self.include_image else self.image_url(notify_type) + ) + + if image_url: + # t2bot can take an avatarUrl Entry + payload["avatarUrl"] = image_url + + return payload + + def _hookshot_webhook_payload( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): + """Format the payload for a matrix-hookshot webhook.""" + + payload = { + "username": self.user if self.user else self.app_id, + "text": "", + } + + if self.notify_format == NotifyFormat.HTML: + payload["text"] = body if not title else f"{title}\r\n{body}" + payload["html"] = "{title}{body}".format( + title=( + "" + if not title + else f"

{NotifyMatrix.escape_html(title)}

" + ), + body=body, + ) + + elif self.notify_format == NotifyFormat.MARKDOWN: + payload["text"] = body if not title else f"{title}\r\n{body}" + payload["html"] = "{title}{body}".format( + title=( + "" + if not title + else f"

{NotifyMatrix.escape_html(title)}

" + ), + body=markdown(body), + ) + + else: # NotifyFormat.TEXT + payload["text"] = body if not title else f"{title}\r\n{body}" + payload["html"] = NotifyMatrix.escape_html( + payload["text"], convert_new_lines=True, whitespace=False + ) + + return payload + + def _send_server_notification( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): + """Perform Direct Matrix Server Notification (no webhook)""" + + if self.access_token is None and self.password and not self.user: + self.access_token = self.password + self.transaction_id = uuid.uuid4() + + if ( + self.access_token is None + and not self._login() + and not self._register() + ): + # We need to register + return False + + # Resolve user_id (and device_id / home_server as a side-effect) via + # /whoami whenever user_id is still absent after login/token setup. + # This covers all paths where the server does not return user_id: + # - raw access-token auth (no /login flow at all) + # - username + ?token= (password treated as token, not a login) + # - servers that omit optional /login response fields + # Without user_id the m.direct lookup is skipped and + # each send creates a fresh orphan DM room instead of reusing the + # existing one. home_server is recovered from user_id inside + # _whoami(); the fallback at handles any remaining gap. + if not self.user_id: + self._whoami() + + # Last-resort fallback: if home_server is still unknown, assume + # it matches the Matrix host we are connecting to. + if not self.home_server: + self.home_server = self.host + + if len(self.rooms) == 0 and not self.users: + # Attempt to retrieve a list of already joined channels + self.rooms = self._joined_rooms() + + if len(self.rooms) == 0: + # Nothing to notify + self.logger.warning( + "There were no Matrix rooms specified to notify." + ) + return False + + # Create a copy of our rooms to join and message + rooms = list(self.rooms) + + # Initialize our error tracking + has_error = False + + # Resolve DM user targets (@user) to room IDs + for _user in self.users: + dm_room_id = self._dm_room_find_or_create(_user) + if dm_room_id: + rooms.append(dm_room_id) + else: + self.logger.warning( + "Could not find or create a DM room for Matrix user %s.", + _user, + ) + has_error = True + + # E2EE setup (once per send call, not per room). + # e2ee_capable means prerequisites are met; encryption is still + # decided per-room based on whether that room requires E2EE. + e2ee_capable = False + if self.e2ee and self.secure and MATRIX_E2EE_SUPPORT: + e2ee_capable = self._e2ee_setup() + if not e2ee_capable: + self.logger.warning( + "Matrix E2EE setup failed; " + "messages will be sent unencrypted." + ) + + # Plaintext attachment payloads for unencrypted rooms. + # Lazy-initialized on the first unencrypted room so that purely + # E2EE setups never upload attachments in plaintext. + attachments = None + attachments_ready = False + + while len(rooms) > 0: + # Get our room + room = rooms.pop(0) + + # Get our room_id from our response + room_id = self._room_join(room) + if not room_id: + # Notify our user about our failure + self.logger.warning(f"Could not join Matrix room {room}.") + + # Mark our failure + has_error = True + continue + + if e2ee_capable and self._e2ee_room_encrypted(room_id): + # E2EE path: encrypt message and any attachments + if not self._e2ee_send_to_room( + room_id, body, title, notify_type + ): + has_error = True + continue + + if attach and self.attachment_support: + session = self._e2ee_get_megolm(room_id) + for attachment in attach: + if not attachment: + has_error = True + break + if not self._e2ee_send_attachment( + attachment, room_id, session + ): + has_error = True + continue + + # --- Unencrypted path (existing behaviour) --- + + # Upload attachments once; reuse content_uris for every + # subsequent unencrypted room in this send call. + if attach and self.attachment_support and not attachments_ready: + attachments = self._send_attachments(attach) + attachments_ready = True + if attachments is False: + return False + + # Acquire our image url if we're configured to do so + image_url = ( + None if not self.include_image else self.image_url(notify_type) + ) + + # Always use PUT with a transaction ID + # (spec-compliant since 2015) + path = "/rooms/{}/send/m.room.message/{}".format( + NotifyMatrix.quote(room_id), self.transaction_id + ) + + if image_url and self.version == MatrixVersion.V2: + # Define our payload + image_payload = { + "msgtype": "m.image", + "url": image_url, + "body": f"{title if title else notify_type}", + } + + # Post our content + postokay, _, _ = self._fetch( + path, payload=image_payload, method="PUT" + ) + if not postokay: + # Mark our failure + has_error = True + continue + + # Increment transaction ID so subsequent sends + # don't reuse the same path + if self.access_token != self.password: + self.transaction_id += 1 + self.store.set( + "transaction_id", + self.transaction_id, + expires=self.default_cache_expiry_sec, + ) + path = "/rooms/{}/send/m.room.message/{}".format( + NotifyMatrix.quote(room_id), self.transaction_id + ) + + if attachments: + for attachment in attachments: + attachment["room_id"] = room_id + attachment["type"] = "m.room.message" + + postokay, _, _ = self._fetch( + path, payload=attachment, method="PUT" + ) + + # Increment the transaction ID to avoid future messages + # being recognized as retransmissions and ignored + if self.access_token != self.password: + self.transaction_id += 1 + self.store.set( + "transaction_id", + self.transaction_id, + expires=self.default_cache_expiry_sec, + ) + path = "/rooms/{}/send/m.room.message/{}".format( + NotifyMatrix.quote(room_id), + self.transaction_id, + ) + + if not postokay: + # Mark our failure + has_error = True + continue + + # Define our payload + payload = { + "msgtype": f"m.{self.msgtype}", + "body": "{title}{body}".format( + title="" if not title else f"# {title}\r\n", + body=body, + ), + } + + # Update our payload advance formatting for the services that + # support them. + if self.notify_format == NotifyFormat.HTML: + payload.update( + { + "format": "org.matrix.custom.html", + "formatted_body": "{title}{body}".format( + title=("" if not title else f"

{title}

"), + body=body, + ), + } + ) + + elif self.notify_format == NotifyFormat.MARKDOWN: + title_ = ( + "" + if not title + else ( + "

{}".format( + NotifyMatrix.escape_html(title, whitespace=False) + ) + + "

" + ) + ) + + payload.update( + { + "format": "org.matrix.custom.html", + "formatted_body": "{title}{body}".format( + title=title_, + body=markdown(body), + ), + } + ) + + # Post our content + postokay, _, _ = self._fetch(path, payload=payload, method="PUT") + + # Increment the transaction ID to avoid future messages being + # recognized as retransmissions and ignored + if self.access_token != self.password: + self.transaction_id += 1 + self.store.set( + "transaction_id", + self.transaction_id, + expires=self.default_cache_expiry_sec, + ) + + if not postokay: + # Notify our user + self.logger.warning( + f"Could not send notification Matrix room {room}." + ) + + # Mark our failure + has_error = True + continue + + return not has_error + + def _send_attachments(self, attach): + """Posts all of the provided attachments.""" + + payloads = [] + + for attachment in attach: + if not attachment: + # invalid attachment (bad file) + return False + + if ( + not IS_IMAGE.match(attachment.mimetype) + and self.version == MatrixVersion.V2 + ): + # unsuppored at this time + continue + + postokay, response, _ = self._fetch( + "/upload", attachment=attachment + ) + if not (postokay and isinstance(response, dict)): + # Failed to perform upload + return False + + # If we get here, we'll have a response that looks like: + # { + # "content_uri": "mxc://example.com/a-unique-key" + # } + + if self.version == MatrixVersion.V3: + # Prepare our payload + is_image = IS_IMAGE.match(attachment.mimetype) + payloads.append( + { + "body": attachment.name, + "info": { + "mimetype": attachment.mimetype, + "size": len(attachment), + }, + "msgtype": "m.image" if is_image else "m.file", + "url": response.get("content_uri"), + } + ) + if not is_image: + # Setup `m.file' + payloads[-1]["filename"] = attachment.name + + else: + # Prepare our payload + payloads.append( + { + "info": { + "mimetype": attachment.mimetype, + }, + "msgtype": "m.image", + "body": "tta.webp", + "url": response.get("content_uri"), + } + ) + + return payloads + + def _register(self): + """Register with the service if possible.""" + + # Prepare our Registration Payload. This will only work if + # registration is enabled for the public + payload = { + "kind": "user", + "auth": {"type": "m.login.dummy"}, + } + + # parameters + params = { + "kind": "user", + } + + # If a user is not specified, one will be randomly generated for + # you. If you do not specify a password, you will be unable to + # login to the account if you forget the access_token. + if self.user: + payload["username"] = self.user + + if self.password: + payload["password"] = self.password + + # Reuse a previously assigned device ID when available so Matrix + # keeps this notifier on a stable device identity across runs. + if self.device_id: + payload["device_id"] = self.device_id + else: + payload["initial_device_display_name"] = self.app_id + + # Register + postokay, response, _ = self._fetch( + "/register", payload=payload, params=params + ) + if not (postokay and isinstance(response, dict)): + # Failed to register + return False + + # Pull the response details + self.access_token = response.get("access_token") + self.user_id = response.get("user_id") + self.device_id = response.get("device_id") + + # home_server may be absent in modern Matrix responses; derive + # from user_id when the server does not include it explicitly. + hs_from_response = response.get("home_server") + if hs_from_response: + self.home_server = hs_from_response + elif self.user_id and not self.home_server: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + self.home_server = parts[1] + + self.store.set( + "access_token", + self.access_token, + expires=self.default_cache_expiry_sec, + ) + if self.home_server: + self.store.set( + "home_server", + self.home_server, + expires=self.default_cache_expiry_sec, + ) + self.store.set( + "user_id", self.user_id, expires=self.default_cache_expiry_sec + ) + if self.device_id: + self.store.set( + "device_id", + self.device_id, + expires=self.default_cache_expiry_sec, + ) + + if self.access_token is not None: + # Store our token into our store + self.logger.debug("Registered successfully with Matrix server.") + return True + + return False + + def _login(self): + """Acquires the matrix token required for making future requests. + + If we fail we return False, otherwise we return True + """ + + if self.access_token: + # Login not required; silently skip-over + return True + + if self.user and self.password: + # Prepare our Authentication Payload + if self.version == MatrixVersion.V3: + payload = { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": self.user, + }, + "password": self.password, + } + + else: + payload = { + "type": "m.login.password", + "user": self.user, + "password": self.password, + } + + # Reuse our last-known device ID when possible to avoid + # creating a brand-new Matrix device on every login. + if self.device_id: + payload["device_id"] = self.device_id + else: + payload["initial_device_display_name"] = self.app_id + + else: + # It's not possible to register since we need these 2 values + # to make the action possible. + self.logger.warning( + "Failed to login to Matrix server: " + "token or user/pass combo is missing." + ) + return False + + # Build our URL + postokay, response, _ = self._fetch("/login", payload=payload) + if not (postokay and isinstance(response, dict)): + # Failed to login + return False + + # Pull the response details + self.access_token = response.get("access_token") + self.user_id = response.get("user_id") + self.device_id = response.get("device_id") + + # home_server was dropped from login responses in recent Matrix + # spec versions. Only update if the server still returns it; + # otherwise derive it from user_id so room-alias resolution works. + hs_from_response = response.get("home_server") + if hs_from_response: + self.home_server = hs_from_response + elif self.user_id and not self.home_server: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + self.home_server = parts[1] + + if not self.access_token: + return False + + self.logger.debug("Authenticated successfully with Matrix server.") + + # Store our token into our store + self.store.set( + "access_token", + self.access_token, + expires=self.default_cache_expiry_sec, + ) + if self.home_server: + self.store.set( + "home_server", + self.home_server, + expires=self.default_cache_expiry_sec, + ) + self.store.set( + "user_id", self.user_id, expires=self.default_cache_expiry_sec + ) + if self.device_id: + self.store.set( + "device_id", + self.device_id, + expires=self.default_cache_expiry_sec, + ) + + return True + + def _whoami(self): + """Resolve user_id, device_id, and home_server via GET /account/whoami. + + Called when a raw access token is supplied (no login flow), so + the server never returned these identifiers directly. Results + are cached in the persistent store for future calls. + + Returns True on success, False otherwise. + """ + ok, response, _ = self._fetch( + "/account/whoami", payload=None, method="GET" + ) + if not (ok and isinstance(response, dict)): + return False + + self.user_id = response.get("user_id") or self.user_id + self.device_id = response.get("device_id") or self.device_id + + # Extract home_server from user_id (@localpart:homeserver) so that + # DM targets without an explicit homeserver resolve correctly. + if self.user_id and not self.home_server: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + self.home_server = parts[1] + + if self.user_id: + self.store.set( + "user_id", + self.user_id, + expires=self.default_cache_expiry_sec, + ) + if self.device_id: + self.store.set( + "device_id", + self.device_id, + expires=self.default_cache_expiry_sec, + ) + if self.home_server: + self.store.set( + "home_server", + self.home_server, + expires=self.default_cache_expiry_sec, + ) + return True + + def _logout(self): + """Relinquishes token from remote server.""" + + if not self.access_token: + # Login not required; silently skip-over + return True + + # Prepare our Registration Payload + payload = {} + + # Expire our token + postokay, response, _ = self._fetch("/logout", payload=payload) + if not postokay and response.get("errcode") != "M_UNKNOWN_TOKEN": + # If we get here, the token was declared as having already + # been expired. The response looks like this: + # { + # u'errcode': u'M_UNKNOWN_TOKEN', + # u'error': u'Access Token unknown or expired', + # } + # + # In this case it's okay to safely return True because + # we're logged out in this case. + return False + + # else: The response object looks like this if we were successful: + # {} + + # Pull the response details + self.access_token = None + self.home_server = None + self.user_id = None + self.device_id = None + self._e2ee_account = None + + # clear our tokens (including E2EE upload flag so it re-uploads + # after a fresh login) + self.store.clear( + "access_token", + "home_server", + "user_id", + "transaction_id", + "device_id", + "e2ee_keys_uploaded", + ) + + self.logger.debug("Unauthenticated successfully with Matrix server.") + + return True + + def _room_join(self, room): + """Joins a matrix room if we're not already in it. + + Otherwise it attempts to create it if it doesn't exist and + always returns the room_id if it was successful, otherwise it + returns None + """ + + if not self.access_token: + # We can't join a room if we're not logged in + return None + + if not isinstance(room, str): + # Not a supported string + return None + + # Prepare our Join Payload + payload = {} + + # Check if it's a room id... + result = IS_ROOM_ID.match(room) + if result: + room_token = result.group("room") + explicit_home_server = result.group("home_server") + + # Determine the homeserver context (used for cache metadata) + home_server = ( + explicit_home_server + if explicit_home_server + else self.home_server + ) + + # When hsreq is enabled (legacy behaviour), we always require + # a ':homeserver' segment on room IDs. Otherwise, we honour + # exactly what the caller provided and do not synthesise a + # homeserver when it was not specified. + cache_key = f"!{room_token}:{home_server}" + if explicit_home_server or self.hsreq: + room_id = cache_key + else: + room_id = f"!{room_token}" + + # Check our cache for speed: + try: + return self.store[cache_key]["id"] + + except KeyError: + pass + + # Build our URL + path = f"/join/{NotifyMatrix.quote(room_id)}" + + # Attempt to join the channel + postokay, response, _status_code = self._fetch( + path, payload=payload + ) + if not postokay: + return None + + # Prefer the server-provided room_id if one was returned, + # otherwise fall back to whatever we joined with. + joined_id = ( + response.get("room_id") if isinstance(response, dict) else None + ) or room_id + + # Cache mapping for faster future lookups. + self.store.set( + cache_key, + { + "id": joined_id, + "home_server": home_server, + }, + ) + + return joined_id + + # Try to see if it's an alias then... + result = IS_ROOM_ALIAS.match(room) + if not result: + # There is nothing else it could be + self.logger.warning( + f"Ignoring illegally formed room {room} " + "from Matrix server list." + ) + return None + + # If we reach here, we're dealing with a channel alias + home_server = ( + self.home_server + if not result.group("home_server") + else result.group("home_server") + ) + if not home_server and self.user_id: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + home_server = parts[1] + if not home_server: + self.logger.warning( + "Could not resolve a homeserver for Matrix room alias %s.", + room, + ) + return None + + # tidy our room (alias) identifier + room = "#{}:{}".format(result.group("room"), home_server) + + # Check our cache for speed: + try: + # We're done as we've already joined the channel + return self.store[room]["id"] + + except KeyError: + # No worries, we'll try to acquire the info + pass + + # If we reach here, we need to join the channel + + # Build our URL + path = f"/join/{NotifyMatrix.quote(room)}" + + # Attempt to join the channel + postokay, response, status_code = self._fetch(path, payload=payload) + if postokay: + # Cache our entry for fast access later + self.store.set( + room, + { + "id": response.get("room_id"), + "home_server": home_server, + }, + ) + + return response.get("room_id") + + # Only attempt to create a room when the server clearly indicates + # the alias does not exist. A join can fail for many reasons, such + # as invite required, auth failure, or permissions, and in those + # cases auto-creating is both noisy and incorrect. + if ( + status_code == requests.codes.not_found + or response.get("errcode") == "M_NOT_FOUND" + ): + return self._room_create(room) + + self.logger.warning( + "Could not join Matrix room alias %s (error=%s). " + "If this is a private room, ensure the user is invited or " + "already joined, or specify the room_id (!...).", + room, + status_code, + ) + return None + + def _room_create(self, room): + """Creates a matrix room and return it's room_id if successful + otherwise None is returned.""" + if not self.access_token: + # We can't create a room if we're not logged in + return None + + if not isinstance(room, str): + # Not a supported string + return None + + # Build our room if we have to: + result = IS_ROOM_ALIAS.match(room) + if not result: + # Illegally formed room + return None + + # Our home_server + home_server = ( + result.group("home_server") + if result.group("home_server") + else self.home_server + ) + if not home_server and self.user_id: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + home_server = parts[1] + if not home_server: + return None + + # update our room details + room = "#{}:{}".format(result.group("room"), home_server) + + # Prepare our Create Payload + payload = { + "room_alias_name": result.group("room"), + # Set our channel name + "name": "#{} - {}".format(result.group("room"), self.app_desc), + # hide the room by default; let the user open it up if they + # wish to others. + "visibility": "private", + "preset": "trusted_private_chat", + } + + # When E2EE is requested, enable encryption at room-creation time so + # that the room is encrypted from its very first message. This only + # applies when Apprise is the one creating the room; pre-existing + # rooms keep whatever encryption state the server already has. + if self.e2ee and self.secure and MATRIX_E2EE_SUPPORT: + payload["initial_state"] = [ + { + "type": "m.room.encryption", + "state_key": "", + "content": {"algorithm": "m.megolm.v1.aes-sha2"}, + } + ] + + postokay, response, _ = self._fetch("/createRoom", payload=payload) + if not postokay: + # Failed to create channel + # Typical responses: + # - {u'errcode': u'M_ROOM_IN_USE', + # u'error': u'Room alias already taken'} + # - {u'errcode': u'M_UNKNOWN', + # u'error': u'Internal server error'} + if response and response.get("errcode") == "M_ROOM_IN_USE": + return self._room_id(room) + return None + + room_id = response.get("room_id") + + # Cache our entry for fast access later + self.store.set( + response.get("room_alias"), + { + "id": room_id, + "home_server": home_server, + }, + ) + + # Pre-seed the room encryption cache so _e2ee_room_encrypted() does + # not issue a redundant GET -- we just set the encryption state. + if room_id and self.e2ee and self.secure and MATRIX_E2EE_SUPPORT: + self.store.set( + "e2ee_room_enc_{}".format(room_id), + True, + expires=self.default_cache_expiry_sec, + ) + + return room_id + + def _joined_rooms(self): + """Returns a list of the current rooms the logged in user is a + part of.""" + + if not self.access_token: + # No list is possible + return [] + + postokay, response, _ = self._fetch( + "/joined_rooms", payload=None, method="GET" + ) + if not postokay: + # Failed to retrieve listings + return [] + + # Return our list of rooms + return response.get("joined_rooms", []) + + def _room_id(self, room): + """Get room id from its alias. + Args: + room (str): The room alias name. + + Returns: + returns the room id if it can, otherwise it returns None + """ + + if not self.access_token: + # We can't get a room id if we're not logged in + return None + + if not isinstance(room, str): + # Not a supported string + return None + + # Build our room if we have to: + result = IS_ROOM_ALIAS.match(room) + if not result: + # Illegally formed room + return None + + # Our home_server + home_server = ( + result.group("home_server") + if result.group("home_server") + else self.home_server + ) + if not home_server and self.user_id: + parts = self.user_id.split(":", 1) + if len(parts) == 2: + home_server = parts[1] + if not home_server: + return None + + # update our room details + room = "#{}:{}".format(result.group("room"), home_server) + + # Make our request + postokay, response, _ = self._fetch( + f"/directory/room/{NotifyMatrix.quote(room)}", + payload=None, + method="GET", + ) + + if postokay: + return response.get("room_id") + + return None + + def _fetch( + self, + path, + payload=None, + params=None, + attachment=None, + method="POST", + url_override=None, + ok_status=None, + ): + """Wrapper to request.post() to manage it's response better and + make the send() function cleaner and easier to maintain. + + This function always returns a 3-tuple: + (success, response, status_code) + + The response is a dict when JSON is parseable, otherwise an empty + dict. The status_code defaults to 500 on local failures. + + *ok_status* is an optional collection of additional HTTP status codes + to treat as success (no warning logged). Use it for calls where a + non-200 response is expected and meaningful, e.g. 404 on a state-event + probe that returns "not found" = "feature not enabled". + """ + + # Define our headers + if params is None: + params = {} + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + "Accept": "application/json", + } + + if self.access_token is not None: + headers["Authorization"] = f"Bearer {self.access_token}" + + # Server Discovery / Well-known URI + if url_override: + url = url_override + + else: + try: + url = self.base_url + + except MatrixDiscoveryException: + # Discovery failed; we're done + return (False, {}, requests.codes.internal_server_error) + + # Default return status code + status_code = requests.codes.internal_server_error + + if path == "/upload": + if self.version == MatrixVersion.V3: + url += MATRIX_V3_MEDIA_PATH + path + + else: + url += MATRIX_V2_MEDIA_PATH + path + + params.update({"filename": attachment.name}) + with open(attachment.path, "rb") as fp: + payload = fp.read() + + # Update our content type + headers["Content-Type"] = attachment.mimetype + + elif not url_override: + if self.version == MatrixVersion.V3: + url += MATRIX_V3_API_PATH + path + + else: + url += MATRIX_V2_API_PATH + path + + # Our response object + response = {} + + # fetch function + fn = ( + requests.post + if method == "POST" + else (requests.put if method == "PUT" else requests.get) + ) + + # Always call throttle before any remote server i/o is made + self.throttle() + + # Define how many attempts we'll make if we get caught in a + # throttle event + retries = self.default_retries if self.default_retries > 0 else 1 + while retries > 0: + # Decrement our throttle retry count + retries -= 1 + + self.logger.debug( + "Matrix {} URL: {} (cert_verify={!r})".format( + ( + "POST" + if method == "POST" + else ("PUT" if method == "PUT" else "GET") + ), + url, + self.verify_certificate, + ) + ) + self.logger.debug(f"Matrix Payload: {payload!s}") + + # Initialize our response object + r = None + + try: + r = fn( + url, + data=dumps(payload) if not attachment else payload, + params=params if params else None, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + # Store status code + status_code = r.status_code + + self.logger.debug( + "Matrix Response: code={}, {}".format( + r.status_code, r.content + ) + ) + response = loads(r.content) + + if r.status_code == requests.codes.too_many_requests: + wait_ms = self.default_wait_ms + try: + wait_ms = response["retry_after_ms"] + + except KeyError: + try: + errordata = response["error"] + wait_ms = errordata["retry_after_ms"] + except KeyError: + pass + + self.logger.warning( + "Matrix server requested we throttle back " + "{}ms; retries left {}.".format(wait_ms, retries) + ) + self.logger.debug(f"Response Details:\r\n{r.content}") + + # Throttle for specified wait + self.throttle(wait=wait_ms / 1000) + + # Try again + continue + + elif r.status_code != requests.codes.ok: + # We had a problem + if ok_status and r.status_code in ok_status: + # Caller declared this status code acceptable + # (e.g. 404 on a state-event probe). Return + # failure tuple silently -- no warning logged. + return (False, response, status_code) + + status_str = NotifyMatrix.http_response_code_lookup( + r.status_code, MATRIX_HTTP_ERROR_MAP + ) + + self.logger.warning( + "Failed to handshake with Matrix server: " + "{}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + + self.logger.debug(f"Response Details:\r\n{r.content}") + + # Return; we're done + return (False, response, status_code) + + except (AttributeError, TypeError, ValueError): + # This gets thrown if we can't parse our JSON Response + # - ValueError = r.content is Unparsable + # - TypeError = r.content is None + # - AttributeError = r is None + self.logger.warning("Invalid response from Matrix server.") + self.logger.debug( + "Response Details:\r\n%r", + b"" if not r else (r.content or b""), + ) + return (False, {}, status_code) + + except ( + requests.TooManyRedirects, + requests.RequestException, + ) as e: + self.logger.warning( + "A Connection error occurred while registering " + "with Matrix server." + ) + self.logger.debug("Socket Exception: %s", e) + # Return; we're done + return (False, response, status_code) + + except OSError as e: + self.logger.warning( + "An I/O error occurred while reading {}.".format( + attachment.name if attachment else "unknown file" + ) + ) + self.logger.debug("I/O Exception: %s", e) + return (False, {}, status_code) + + return (True, response, status_code) + + # If we get here, we ran out of retries + return (False, {}, status_code) + + # --------------------------------------------------------------- + # E2EE helpers + # --------------------------------------------------------------- + + def _e2ee_room_encrypted(self, room_id): + """Return ``True`` if *room_id* has E2EE enabled on the server. + + The result is cached in the persistent store so subsequent sends + to the same room do not issue additional network requests. + """ + cache_key = "e2ee_room_enc_{}".format(room_id) + cached = self.store.get(cache_key) + if cached is not None: + return cached + + ok, response, _ = self._fetch( + "/rooms/{}/state/m.room.encryption".format( + NotifyMatrix.quote(room_id) + ), + method="GET", + # 404 = no encryption state event = room is not encrypted; + # suppress the warning that _fetch would otherwise log. + ok_status={requests.codes.not_found}, + ) + result = ok and bool(response) + self.store.set( + cache_key, + result, + expires=self.default_cache_expiry_sec, + ) + return result + + def _e2ee_setup(self): + """Ensure the E2EE device account exists and keys are uploaded. + + Creates a new :class:`MatrixOlmAccount` if one does not yet + exist in the persistent store, then calls + :meth:`_e2ee_upload_keys` if the server has not yet received + our device keys for the current access token. + + Returns ``True`` on success, ``False`` on failure. + """ + if self._e2ee_account is None: + acct_data = self.store.get("e2ee_account") + if acct_data: + try: + self._e2ee_account = MatrixOlmAccount.from_dict(acct_data) + except Exception: + self._e2ee_account = None + + if self._e2ee_account is None: + self._e2ee_account = MatrixOlmAccount() + self.store.set( + "e2ee_account", + self._e2ee_account.to_dict(), + expires=self.default_cache_expiry_sec, + ) + + # Keys uploaded status must match the current Matrix device identity + # and the account keys we are about to use. This lets us recover from + # cached state where the homeserver assigned a different device_id or + # where the local E2EE account changed. + current_binding = ( + "{}|{}|{}|{}".format( + self.user_id or "", + self.device_id or "", + self._e2ee_account.identity_key, + self._e2ee_account.signing_key, + ) + if self._e2ee_account is not None + else "" + ) + if self.store.get("e2ee_device_binding") != current_binding: + self.store.clear("e2ee_keys_uploaded") + + if not self.store.get("e2ee_keys_uploaded"): + return self._e2ee_upload_keys() + + return True + + def _e2ee_upload_keys(self): + """POST device keys to ``/_matrix/client/v3/keys/upload``.""" + if not self.user_id or not self.device_id: + self.logger.warning( + "Matrix E2EE: cannot upload keys without user_id " + "and device_id; ensure login completes first." + ) + return False + + payload = { + "device_keys": self._e2ee_account.device_keys_payload( + self.user_id, self.device_id + ), + "one_time_keys": self._e2ee_account.one_time_keys_payload( + self.user_id, + self.device_id, + count=self.default_e2ee_otk_count, + ), + "fallback_keys": self._e2ee_account.fallback_keys_payload( + self.user_id, self.device_id + ), + } + postokay, response, _ = self._fetch("/keys/upload", payload=payload) + if not postokay: + self.logger.warning("Matrix E2EE: device key upload failed.") + return False + + # Mirror stable python-olm account behaviour: once uploaded, the + # current one-time key batch is considered published and a future + # upload should generate a fresh set. + self._e2ee_account.mark_keys_as_published() + self.store.set( + "e2ee_account", + self._e2ee_account.to_dict(), + expires=self.default_cache_expiry_sec, + ) + + self.store.set( + "e2ee_keys_uploaded", + True, + expires=self.default_cache_expiry_sec, + ) + self.store.set( + "e2ee_device_binding", + "{}|{}|{}|{}".format( + self.user_id, + self.device_id, + self._e2ee_account.identity_key, + self._e2ee_account.signing_key, + ), + expires=self.default_cache_expiry_sec, + ) + + # Track the server-side OTK count so _e2ee_replenish_otks can + # decide whether a top-up is needed after the next /keys/claim. + counts = ( + response.get("one_time_key_counts", {}) + if isinstance(response, dict) + else {} + ) + self.store.set( + "e2ee_otk_server_count", + counts.get("signed_curve25519", 0), + expires=self.default_cache_expiry_sec, + ) + + self.logger.debug( + "Matrix E2EE: device keys uploaded for %s / %s " + "(server OTK count: %d).", + self.user_id, + self.device_id, + counts.get("signed_curve25519", 0), + ) + return True + + def _e2ee_replenish_otks(self, claimed_count=0, skipped_no_otk=0): + """Top up the server-side OTK pool after a ``/keys/claim`` event. + + Parameters: + claimed_count -- number of OTKs successfully consumed by the + preceding ``/keys/claim`` (= ``built_count`` + from :meth:`_e2ee_share_room_key`) + skipped_no_otk -- devices that were skipped because the server + returned no OTK for them (pool already dry) + + A replenishment upload is issued when any of the following is true: + + - ``skipped_no_otk > 0``: the pool was already depleted during + the current claim -- top up immediately so the next key share + can reach those devices. + - estimated remaining OTKs after claim < + ``default_e2ee_otk_replenish_threshold``: pool is running low. + - server count was never recorded (unknown state): replenish as a + precaution. + + Only ``one_time_keys`` is uploaded so the server does not treat + this as a device re-registration. + + Returns ``True`` on success (or when no top-up was needed), + ``False`` on network failure (non-fatal -- the preceding send + already succeeded). + """ + if not self.user_id or not self.device_id: + return False + + server_count = self.store.get("e2ee_otk_server_count") + unknown_count = server_count is None + + if unknown_count: + remaining = 0 + else: + remaining = max(0, server_count - claimed_count) + + need_replenish = ( + skipped_no_otk > 0 + or unknown_count + or remaining < self.default_e2ee_otk_replenish_threshold + ) + + if not need_replenish: + self.logger.trace( + "Matrix E2EE: OTK pool sufficient " + "(~%d remaining after claim); skipping replenishment.", + remaining, + ) + return True + + if skipped_no_otk > 0: + self.logger.warning( + "Matrix E2EE: %d device(s) had no OTK available during " + "key share (server pool was depleted). Those device(s) " + "will not decrypt the current message. Replenishing OTK " + "pool now so the next session rotation can reach them.", + skipped_no_otk, + ) + elif unknown_count: + self.logger.debug( + "Matrix E2EE: server OTK count unknown; " + "replenishing as a precaution.", + ) + else: + self.logger.debug( + "Matrix E2EE: OTK pool low (~%d remaining after claim, " + "threshold=%d); replenishing.", + remaining, + self.default_e2ee_otk_replenish_threshold, + ) + + payload = { + "one_time_keys": self._e2ee_account.one_time_keys_payload( + self.user_id, + self.device_id, + count=self.default_e2ee_otk_count, + ), + } + postokay, response, _ = self._fetch("/keys/upload", payload=payload) + if not postokay: + self.logger.warning( + "Matrix E2EE: OTK replenishment upload failed " + "(estimated ~%d remaining); pool may be depleted " + "on the next key share.", + remaining, + ) + return False + + self._e2ee_account.mark_keys_as_published() + self.store.set( + "e2ee_account", + self._e2ee_account.to_dict(), + expires=self.default_cache_expiry_sec, + ) + + counts = ( + response.get("one_time_key_counts", {}) + if isinstance(response, dict) + else {} + ) + new_count = counts.get("signed_curve25519", 0) + self.store.set( + "e2ee_otk_server_count", + new_count, + expires=self.default_cache_expiry_sec, + ) + self.logger.debug( + "Matrix E2EE: OTK pool replenished; " + "server now reports %d signed_curve25519 key(s).", + new_count, + ) + return True + + def _e2ee_get_megolm(self, room_id): + """Return the current outbound MegOLM session for *room_id*. + + Creates a new session when none exists or when the existing one + has reached the rotation threshold. Also clears the + ``e2ee_key_shared_*`` flag so the new session key is re-shared. + """ + store_key = "e2ee_megolm_{}".format(room_id) + session_data = self.store.get(store_key) + + if session_data: + try: + session = MatrixMegOlmSession.from_dict(session_data) + if not session.should_rotate(): + return session + except Exception: + # Cached session is unreadable or from an older incompatible + # format; force creation of a fresh one below. + pass + + # New or rotated session + session = MatrixMegOlmSession() + self.store.set( + store_key, + session.to_dict(), + expires=self.default_cache_expiry_sec, + ) + # Clear key-shared flag so we share the new session key + self.store.clear("e2ee_key_shared_{}".format(room_id)) + return session + + def _e2ee_save_megolm(self, room_id, session): + """Persist the updated MegOLM session state.""" + self.store.set( + "e2ee_megolm_{}".format(room_id), + session.to_dict(), + expires=self.default_cache_expiry_sec, + ) + + def _e2ee_room_members(self, room_id): + """Query device keys for all joined members of *room_id*. + + Returns a nested dict:: + + {user_id: {device_id: {"curve25519": ..., "ed25519": ...}}} + + Returns ``None`` on HTTP failure, empty dict when the room has + no members (unlikely but tolerated). + """ + path = "/rooms/{}/joined_members".format(NotifyMatrix.quote(room_id)) + postokay, response, _ = self._fetch(path, payload=None, method="GET") + if not postokay or not isinstance(response, dict): + return None + + member_ids = list(response.get("joined", {}).keys()) + if not member_ids: + return {} + + postokay, resp, _ = self._fetch( + "/keys/query", + payload={"device_keys": {uid: [] for uid in member_ids}}, + ) + if not postokay or not isinstance(resp, dict): + return None + + result = {} + for uid, devices in resp.get("device_keys", {}).items(): + result[uid] = {} + for dev_id, dev_info in devices.items(): + if not verify_device_keys(dev_info, uid, dev_id): + self.logger.debug( + "Matrix E2EE: device key signature invalid " + "for %s / %s; device skipped.", + uid, + dev_id, + ) + continue + keys = dev_info.get("keys", {}) + result[uid][dev_id] = { + "curve25519": keys.get("curve25519:{}".format(dev_id), ""), + "ed25519": keys.get("ed25519:{}".format(dev_id), ""), + } + return result + + def _e2ee_share_room_key(self, room_id, session): + """Send the MegOLM session key to all devices in *room_id*. + + Flow: + 1. Fetch joined-member device keys via /keys/query + 2. Claim one-time keys via /keys/claim + 3. Create outbound Olm sessions and encrypt the room-key event + 4. Deliver via PUT /sendToDevice/m.room.encrypted/{txnId} + + Returns ``True`` on success (partial device failures are + tolerated), ``False`` only when a critical step fails. + """ + members = self._e2ee_room_members(room_id) + if members is None: + self.logger.warning( + "Matrix E2EE: failed to query room members for %s.", + room_id, + ) + return False + + if not members: + self.logger.trace( + "Matrix E2EE: no room members found for %s; " + "skipping key share.", + room_id, + ) + return True + + # Count total device slots being requested (for diagnostics) + total_devices = sum(len(devs) for devs in members.values()) + self.logger.debug( + "Matrix E2EE: sharing session %s for room %s with " + "%d member(s) / %d device(s).", + session.session_id[:12], + room_id, + len(members), + total_devices, + ) + + # Build the claim request for all member devices. + # "signed_curve25519" is the algorithm Matrix clients publish and + # servers are required to support; "curve25519" (unsigned) is + # deprecated and usually yields no keys on current servers. + otk_request = {} + for uid, devs in members.items(): + otk_request[uid] = dict.fromkeys(devs, "signed_curve25519") + + postokay, otk_resp, _ = self._fetch( + "/keys/claim", + payload={"one_time_keys": otk_request}, + ) + if not postokay: + self.logger.warning("Matrix E2EE: failed to claim one-time keys.") + return False + + otk_keys = ( + otk_resp.get("one_time_keys", {}) + if isinstance(otk_resp, dict) + else {} + ) + + # Log failures from the claim response + # spec: server populates failures{} with unreachable servers + failures = ( + otk_resp.get("failures", {}) if isinstance(otk_resp, dict) else {} + ) + if failures: + self.logger.debug( + "Matrix E2EE: /keys/claim reported failures for server(s): %s", + list(failures.keys()), + ) + + # Build to-device message payload + to_device_msgs = {} + room_key_content = { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": room_id, + "session_id": session.session_id, + "session_key": session.session_key(), + } + self.logger.trace( + "Matrix E2EE: room_key session_id=%s counter=%d", + session.session_id[:12], + session._counter, + ) + + skipped_own = 0 + skipped_no_ik = 0 + skipped_no_otk = 0 + skipped_otk_invalid = 0 + skipped_olm_fail = 0 + built_count = 0 + + for uid, devices in members.items(): + to_device_msgs[uid] = {} + for dev_id, dev_info in devices.items(): + # Skip our own device to avoid self-Olm-session setup + if uid == self.user_id and dev_id == self.device_id: + skipped_own += 1 + self.logger.trace( + "Matrix E2EE: skipping own device %s / %s.", + uid, + dev_id, + ) + continue + + their_ik = dev_info.get("curve25519", "") + if not their_ik: + skipped_no_ik += 1 + self.logger.trace( + "Matrix E2EE: no curve25519 key for " + "%s / %s; device skipped.", + uid, + dev_id, + ) + continue + + # Locate and verify the OTK for this device. + # Servers return signed_curve25519 keys (the algorithm we + # requested) as {"key": ..., "signatures": ...} dicts. + # signed_curve25519 OTKs are always KeyObjects + # {"key": ..., "signatures": ...}; plain-string values + # are not valid for this algorithm and are rejected. + their_otk = None + otk_entry = otk_keys.get(uid, {}).get(dev_id, {}) + self.logger.trace( + "Matrix E2EE: OTK entry for %s / %s: keys=%s", + uid, + dev_id, + list(otk_entry.keys()) + if isinstance(otk_entry, dict) + else repr(type(otk_entry)), + ) + if isinstance(otk_entry, dict): + for k, v in otk_entry.items(): + if not k.startswith("signed_curve25519:"): + self.logger.trace( + "Matrix E2EE: OTK key %r for %s / %s " + "is not signed_curve25519; skipped.", + k, + uid, + dev_id, + ) + continue + if not isinstance(v, dict): + self.logger.trace( + "Matrix E2EE: OTK for %s / %s is " + "not a KeyObject (got %s); skipped.", + uid, + dev_id, + type(v).__name__, + ) + break + ed25519_pub = dev_info.get("ed25519", "") + if not ed25519_pub or not verify_signed_otk( + v, uid, dev_id, ed25519_pub + ): + skipped_otk_invalid += 1 + # Keep at debug -- invalid signature is unexpected + # and worth surfacing at -vv. + self.logger.debug( + "Matrix E2EE: OTK signature " + "invalid for %s / %s (ed25519_pub=%s); " + "skipped.", + uid, + dev_id, + ed25519_pub[:12] if ed25519_pub else "(empty)", + ) + else: + their_otk = v.get("key") + self.logger.trace( + "Matrix E2EE: OTK accepted for " + "%s / %s (key_id=%s).", + uid, + dev_id, + k, + ) + break + else: + self.logger.trace( + "Matrix E2EE: no OTK dict for %s / %s " + "(type=%s); device skipped.", + uid, + dev_id, + type(otk_entry).__name__, + ) + + if not their_otk: + skipped_no_otk += 1 + self.logger.trace( + "Matrix E2EE: no usable OTK for %s / %s; " + "device skipped.", + uid, + dev_id, + ) + continue + + try: + olm_session = self._e2ee_account.create_outbound_session( + their_ik, their_otk + ) + except Exception as exc: + skipped_olm_fail += 1 + # Keep at debug -- Olm session failure is unexpected. + self.logger.debug( + "Matrix E2EE: failed to build Olm session " + "for %s / %s: %s", + uid, + dev_id, + exc, + ) + continue + + # Build the m.room_key inner plaintext per Matrix spec: + # https://spec.matrix.org/v1.11/client-server-api/#mroomkey + # + # Required fields only; non-standard extension fields + # (sender_device_keys, org.matrix.msc4147.device_keys) have + # been removed because they: + # - Add ~930 bytes to an otherwise ~400-byte payload, + # bloating the Olm ciphertext from ~400B to ~1640B. + # - Are not part of the spec and may confuse strict + # implementations (Element/matrix-sdk-crypto warns on + # unknown fields in to-device events in some builds). + # - Contain unsigned device-key material that recipients + # should instead fetch via /keys/query for authenticity. + inner = dumps( + { + "type": "m.room_key", + "content": room_key_content, + "sender": self.user_id, + "recipient": uid, + "recipient_keys": { + "ed25519": dev_info.get("ed25519", "") + }, + "keys": {"ed25519": self._e2ee_account.signing_key}, + } + ) + ciphertext = olm_session.encrypt(inner) + built_count += 1 + + self.logger.trace( + "Matrix E2EE: Olm-encrypted room key for " + "%s / %s (ciphertext type=%d, inner_len=%d).", + uid, + dev_id, + ciphertext.get("type", -1), + len(inner), + ) + + to_device_msgs[uid][dev_id] = { + "algorithm": "m.olm.v1.curve25519-aes-sha2", + "ciphertext": {their_ik: ciphertext}, + "sender_key": self._e2ee_account.identity_key, + } + + self.logger.debug( + "Matrix E2EE: key-share summary for room %s: " + "built=%d skipped_own=%d skipped_no_ik=%d " + "skipped_no_otk=%d skipped_otk_invalid=%d " + "skipped_olm_fail=%d", + room_id, + built_count, + skipped_own, + skipped_no_ik, + skipped_no_otk, + skipped_otk_invalid, + skipped_olm_fail, + ) + + # Only send if at least one device message was built + if not any(v for v in to_device_msgs.values()): + self.logger.trace( + "Matrix E2EE: no to-device messages built for " + "room %s; nothing to send.", + room_id, + ) + return True + + if self.access_token != self.password: + self.transaction_id += 1 + self.store.set( + "transaction_id", + self.transaction_id, + expires=self.default_cache_expiry_sec, + ) + path = "/sendToDevice/m.room.encrypted/{}".format(self.transaction_id) + postokay, _, _ = self._fetch( + path, + payload={"messages": to_device_msgs}, + method="PUT", + ) + if not postokay: + self.logger.warning( + "Matrix E2EE: failed to deliver room key to devices in %s.", + room_id, + ) + return False + + self.logger.debug( + "Matrix E2EE: room key delivered to %d device(s) in %s " + "(txnId=%s).", + built_count, + room_id, + self.transaction_id, + ) + + # Check whether the OTK pool needs topping up. Pass the number of + # OTKs consumed (built_count) and any devices skipped because the + # server had no OTK for them so _e2ee_replenish_otks can log the + # right diagnostic and decide whether an upload is needed. + # We always reach here with built_count >= 1 (the any() guard above + # returns early when no messages were built), so the call is never + # redundant -- _e2ee_replenish_otks itself decides whether to upload. + self._e2ee_replenish_otks( + claimed_count=built_count, + skipped_no_otk=skipped_no_otk, + ) + + return True + + def _e2ee_send_to_room(self, room_id, body, title, notify_type): + """Encrypt and send one message to *room_id* via MegOLM. + + Shares the MegOLM session key with room members when the + session is new or has just been rotated. + Returns ``True`` on success, ``False`` on failure. + """ + session = self._e2ee_get_megolm(room_id) + + self.logger.trace( + "Matrix E2EE: using MegOLM session %s counter=%d for %s.", + session.session_id[:12], + session._counter, + room_id, + ) + + # Share the room key unless this exact MegOLM session was already + # announced. Older stores may contain a legacy boolean flag; treat it + # as stale so the next send re-shares the key and repairs recipients + # that never received the original m.room_key. + shared_flag = "e2ee_key_shared_{}".format(room_id) + cached_shared = self.store.get(shared_flag) + if cached_shared != session.session_id: + self.logger.trace( + "Matrix E2EE: session key not yet shared " + "(cached=%r current=%r); sharing now.", + cached_shared[:12] + if isinstance(cached_shared, str) + else cached_shared, + session.session_id[:12], + ) + if not self._e2ee_share_room_key(room_id, session): + return False + self.store.set( + shared_flag, + session.session_id, + expires=self.default_cache_expiry_sec, + ) + else: + self.logger.trace( + "Matrix E2EE: session key already shared for " + "session %s; skipping key share.", + session.session_id[:12], + ) + + # Build the inner plaintext event + msg_content = { + "msgtype": "m.{}".format(self.msgtype), + "body": "{title}{body}".format( + title="" if not title else "# {}\r\n".format(title), + body=body, + ), + } + + if self.notify_format == NotifyFormat.HTML: + msg_content.update( + { + "format": "org.matrix.custom.html", + "formatted_body": "{title}{body}".format( + title=( + "" if not title else "

{}

".format(title) + ), + body=body, + ), + } + ) + + elif self.notify_format == NotifyFormat.MARKDOWN: + msg_content.update( + { + "format": "org.matrix.custom.html", + "formatted_body": "{title}{body}".format( + title=( + "" + if not title + else "

{}

".format( + NotifyMatrix.escape_html( + title, whitespace=False + ) + ) + ), + body=markdown(body), + ), + } + ) + + inner_event = { + "type": "m.room.message", + "content": msg_content, + "room_id": room_id, + } + + ciphertext = session.encrypt(inner_event) + self._e2ee_save_megolm(room_id, session) + + self.logger.trace( + "Matrix E2EE: MegOLM ciphertext produced for room %s " + "(session_id=%s counter_before_encrypt=%d).", + room_id, + session.session_id[:12], + # _advance() already ran; counter is now N+1 after encrypt + session._counter - 1, + ) + + path = "/rooms/{}/send/m.room.encrypted/{}".format( + NotifyMatrix.quote(room_id), self.transaction_id + ) + encrypted_payload = { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": ciphertext, + "sender_key": self._e2ee_account.identity_key, + "session_id": session.session_id, + "device_id": self.device_id or "", + } + + postokay, _, _ = self._fetch( + path, payload=encrypted_payload, method="PUT" + ) + + if self.access_token != self.password: + self.transaction_id += 1 + self.store.set( + "transaction_id", + self.transaction_id, + expires=self.default_cache_expiry_sec, + ) + + return postokay + + def _e2ee_send_attachment(self, attachment, room_id, session): + """Encrypt *attachment* and deliver it to *room_id* via MegOLM. + + Steps: + 1. Read the file into memory and encrypt with AES-256-CTR. + 2. Upload the ciphertext to the media server (content_uri). + 3. Build an ``m.room.message`` inner event whose ``file`` field + carries the EncryptedFile metadata (key + iv + sha256). + 4. Encrypt the inner event with MegOLM and PUT to the room. + + Returns ``True`` on success, ``False`` on any failure. + """ + # Read file bytes + try: + with open(attachment.path, "rb") as fh: + file_data = fh.read() + except OSError as e: + self.logger.warning( + "Matrix E2EE: could not read attachment {}.".format( + attachment.name or "file" + ) + ) + self.logger.debug(f"I/O Exception: {e!s}") + return False + + # Encrypt locally with AES-256-CTR + ciphertext, file_info = encrypt_attachment(file_data) + + # Upload the ciphertext to the media server. + # The encrypted bytes are posted directly rather than from a file + # path, so we call requests.post() directly instead of _fetch(). + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/octet-stream", + "Accept": "application/json", + } + if self.access_token: + headers["Authorization"] = f"Bearer {self.access_token}" + + try: + base = self.base_url + except Exception: + return False + + media_path = ( + MATRIX_V3_MEDIA_PATH + if self.version == MatrixVersion.V3 + else MATRIX_V2_MEDIA_PATH + ) + upload_url = base + media_path + "/upload" + + self.logger.debug( + "Matrix E2EE: uploading encrypted attachment to %s " + "(name=%s size=%d iv=%s sha256=%s).", + upload_url, + attachment.name or "file", + len(ciphertext), + file_info.get("iv", "?"), + file_info.get("hashes", {}).get("sha256", "?"), + ) + + self.throttle() + try: + r = requests.post( + upload_url, + data=ciphertext, + params={"filename": attachment.name or "file"}, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + except requests.RequestException as e: + self.logger.warning( + "Matrix E2EE: connection error uploading encrypted attachment." + ) + self.logger.debug(f"Socket Exception: {e!s}") + return False + + try: + upload_resp = loads(r.content) + except Exception: + upload_resp = {} + + self.logger.debug( + "Matrix E2EE: upload response HTTP %d body=%r.", + r.status_code, + r.content[:200], + ) + + if r.status_code != requests.codes.ok or not upload_resp.get( + "content_uri" + ): + self.logger.warning( + "Matrix E2EE: media upload failed (HTTP {}).".format( + r.status_code + ) + ) + return False + + file_info["url"] = upload_resp["content_uri"] + self.logger.debug( + "Matrix E2EE: attachment content_uri=%s.", + upload_resp["content_uri"], + ) + + # Build the inner plaintext attachment event + is_image = IS_IMAGE.match(attachment.mimetype) + content = { + "msgtype": "m.image" if is_image else "m.file", + "body": attachment.name or "file", + "file": file_info, + "info": { + "mimetype": attachment.mimetype, + "size": len(attachment), + }, + } + if not is_image: + content["filename"] = attachment.name or "file" + + inner_event = { + "type": "m.room.message", + "content": content, + "room_id": room_id, + } + + # Encrypt with MegOLM and send + ciphertext_event = session.encrypt(inner_event) + self._e2ee_save_megolm(room_id, session) + + path = "/rooms/{}/send/m.room.encrypted/{}".format( + NotifyMatrix.quote(room_id), self.transaction_id + ) + encrypted_payload = { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": ciphertext_event, + "sender_key": self._e2ee_account.identity_key, + "session_id": session.session_id, + "device_id": self.device_id or "", + } + postokay, _, _ = self._fetch( + path, payload=encrypted_payload, method="PUT" + ) + if self.access_token != self.password: + self.transaction_id += 1 + self.store.set( + "transaction_id", + self.transaction_id, + expires=self.default_cache_expiry_sec, + ) + return postokay + + def _dm_room_find_or_create(self, user): + """Resolve *user* (``@localpart`` or ``@localpart:homeserver``) + to a Matrix room ID suitable for direct messaging. + + Lookup order: + 1. Persistent-store cache. + 2. ``GET /user/{selfId}/account_data/m.direct`` -- check whether + an existing DM room already exists for this user. + 3. ``POST /createRoom`` with ``is_direct=true`` and an invite for + the target user. The ``m.direct`` account-data entry is then + updated so other clients also recognise the room as a DM. + + Returns the room ID string on success, or ``None`` on failure. + """ + result = IS_USER.match(user) + if not result: + self.logger.warning("Matrix DM: invalid user identifier %r.", user) + return None + + home_server = ( + result.group("home_server") + if result.group("home_server") + else self.home_server + ) + user_id = "@{}:{}".format(result.group("user"), home_server) + + cache_key = "dm_room_{}".format(user_id) + cached = self.store.get(cache_key) + if cached: + return cached + + # Fetch existing m.direct mapping from the server + mdirect = {} + if self.user_id: + ok, resp, _ = self._fetch( + "/user/{}/account_data/m.direct".format( + NotifyMatrix.quote(self.user_id) + ), + method="GET", + ) + if ok and isinstance(resp, dict): + mdirect = resp + rooms = mdirect.get(user_id, []) + if rooms: + room_id = rooms[0] + self.store.set( + cache_key, + room_id, + expires=self.default_cache_expiry_sec, + ) + return room_id + + # No existing DM room -- create one + dm_payload = { + "is_direct": True, + "preset": "trusted_private_chat", + "invite": [user_id], + } + + # When E2EE is requested, enable encryption at room-creation time. + if self.e2ee and self.secure and MATRIX_E2EE_SUPPORT: + dm_payload["initial_state"] = [ + { + "type": "m.room.encryption", + "state_key": "", + "content": {"algorithm": "m.megolm.v1.aes-sha2"}, + } + ] + + ok, response, _ = self._fetch("/createRoom", payload=dm_payload) + if not ok or not isinstance(response, dict): + self.logger.warning( + "Matrix DM: failed to create room for %s.", user_id + ) + return None + + room_id = response.get("room_id") + if not room_id: + return None + + self.store.set( + cache_key, + room_id, + expires=self.default_cache_expiry_sec, + ) + + # Pre-seed the room encryption cache. + if self.e2ee and self.secure and MATRIX_E2EE_SUPPORT: + self.store.set( + "e2ee_room_enc_{}".format(room_id), + True, + expires=self.default_cache_expiry_sec, + ) + + # Update the m.direct account-data mapping so other clients + # recognise this room as a DM conversation. + if self.user_id: + mdirect[user_id] = [*mdirect.get(user_id, []), room_id] + self._fetch( + "/user/{}/account_data/m.direct".format( + NotifyMatrix.quote(self.user_id) + ), + payload=mdirect, + method="PUT", + ) + + return room_id + + # --------------------------------------------------------------- + # Destructor / URL / parse + # --------------------------------------------------------------- + + def __del__(self): + """Ensure we relinquish our token.""" + if self.mode == MatrixWebhookMode.T2BOT: + # nothing to do + return + + if self.store.mode != PersistentStoreMode.MEMORY: + # We no longer have to log out as we have persistant storage + # to re-use our credentials with + return + + if ( + self.access_token is not None + and self.access_token == self.password + and not self.user + ): + return + + # Best-effort cleanup only + with contextlib.suppress(Exception): + self._logout() + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another simliar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol if self.secure else self.protocol, + self.mode, + ( + self.host + if self.mode != MatrixWebhookMode.T2BOT + else self.access_token + ), + self.port if self.port else (443 if self.secure else 80), + ( + self.webhook_path + if self.mode == MatrixWebhookMode.HOOKSHOT + else None + ), + self.user if self.mode != MatrixWebhookMode.T2BOT else None, + self.password if self.mode != MatrixWebhookMode.T2BOT else None, + ) + + @staticmethod + def runtime_deps(): + """Return runtime dependency package names. + + E2EE support requires the `cryptography` package. + """ + return ("cryptography",) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified + arguments.""" + + # Define any URL parameters + params = { + "image": "yes" if self.include_image else "no", + "mode": self.mode, + "version": self.version, + "msgtype": self.msgtype, + "discovery": "yes" if self.discovery else "no", + "hsreq": "yes" if self.hsreq else "no", + } + + if self.mode == MatrixWebhookMode.HOOKSHOT: + params["path"] = self.webhook_path + + if not self.e2ee: + params["e2ee"] = "no" + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + auth = "" + if self.mode != MatrixWebhookMode.T2BOT: + # Determine Authentication + if self.user and self.password: + auth = "{user}:{password}@".format( + user=NotifyMatrix.quote(self.user, safe=""), + password=self.pprint( + self.password, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + ) + + elif self.user or self.password: + auth = "{value}@".format( + value=NotifyMatrix.quote( + self.user if self.user else self.password, safe="" + ), + ) + + return "{schema}://{auth}{hostname}{port}/{rooms}?{params}".format( + schema=(self.secure_protocol if self.secure else self.protocol), + auth=auth, + hostname=( + NotifyMatrix.quote(self.host, safe="") + if self.mode != MatrixWebhookMode.T2BOT + else self.pprint(self.access_token, privacy, safe="") + ), + port=("" if not self.port else f":{self.port}"), + rooms=NotifyMatrix.quote("/".join(self.rooms + self.users)), + params=NotifyMatrix.urlencode(params), + ) + + def __len__(self): + """Returns the number of targets associated with this + notification.""" + targets = len(self.rooms) + len(self.users) + return targets if targets > 0 else 1 + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow us + to re-instantiate this object.""" + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + if not results.get("host"): + return None + + # Get our rooms + results["targets"] = NotifyMatrix.split_path(results["fullpath"]) + + # Support the 'to' variable so that we can support rooms this + # way too. The 'to' makes it easier to use yaml configuration + if "to" in results["qsd"] and len(results["qsd"]["to"]): + results["targets"] += NotifyMatrix.parse_list(results["qsd"]["to"]) + + # Boolean to include an image or not + results["include_image"] = parse_bool( + results["qsd"].get( + "image", + NotifyMatrix.template_args["image"]["default"], + ) + ) + + # Boolean to perform a server discovery + results["discovery"] = parse_bool( + results["qsd"].get( + "discovery", + NotifyMatrix.template_args["discovery"]["default"], + ) + ) + + # Boolean to enforce ':homeserver' on room IDs when missing + results["hsreq"] = parse_bool( + results["qsd"].get( + "hsreq", + NotifyMatrix.template_args["hsreq"]["default"], + ) + ) + + if "path" in results["qsd"]: + results["webhook_path"] = NotifyMatrix.unquote( + results["qsd"]["path"] + ) + + # E2EE flag + if "e2ee" in results["qsd"]: + results["e2ee"] = parse_bool(results["qsd"]["e2ee"]) + + # Get our mode + results["mode"] = results["qsd"].get("mode") + + # t2bot detection... look for just a hostname, and/or just a + # user/host if we match this; we can go ahead and set the mode + # (but only if it was otherwise not set) + if ( + results["mode"] is None + and not results["password"] + and not results["targets"] + ): + # Default mode to t2bot + results["mode"] = MatrixWebhookMode.T2BOT + + if ( + results["mode"] + and results["mode"].lower() == MatrixWebhookMode.T2BOT + ): + # unquote our hostname and pass it in as the password/token + results["password"] = NotifyMatrix.unquote(results["host"]) + + # Support the message type keyword + if "msgtype" in results["qsd"] and len(results["qsd"]["msgtype"]): + results["msgtype"] = NotifyMatrix.unquote( + results["qsd"]["msgtype"] + ) + + # Support the use of the token= keyword + if "token" in results["qsd"] and len(results["qsd"]["token"]): + results["password"] = NotifyMatrix.unquote(results["qsd"]["token"]) + + elif not results["password"] and results["user"]: + # swap + results["password"] = results["user"] + results["user"] = None + + # Support the use of the version= or v= keyword + if "version" in results["qsd"] and len(results["qsd"]["version"]): + results["version"] = NotifyMatrix.unquote( + results["qsd"]["version"] + ) + + elif "v" in results["qsd"] and len(results["qsd"]["v"]): + results["version"] = NotifyMatrix.unquote(results["qsd"]["v"]) + + return results + + @staticmethod + def parse_native_url(url): + """ + Support https://webhooks.t2bot.io/api/v1/matrix/hook/WEBHOOK_TOKEN/ + """ + + result = re.match( + r"^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/" + r"(?P[A-Z0-9_-]+)/?" + r"(?P\?.+)?$", + url, + re.I, + ) + + if result: + mode = f"mode={MatrixWebhookMode.T2BOT}" + + return NotifyMatrix.parse_url( + "{schema}://{webhook_token}/{params}".format( + schema=NotifyMatrix.secure_protocol, + webhook_token=result.group("webhook_token"), + params=( + f"?{mode}" + if not result.group("params") + else "{}&{}".format(result.group("params"), mode) + ), + ) + ) + + return None + + def server_discovery(self): + """ + Home Server Discovery as documented here: + https://spec.matrix.org/v1.11/client-server-api/#well-known-uri + """ + + if not (self.discovery and self.secure): + # Nothing further to do with insecure server setups + return "" + + # Get our content from cache + base_url, identity_url = ( + self.store.get(self.discovery_base_key), + self.store.get(self.discovery_identity_key), + ) + + if not (base_url is None and identity_url is None): + # We can use our cached value and return early + return base_url + + # the Matrix ID at the first colon. + verify_url = ( + "{schema}://{hostname}{port}/.well-known/matrix/client".format( + schema="https" if self.secure else "http", + hostname=self.host, + port=("" if not self.port else f":{self.port}"), + ) + ) + + _, response, status_code = self._fetch( + None, method="GET", url_override=verify_url + ) + + # Output may look as follows: + # { + # "m.homeserver": { + # "base_url": "https://matrix.example.com" + # }, + # "m.identity_server": { + # "base_url": "https://nuxref.com" + # } + # } + + if status_code == requests.codes.not_found: + # This is an acceptable response; we're done + self.logger.debug( + "Matrix Well-Known Base URI not found at %s", + verify_url, + ) + + # Set our keys out for fast recall later on + self.store.set( + self.discovery_base_key, + "", + expires=self.discovery_cache_length_sec, + ) + self.store.set( + self.discovery_identity_key, + "", + expires=self.discovery_cache_length_sec, + ) + return "" + + elif status_code != requests.codes.ok: + # We're done early as we couldn't load the results + msg = "Matrix Well-Known Base URI Discovery Failed" + self.logger.warning( + "%s - %s returned error code: %d", + msg, + verify_url, + status_code, + ) + raise MatrixDiscoveryException(msg, error_code=status_code) + + if not response: + # This is an acceptable response; we simply do nothing + self.logger.debug( + "Matrix Well-Known Base URI not defined %s", verify_url + ) + + # Set our keys out for fast recall later on + self.store.set( + self.discovery_base_key, + "", + expires=self.discovery_cache_length_sec, + ) + self.store.set( + self.discovery_identity_key, + "", + expires=self.discovery_cache_length_sec, + ) + return "" + + # + # Parse our m.homeserver information + # + try: + base_url = response["m.homeserver"]["base_url"].rstrip("/") + results = NotifyBase.parse_url(base_url, verify_host=True) + + except (AttributeError, TypeError, KeyError): + # AttributeError: result wasn't a string (rstrip failed) + # TypeError : response wasn't a dictionary + # KeyError : response not to standards + results = None + + if not results: + msg = "Matrix Well-Known Base URI Discovery Failed" + self.logger.warning( + "%s - m.homeserver payload is missing or invalid: %s", + msg, + response, + ) + raise MatrixDiscoveryException(msg) + + # + # Our .well-known extraction was successful; now we need to + # verify that the version information resolves. + # + verify_url = f"{base_url}/_matrix/client/versions" + # Post our content + _, _, status_code = self._fetch( + None, method="GET", url_override=verify_url + ) + if status_code != requests.codes.ok: + # We're done early as we couldn't load the results + msg = "Matrix Well-Known Base URI Discovery Verification Failed" + self.logger.warning( + "%s - %s returned error code: %d", + msg, + verify_url, + status_code, + ) + raise MatrixDiscoveryException(msg, error_code=status_code) + + # + # Phase 2: Handle m.identity_server IF defined + # + if "m.identity_server" in response: + try: + identity_url = response["m.identity_server"][ + "base_url" + ].rstrip("/") + results = NotifyBase.parse_url(identity_url, verify_host=True) + + except (AttributeError, TypeError, KeyError): + # AttributeError: result wasn't a string (rstrip failed) + # TypeError : response wasn't a dictionary + # KeyError : response not to standards + results = None + + if not results: + msg = "Matrix Well-Known Identity URI Discovery Failed" + self.logger.warning( + "%s - m.identity_server payload is missing or invalid: %s", + msg, + response, + ) + raise MatrixDiscoveryException(msg) + + # + # Verify identity server found + # + verify_url = f"{identity_url}/_matrix/identity/v2" + + # Post our content + _postokay, _, status_code = self._fetch( + None, method="GET", url_override=verify_url + ) + if status_code != requests.codes.ok: + # We're done early as we couldn't load the results + msg = "Matrix Well-Known Identity URI Discovery Failed" + self.logger.warning( + "%s - %s returned error code: %d", + msg, + verify_url, + status_code, + ) + raise MatrixDiscoveryException(msg, error_code=status_code) + + # Update our cache + self.store.set( + self.discovery_identity_key, + identity_url, + # Add 2 seconds to prevent this key from expiring before + # base + expires=self.discovery_cache_length_sec + 2, + ) + else: + # No identity server + self.store.set( + self.discovery_identity_key, + "", + # Add 2 seconds to prevent this key from expiring before + # base + expires=self.discovery_cache_length_sec + 2, + ) + + # Update our cache + self.store.set( + self.discovery_base_key, + base_url, + expires=self.discovery_cache_length_sec, + ) + + return base_url + + @property + def base_url(self): + """Returns the base_url if known.""" + try: + base_url = self.server_discovery() + if base_url: + # We can use our cached value and return early + return base_url + + except MatrixDiscoveryException: + self.store.clear( + self.discovery_base_key, self.discovery_identity_key + ) + raise + + # If we get hear, we need to build our URL dynamically based on + # what was provided to us during the plugins initialization + return "{schema}://{hostname}{port}".format( + schema="https" if self.secure else "http", + hostname=self.host, + port=("" if not self.port else f":{self.port}"), + ) + + @property + def identity_url(self): + """Returns the identity_url if known.""" + base_url = self.base_url + identity_url = self.store.get(self.discovery_identity_key) + return identity_url if identity_url else base_url diff --git a/libs/apprise/plugins/matrix/e2ee.py b/libs/apprise/plugins/matrix/e2ee.py new file mode 100644 index 000000000..12e52f5f6 --- /dev/null +++ b/libs/apprise/plugins/matrix/e2ee.py @@ -0,0 +1,898 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Minimal Olm + MegOLM outbound implementation for Matrix E2EE. +# +# Only the *sending* path is implemented, which is all Apprise requires. +# All cryptographic primitives come from the `cryptography` package that +# is already an optional Apprise dependency (used by pem.py, VAPID, FCM). +# +# Protocol references: +# Olm spec: +# https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md +# MegOLM spec: +# https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md +# Matrix E2EE client-server API: +# https://spec.matrix.org/v1.11/client-server-api/ +# #end-to-end-encryption + +import base64 +from json import dumps +import os +import struct +import time as _time +import uuid + +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import ( + hashes, + hmac as _hmac_mod, + padding as _pad_mod, + ) + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PrivateKey, + X25519PublicKey, + ) + from cryptography.hazmat.primitives.ciphers import ( + Cipher, + algorithms, + modes, + ) + from cryptography.hazmat.primitives.kdf.hkdf import HKDF + from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, + ) + + # E2EE support is available + MATRIX_E2EE_SUPPORT = True + +except ImportError: + # E2EE support unavailable; `pip install cryptography` to enable + MATRIX_E2EE_SUPPORT = False + + +# Rotate the MegOLM session after this many messages +MEGOLM_ROTATION_MSGS = 100 + +# Rotate the MegOLM session after this many seconds (7 days) +MEGOLM_ROTATION_AGE = 60 * 60 * 24 * 7 + +# Bump this whenever the custom outbound MegOLM/session serialization or +# interoperability-critical behavior changes. Older cached sessions are then +# treated as incompatible and rotated automatically. +MATRIX_MEGOLM_STORE_VERSION = 1 + + +# ----------------------------------------------------------------------- +# Private helpers +# ----------------------------------------------------------------------- + + +def _b64enc(data): + """Matrix-style unpadded base64 of *data* as ASCII.""" + return base64.b64encode(data).rstrip(b"=").decode("utf-8") + + +def _b64dec(s): + """Decode a base64 string; tolerates missing padding and URL-safe chars.""" + s = s.replace("-", "+").replace("_", "/") + pad = len(s) % 4 + if pad: + s += "=" * (4 - pad) + return base64.b64decode(s) + + +def _hmac_sha256(key, data): + """32-byte HMAC-SHA-256 of *data* keyed by *key*.""" + h = _hmac_mod.HMAC(key, hashes.SHA256(), backend=default_backend()) + h.update(data) + return h.finalize() + + +def _hkdf_sha256(ikm, length, salt, info): + """HKDF-SHA-256. *salt* may be ``None`` or explicit ``bytes``.""" + return HKDF( + algorithm=hashes.SHA256(), + length=length, + salt=salt, + info=info, + backend=default_backend(), + ).derive(ikm) + + +def _aes_cbc_encrypt(key, iv, plaintext): + """AES-256-CBC-encrypt *plaintext* with PKCS#7 padding.""" + padder = _pad_mod.PKCS7(128).padder() + padded = padder.update(plaintext) + padder.finalize() + cipher = Cipher( + algorithms.AES(key), + modes.CBC(iv), + backend=default_backend(), + ) + enc = cipher.encryptor() + return enc.update(padded) + enc.finalize() + + +def _varint(n): + """Encode *n* as a protobuf-style base-128 varint.""" + if n == 0: + return b"\x00" + out = [] + while n: + byte = n & 0x7F + n >>= 7 + if n: + byte |= 0x80 + out.append(byte) + return bytes(out) + + +def _pb_bytes(field_num, data): + """Protobuf wire-type 2 (length-delimited bytes) field.""" + tag = _varint((field_num << 3) | 2) + return tag + _varint(len(data)) + data + + +def _pb_varint_field(field_num, value): + """Protobuf wire-type 0 (varint) field.""" + tag = _varint((field_num << 3) | 0) + return tag + _varint(value) + + +def _canonical_json(obj): + """UTF-8 canonical JSON (sorted keys, no spaces) for signing.""" + return dumps( + obj, + sort_keys=True, + ensure_ascii=False, + separators=(",", ":"), + ).encode("utf-8") + + +def _verify_ed25519(public_key_b64, message_bytes, signature_b64): + """Verify an Ed25519 signature. + + Returns ``True`` when *signature_b64* is a valid signature of + *message_bytes* under *public_key_b64*. Returns ``False`` on any + error (wrong key, bad signature, decode failure, etc.). + + Requires ``MATRIX_E2EE_SUPPORT`` (the ``cryptography`` package). + """ + try: + pub = Ed25519PublicKey.from_public_bytes(_b64dec(public_key_b64)) + pub.verify(_b64dec(signature_b64), message_bytes) + return True + except Exception: + return False + + +def verify_device_keys(dev_info, user_id, device_id): + """Verify the Ed25519 self-signature on a /keys/query device object. + + Per the spec, the signed payload must carry ``user_id`` and + ``device_id`` fields whose values match the identity being + verified. This prevents a malicious homeserver from substituting + keys from one device into the record of another. + + Returns ``True`` only when all of the following hold: + - ``dev_info["user_id"] == user_id`` + - ``dev_info["device_id"] == device_id`` + - The Ed25519 self-signature over the canonical payload is valid. + """ + # Identity binding: payload fields must match who we think we're + # verifying. Without this a server could swap key objects across + # users/devices and the signature would still verify. + if dev_info.get("user_id") != user_id: + return False + if dev_info.get("device_id") != device_id: + return False + + sig_key = "ed25519:{}".format(device_id) + ed25519_pub = dev_info.get("keys", {}).get(sig_key, "") + if not ed25519_pub: + return False + + sig = dev_info.get("signatures", {}).get(user_id, {}).get(sig_key, "") + if not sig: + return False + + # Signed payload: all fields except 'signatures' and 'unsigned' + signed_obj = { + k: v + for k, v in dev_info.items() + if k not in ("signatures", "unsigned") + } + return _verify_ed25519(ed25519_pub, _canonical_json(signed_obj), sig) + + +def verify_signed_otk(otk_obj, user_id, device_id, ed25519_pub_b64): + """Verify the Ed25519 signature on a ``signed_curve25519`` OTK. + + The device signs the OTK object (excluding ``signatures``) with the + same Ed25519 key published in its device keys. + + Returns ``True`` only when the signature is present and valid. + """ + sig = ( + otk_obj.get("signatures", {}) + .get(user_id, {}) + .get("ed25519:{}".format(device_id), "") + ) + if not sig: + return False + + signed_obj = {k: v for k, v in otk_obj.items() if k != "signatures"} + return _verify_ed25519(ed25519_pub_b64, _canonical_json(signed_obj), sig) + + +def encrypt_attachment(data): + """Encrypt *data* bytes for upload to a Matrix E2EE room. + + Implements the Matrix attachment encryption spec (v2): + https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments + + Algorithm: AES-256-CTR. + IV: 8 random bytes followed by 8 zero bytes (avoids counter wrap). + + Returns a ``(ciphertext, file_info)`` tuple where *file_info* is the + ``EncryptedFile`` object to embed in the ``m.room.message`` event: + + .. code-block:: json + + { + "v": "v2", + "key": { "kty": "oct", "alg": "A256CTR", "k": "", + "key_ops": ["encrypt", "decrypt"], "ext": true }, + "iv": "", + "hashes": { "sha256": "" } + } + """ + key = os.urandom(32) + # IV: 8 random bytes + 8 zero bytes (spec requirement) + iv = os.urandom(8) + b"\x00" * 8 + + cipher = Cipher( + algorithms.AES(key), + modes.CTR(iv), + backend=default_backend(), + ) + enc = cipher.encryptor() + ciphertext = enc.update(data) + enc.finalize() + + # SHA-256 of the ciphertext (for integrity verification by recipients) + h = hashes.Hash(hashes.SHA256(), backend=default_backend()) + h.update(ciphertext) + sha256_digest = h.finalize() + + # JWK key: base64url no-padding + k_b64url = base64.urlsafe_b64encode(key).rstrip(b"=").decode() + # IV: base64url no-padding (spec uses unpadded base64) + iv_b64url = base64.urlsafe_b64encode(iv).rstrip(b"=").decode() + # SHA-256 hash: standard base64 no-padding + sha256_b64 = base64.b64encode(sha256_digest).rstrip(b"=").decode() + + file_info = { + "v": "v2", + "key": { + "kty": "oct", + "key_ops": ["encrypt", "decrypt"], + "alg": "A256CTR", + "k": k_b64url, + "ext": True, + }, + "iv": iv_b64url, + "hashes": {"sha256": sha256_b64}, + } + + return ciphertext, file_info + + +# ----------------------------------------------------------------------- +# MatrixOlmAccount +# ----------------------------------------------------------------------- + + +class MatrixOlmAccount: + """Device-level Curve25519 + Ed25519 key pair. + + Generates a new key pair on first use and persists it via + ``to_dict()`` / ``from_dict()``. Also creates outbound Olm sessions + used to distribute MegOLM room keys to other devices. + + Reference: Olm spec, Section 2 ("Keys"). + """ + + def __init__( + self, + ik_priv_b64=None, + sk_priv_b64=None, + otks=None, + fallback_otk=None, + ): + """Initialise from saved keys or generate a fresh key pair. + + Parameters are the base64-encoded raw 32-byte private key bytes + for the Curve25519 identity key (*ik*) and Ed25519 signing key + (*sk*). Supply both or neither. + """ + if ik_priv_b64 and sk_priv_b64: + self._ik = X25519PrivateKey.from_private_bytes( + _b64dec(ik_priv_b64) + ) + self._sk = Ed25519PrivateKey.from_private_bytes( + _b64dec(sk_priv_b64) + ) + else: + self._ik = X25519PrivateKey.generate() + self._sk = Ed25519PrivateKey.generate() + + # Cache public-key bytes for efficiency + self._ik_pub = self._ik.public_key().public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + self._sk_pub = self._sk.public_key().public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + self._otks = dict(otks or {}) + self._fallback_otk = fallback_otk + + # --- Public-key properties ------------------------------------------- + + @property + def identity_key(self): + """Base64-encoded Curve25519 public identity key.""" + return _b64enc(self._ik_pub) + + @property + def signing_key(self): + """Base64-encoded Ed25519 public signing key.""" + return _b64enc(self._sk_pub) + + # --- Signing --------------------------------------------------------- + + def sign(self, data): + """Ed25519-sign *data* (bytes or str) and return base64.""" + if isinstance(data, str): + data = data.encode("utf-8") + return _b64enc(self._sk.sign(data)) + + # --- Serialisation --------------------------------------------------- + + def to_dict(self): + """Export private keys for persistent storage.""" + return { + "ik": _b64enc( + self._ik.private_bytes( + Encoding.Raw, PrivateFormat.Raw, NoEncryption() + ) + ), + "sk": _b64enc( + self._sk.private_bytes( + Encoding.Raw, PrivateFormat.Raw, NoEncryption() + ) + ), + "otks": self._otks, + "fallback_otk": self._fallback_otk, + } + + @staticmethod + def from_dict(data): + """Restore from a ``to_dict()`` snapshot.""" + return MatrixOlmAccount( + ik_priv_b64=data["ik"], + sk_priv_b64=data["sk"], + otks=data.get("otks"), + fallback_otk=data.get("fallback_otk"), + ) + + # --- Key-upload payload ---------------------------------------------- + + def device_keys_payload(self, user_id, device_id): + """Build the signed ``device_keys`` object for ``POST /keys/upload``. + + Reference: + https://spec.matrix.org/v1.11/client-server-api/ + #post_matrixclientv3keysupload + """ + device_keys = { + "algorithms": [ + "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2", + ], + "device_id": device_id, + "keys": { + "curve25519:{}".format(device_id): self.identity_key, + "ed25519:{}".format(device_id): self.signing_key, + }, + "user_id": user_id, + } + sig = self.sign(_canonical_json(device_keys)) + device_keys["signatures"] = { + user_id: {"ed25519:{}".format(device_id): sig} + } + return device_keys + + def _signed_curve25519_key(self, user_id, device_id, key_b64): + """Wrap a Curve25519 key in a signed KeyObject.""" + payload = {"key": key_b64} + payload["signatures"] = { + user_id: { + "ed25519:{}".format(device_id): self.sign( + _canonical_json(payload) + ) + } + } + return payload + + def _ensure_otks(self, count=10): + """Ensure at least *count* signed_curve25519 one-time keys exist.""" + while len(self._otks) < count: + key_id = uuid.uuid4().hex[:10] + priv = X25519PrivateKey.generate() + self._otks[key_id] = _b64enc( + priv.private_bytes( + Encoding.Raw, PrivateFormat.Raw, NoEncryption() + ) + ) + + def one_time_keys_payload(self, user_id, device_id, count=10): + """Build signed ``one_time_keys`` for ``POST /keys/upload``.""" + self._ensure_otks(count=count) + payload = {} + for key_id, priv_b64 in self._otks.items(): + priv = X25519PrivateKey.from_private_bytes(_b64dec(priv_b64)) + pub = priv.public_key().public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + payload["signed_curve25519:{}".format(key_id)] = ( + self._signed_curve25519_key(user_id, device_id, _b64enc(pub)) + ) + return payload + + def fallback_keys_payload(self, user_id, device_id): + """Build signed ``fallback_keys`` for ``POST /keys/upload``.""" + if not self._fallback_otk: + key_id = uuid.uuid4().hex[:10] + priv = X25519PrivateKey.generate() + self._fallback_otk = { + "id": key_id, + "sk": _b64enc( + priv.private_bytes( + Encoding.Raw, PrivateFormat.Raw, NoEncryption() + ) + ), + } + + priv = X25519PrivateKey.from_private_bytes( + _b64dec(self._fallback_otk["sk"]) + ) + pub = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + key_id = self._fallback_otk["id"] + return { + "signed_curve25519:{}".format(key_id): self._signed_curve25519_key( + user_id, device_id, _b64enc(pub) + ) + } + + def mark_keys_as_published(self): + """Mark the current OTK batch as published. + + This mirrors stable python-olm's ``Account.mark_keys_as_published()``: + the uploaded one-time keys are no longer treated as the next + unpublished batch, so a subsequent upload can generate a fresh set. + """ + self._otks.clear() + + # --- Outbound session ------------------------------------------------ + + def create_outbound_session( + self, their_identity_key_b64, their_one_time_key_b64 + ): + """Create an outbound Olm session to a remote device. + + Performs the X3DH triple-DH key exchange and returns a + :class:`MatrixOlmSession` ready to encrypt the first message. + + Parameters: + their_identity_key_b64 - recipient's base64 Curve25519 pub key + their_one_time_key_b64 - recipient's base64 Curve25519 OTK + + Reference: Olm spec, Section 4.1 ("Session establishment"). + """ + their_ik = X25519PublicKey.from_public_bytes( + _b64dec(their_identity_key_b64) + ) + their_otk = X25519PublicKey.from_public_bytes( + _b64dec(their_one_time_key_b64) + ) + + # E_A is Alice's ephemeral key. It serves BOTH as the Base-Key + # (outer pre-key field 2) AND as the initial Ratchet-Key (inner + # field 1). The Olm spec Section 5.1 is explicit: + # "E_A^pub is also the ratchet key for the first message." + # libolm passes the same keypair to both the X3DH and the ratchet + # initialisation (ratchet.cpp: initialise_as_alice receives base_key + # and uses it as the initial sender ratchet key). Using two + # different keys here breaks decryption. + eph = X25519PrivateKey.generate() + eph_pub = eph.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + + # Triple DH (Olm spec, Section 4.1) + # DH1 = X25519(IK_A, OTK_B) + # DH2 = X25519(E_A, IK_B) + # DH3 = X25519(E_A, OTK_B) + dh1 = self._ik.exchange(their_otk) + dh2 = eph.exchange(their_ik) + dh3 = eph.exchange(their_otk) + + # Root-key derivation (libolm ratchet.cpp initialise_as_alice / + # vodozemac shared_secret.rs Shared3DHSecret::expand): + # IKM = DH1 || DH2 || DH3 (96 bytes — no zero prefix) + # salt = nullptr / 0x00*32 (RFC 5869: missing salt = HashLen zeros) + # info = "OLM_ROOT" + # + # libolm passes the 96-byte secret directly + # (session.cpp: secret[3 * CURVE25519_SHARED_SECRET_LENGTH]). + # vodozemac does the same (Shared3DHSecret is Box<[u8; 96]>). + # Adding any prefix produces a different PRK and therefore + # different root/chain keys, causing the recipient to fail to + # decrypt the Olm pre-key message that carries the MegOLM room key. + ikm = dh1 + dh2 + dh3 + keys = _hkdf_sha256(ikm, 64, salt=None, info=b"OLM_ROOT") + root_key = keys[:32] + chain_key = keys[32:] + + return MatrixOlmSession( + our_ik_pub=self._ik_pub, + eph_pub=eph_pub, + their_otk_pub=_b64dec(their_one_time_key_b64), + their_ik_pub=_b64dec(their_identity_key_b64), + root_key=root_key, + chain_key=chain_key, + ) + + +# ----------------------------------------------------------------------- +# MatrixOlmSession +# ----------------------------------------------------------------------- + + +class MatrixOlmSession: + """Single-use outbound Olm session (type-0 pre-key messages only). + + Sufficient for delivering the MegOLM room-key to one recipient device. + Each call to :meth:`encrypt` advances the chain ratchet once. + + Reference: Olm spec, Section 5 ("Message format"). + """ + + def __init__( + self, + our_ik_pub, + eph_pub, + their_otk_pub, + their_ik_pub, + root_key, + chain_key, + ): + self._our_ik_pub = our_ik_pub + # eph_pub is Alice's ephemeral key E_A. Per Olm spec Section 5.1, + # E_A^pub is used in BOTH the outer pre-key Base-Key field AND the + # inner normal-message Ratchet-Key field. They must be identical. + self._eph_pub = eph_pub + self._their_otk_pub = their_otk_pub + self._their_ik_pub = their_ik_pub + self._root_key = root_key + self._chain_key = chain_key + self._counter = 0 + + @property + def their_identity_key(self): + """Base64-encoded Curve25519 identity key of the remote device.""" + return _b64enc(self._their_ik_pub) + + def encrypt(self, plaintext): + """Encrypt *plaintext* (str) as an Olm pre-key (type-0) message. + + Returns ``{"type": 0, "body": ""}`` suitable for + inclusion in the ``ciphertext`` object of an + ``m.olm.v1.curve25519-aes-sha2`` event. + + Reference: Olm spec, Section 5.1. + """ + if isinstance(plaintext, str): + plaintext = plaintext.encode("utf-8") + + # -- Chain ratchet (Olm spec Section 6.1) ----------------- + msg_key = _hmac_sha256(self._chain_key, b"\x01") + self._chain_key = _hmac_sha256(self._chain_key, b"\x02") + + # -- Expand msg_key -> AES key / MAC key / IV ----------- + # HKDF(msg_key, 80, salt=0x00*32, info="OLM_KEYS") + keys = _hkdf_sha256(msg_key, 80, salt=b"\x00" * 32, info=b"OLM_KEYS") + aes_key = keys[:32] + mac_key = keys[32:64] + iv = keys[64:80] + + # -- AES-256-CBC ------------------------------------------ + ciphertext = _aes_cbc_encrypt(aes_key, iv, plaintext) + + # -- Inner message (version | fields | MAC) --------------- + # Field numbers from the Olm spec wire format: + # 0x0A = field 1, wire-type 2 (bytes) -> Ratchet-Key + # 0x10 = field 2, wire-type 0 (varint) -> Chain-Index + # 0x22 = field 4, wire-type 2 (bytes) -> Cipher-Text + # Note: there is no field 3 in the normal-message format; the + # ciphertext is field 4 (tag 0x22), NOT field 3 (tag 0x1A). + # E_A^pub appears in BOTH the inner Ratchet-Key (field 1) and the + # outer Base-Key (field 2) -- same bytes, same key, per spec. + inner = ( + b"\x03" + + _pb_bytes(1, self._eph_pub) + + _pb_varint_field(2, self._counter) + + _pb_bytes(4, ciphertext) + ) + inner_mac = _hmac_sha256(mac_key, inner)[:8] + + # -- Outer pre-key message -------------------------------- + # Field numbers from the Olm spec wire format: + # 0x0A = field 1 (bytes) -> One-Time-Key (Bob's OTK being consumed) + # 0x12 = field 2 (bytes) -> Base-Key (Alice's E_A; same key as the + # first Ratchet-Key) + # 0x1A = field 3 (bytes) -> Identity-Key (Alice's identity key) + # 0x22 = field 4 (bytes) -> Message (inner message + inner MAC) + # + # The outer pre-key message has NO trailing MAC of its own. + # libolm session.cpp allocates exactly + # encode_one_time_key_message_length() bytes — no extra space for an + # outer MAC. vodozemac decodes the outer payload with prost (strict + # protobuf) so extra bytes after the last field cause a DecodeError + # and the session fails to establish. + outer = ( + b"\x03" + + _pb_bytes(1, self._their_otk_pub) + + _pb_bytes(2, self._eph_pub) + + _pb_bytes(3, self._our_ik_pub) + + _pb_bytes(4, inner + inner_mac) + ) + + self._counter += 1 + return {"type": 0, "body": _b64enc(outer)} + + +# ----------------------------------------------------------------------- +# MatrixMegOlmSession +# ----------------------------------------------------------------------- + + +class MatrixMegOlmSession: + """Outbound MegOLM session for room-message encryption. + + State: a 4-component 256-bit ratchet R[0..3], a 32-bit counter, + and a per-session Ed25519 signing key. The ratchet advances after + every encrypted message. See :data:`MEGOLM_ROTATION_MSGS` and + :data:`MEGOLM_ROTATION_AGE` for rotation thresholds. + + Reference: MegOLM spec. + """ + + def __init__( + self, + ratchet=None, + counter=0, + sk_priv_b64=None, + created_at=None, + ): + """New session (random state) or restore from ``to_dict()``.""" + self._ratchet = ( + [os.urandom(32) for _ in range(4)] + if ratchet is None + else [bytes(r) for r in ratchet] + ) + self._counter = counter + + if sk_priv_b64 is None: + self._sk = Ed25519PrivateKey.generate() + else: + self._sk = Ed25519PrivateKey.from_private_bytes( + _b64dec(sk_priv_b64) + ) + + self._sk_pub = self._sk.public_key().public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + # Session ID is the base64 of the Ed25519 signing public key + self.session_id = _b64enc(self._sk_pub) + self.created_at = ( + created_at if created_at is not None else _time.time() + ) + + # --- Ratchet ---------------------------------------------------------- + + def _advance(self): + """Advance the MegOLM ratchet by one step. + + Mirrors libolm megolm.c ``megolm_advance`` and vodozemac ratchet.rs + ``Ratchet::advance``. + + The ratchet has 4 parts R[0..3]. On each step, determine the + highest-index part h that stays constant: + - counter+1 is a multiple of 2^24 → h=0 (advance R[0..3] from R[0]) + - counter+1 is a multiple of 2^16 → h=1 (advance R[1..3] from R[1]) + - counter+1 is a multiple of 2^8 → h=2 (advance R[2..3] from R[2]) + - otherwise → h=3 (advance R[3] from R[3]) + + All derived parts are computed from the ORIGINAL value of R[h] + (saved before any modification), then R[h] itself is updated last. + This matches libolm's loop which processes higher indices first + (i = 3 down to h), ensuring data[h] is still the original when it + is finally overwritten at i==h. + """ + r = self._ratchet + n1 = self._counter + 1 # next counter value + + if n1 % (1 << 24) == 0: + # h=0: all four parts derived from original R[0] + orig = r[0] + r[3] = _hmac_sha256(orig, b"\x03") + r[2] = _hmac_sha256(orig, b"\x02") + r[1] = _hmac_sha256(orig, b"\x01") + r[0] = _hmac_sha256(orig, b"\x00") + elif n1 % (1 << 16) == 0: + # h=1: R[1..3] derived from original R[1] + orig = r[1] + r[3] = _hmac_sha256(orig, b"\x03") + r[2] = _hmac_sha256(orig, b"\x02") + r[1] = _hmac_sha256(orig, b"\x01") + elif n1 % (1 << 8) == 0: + # h=2: R[2..3] derived from original R[2] + orig = r[2] + r[3] = _hmac_sha256(orig, b"\x03") + r[2] = _hmac_sha256(orig, b"\x02") + else: + # h=3: R[3] re-seeded from itself + r[3] = _hmac_sha256(r[3], b"\x03") + + self._counter += 1 + + def _message_keys(self): + """Derive (aes_key, mac_key, iv) from the full ratchet state R_i. + + Per the MegOLM spec Section 4.3 and vodozemac (cipher/key.rs + ``new_megolm``), the HKDF IKM is the complete 128-byte ratchet value + R_i = R[0]||R[1]||R[2]||R[3]. Using only R[3] (32 bytes) produces + different keys from what any standard client derives. + + Spec: AES_KEY||HMAC_KEY||AES_IV = HKDF(0, R_i, "MEGOLM_KEYS", 80) + """ + keys = _hkdf_sha256( + b"".join(self._ratchet), 80, salt=None, info=b"MEGOLM_KEYS" + ) + return keys[:32], keys[32:64], keys[64:80] + + # --- Rotation -------------------------------------------------------- + + def should_rotate(self, msg_count=None): + """Return ``True`` if this session has reached a rotation threshold.""" + if msg_count is None: + msg_count = self._counter + if msg_count >= MEGOLM_ROTATION_MSGS: + return True + return (_time.time() - self.created_at) >= MEGOLM_ROTATION_AGE + + # --- Encryption ------------------------------------------------------ + + def encrypt(self, payload_dict): + """Encrypt *payload_dict* and return base64 MegOLM ciphertext. + + Wire format (MegOLM spec, Section 4): + version (1 B = 0x03) + | Protobuf body (field 8: message_index varint, + | field 9: ciphertext bytes) + | HMAC-SHA-256 (8 B) + | Ed25519 sig (64 B) + + Reference: MegOLM spec, Section 4. + """ + plaintext = dumps(payload_dict).encode("utf-8") + aes_key, mac_key, iv = self._message_keys() + + ct_bytes = _aes_cbc_encrypt(aes_key, iv, plaintext) + # MegOLM spec wire format (libolm message.cpp): + # GROUP_MESSAGE_INDEX_TAG = 0x08 (field 1, wire-type 0 varint) + # GROUP_CIPHERTEXT_TAG = 0x12 (field 2, wire-type 2 bytes) + pb_body = _pb_varint_field(1, self._counter) + _pb_bytes(2, ct_bytes) + body = b"\x03" + pb_body + mac = _hmac_sha256(mac_key, body)[:8] + sig = self._sk.sign(body + mac) + + self._advance() + return _b64enc(body + mac + sig) + + # --- Session-key export (shared via Olm to room members) ------------ + + def session_key(self): + """Base64 MegOLM session key for sharing in ``m.room_key`` events. + + Wire format (libolm outbound_group_session.c, + ``olm_outbound_group_session_key``): + version (1 B = 0x02) | counter (4 B big-endian) + | R[0..3] (128 B) | Ed25519 signing pub key (32 B) + | Ed25519 signature (64 B) over all preceding 165 bytes + + The signature lets the recipient verify the session key came from the + device that owns the Ed25519 signing key published in /keys/upload. + Without it, vodozemac and other clients reject the key. + + Reference: MegOLM spec, Section 2; libolm + ``outbound_group_session.c``. + """ + payload = ( + b"\x02" + + struct.pack(">I", self._counter) + + b"".join(self._ratchet) + + self._sk_pub + ) + sig = self._sk.sign(payload) + return _b64enc(payload + sig) + + # --- Serialisation --------------------------------------------------- + + def to_dict(self): + """Export session state for persistent storage.""" + return { + "version": MATRIX_MEGOLM_STORE_VERSION, + "ratchet": [_b64enc(r) for r in self._ratchet], + "counter": self._counter, + "sk": _b64enc( + self._sk.private_bytes( + Encoding.Raw, PrivateFormat.Raw, NoEncryption() + ) + ), + "session_id": self.session_id, + "created_at": self.created_at, + } + + @staticmethod + def from_dict(data): + """Restore from a ``to_dict()`` snapshot.""" + if data.get("version") != MATRIX_MEGOLM_STORE_VERSION: + raise ValueError("Incompatible MegOLM session cache format") + return MatrixMegOlmSession( + ratchet=[_b64dec(r) for r in data["ratchet"]], + counter=data["counter"], + sk_priv_b64=data["sk"], + created_at=data.get("created_at"), + ) diff --git a/libs/apprise/plugins/mattermost.py b/libs/apprise/plugins/mattermost.py index 4cee958c1..6c6dc5cd6 100644 --- a/libs/apprise/plugins/mattermost.py +++ b/libs/apprise/plugins/mattermost.py @@ -44,6 +44,7 @@ This plugin supports 2 modes of operation: from __future__ import annotations +import io from itertools import chain # Create an incoming webhook; the website will provide you with something like: @@ -118,6 +119,9 @@ class NotifyMattermost(NotifyBase): # Mattermost does not have a title title_maxlen = 0 + # Attachment support defaults True; toggled off for webhook mode + attachment_support = True + # Allow persistent caching of bot channel lookups storage_mode = PersistentStoreMode.AUTO @@ -192,9 +196,6 @@ class NotifyMattermost(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "channel": { # Backwards compatible "alias_of": "targets", @@ -225,6 +226,9 @@ class NotifyMattermost(NotifyBase): "values": MATTERMOST_MODES, "default": MATTERMOST_MODES[0], }, + "to": { + "alias_of": "targets", + }, }, ) @@ -259,6 +263,9 @@ class NotifyMattermost(NotifyBase): else: self.mode = self.template_args["mode"]["default"] + # Enable attachment support only in bot mode + self.attachment_support = self.mode == MattermostMode.BOT + # Token (webhook token in webhook mode, bearer token in bot mode) self.token = validate_regex(token) if not self.token: @@ -360,6 +367,7 @@ class NotifyMattermost(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -413,6 +421,7 @@ class NotifyMattermost(NotifyBase): body: str, title: str = "", notify_type: NotifyType = NotifyType.INFO, + attach=None, **kwargs: Any, ) -> bool: """Perform Mattermost Notification.""" @@ -435,15 +444,60 @@ class NotifyMattermost(NotifyBase): "Content-Type": "application/json", } + # Pre-read attachment content before the target loop so the same + # bytes can be sent to every target via a fresh io.BytesIO wrapper. + # AttachMemory streams close after the first read and cannot be + # re-opened; reading everything upfront avoids that problem. + attach_data: list[tuple[str, bytes, str]] = [] + if self.mode == MattermostMode.BOT: url = "{}://{}{}{}/api/v4/posts".format( self.schema, self.host, port, self.fullpath.rstrip("/") ) + # URL used to upload file attachments before posting + upload_url = "{}://{}{}{}/api/v4/files".format( + self.schema, self.host, port, self.fullpath.rstrip("/") + ) + # Append headers headers["Authorization"] = f"Bearer {self.token}" expected = (requests.codes.created, requests.codes.ok) + if attach and self.attachment_support: + for no, attachment in enumerate(attach, start=1): + if not attachment: + self.logger.error( + "Could not access Mattermost attachment %s.", + attachment.url(privacy=True), + ) + return False + + # Capture name and mimetype before open() so that + # AttachMemory's exists() check (triggered by these + # properties) does not fail on a closed stream. + fname = attachment.name or f"file{no:03}.dat" + mimetype = attachment.mimetype + + self.logger.debug( + "Reading Mattermost attachment %s", + attachment.url(privacy=True), + ) + try: + with attachment.open() as fp: + content = fp.read() + + except (OSError, ValueError) as e: + self.logger.warning( + "An I/O error occurred reading" + " Mattermost attachment %s.", + fname, + ) + self.logger.debug("I/O Exception: %s", e) + return False + + attach_data.append((fname, content, mimetype)) + else: # self.mode == MattermostMode.WEBHOOK url = "{}://{}{}{}/hooks/{}".format( self.schema, @@ -468,10 +522,109 @@ class NotifyMattermost(NotifyBase): continue if self.mode == MattermostMode.BOT: + # Upload pre-read attachment bytes scoped to this + # channel_id. Fresh io.BytesIO wrappers are created for + # each target so all targets receive the same content. + file_ids: list[str] = [] + attach_error = False + if attach_data: + upload_headers: dict[str, str] = { + "User-Agent": self.app_id, + "Authorization": f"Bearer {self.token}", + } + for fname, content, mimetype in attach_data: + self.logger.debug( + "Posting Mattermost attachment %s", + fname, + ) + + try: + self.throttle() + r = requests.post( + upload_url, + data={"channel_id": target}, + headers=upload_headers, + files={ + "files": ( + fname, + io.BytesIO(content), + mimetype, + ) + }, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred" + " uploading Mattermost" + " attachment." + ) + self.logger.debug("Socket Exception: %s", e) + attach_error = True + break + + if r.status_code not in ( + requests.codes.created, + requests.codes.ok, + ): + status = self.http_response_code_lookup( + r.status_code + ) + self.logger.warning( + "Failed to upload Mattermost" + " attachment %s: %s, error=%d.", + fname, + status, + r.status_code, + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + attach_error = True + break + + try: + data = loads(r.content.decode("utf-8")) + file_id = data.get("file_infos", [{}])[0].get( + "id", "" + ) + + except ( + AttributeError, + TypeError, + ValueError, + IndexError, + ): + self.logger.warning( + "Failed to parse Mattermost" + " file upload response." + ) + attach_error = True + break + + if not file_id: + self.logger.warning( + "Mattermost file upload returned no file ID." + ) + attach_error = True + break + + file_ids.append(file_id) + + if attach_error: + has_error = True + continue + payload: dict[str, Any] = { "channel_id": target, "message": body, } + if file_ids: + payload["file_ids"] = file_ids else: payload: dict[str, Any] = { @@ -485,9 +638,7 @@ class NotifyMattermost(NotifyBase): if image_url: payload["icon_url"] = image_url - payload["username"] = ( - self.user if self.user else self.app_id - ) + payload["username"] = self.user if self.user else self.app_id if target: payload["channel"] = target @@ -498,9 +649,7 @@ class NotifyMattermost(NotifyBase): url, self.verify_certificate, ) - self.logger.debug( - "Mattermost %s Payload: %s", self.mode, payload - ) + self.logger.debug("Mattermost %s Payload: %s", self.mode, payload) self.throttle() @@ -511,6 +660,7 @@ class NotifyMattermost(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in expected: @@ -541,7 +691,8 @@ class NotifyMattermost(NotifyBase): self.logger.warning( "A Connection error occurred sending Mattermost " "%s notification to %s.", - self.mode, target, + self.mode, + target, ) self.logger.debug("Socket Exception: %s", e) has_error = True @@ -593,11 +744,15 @@ class NotifyMattermost(NotifyBase): else: entries.append(f"+{value}") - params["to"] = ",".join(chain( - [NotifyMattermost.quote(v, safe="#+") for v in entries], - [NotifyMattermost.quote(x, safe="") - for x in self._invalid_targets], - )) + params["to"] = ",".join( + chain( + [NotifyMattermost.quote(v, safe="#+") for v in entries], + [ + NotifyMattermost.quote(x, safe="") + for x in self._invalid_targets + ], + ) + ) default_port = 443 if self.secure else 80 default_schema = self.secure_protocol if self.secure else self.protocol @@ -682,15 +837,11 @@ class NotifyMattermost(NotifyBase): # Our Mode if "mode" in results["qsd"] and results["qsd"]["mode"]: - results["mode"] = NotifyMattermost.unquote( - results["qsd"]["mode"] - ) + results["mode"] = NotifyMattermost.unquote(results["qsd"]["mode"]) # Team support (bot mode lookup). This maps to `user`. if "team" in results["qsd"] and results["qsd"]["team"]: - results["user"] = NotifyMattermost.unquote( - results["qsd"]["team"] - ) + results["user"] = NotifyMattermost.unquote(results["qsd"]["team"]) if "mode" not in results: results["mode"] = MattermostMode.BOT diff --git a/libs/apprise/plugins/messagebird.py b/libs/apprise/plugins/messagebird.py index 671187ca5..873831b01 100644 --- a/libs/apprise/plugins/messagebird.py +++ b/libs/apprise/plugins/messagebird.py @@ -212,6 +212,7 @@ class NotifyMessageBird(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Sample output of a successful transmission @@ -267,7 +268,8 @@ class NotifyMessageBird(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -281,8 +283,7 @@ class NotifyMessageBird(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending" - f" MessageBird:{target} " - + "notification." + f" MessageBird:{target} " + "notification." ) self.logger.debug(f"Socket Exception: {e!s}") diff --git a/libs/apprise/plugins/misskey.py b/libs/apprise/plugins/misskey.py index f52987d5b..ca5ee56e0 100644 --- a/libs/apprise/plugins/misskey.py +++ b/libs/apprise/plugins/misskey.py @@ -260,6 +260,7 @@ class NotifyMisskey(NotifyBase): data=dumps(payload), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -275,7 +276,8 @@ class NotifyMisskey(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/mqtt.py b/libs/apprise/plugins/mqtt.py index a7c0b26c3..4db070d43 100644 --- a/libs/apprise/plugins/mqtt.py +++ b/libs/apprise/plugins/mqtt.py @@ -50,6 +50,7 @@ NOTIFY_MQTT_SUPPORT_ENABLED = False try: # 3rd party modules import paho.mqtt.client as mqtt + from paho.mqtt.client import CallbackAPIVersion as _MQTTCallbackAPI # We're good to go! NOTIFY_MQTT_SUPPORT_ENABLED = True @@ -67,7 +68,8 @@ try: except ImportError: # No problem; we just simply can't support this plugin because we're - # either using Linux, or simply do not have pywin32 installed. + # missing the required paho-mqtt >= 2.1.0 library. + _MQTTCallbackAPI = None MQTT_PROTOCOL_MAP = {} # A lookup map for relaying version to user @@ -86,7 +88,7 @@ class NotifyMQTT(NotifyBase): requirements = { # Define our required packaging in order to work - "packages_required": "paho-mqtt != 2.0.*" + "packages_required": "paho-mqtt >= 2.1.0" } # The default descriptive name associated with the Notification @@ -168,6 +170,7 @@ class NotifyMQTT(NotifyBase): "topic": { "name": _("Target Queue"), "type": "string", + "required": True, "map_to": "targets", }, "targets": { @@ -181,9 +184,6 @@ class NotifyMQTT(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "qos": { "name": _("QOS"), "type": "int", @@ -211,6 +211,9 @@ class NotifyMQTT(NotifyBase): "type": "bool", "default": False, }, + "to": { + "alias_of": "targets", + }, }, ) @@ -312,6 +315,7 @@ class NotifyMQTT(NotifyBase): # Our MQTT Client Object self.client = mqtt.Client( + _MQTTCallbackAPI.VERSION2, client_id=self.client_id, clean_session=not self.session, userdata=None, @@ -346,7 +350,10 @@ class NotifyMQTT(NotifyBase): ) if self.secure: - if self.ca_certs is None: + if self.verify_certificate and self.ca_certs is None: + # CA certificates are only required when verification + # is enabled; skip this check when verify=False so + # that self-signed certificates are accepted. self.logger.error( "MQTT secure communication can not be verified, " "CA certificates file missing" @@ -354,10 +361,21 @@ class NotifyMQTT(NotifyBase): return False self.client.tls_set( - ca_certs=self.ca_certs, + # Only supply CA certs when verification is enabled; + # passing them with CERT_NONE has no effect but is + # cleaner to omit. + ca_certs=( + self.ca_certs if self.verify_certificate else None + ), certfile=None, keyfile=None, - cert_reqs=ssl.CERT_REQUIRED, + # CERT_NONE disables chain validation when verify=False + # so that self-signed certificates are accepted. + cert_reqs=( + ssl.CERT_REQUIRED + if self.verify_certificate + else ssl.CERT_NONE + ), tls_version=ssl.PROTOCOL_TLS, ciphers=None, ) @@ -457,15 +475,32 @@ class NotifyMQTT(NotifyBase): # if we reach here; we're at the bottom of our loop # we loop around and do the next topic now + except ssl.CertificateError as e: + # Hostname/certificate mismatch; most specific SSL error + self.logger.warning( + f"MQTT SSL Certificate Error received from {url}" + ) + self.logger.debug(f"Socket Exception: {e!s}") + return False + + except ssl.SSLError as e: + # TLS handshake failure, ALPN negotiation error, malformed CA file, + # or any other SSL-layer problem not covered by CertificateError + self.logger.warning(f"MQTT SSL Error received from {url}") + self.logger.debug(f"Socket Exception: {e!s}") + return False + except ConnectionError as e: + # Refused, reset, or dropped connections (subclass of OSError) self.logger.warning(f"MQTT Connection Error received from {url}") self.logger.debug(f"Socket Exception: {e!s}") return False - except ssl.CertificateError as e: - self.logger.warning( - f"MQTT SSL Certificate Error received from {url}" - ) + except OSError as e: + # Remaining socket/OS errors: TimeoutError, BrokenPipeError, + # PermissionError, etc. -- all subclasses of OSError not caught + # by the more specific handlers above + self.logger.warning(f"MQTT Socket Error received from {url}") self.logger.debug(f"Socket Exception: {e!s}") return False @@ -650,3 +685,10 @@ class NotifyMQTT(NotifyBase): pass return candidates + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("paho",) diff --git a/libs/apprise/plugins/msg91.py b/libs/apprise/plugins/msg91.py index e69bda17b..b48ca4b4b 100644 --- a/libs/apprise/plugins/msg91.py +++ b/libs/apprise/plugins/msg91.py @@ -236,7 +236,6 @@ class NotifyMSG91(NotifyBase): # Prepare Recipient Payload Object for key, value in self.template_mapping.items(): - if key in RESERVED_KEYWORDS: self.logger.warning( "Ignoring MSG91 custom payload entry %s", key @@ -289,6 +288,7 @@ class NotifyMSG91(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -308,7 +308,8 @@ class NotifyMSG91(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False diff --git a/libs/apprise/plugins/msteams.py b/libs/apprise/plugins/msteams.py deleted file mode 100644 index fc35c5cc9..000000000 --- a/libs/apprise/plugins/msteams.py +++ /dev/null @@ -1,776 +0,0 @@ -# BSD 2-Clause License -# -# Apprise - Push Notification Library. -# Copyright (c) 2026, Chris Caron -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# To use this plugin, you need to create a webhook; you can read more about -# this here: -# https://dev.outlook.com/Connectors/\ -# GetStarted#creating-messages-through-office-365-connectors-\ -# in-microsoft-teams -# -# More details are here on API Construction: -# https://docs.microsoft.com/en-ca/outlook/actionable-messages/\ -# message-card-reference -# -# I personally created a free account at teams.microsoft.com and then -# went to the store (bottom left hand side of slack like interface). -# -# From here you can search for 'Incoming Webhook'. Once you click on it, -# you can associate the webhook with your team. At this point, you can -# optionally also assign it a name, an avatar. Finally you'll have to -# assign it a channel it will notify. -# -# When you've completed this, it will generate you a (webhook) URL that -# looks like: -# https://team-name.webhook.office.com/webhookb2/ \ -# abcdefgf8-2f4b-4eca-8f61-225c83db1967@abcdefg2-5a99-4849-8efc-\ -# c9e78d28e57d/IncomingWebhook/291289f63a8abd3593e834af4d79f9fe/\ -# a2329f43-0ffb-46ab-948b-c9abdad9d643 -# -# Yes... The URL is that big... But it looks like this (greatly simplified): -# https://TEAM-NAME.webhook.office.com/webhookb2/ABCD/IncomingWebhook/DEFG/HIJK -# ^ ^ ^ ^ -# | | | | -# These are important <--------------------------^--------------------^----^ -# - -# The Legacy format didn't have the team name identified and reads 'outlook' -# While this still works, consider that Microsoft will be dropping support -# for this soon, so you may need to update your IncomingWebhook. Here is -# what a legacy URL looked like: -# https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK -# ^ ^ ^ ^ -# | | | | -# legacy team reference: 'outlook' | | | -# | | | -# These are important <--------------^--------------------^----^ -# - -# You'll notice that the first token is actually 2 separated by an @ symbol -# But lets just ignore that and assume it's one great big token instead. -# -# These 3 tokens need to be placed in the URL after the Team -# msteams://TEAM/ABCD/DEFG/HIJK -# -import json -from json.decoder import JSONDecodeError -import re - -import requests - -from ..apprise_attachment import AppriseAttachment -from ..common import NotifyFormat, NotifyImageSize, NotifyType -from ..locale import gettext_lazy as _ -from ..utils.parse import parse_bool, validate_regex -from ..utils.templates import TemplateType, apply_template -from .base import NotifyBase - - -class NotifyMSTeams(NotifyBase): - """A wrapper for Microsoft Teams Notifications.""" - - # The default descriptive name associated with the Notification - service_name = "MSTeams" - - # The services URL - service_url = "https://teams.micrsoft.com/" - - # The default secure protocol - secure_protocol = "msteams" - - # A URL that takes you to the setup/help of the specific protocol - setup_url = "https://appriseit.com/services/msteams/" - - # MSTeams uses the http protocol with JSON requests - notify_url_v1 = ( - "https://outlook.office.com/webhook/" - "{token_a}/IncomingWebhook/{token_b}/{token_c}" - ) - - # New MSTeams webhook (as of April 11th, 2021) - notify_url_v2 = ( - "https://{team}.webhook.office.com/webhookb2/" - "{token_a}/IncomingWebhook/{token_b}/{token_c}" - ) - - notify_url_v3 = ( - "https://{team}.webhook.office.com/webhookb2/" - "{token_a}/IncomingWebhook/{token_b}/{token_c}/{token_d}" - ) - - # Allows the user to specify the NotifyImageSize object - image_size = NotifyImageSize.XY_72 - - # The maximum allowable characters allowed in the body per message - body_maxlen = 1000 - - # Default Notification Format - notify_format = NotifyFormat.MARKDOWN - - # There is no reason we should exceed 35KB when reading in a JSON file. - # If it is more than this, then it is not accepted - max_msteams_template_size = 35000 - - # Define object templates - templates = ( - # New required format - "{schema}://{team}/{token_a}/{token_b}/{token_c}", - # Deprecated - "{schema}://{token_a}/{token_b}/{token_c}", - ) - - # Define our template tokens - template_tokens = dict( - NotifyBase.template_tokens, - **{ - # The Microsoft Team Name - "team": { - "name": _("Team Name"), - "type": "string", - "required": True, - "regex": (r"^[A-Z0-9_-]+$", "i"), - }, - # Token required as part of the API request - # /AAAAAAAAA@AAAAAAAAA/........./......... - "token_a": { - "name": _("Token A"), - "type": "string", - "private": True, - "required": True, - "regex": (r"^[A-Z0-9-]+@[A-Z0-9-]+$", "i"), - }, - # Token required as part of the API request - # /................../BBBBBBBBB/.......... - "token_b": { - "name": _("Token B"), - "type": "string", - "private": True, - "required": True, - "regex": (r"^[a-z0-9]+$", "i"), - }, - # Token required as part of the API request - # /........./........./CCCCCCCCCCCCCCCCCCCCCCCC - "token_c": { - "name": _("Token C"), - "type": "string", - "private": True, - "required": True, - "regex": (r"^[a-z0-9-]+$", "i"), - }, - # Token required as part of the API request - # /........./........./........./DDDDDDDDDDDDDDDDD - "token_d": { - "name": _("Token D"), - "type": "string", - "private": True, - "required": False, - "regex": (r"^V2[a-zA-Z0-9-_]+$", "i"), - }, - }, - ) - - # Define our template arguments - template_args = dict( - NotifyBase.template_args, - **{ - "image": { - "name": _("Include Image"), - "type": "bool", - "default": False, - "map_to": "include_image", - }, - "version": { - "name": _("Version"), - "type": "choice:int", - "values": (1, 2, 3), - "default": 2, - }, - "template": { - "name": _("Template Path"), - "type": "string", - "private": True, - }, - }, - ) - - # Define our token control - template_kwargs = { - "tokens": { - "name": _("Template Tokens"), - "prefix": ":", - }, - } - - def __init__( - self, - token_a, - token_b, - token_c, - token_d=None, - team=None, - version=None, - include_image=True, - template=None, - tokens=None, - **kwargs, - ): - """Initialize Microsoft Teams Object. - - You can optional specify a template and identify arguments you - wish to populate your template with when posting. Some reserved - template arguments that can not be over-ridden are: - `body`, `title`, and `type`. - """ - super().__init__(**kwargs) - - try: - self.version = int(version) - - except TypeError: - # None was specified... take on default - self.version = self.template_args["version"]["default"] - - except ValueError: - # invalid content was provided; let this get caught in the next - # validation check for the version - self.version = None - - if self.version not in self.template_args["version"]["values"]: - msg = f"An invalid MSTeams Version ({version}) was specified." - self.logger.warning(msg) - raise TypeError(msg) - - self.team = validate_regex(team) - if not self.team: - NotifyBase.logger.deprecate( - "Apprise requires you to identify your Microsoft Team name as " - "part of the URL. e.g.: " - f"msteams://TEAM-NAME/{token_a}/{token_b}/{token_c}" - ) - - # Fallback - self.team = "outlook" - - self.token_a = validate_regex( - token_a, *self.template_tokens["token_a"]["regex"] - ) - if not self.token_a: - msg = ( - f"An invalid MSTeams (first) Token ({token_a}) was specified." - ) - self.logger.warning(msg) - raise TypeError(msg) - - self.token_b = validate_regex( - token_b, *self.template_tokens["token_b"]["regex"] - ) - if not self.token_b: - msg = ( - f"An invalid MSTeams (second) Token ({token_b}) was specified." - ) - self.logger.warning(msg) - raise TypeError(msg) - - self.token_c = validate_regex( - token_c, *self.template_tokens["token_c"]["regex"] - ) - if not self.token_c: - msg = ( - f"An invalid MSTeams (third) Token ({token_c}) was specified." - ) - self.logger.warning(msg) - raise TypeError(msg) - - self.token_d = validate_regex( - token_d, *self.template_tokens["token_d"]["regex"] - ) - - # Place a thumbnail image inline with the message body - self.include_image = include_image - - # Our template object is just an AppriseAttachment object - self.template = AppriseAttachment(asset=self.asset) - if template: - # Add our definition to our template - self.template.add(template) - # Enforce maximum file size - self.template[0].max_file_size = self.max_msteams_template_size - - # Template functionality - self.tokens = {} - if isinstance(tokens, dict): - self.tokens.update(tokens) - - elif tokens: - msg = ( - "The specified MSTeams Template Tokens " - f"({tokens}) are not identified as a dictionary." - ) - self.logger.warning(msg) - raise TypeError(msg) - - self.logger.deprecate( - "Microsoft is deprecating their MSTeams webhooks on " - "December 31, 2025. It is advised that you switch to " - "Microsoft Power Automate (already supported by Apprise as " - "workflows://. For more information visit: " - "https://appriseit.com/services/workflows/" - ) - - def gen_payload( - self, body, title="", notify_type=NotifyType.INFO, **kwargs - ): - """This function generates our payload whether it be the generic one - Apprise generates by default, or one provided by a specified external - template.""" - - # Acquire our to-be footer icon if configured to do so - image_url = ( - None if not self.include_image else self.image_url(notify_type) - ) - - if not self.template: - # By default we use a generic working payload if there was - # no template specified - payload = { - "@type": "MessageCard", - "@context": "https://schema.org/extensions", - "summary": self.app_desc, - "themeColor": self.color(notify_type), - "sections": [ - { - "activityImage": None, - "activityTitle": title, - "text": body, - }, - ], - } - - if image_url: - payload["sections"][0]["activityImage"] = image_url - - return payload - - # If our code reaches here, then we generate ourselves the payload - template = self.template[0] - if not template: - # We could not access the attachment - self.logger.error( - "Could not access MSTeam template" - f" {template.url(privacy=True)}." - ) - return False - - # Take a copy of our token dictionary - tokens = self.tokens.copy() - - # Apply some defaults template values - tokens["app_body"] = body - tokens["app_title"] = title - tokens["app_type"] = notify_type.value - tokens["app_id"] = self.app_id - tokens["app_desc"] = self.app_desc - tokens["app_color"] = self.color(notify_type) - tokens["app_image_url"] = image_url - tokens["app_url"] = self.app_url - - # Enforce Application mode - tokens["app_mode"] = TemplateType.JSON - - try: - with open(template.path) as fp: - content = json.loads(apply_template(fp.read(), **tokens)) - - except OSError: - self.logger.error( - f"MSTeam template {template.url(privacy=True)} could not be" - " read." - ) - return None - - except JSONDecodeError as e: - self.logger.error( - f"MSTeam template {template.url(privacy=True)} contains" - " invalid JSON." - ) - self.logger.debug(f"JSONDecodeError: {e}") - return None - - # Load our JSON data (if valid) - has_error = False - if "@type" not in content: - self.logger.error( - f"MSTeam template {template.url(privacy=True)} is missing" - " @type kwarg." - ) - has_error = True - - if "@context" not in content: - self.logger.error( - f"MSTeam template {template.url(privacy=True)} is missing" - " @context kwarg." - ) - has_error = True - - return content if not has_error else None - - def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): - """Perform Microsoft Teams Notification.""" - - headers = { - "User-Agent": self.app_id, - "Content-Type": "application/json", - } - - if self.version == 1: - notify_url = self.notify_url_v1.format( - token_a=self.token_a, - token_b=self.token_b, - token_c=self.token_c, - ) - - if self.version == 2: - notify_url = self.notify_url_v2.format( - team=self.team, - token_a=self.token_a, - token_b=self.token_b, - token_c=self.token_c, - ) - if self.version == 3: - notify_url = self.notify_url_v3.format( - team=self.team, - token_a=self.token_a, - token_b=self.token_b, - token_c=self.token_c, - token_d=self.token_d, - ) - - # Generate our payload if it's possible - payload = self.gen_payload( - body=body, title=title, notify_type=notify_type, **kwargs - ) - if not payload: - # No need to present a reason; that will come from the - # gen_payload() function itself - return False - - self.logger.debug( - "MSTeams POST URL:" - f" {notify_url} (cert_verify={self.verify_certificate!r})" - ) - self.logger.debug(f"MSTeams Payload: {payload!s}") - - # Always call throttle before any remote server i/o is made - self.throttle() - try: - r = requests.post( - notify_url, - data=json.dumps(payload), - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - if r.status_code != requests.codes.ok: - # We had a problem - status_str = NotifyMSTeams.http_response_code_lookup( - r.status_code - ) - - self.logger.warning( - "Failed to send MSTeams notification: " - "{}{}error={}.".format( - status_str, ", " if status_str else "", r.status_code - ) - ) - - self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) - - # We failed - return False - - else: - self.logger.info("Sent MSTeams notification.") - - except requests.RequestException as e: - self.logger.warning( - "A Connection error occurred sending MSTeams notification." - ) - self.logger.debug(f"Socket Exception: {e!s}") - - # We failed - return False - - return True - - @property - def url_identifier(self): - """Returns all of the identifiers that make this URL unique from - another simliar one. - - Targets or end points should never be identified here. - """ - return ( - self.secure_protocol, - self.team if self.version > 1 else None, - self.token_a, - self.token_b, - self.token_c, - ) - - def url(self, privacy=False, *args, **kwargs): - """Returns the URL built dynamically based on specified arguments.""" - - # Define any URL parameters - params = { - "image": "yes" if self.include_image else "no", - } - - if self.version != self.template_args["version"]["default"]: - params["version"] = str(self.version) - - if self.template: - params["template"] = NotifyMSTeams.quote( - self.template[0].url(), safe="" - ) - - # Extend our parameters - params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - # Store any template entries if specified - params.update({f":{k}": v for k, v in self.tokens.items()}) - - result = None - - if self.version == 1: - result = ( - "{schema}://{token_a}/{token_b}/{token_c}/?{params}".format( - schema=self.secure_protocol, - token_a=self.pprint(self.token_a, privacy, safe="@"), - token_b=self.pprint(self.token_b, privacy, safe=""), - token_c=self.pprint(self.token_c, privacy, safe=""), - params=NotifyMSTeams.urlencode(params), - ) - ) - - if self.version == 2: - result = ( - "{schema}://{team}/{token_a}/{token_b}/{token_c}/" - "?{params}".format( - schema=self.secure_protocol, - team=NotifyMSTeams.quote(self.team, safe=""), - token_a=self.pprint(self.token_a, privacy, safe=""), - token_b=self.pprint(self.token_b, privacy, safe=""), - token_c=self.pprint(self.token_c, privacy, safe=""), - params=NotifyMSTeams.urlencode(params), - ) - ) - - if self.version == 3: - result = ( - "{schema}://{team}/{token_a}/{token_b}/{token_c}/" - "{token_d}/?{params}".format( - schema=self.secure_protocol, - team=NotifyMSTeams.quote(self.team, safe=""), - token_a=self.pprint(self.token_a, privacy, safe=""), - token_b=self.pprint(self.token_b, privacy, safe=""), - token_c=self.pprint(self.token_c, privacy, safe=""), - token_d=self.pprint(self.token_d, privacy, safe=""), - params=NotifyMSTeams.urlencode(params), - ) - ) - return result - - @staticmethod - def parse_url(url): - """Parses the URL and returns enough arguments that can allow us to re- - instantiate this object.""" - - results = NotifyBase.parse_url(url, verify_host=False) - if not results: - # We're done early as we couldn't load the results - return results - - # Get unquoted entries - entries = NotifyMSTeams.split_path(results["fullpath"]) - - # Deprecated mode (backwards compatibility) - if results.get("user"): - # If a user was found, it's because it's still part of the first - # token, so we concatinate them - results["token_a"] = "{}@{}".format( - NotifyMSTeams.unquote(results["user"]), - NotifyMSTeams.unquote(results["host"]), - ) - - else: - # Get the Team from the hostname - results["team"] = NotifyMSTeams.unquote(results["host"]) - - # Get the token from the path - results["token_a"] = ( - None if not entries else NotifyMSTeams.unquote(entries.pop(0)) - ) - - results["token_b"] = ( - None if not entries else NotifyMSTeams.unquote(entries.pop(0)) - ) - results["token_c"] = ( - None if not entries else NotifyMSTeams.unquote(entries.pop(0)) - ) - results["token_d"] = ( - None if not entries else NotifyMSTeams.unquote(entries.pop(0)) - ) - - # Get Image - results["include_image"] = parse_bool( - results["qsd"].get("image", True) - ) - - # Get Team name if defined - if "team" in results["qsd"] and results["qsd"]["team"]: - results["team"] = NotifyMSTeams.unquote(results["qsd"]["team"]) - - # Template Handling - if "template" in results["qsd"] and results["qsd"]["template"]: - results["template"] = NotifyMSTeams.unquote( - results["qsd"]["template"] - ) - - # Override version if defined - if "version" in results["qsd"] and results["qsd"]["version"]: - results["version"] = NotifyMSTeams.unquote( - results["qsd"]["version"] - ) - - else: - version = 1 - if results.get("team"): - version = 2 - if results.get("token_d"): - version = 3 - # Set our version if not otherwise set - results["version"] = version - - # Store our tokens - results["tokens"] = results["qsd:"] - - return results - - @staticmethod - def parse_native_url(url): - """ - Legacy Support: - https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK - - New Hook Support: - https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK - - Newer Hook Support: - https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK/V2LMNOP - """ - - # We don't need to do incredibly details token matching as the purpose - # of this is just to detect that were dealing with an msteams url - # token parsing will occur once we initialize the function - result = re.match( - r"^https?://(?P[^.]+)(?P\.webhook)?\.office\.com/" - r"webhook(?Pb2)?/" - r"(?P[A-Z0-9-]+@[A-Z0-9-]+)/" - r"IncomingWebhook/" - r"(?P[A-Z0-9]+)/" - r"(?P[A-Z0-9-]+)/" - r"(?PV2[A-Z0-9-_]+)/?" - r"(?P\?.+)?$", - url, - re.I, - ) - - if result: - # Version 3 URL - return NotifyMSTeams.parse_url( - "{schema}://{team}/{token_a}/{token_b}/{token_c}/{token_d}" - "/{params}".format( - schema=NotifyMSTeams.secure_protocol, - team=result.group("team"), - token_a=result.group("token_a"), - token_b=result.group("token_b"), - token_c=result.group("token_c"), - token_d=result.group("token_d"), - params=( - "" - if not result.group("params") - else result.group("params") - ), - ) - ) - - result = re.match( - r"^https?://(?P[^.]+)(?P\.webhook)?\.office\.com/" - r"webhook(?Pb2)?/" - r"(?P[A-Z0-9-]+@[A-Z0-9-]+)/" - r"IncomingWebhook/" - r"(?P[A-Z0-9]+)/" - r"(?P[A-Z0-9-]+)/?" - r"(?P\?.+)?$", - url, - re.I, - ) - - if result: - if result.group("v2a"): - # Version 2 URL - return NotifyMSTeams.parse_url( - "{schema}://{team}/{token_a}/{token_b}/{token_c}" - "/{params}".format( - schema=NotifyMSTeams.secure_protocol, - team=result.group("team"), - token_a=result.group("token_a"), - token_b=result.group("token_b"), - token_c=result.group("token_c"), - params=( - "" - if not result.group("params") - else result.group("params") - ), - ) - ) - else: - # Version 1 URLs - # team is also set to 'outlook' in this case - return NotifyMSTeams.parse_url( - "{schema}://{token_a}/{token_b}/{token_c}/{params}".format( - schema=NotifyMSTeams.secure_protocol, - token_a=result.group("token_a"), - token_b=result.group("token_b"), - token_c=result.group("token_c"), - params=( - "" - if not result.group("params") - else result.group("params") - ), - ) - ) - return None diff --git a/libs/apprise/plugins/nextcloud.py b/libs/apprise/plugins/nextcloud.py index d762768fa..343c59298 100644 --- a/libs/apprise/plugins/nextcloud.py +++ b/libs/apprise/plugins/nextcloud.py @@ -203,7 +203,9 @@ class NotifyNextcloud(NotifyBase): if results: group_id = ( self.all_group_id - if results.group("all") else results.group("group")) + if results.group("all") + else results.group("group") + ) self.groups.add(group_id) self.logger.debug("Added Nextcloud group '%s'", group_id) @@ -214,11 +216,13 @@ class NotifyNextcloud(NotifyBase): # Store our target self.targets.append(results.group("user")) self.logger.debug( - "Added Nextcloud user '%s'", self.targets[-1]) + "Added Nextcloud user '%s'", self.targets[-1] + ) continue self.logger.warning( - "Ignored invalid Nextcloud user/group '%s'", target) + "Ignored invalid Nextcloud user/group '%s'", target + ) self.version = self.template_args["version"]["default"] if version is not None: @@ -236,8 +240,9 @@ class NotifyNextcloud(NotifyBase): raise TypeError(msg) from None # Support URL Prefix - self.url_prefix = "" if not url_prefix else ( - "/" + url_prefix.strip("/")) + self.url_prefix = ( + "" if not url_prefix else ("/" + url_prefix.strip("/")) + ) self.headers = {} if headers: @@ -339,6 +344,7 @@ class NotifyNextcloud(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -364,13 +370,15 @@ class NotifyNextcloud(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) if target: return (False, content) raise NextcloudGroupDiscoveryException( - f"{query} non-200 response") + f"{query} non-200 response" + ) except requests.RequestException as e: self.logger.warning( @@ -382,7 +390,8 @@ class NotifyNextcloud(NotifyBase): if target: return (False, None) raise NextcloudGroupDiscoveryException( - f"{query} socket exception") from None + f"{query} socket exception" + ) from None self.logger.info("Sent Nextcloud %s", query) return (True, content) @@ -443,8 +452,8 @@ class NotifyNextcloud(NotifyBase): if targets is not None: # Returned cached value self.logger.trace( - f"Using Nextcloud cached response for group '{group}' " - "query") + f"Using Nextcloud cached response for group '{group}' query" + ) return set(targets) # _fetch throws an exception if it fails, so we can @@ -457,9 +466,7 @@ class NotifyNextcloud(NotifyBase): # If we get here, our fetch was successful; look up our users users = response.get("ocs", {}).get("data", {}).get("users") if isinstance(users, list): - targets = { - s for u in users if (s := str(u).strip()) - } + targets = {s for u in users if (s := str(u).strip())} if not targets: self.logger.warning( @@ -467,8 +474,8 @@ class NotifyNextcloud(NotifyBase): ) self.store.set( - group, list(targets), - expires=self.group_discovery_cache_length_sec) + group, list(targets), expires=self.group_discovery_cache_length_sec + ) return targets def all_users(self): @@ -479,7 +486,8 @@ class NotifyNextcloud(NotifyBase): targets = self.store.get(self.all_group_id) if targets is not None: self.logger.trace( - "Using Nextcloud cached response for all-user query") + "Using Nextcloud cached response for all-user query" + ) return set(targets) # _fetch throws an exception if it fails, so we can @@ -492,9 +500,7 @@ class NotifyNextcloud(NotifyBase): # If we get here, our fetch was successful; look up our users users = response.get("ocs", {}).get("data", {}).get("users") if isinstance(users, list): - targets = { - s for u in users if (s := str(u).strip()) - } + targets = {s for u in users if (s := str(u).strip())} if not targets: self.logger.warning( @@ -505,7 +511,9 @@ class NotifyNextcloud(NotifyBase): self.store.set( self.all_group_id, - list(targets), expires=self.group_discovery_cache_length_sec) + list(targets), + expires=self.group_discovery_cache_length_sec, + ) return targets @property @@ -568,16 +576,17 @@ class NotifyNextcloud(NotifyBase): if self.port is None or self.port == default_port else f":{self.port}" ), - targets="/".join([ - NotifyNextcloud.quote(x, safe=(group_prefix + user_prefix)) - for x in chain( - # Groups are prefixed with a pound/hashtag symbol - [f"{group_prefix}{x}" for x in self.groups], - # Users - [f"{user_prefix}{x}" for x in self.targets], - ) - ]), - + targets="/".join( + [ + NotifyNextcloud.quote(x, safe=(group_prefix + user_prefix)) + for x in chain( + # Groups are prefixed with a pound/hashtag symbol + [f"{group_prefix}{x}" for x in self.groups], + # Users + [f"{user_prefix}{x}" for x in self.targets], + ) + ] + ), params=NotifyNextcloud.urlencode(params), ) diff --git a/libs/apprise/plugins/nextcloudtalk.py b/libs/apprise/plugins/nextcloudtalk.py index 03da7bdf6..c0d499629 100644 --- a/libs/apprise/plugins/nextcloudtalk.py +++ b/libs/apprise/plugins/nextcloudtalk.py @@ -224,6 +224,7 @@ class NotifyNextcloudTalk(NotifyBase): auth=(self.user, self.password), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.created, @@ -244,7 +245,8 @@ class NotifyNextcloudTalk(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # track our failure has_error = True diff --git a/libs/apprise/plugins/notica.py b/libs/apprise/plugins/notica.py index 4375f447e..864faec0c 100644 --- a/libs/apprise/plugins/notica.py +++ b/libs/apprise/plugins/notica.py @@ -238,6 +238,7 @@ class NotifyNotica(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -252,7 +253,8 @@ class NotifyNotica(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -324,21 +326,18 @@ class NotifyNotica(NotifyBase): ) default_port = 443 if self.secure else 80 - return ( - "{schema}://{auth}{hostname}{port}{fullpath}{token}/?{params}" - .format( - schema=self.secure_protocol if self.secure else self.protocol, - auth=auth, - hostname=NotifyNotica.quote(self.host, safe=""), - port=( - "" - if self.port is None or self.port == default_port - else f":{self.port}" - ), - fullpath=NotifyNotica.quote(self.fullpath, safe="/"), - token=self.pprint(self.token, privacy, safe=""), - params=NotifyNotica.urlencode(params), - ) + return "{schema}://{auth}{hostname}{port}{fullpath}{token}/?{params}".format( + schema=self.secure_protocol if self.secure else self.protocol, + auth=auth, + hostname=NotifyNotica.quote(self.host, safe=""), + port=( + "" + if self.port is None or self.port == default_port + else f":{self.port}" + ), + fullpath=NotifyNotica.quote(self.fullpath, safe="/"), + token=self.pprint(self.token, privacy, safe=""), + params=NotifyNotica.urlencode(params), ) @staticmethod diff --git a/libs/apprise/plugins/notifiarr.py b/libs/apprise/plugins/notifiarr.py index fdf1dfe4c..96b00784d 100644 --- a/libs/apprise/plugins/notifiarr.py +++ b/libs/apprise/plugins/notifiarr.py @@ -235,15 +235,17 @@ class NotifyNotifiarr(NotifyBase): return "{schema}://{apikey}/{targets}?{params}".format( schema=self.secure_protocol, apikey=self.pprint(self.apikey, privacy, safe=""), - targets="/".join([ - NotifyNotifiarr.quote(x, safe="+#@") - for x in chain( - # Channels - [f"#{x}" for x in self.targets["channels"]], - # Pass along the same invalid entries as were provided - self.targets["invalid"], - ) - ]), + targets="/".join( + [ + NotifyNotifiarr.quote(x, safe="+#@") + for x in chain( + # Channels + [f"#{x}" for x in self.targets["channels"]], + # Pass along the same invalid entries as were provided + self.targets["invalid"], + ) + ] + ), params=NotifyNotifiarr.urlencode(params), ) @@ -363,6 +365,7 @@ class NotifyNotifiarr(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code < 200 or r.status_code >= 300: # We had a problem @@ -378,7 +381,8 @@ class NotifyNotifiarr(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/notificationapi.py b/libs/apprise/plugins/notificationapi.py index 370e651df..67b38652c 100644 --- a/libs/apprise/plugins/notificationapi.py +++ b/libs/apprise/plugins/notificationapi.py @@ -51,8 +51,7 @@ from ..utils.parse import ( from .base import NotifyBase # Used to detect ID -IS_VALID_ID_RE = re.compile( - r"^\s*(@|%40)?(?P[\w_-]+)\s*$", re.I) +IS_VALID_ID_RE = re.compile(r"^\s*(@|%40)?(?P[\w_-]+)\s*$", re.I) class NotificationAPIRegion: @@ -90,14 +89,16 @@ class NotificationAPIChannel: # A List of our channels we can use for verification -NOTIFICATIONAPI_CHANNELS: frozenset[str] = frozenset([ - NotificationAPIChannel.EMAIL, - NotificationAPIChannel.SMS, - NotificationAPIChannel.INAPP, - NotificationAPIChannel.WEB_PUSH, - NotificationAPIChannel.MOBILE_PUSH, - NotificationAPIChannel.SLACK, -]) +NOTIFICATIONAPI_CHANNELS: frozenset[str] = frozenset( + [ + NotificationAPIChannel.EMAIL, + NotificationAPIChannel.SMS, + NotificationAPIChannel.INAPP, + NotificationAPIChannel.WEB_PUSH, + NotificationAPIChannel.MOBILE_PUSH, + NotificationAPIChannel.SLACK, + ] +) class NotificationAPIMode: @@ -108,10 +109,12 @@ class NotificationAPIMode: # A List of our channels we can use for verification -NOTIFICATIONAPI_MODES: frozenset[str] = frozenset([ - NotificationAPIMode.TEMPLATE, - NotificationAPIMode.MESSAGE, -]) +NOTIFICATIONAPI_MODES: frozenset[str] = frozenset( + [ + NotificationAPIMode.TEMPLATE, + NotificationAPIMode.MESSAGE, + ] +) class NotifyNotificationAPI(NotifyBase): @@ -149,96 +152,103 @@ class NotifyNotificationAPI(NotifyBase): ) # Explicit URL tokens we care about (all others from base are ignored) - template_tokens = dict(NotifyBase.template_tokens, **{ - "type": { - "name": _("Message Type"), - "type": "string", - "regex": (r"^[A-Z0-9_-]+$", "i"), - "required": True, - "map_to": "message_type", + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "type": { + "name": _("Message Type"), + "type": "string", + "regex": (r"^[A-Z0-9_-]+$", "i"), + "required": True, + "map_to": "message_type", + }, + "client_id": { + "name": _("Client ID"), + "type": "string", + "required": True, + }, + "client_secret": { + "name": _("Client Secret"), + "type": "string", + "required": True, + "private": True, + }, + "target_email": { + "name": _("Target Email"), + "type": "string", + "map_to": "targets", + }, + "target_id": { + "name": _("Target ID"), + "type": "string", + "map_to": "targets", + }, + "target_sms": { + "name": _("Target SMS"), + "type": "string", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + "required": True, + }, }, - "client_id": { - "name": _("Client ID"), - "type": "string", - "required": True, - }, - "client_secret": { - "name": _("Client Secret"), - "type": "string", - "required": True, - "private": True, - }, - "target_email": { - "name": _("Target Email"), - "type": "string", - "map_to": "targets", - }, - "target_id": { - "name": _("Target ID"), - "type": "string", - "map_to": "targets", - }, - "target_sms": { - "name": _("Target SMS"), - "type": "string", - "map_to": "targets", - }, - "targets": { - "name": _("Targets"), - "type": "list:string", - }, - }) + ) # Supported query args - template_args = dict(NotifyBase.template_args, **{ - "type": { - "alias_of": "type", + template_args = dict( + NotifyBase.template_args, + **{ + "type": { + "alias_of": "type", + }, + "channels": { + "name": _("Channels"), + "type": "list:string", + "values": NOTIFICATIONAPI_CHANNELS, + }, + "region": { + "name": _("Region Name"), + "type": "choice:string", + "values": NOTIFICATIONAPI_REGIONS, + "default": NotificationAPIRegion.US, + }, + "mode": { + "name": _("Mode"), + "type": "choice:string", + "values": NOTIFICATIONAPI_MODES, + }, + "reply": { + "name": _("Reply To"), + "type": "string", + "map_to": "reply_to", + }, + "from": { + "name": _("From Email"), + "type": "string", + "map_to": "from_addr", + }, + "id": { + "alias_of": "client_id", + }, + "secret": { + "alias_of": "client_secret", + }, + "to": { + "alias_of": "targets", + }, + # Email Values + "cc": { + "name": _("Carbon Copy"), + "type": "list:string", + }, + "bcc": { + "name": _("Blind Carbon Copy"), + "type": "list:string", + }, }, - "channels": { - "name": _("Channels"), - "type": "list:string", - "values": NOTIFICATIONAPI_CHANNELS, - }, - "region": { - "name": _("Region Name"), - "type": "choice:string", - "values": NOTIFICATIONAPI_REGIONS, - "default": NotificationAPIRegion.US, - }, - "mode": { - "name": _("Mode"), - "type": "choice:string", - "values": NOTIFICATIONAPI_MODES, - }, - "to": { - "alias_of": "targets", - }, - # Email Values - "cc": { - "name": _("Carbon Copy"), - "type": "list:string", - }, - "bcc": { - "name": _("Blind Carbon Copy"), - "type": "list:string", - }, - "reply": { - "name": _("Reply To"), - "type": "string", - "map_to": "reply_to", - }, - "from": { - "name": _("From Email"), - "type": "string", - "map_to": "from_addr", - }, - "id": { - "alias_of": "client_id", - }, - "secret": { - "alias_of": "client_secret", - } - }) + ) # Define our token control template_kwargs = { @@ -248,10 +258,22 @@ class NotifyNotificationAPI(NotifyBase): }, } - def __init__(self, client_id, client_secret, message_type=None, - targets=None, cc=None, bcc=None, reply_to=None, - channels=None, region=None, mode=None, from_addr=None, - tokens=None, **kwargs): + def __init__( + self, + client_id, + client_secret, + message_type=None, + targets=None, + cc=None, + bcc=None, + reply_to=None, + channels=None, + region=None, + mode=None, + from_addr=None, + tokens=None, + **kwargs, + ): """ Initialize Notify NotificationAPI Object """ @@ -260,16 +282,20 @@ class NotifyNotificationAPI(NotifyBase): # Client ID self.client_id = validate_regex(client_id) if not self.client_id: - msg = "An invalid NotificationAPI Client ID " \ - "({}) was specified.".format(client_id) + msg = ( + "An invalid NotificationAPI Client ID " + "({}) was specified.".format(client_id) + ) self.logger.warning(msg) raise TypeError(msg) # Client Secret self.client_secret = validate_regex(client_secret) if not self.client_secret: - msg = "An invalid NotificationAPI Client Secret " \ - "({}) was specified.".format(client_secret) + msg = ( + "An invalid NotificationAPI Client Secret " + "({}) was specified.".format(client_secret) + ) self.logger.warning(msg) raise TypeError(msg) @@ -284,7 +310,8 @@ class NotifyNotificationAPI(NotifyBase): if result: from_addr_ = ( result["name"] if result["name"] else from_addr_[0], - result["full_email"]) + result["full_email"], + ) else: # Only update the string but use the already detected info from_addr_[0] = from_addr @@ -300,7 +327,8 @@ class NotifyNotificationAPI(NotifyBase): if result and "full_email" in result: self.reply_to = { "senderName": result["name"] - if result["name"] else from_addr_[0], + if result["name"] + else from_addr_[0], "senderEmail": result["full_email"], } @@ -311,14 +339,18 @@ class NotifyNotificationAPI(NotifyBase): (a for a in NOTIFICATIONAPI_MODES if a.startswith(mode)), None ) if self.mode not in NOTIFICATIONAPI_MODES: - msg = \ + msg = ( f"The NotificationAPI mode specified ({mode}) is invalid." + ) self.logger.warning(msg) raise TypeError(msg) else: # Detect mode based on whether or not a message_type was provided - self.mode = NotificationAPIMode.MESSAGE if not message_type else \ - NotificationAPIMode.TEMPLATE + self.mode = ( + NotificationAPIMode.MESSAGE + if not message_type + else NotificationAPIMode.TEMPLATE + ) if not message_type: # Assign a default message type @@ -326,10 +358,13 @@ class NotifyNotificationAPI(NotifyBase): else: self.message_type = validate_regex( - message_type, *self.template_tokens["type"]["regex"]) + message_type, *self.template_tokens["type"]["regex"] + ) if not self.message_type: - msg = "An invalid NotificationAPI Message Type " \ - "({}) was specified.".format(message_type) + msg = ( + "An invalid NotificationAPI Message Type " + "({}) was specified.".format(message_type) + ) self.logger.warning(msg) raise TypeError(msg) @@ -338,7 +373,8 @@ class NotifyNotificationAPI(NotifyBase): # Basic base64(client_id:client_secret) # https://www.notificationapi.com/docs/reference/server token = base64.b64encode( - f"{self.client_id}:{self.client_secret}".encode()).decode("ascii") + f"{self.client_id}:{self.client_secret}".encode() + ).decode("ascii") self.auth_header = f"Basic {token}" # Acquire Carbon Copies @@ -351,7 +387,8 @@ class NotifyNotificationAPI(NotifyBase): try: self.region = ( self.template_args["region"]["default"] - if region is None else region.lower() + if region is None + else region.lower() ) if self.region not in NOTIFICATIONAPI_REGIONS: @@ -360,8 +397,9 @@ class NotifyNotificationAPI(NotifyBase): except (AttributeError, IndexError, TypeError): # Invalid region specified - msg = \ + msg = ( f"The NotificationAPI region specified ({region}) is invalid." + ) self.logger.warning(msg) raise TypeError(msg) from None @@ -373,7 +411,8 @@ class NotifyNotificationAPI(NotifyBase): # Invalid channel specified msg = ( "The NotificationAPI forced channel specified " - f"({channel}) is invalid.") + f"({channel}) is invalid." + ) self.logger.warning(msg) raise TypeError(msg) from None self.channels.add(channel) @@ -392,15 +431,14 @@ class NotifyNotificationAPI(NotifyBase): self.channels.add(NotificationAPIChannel.EMAIL) self.logger.info( "The NotificationAPI default channel of " - f"{NotificationAPIChannel.EMAIL} was set.") + f"{NotificationAPIChannel.EMAIL} was set." + ) continue elif "id" in current_target: # Store and move on self.targets.append(current_target) - current_target = { - "email": result["full_email"] - } + current_target = {"email": result["full_email"]} continue # if we got here, we have to many emails making it now @@ -408,28 +446,29 @@ class NotifyNotificationAPI(NotifyBase): msg = ( "The NotificationAPI received too many emails " "creating an ambiguous situation; aborted at " - f"'{entry}'.") + f"'{entry}'." + ) self.logger.warning(msg) raise TypeError(msg) from None result = is_phone_no(entry) if result: if "number" not in current_target: - current_target["number"] = \ - ("+" if entry[0] == "+" else "") + result["full"] + current_target["number"] = ( + "+" if entry[0] == "+" else "" + ) + result["full"] if not self.channels: self.channels.add(NotificationAPIChannel.SMS) self.logger.info( "The NotificationAPI default channel of " - f"{NotificationAPIChannel.SMS} was set.") + f"{NotificationAPIChannel.SMS} was set." + ) continue elif "id" in current_target: # Store and move on self.targets.append(current_target) - current_target = { - "number": result["full"] - } + current_target = {"number": result["full"]} continue # if we got here, we have to many emails making it now @@ -437,7 +476,8 @@ class NotifyNotificationAPI(NotifyBase): msg = ( "The NotificationAPI received too many phone no's " "creating an ambiguous situation; aborted at " - f"'{entry}'.") + f"'{entry}'." + ) self.logger.warning(msg) raise TypeError(msg) from None @@ -449,14 +489,13 @@ class NotifyNotificationAPI(NotifyBase): # Store id in next target and move on self.targets.append(current_target) - current_target = { - "id": result.group("id") - } + current_target = {"id": result.group("id")} continue self.logger.warning( "Dropped invalid NotificationAPI target " - f"({entry}) specified") + f"({entry}) specified" + ) self._invalid_targets.append(entry) continue @@ -470,7 +509,9 @@ class NotifyNotificationAPI(NotifyBase): msg = ( "The NotificationAPI did not detect an id to " "correlate the following with {}".format( - str(current_target))) + str(current_target) + ) + ) self.logger.warning(msg) raise TypeError(msg) from None @@ -484,13 +525,13 @@ class NotifyNotificationAPI(NotifyBase): continue self.logger.warning( - "Dropped invalid Carbon Copy email " - "({}) specified.".format(recipient), + "Dropped invalid Carbon Copy email ({}) specified.".format( + recipient + ), ) # Validate recipients (bcc:) and drop bad ones: for recipient in parse_emails(bcc): - result = is_email(recipient) if result: self.bcc.add(result["full_email"]) @@ -534,23 +575,31 @@ class NotifyNotificationAPI(NotifyBase): if len(self.cc) > 0: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8").replace(",", "%2C") - for e in self.cc]) + params["cc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.cc + ] + ) if len(self.bcc) > 0: # Handle our Blind Carbon Copy Addresses - params["bcc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8").replace(",", "%2C") - for e in self.bcc]) + params["bcc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.bcc + ] + ) if self.reply_to: # Handle our Reply-To Address @@ -587,15 +636,19 @@ class NotifyNotificationAPI(NotifyBase): if "email" in target: targets.append(f"{target['email']}") - mtype = f"{self.message_type}@" \ - if self.message_type != self.default_message_type else "" + mtype = ( + f"{self.message_type}@" + if self.message_type != self.default_message_type + else "" + ) return "{schema}://{mtype}{cid}/{secret}/{targets}?{params}".format( schema=self.secure_protocol[0], mtype=mtype, cid=self.pprint(self.client_id, privacy, safe=""), secret=self.pprint(self.client_secret, privacy, safe=""), - targets=NotifyNotificationAPI.quote("/".join( - chain(targets, self._invalid_targets)), safe="/"), + targets=NotifyNotificationAPI.quote( + "/".join(chain(targets, self._invalid_targets)), safe="/" + ), params=NotifyNotificationAPI.urlencode(params), ) @@ -606,8 +659,9 @@ class NotifyNotificationAPI(NotifyBase): return max(1, len(self.targets)) - def gen_payload(self, body, title="", notify_type=NotifyType.INFO, - **kwargs): + def gen_payload( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): """ generates our NotificationAPI payload """ @@ -630,87 +684,116 @@ class NotifyNotificationAPI(NotifyBase): parameters["appUrl"] = self.app_url # A Simple Email Payload Template - payload_.update({ - "parameters": {**parameters}, - }) + payload_.update( + { + "parameters": {**parameters}, + } + ) else: # Acquire text version of body if provided - text_body = convert_between( - NotifyFormat.HTML, NotifyFormat.TEXT, body) \ - if self.notify_format == NotifyFormat.HTML else body + text_body = ( + convert_between(NotifyFormat.HTML, NotifyFormat.TEXT, body) + if self.notify_format == NotifyFormat.HTML + else body + ) for channel in self.channels: # Python v3.10 supports `match/case` but since Apprise aims to # be compatible with Python v3.9+, we must use if/else for the # time being if channel == NotificationAPIChannel.SMS: - payload_.update({ - NotificationAPIChannel.SMS: { - "message": (title + "\n" + text_body) - if title else text_body, - }, - }) + payload_.update( + { + NotificationAPIChannel.SMS: { + "message": (title + "\n" + text_body) + if title + else text_body, + }, + } + ) elif channel == NotificationAPIChannel.EMAIL: - html_body = convert_between( - NotifyFormat.TEXT, NotifyFormat.HTML, body) \ - if self.notify_format != NotifyFormat.HTML else body + html_body = ( + convert_between( + NotifyFormat.TEXT, NotifyFormat.HTML, body + ) + if self.notify_format != NotifyFormat.HTML + else body + ) - payload_.update({ - NotificationAPIChannel.EMAIL: { - "subject": title if title else self.app_id, - "html": html_body, - }, - }) + payload_.update( + { + NotificationAPIChannel.EMAIL: { + "subject": title if title else self.app_id, + "html": html_body, + }, + } + ) if self.from_addr: - payload_[NotificationAPIChannel.EMAIL].update({ - "senderEmail": self.from_addr, - "senderName": self.names[self.from_addr], - }) + payload_[NotificationAPIChannel.EMAIL].update( + { + "senderEmail": self.from_addr, + "senderName": self.names[self.from_addr], + } + ) elif channel == NotificationAPIChannel.INAPP: - payload_.update({ - NotificationAPIChannel.INAPP: { - "title": title if title else self.app_id, - "image": self.image_url(notify_type), - }, - }) + payload_.update( + { + NotificationAPIChannel.INAPP: { + "title": title if title else self.app_id, + "image": self.image_url(notify_type), + }, + } + ) elif channel == NotificationAPIChannel.WEB_PUSH: - payload_.update({ - NotificationAPIChannel.WEB_PUSH: { - "title": title if title else self.app_id, - "message": text_body, - "icon": self.image_url(notify_type), - }, - }) + payload_.update( + { + NotificationAPIChannel.WEB_PUSH: { + "title": title if title else self.app_id, + "message": text_body, + "icon": self.image_url(notify_type), + }, + } + ) elif channel == NotificationAPIChannel.MOBILE_PUSH: - payload_.update({ - NotificationAPIChannel.MOBILE_PUSH: { - "title": title if title else self.app_id, - "message": text_body, - }, - }) + payload_.update( + { + NotificationAPIChannel.MOBILE_PUSH: { + "title": title if title else self.app_id, + "message": text_body, + }, + } + ) else: # channel == NotificationAPIChannel.SLACK - payload_.update({ - NotificationAPIChannel.SLACK: { - "text": (title + "\n" + text_body) - if title else text_body, - }, - }) + payload_.update( + { + NotificationAPIChannel.SLACK: { + "text": (title + "\n" + text_body) + if title + else text_body, + }, + } + ) # Copy our list to work with targets = list(self.targets) if self.from_addr: - payload_.update({ - "options": { - "email": { - "fromAddress": self.from_addr, - "fromName": self.names[self.from_addr]}}}) + payload_.update( + { + "options": { + "email": { + "fromAddress": self.from_addr, + "fromName": self.names[self.from_addr], + } + } + } + ) elif self.cc or self.bcc: # Set up shell @@ -727,8 +810,8 @@ class NotifyNotificationAPI(NotifyBase): # the cc list or bcc list. It also makes sure the cc list does # not contain any of the bcc entries if "email" in target: - cc = (self.cc - self.bcc - {target["email"]}) - bcc = (self.bcc - {target["email"]}) + cc = self.cc - self.bcc - {target["email"]} + bcc = self.bcc - {target["email"]} else: # Assume defaults @@ -766,7 +849,8 @@ class NotifyNotificationAPI(NotifyBase): # Prepare our URL url = ( f"{NOTIFICATIONAPI_API_LOOKUP[self.region]}/" - f"{self.client_id}/sender") + f"{self.client_id}/sender" + ) headers = { "User-Agent": self.app_id, @@ -775,13 +859,17 @@ class NotifyNotificationAPI(NotifyBase): } for payload in self.gen_payload( - body, title=title, notify_type=notify_type, **kwargs): + body, title=title, notify_type=notify_type, **kwargs + ): # Perform our post self.logger.debug( "NotificationAPI POST URL: {} (cert_verify={!r})".format( - url, self.verify_certificate)) + url, self.verify_certificate + ) + ) self.logger.debug( - "NotificationAPI Payload: %s", payload["to"]["id"]) + "NotificationAPI Payload: %s", payload["to"]["id"] + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -792,6 +880,7 @@ class NotifyNotificationAPI(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -803,9 +892,11 @@ class NotifyNotificationAPI(NotifyBase): # - TypeError = r.content is None # - AttributeError = r is None self.logger.warning( - "Invalid response from NotificationAPI server.") + "Invalid response from NotificationAPI server." + ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Record our failure has_error = True @@ -815,11 +906,15 @@ class NotifyNotificationAPI(NotifyBase): status_code = r.status_code if status_code not in ( - requests.codes.ok, requests.codes.accepted): + requests.codes.ok, + requests.codes.accepted, + ): # We had a problem - status_str = \ + status_str = ( NotifyNotificationAPI.http_response_code_lookup( - status_code) + status_code + ) + ) self.logger.warning( "Failed to send NotificationAPI notification to %s: " @@ -827,10 +922,12 @@ class NotifyNotificationAPI(NotifyBase): payload["to"]["id"], status_str, ", " if status_str else "", - status_code) + status_code, + ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Record our failure has_error = True @@ -838,12 +935,15 @@ class NotifyNotificationAPI(NotifyBase): else: self.logger.info( "Sent NotificationAPI notification to %s.", - payload["to"]["id"]) + payload["to"]["id"], + ) except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending NotificationAPI " - "notification to %s.", payload["to"]["id"]) + "notification to %s.", + payload["to"]["id"], + ) self.logger.debug("Socket Exception: {}".format(str(e))) # Record our failure @@ -873,23 +973,27 @@ class NotifyNotificationAPI(NotifyBase): results["targets"] = [] if results["host"]: results["targets"].append( - NotifyNotificationAPI.unquote(results["host"])) + NotifyNotificationAPI.unquote(results["host"]) + ) # For tracking email sources results["from_addr"] = None if "from" in results["qsd"] and len(results["qsd"]["from"]): - results["from_addr"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["from"].rstrip()) + results["from_addr"] = NotifyNotificationAPI.unquote( + results["qsd"]["from"].rstrip() + ) # First 2 elements are the client_id and client_secret # Following are targets - results["targets"] += \ - NotifyNotificationAPI.split_path(results["fullpath"]) + results["targets"] += NotifyNotificationAPI.split_path( + results["fullpath"] + ) # check for our client id if "id" in results["qsd"] and len(results["qsd"]["id"]): # Store our Client ID - results["client_id"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["id"]) + results["client_id"] = NotifyNotificationAPI.unquote( + results["qsd"]["id"] + ) elif results["targets"]: # Store our Client ID @@ -897,53 +1001,61 @@ class NotifyNotificationAPI(NotifyBase): if "secret" in results["qsd"] and len(results["qsd"]["secret"]): # Store our Client Secret - results["client_secret"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["secret"]) + results["client_secret"] = NotifyNotificationAPI.unquote( + results["qsd"]["secret"] + ) elif results["targets"]: # Store our Client Secret results["client_secret"] = results["targets"].pop(0) if "region" in results["qsd"] and len(results["qsd"]["region"]): - results["region"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["region"]) + results["region"] = NotifyNotificationAPI.unquote( + results["qsd"]["region"] + ) if "channels" in results["qsd"] and len(results["qsd"]["channels"]): - results["channels"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["channels"]) + results["channels"] = NotifyNotificationAPI.unquote( + results["qsd"]["channels"] + ) if "mode" in results["qsd"] and len(results["qsd"]["mode"]): - results["mode"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["mode"]) + results["mode"] = NotifyNotificationAPI.unquote( + results["qsd"]["mode"] + ) if "reply" in results["qsd"] and len(results["qsd"]["reply"]): - results["reply_to"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["reply"]) + results["reply_to"] = NotifyNotificationAPI.unquote( + results["qsd"]["reply"] + ) # Handling of Message Type if "type" in results["qsd"] and len(results["qsd"]["type"]): - results["message_type"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["type"]) + results["message_type"] = NotifyNotificationAPI.unquote( + results["qsd"]["type"] + ) elif results["user"]: # Pull from user - results["message_type"] = \ - NotifyNotificationAPI.unquote(results["user"]) + results["message_type"] = NotifyNotificationAPI.unquote( + results["user"] + ) # The 'to' makes it easier to use yaml configuration if "to" in results["qsd"] and len(results["qsd"]["to"]): results["targets"].append( - NotifyNotificationAPI.unquote(results["qsd"]["to"])) + NotifyNotificationAPI.unquote(results["qsd"]["to"]) + ) # Handle Carbon Copy Addresses if "cc" in results["qsd"] and len(results["qsd"]["cc"]): - results["cc"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["cc"]) + results["cc"] = NotifyNotificationAPI.unquote(results["qsd"]["cc"]) # Handle Blind Carbon Copy Addresses if "bcc" in results["qsd"] and len(results["qsd"]["bcc"]): - results["bcc"] = \ - NotifyNotificationAPI.unquote(results["qsd"]["bcc"]) + results["bcc"] = NotifyNotificationAPI.unquote( + results["qsd"]["bcc"] + ) # Store our tokens results["tokens"] = results["qsd:"] diff --git a/libs/apprise/plugins/notifico.py b/libs/apprise/plugins/notifico.py index eca8c3c53..08d199a92 100644 --- a/libs/apprise/plugins/notifico.py +++ b/libs/apprise/plugins/notifico.py @@ -27,18 +27,32 @@ # Notifico allows you to relay notifications into IRC channels. # -# 1. visit https://n.tkte.ch and sign up for an account -# 2. create a project; either manually or sync with github -# 3. from within the project, you can create a message hook +# The original hosted service (https://n.tkte.ch) has gone offline. +# The project is open source and can be self-hosted: +# https://notifico.tech/ # -# the URL will look something like this: +# Official/hosted-instance usage (project_id takes the host position): +# 1. Visit https://n.tkte.ch and sign up for an account. +# 2. Create a project; either manually or sync with GitHub. +# 3. From within the project, create a Plain Text Message Hook. +# +# The hook URL will look something like: # https://n.tkte.ch/h/2144/uJmKaBW9WFk42miB146ci3Kj # ^ ^ # | | # project id message hook # -# This plugin also supports taking the URL (as identified above) directly -# as well. +# To use the official endpoint with Apprise, the two path parts above +# become the arguments: +# notifico://{ProjectID}/{MessageHook} +# +# To point Apprise at a self-hosted instance instead: +# notifico://{host}/{ProjectID}/{MessageHook} (HTTP) +# notificos://{host}/{ProjectID}/{MessageHook} (HTTPS) +# notifico://{host}:{port}/{ProjectID}/{MessageHook} +# notifico://{user}:{pass}@{host}/{ProjectID}/{MessageHook} +# +# This plugin also supports taking the n.tkte.ch URL directly as input. import re @@ -46,10 +60,28 @@ import requests from ..common import NotifyType from ..locale import gettext_lazy as _ +from ..url import PrivacyMode from ..utils.parse import parse_bool, validate_regex from .base import NotifyBase +class NotificoMode: + # Tracks the mode of operation for the Notifico plugin. + + # Notifications delivered via the official n.tkte.ch endpoint + OFFICIAL = "official" + + # Notifications delivered via a self-hosted Notifico instance + SELFHOSTED = "selfhosted" + + +# Define our Notifico Modes +NOTIFICO_MODES = ( + NotificoMode.OFFICIAL, + NotificoMode.SELFHOSTED, +) + + class NotificoFormat: # Resets all formatting Reset = "\x0f" @@ -91,18 +123,19 @@ class NotifyNotifico(NotifyBase): service_name = "Notifico" # The services URL - service_url = "https://n.tkte.ch" + service_url = "https://notifico.tech/" - # The default protocol + # The default protocol (HTTP -- used for self-hosted instances and + # also as the schema for official-mode URLs) protocol = "notifico" - # The default secure protocol - secure_protocol = "notifico" + # The default secure protocol (HTTPS -- self-hosted instances only) + secure_protocol = "notificos" # A URL that takes you to the setup/help of the specific protocol setup_url = "https://appriseit.com/services/notifico/" - # Plain Text Notification URL + # Official Notifico webhook endpoint notify_url = "https://n.tkte.ch/h/{proj}/{hook}" # The title is not used @@ -112,12 +145,42 @@ class NotifyNotifico(NotifyBase): body_maxlen = 512 # Define object templates - templates = ("{schema}://{project_id}/{msghook}",) + templates = ( + # Official endpoint: project_id takes the host position + "{schema}://{project_id}/{msghook}", + # Self-hosted variants (HTTP or HTTPS) + "{schema}://{host}/{project_id}/{msghook}", + "{schema}://{host}:{port}/{project_id}/{msghook}", + "{schema}://{user}@{host}/{project_id}/{msghook}", + "{schema}://{user}@{host}:{port}/{project_id}/{msghook}", + "{schema}://{user}:{password}@{host}/{project_id}/{msghook}", + "{schema}://{user}:{password}@{host}:{port}/{project_id}/{msghook}", + ) - # Define our template arguments + # Define our template tokens template_tokens = dict( NotifyBase.template_tokens, **{ + # Self-hosted connection fields + "host": { + "name": _("Hostname"), + "type": "string", + }, + "port": { + "name": _("Port"), + "type": "int", + "min": 1, + "max": 65535, + }, + "user": { + "name": _("Username"), + "type": "string", + }, + "password": { + "name": _("Password"), + "type": "string", + "private": True, + }, # The Project ID is found as the first part of the URL # /1234/........................ "project_id": { @@ -143,28 +206,43 @@ class NotifyNotifico(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - # You can optionally pass IRC colors into + # You can optionally pass IRC colors into the notification "color": { "name": _("IRC Colors"), "type": "bool", "default": True, }, - # You can optionally pass IRC color into + # You can optionally include a notification-type prefix "prefix": { "name": _("Prefix"), "type": "bool", "default": True, }, + # Backward-compatible query-string aliases + "token": { + "alias_of": "msghook", + }, + "project": { + "alias_of": "project_id", + }, }, ) - def __init__(self, project_id, msghook, color=True, prefix=True, **kwargs): + def __init__( + self, + project_id, + msghook, + color=True, + prefix=True, + **kwargs, + ): """Initialize Notifico Object.""" super().__init__(**kwargs) - # Assign our message hook + # Assign our project id self.project_id = validate_regex( - project_id, *self.template_tokens["project_id"]["regex"] + project_id, + *self.template_tokens["project_id"]["regex"], ) if not self.project_id: msg = ( @@ -175,7 +253,8 @@ class NotifyNotifico(NotifyBase): # Assign our message hook self.msghook = validate_regex( - msghook, *self.template_tokens["msghook"]["regex"] + msghook, + *self.template_tokens["msghook"]["regex"], ) if not self.msghook: msg = ( @@ -184,18 +263,18 @@ class NotifyNotifico(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # Detect operating mode: self-hosted when a hostname is present + self.mode = ( + NotificoMode.SELFHOSTED if self.host else NotificoMode.OFFICIAL + ) + # Prefix messages with a [?] where ? identifies the message type # such as if it's an error, warning, info, or success self.prefix = prefix - # Send colors + # Send IRC colors self.color = color - # Prepare our notification URL now: - self.api_url = self.notify_url.format( - proj=self.project_id, - hook=self.msghook, - ) return @property @@ -205,7 +284,16 @@ class NotifyNotifico(NotifyBase): Targets or end points should never be identified here. """ - return (self.secure_protocol, self.project_id, self.msghook) + return ( + self.secure_protocol if self.secure else self.protocol, + self.mode, + # host and port distinguish self-hosted instances; + # both are None for the official endpoint + self.host, + self.port, + self.project_id, + self.msghook, + ) def url(self, privacy=False, *args, **kwargs): """Returns the URL built dynamically based on specified arguments.""" @@ -219,23 +307,62 @@ class NotifyNotifico(NotifyBase): # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - return "{schema}://{proj}/{hook}/?{params}".format( - schema=self.secure_protocol, + if self.mode == NotificoMode.OFFICIAL: + # Official mode: project_id lives in the host position + return "{schema}://{proj}/{hook}/?{params}".format( + schema=self.protocol, + proj=self.pprint(self.project_id, privacy, safe=""), + hook=self.pprint(self.msghook, privacy, safe=""), + params=NotifyNotifico.urlencode(params), + ) + + # Self-hosted mode: include hostname, optional port and credentials + auth = "" + + # Determine Authentication + if self.user and self.password: + auth = "{user}:{password}@".format( + user=NotifyNotifico.quote(self.user, safe=""), + password=self.pprint( + self.password, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + ) + elif self.user: + auth = "{user}@".format( + user=NotifyNotifico.quote(self.user, safe=""), + ) + + default_port = 443 if self.secure else 80 + + return "{schema}://{auth}{host}{port}/{proj}/{hook}/?{params}".format( + schema=(self.secure_protocol if self.secure else self.protocol), + auth=auth, + host=self.host, + port=( + "" + if self.port is None or self.port == default_port + else f":{self.port}" + ), proj=self.pprint(self.project_id, privacy, safe=""), hook=self.pprint(self.msghook, privacy, safe=""), params=NotifyNotifico.urlencode(params), ) def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): - """Wrapper to _send since we can alert more then one channel.""" + """Perform Notifico Notification.""" - # prepare our headers + # Prepare our headers headers = { "User-Agent": self.app_id, - "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "Content-Type": ( + "application/x-www-form-urlencoded; charset=utf-8" + ), } - # Prepare our IRC Prefix + # Prepare our IRC color and type prefix token color = "" token = "" if notify_type == NotifyType.INFO: @@ -255,14 +382,12 @@ class NotifyNotifico(NotifyBase): token = "✗" if self.color: - # Colors were specified, make sure we capture and correctly - # allow them to exist inline in the message + # Colors were specified; allow IRC color codes to pass through # \g<1> is less ambiguous than \1 body = re.sub(r"\\x03(\d{0,2})", r"\\x03\g<1>", body) else: - # no colors specified, make sure we strip out any colors found - # to make the string read-able + # No colors; strip any IRC color codes to keep the text readable body = re.sub(r"\\x03(\d{1,2}(,[0-9]{1,2})?)?", r"", body) # Prepare our payload @@ -279,7 +404,7 @@ class NotifyNotifico(NotifyBase): NotificoFormat.Bold if self.color else "", self.app_id, NotificoFormat.Reset if self.color else "", - # Message Body + # Message body body, # Reset NotificoFormat.Reset if self.color else "", @@ -287,9 +412,27 @@ class NotifyNotifico(NotifyBase): ), } + # Build the notify URL + if self.mode == NotificoMode.OFFICIAL: + # Always use the official HTTPS endpoint + notify_url = self.notify_url.format( + proj=self.project_id, + hook=self.msghook, + ) + auth = None + + else: + # Self-hosted: construct URL from host/port + schema = "https" if self.secure else "http" + notify_url = f"{schema}://{self.host}" + if isinstance(self.port, int): + notify_url += f":{self.port}" + notify_url += f"/h/{self.project_id}/{self.msghook}" + auth = (self.user, self.password or "") if self.user else None + self.logger.debug( - "Notifico GET URL:" - f" {self.api_url} (cert_verify={self.verify_certificate!r})" + f"Notifico GET URL: {notify_url} " + f"(cert_verify={self.verify_certificate!r})" ) self.logger.debug(f"Notifico Payload: {payload!s}") @@ -298,11 +441,13 @@ class NotifyNotifico(NotifyBase): try: r = requests.get( - self.api_url, + notify_url, + auth=auth, params=payload, headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -313,12 +458,15 @@ class NotifyNotifico(NotifyBase): self.logger.warning( "Failed to send Notifico notification: " "{}{}error={}.".format( - status_str, ", " if status_str else "", r.status_code + status_str, + ", " if status_str else "", + r.status_code, ) ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -339,30 +487,53 @@ class NotifyNotifico(NotifyBase): @staticmethod def parse_url(url): - """Parses the URL and returns enough arguments that can allow us to re- - instantiate this object.""" + """Parses the URL and returns enough arguments that can allow us to + re-instantiate this object.""" results = NotifyBase.parse_url(url, verify_host=False) if not results: # We're done early as we couldn't load the results return results - # The first token is stored in the hostname - results["project_id"] = NotifyNotifico.unquote(results["host"]) + # Retrieve path entries for project_id / msghook extraction + entries = NotifyNotifico.split_path(results["fullpath"]) - # Get Message Hook - try: - results["msghook"] = NotifyNotifico.split_path( - results["fullpath"] - )[0] + # Unquote the host field + host = NotifyNotifico.unquote(results.get("host", "") or "") - except IndexError: - results["msghook"] = None + if re.match(r"^[0-9]+$", host): + # Official mode: numeric host value is the project_id + results["project_id"] = host + # Clear all connection fields -- official mode always routes + # to the hardcoded n.tkte.ch endpoint + results["host"] = None + results["port"] = None + results["user"] = None + results["password"] = None + # First path segment is the message hook + results["msghook"] = entries[0] if entries else None - # Include Color + else: + # Self-hosted mode: host is the server; path carries the IDs + results["project_id"] = entries[0] if entries else None + results["msghook"] = entries[1] if len(entries) > 1 else None + + # Support ?token= as an alias for msghook + if "token" in results["qsd"] and results["qsd"]["token"]: + results["msghook"] = NotifyNotifico.unquote( + results["qsd"]["token"] + ) + + # Support ?project= as an alias for project_id + if "project" in results["qsd"] and results["qsd"]["project"]: + results["project_id"] = NotifyNotifico.unquote( + results["qsd"]["project"] + ) + + # Include Color option results["color"] = parse_bool(results["qsd"].get("color", True)) - # Include Prefix + # Include Prefix option results["prefix"] = parse_bool(results["qsd"].get("prefix", True)) return results @@ -385,7 +556,7 @@ class NotifyNotifico(NotifyBase): if result: return NotifyNotifico.parse_url( "{schema}://{proj}/{hook}/{params}".format( - schema=NotifyNotifico.secure_protocol, + schema=NotifyNotifico.protocol, proj=result.group("proj"), hook=result.group("hook"), params=( diff --git a/libs/apprise/plugins/ntfy.py b/libs/apprise/plugins/ntfy.py index 8be5883a5..e87677887 100644 --- a/libs/apprise/plugins/ntfy.py +++ b/libs/apprise/plugins/ntfy.py @@ -284,7 +284,7 @@ class NotifyNtfy(NotifyBase): "values": NTFY_PRIORITIES, "default": NtfyPriority.NORMAL, }, - "tags": { + "xtags": { "name": _("Tags"), "type": "string", }, @@ -322,7 +322,7 @@ class NotifyNtfy(NotifyBase): delay=None, email=None, priority=None, - tags=None, + xtags=None, actions=None, mode=None, include_image=True, @@ -405,7 +405,7 @@ class NotifyNtfy(NotifyBase): ) # Any optional tags to attach to the notification - self.__tags = parse_list(tags) + self.__tags = parse_list(xtags) # Action buttons self.__actions = actions @@ -467,7 +467,6 @@ class NotifyNtfy(NotifyBase): # We need to upload our payload first so that we can source it # in remaining messages for no, attachment in enumerate(attach): - # First message only includes the text (if defined) body_ = body if not no and body else None title_ = title if not no and title else None @@ -661,6 +660,7 @@ class NotifyNtfy(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -698,7 +698,8 @@ class NotifyNtfy(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False, response @@ -750,10 +751,12 @@ class NotifyNtfy(NotifyBase): if self.mode == NtfyMode.PRIVATE: if self.auth == NtfyAuth.BASIC: - kwargs.extend([ - self.user if self.user else None, - self.password if self.password else None, - ]) + kwargs.extend( + [ + self.user if self.user else None, + self.password if self.password else None, + ] + ) elif self.token: # NtfyAuth.TOKEN also kwargs.append(self.token) @@ -788,7 +791,7 @@ class NotifyNtfy(NotifyBase): params["email"] = self.email if self.__tags: - params["tags"] = ",".join(self.__tags) + params["xtags"] = ",".join(self.__tags) if self.__actions: params["actions"] = self.__actions @@ -888,10 +891,14 @@ class NotifyNtfy(NotifyBase): if "email" in results["qsd"] and len(results["qsd"]["email"]): results["email"] = NotifyNtfy.unquote(results["qsd"]["email"]) - if "tags" in results["qsd"] and len(results["qsd"]["tags"]): - results["tags"] = parse_list( - NotifyNtfy.unquote(results["qsd"]["tags"]) - ) + # Support both 'xtags' (canonical) and legacy 'tags'. + # Storing as 'xtags' prevents the config/base parser from + # misinterpreting this as an apprise-level tag filter. + raw_xtags = ( + results["qsd"].get("xtags") or results["qsd"].get("tags") or "" + ) + if raw_xtags: + results["xtags"] = parse_list(NotifyNtfy.unquote(raw_xtags)) if "actions" in results["qsd"] and len(results["qsd"]["actions"]): results["actions"] = NotifyNtfy.unquote(results["qsd"]["actions"]) diff --git a/libs/apprise/plugins/octopush.py b/libs/apprise/plugins/octopush.py new file mode 100644 index 000000000..e7ed68b1f --- /dev/null +++ b/libs/apprise/plugins/octopush.py @@ -0,0 +1,522 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# To use this plugin, simply signup with Octopush: +# https://octopush.com/ +# +# The API reference used to build this plugin was documented here: +# https://dev.octopush.com/en/sms-gateway-api-documentation/send-sms/ + +from json import dumps + +import requests + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from ..url import PrivacyMode +from ..utils.parse import ( + is_email, + is_phone_no, + parse_bool, + parse_phone_no, + validate_regex, +) +from .base import NotifyBase + + +class OctopushType: + """Octopush message types.""" + + PREMIUM = "sms_premium" + LOW_COST = "sms_low_cost" + + +OCTOPUSH_TYPE_MAP = { + # Maps against string 'sms_premium' + "p": OctopushType.PREMIUM, + "sms_p": OctopushType.PREMIUM, + "smsp": OctopushType.PREMIUM, + "+": OctopushType.PREMIUM, + # Maps against string 'sms_low_cost' + "l": OctopushType.LOW_COST, + "sms_l": OctopushType.LOW_COST, + "smsl": OctopushType.LOW_COST, + "-": OctopushType.LOW_COST, +} + +OCTOPUSH_TYPES = ( + OctopushType.PREMIUM, + OctopushType.LOW_COST, +) + + +class OctopushPurpose: + """Octopush purposes.""" + + ALERT = "alert" + WHOLESALE = "wholesale" + + +OCTOPUSH_PURPOSES = ( + OctopushPurpose.ALERT, + OctopushPurpose.WHOLESALE, +) + + +class NotifyOctopush(NotifyBase): + """A wrapper for Octopush Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "Octopush" + + # The services URL + service_url = "https://octopush.com/" + + # The default secure protocol + secure_protocol = "octopush" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/octopush/" + + # Notification URLs + notify_url = "https://api.octopush.com/v1/public/sms-campaign/send" + + # The maximum length of the body + body_maxlen = 1224 + + # The maximum amount of phone numbers that can reside within a single + # batch/frame transfer + default_batch_size = 500 + + # A title can not be used for SMS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Define object templates + templates = ( + "{schema}://{api_login}/{api_key}/{targets}", + "{schema}://{sender}:{api_login}/{api_key}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "api_login": { + "name": _("API Login"), + "type": "string", + "private": True, + "required": True, + }, + "api_key": { + "name": _("API Key"), + "type": "string", + "private": True, + "required": True, + }, + "sender": { + "name": _("Sender"), + "type": "string", + }, + "target_phone": { + "name": _("Target Phone No"), + "type": "string", + "prefix": "+", + "regex": (r"^[0-9\s)(+-]+$", "i"), + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "to": { + "alias_of": "targets", + }, + "login": { + "alias_of": "api_login", + }, + "key": { + "alias_of": "api_key", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, + "replies": { + "name": _("Accept Replies"), + "type": "bool", + "default": False, + }, + "purpose": { + "name": _("Purpose"), + "type": "choice:string", + "values": OCTOPUSH_PURPOSES, + "default": OctopushPurpose.ALERT, + }, + "type": { + "name": _("Type"), + "type": "choice:string", + "values": OCTOPUSH_TYPES, + "default": OctopushType.PREMIUM, + "map_to": "mtype", + }, + }, + ) + + def __init__( + self, + api_login, + api_key, + targets=None, + batch=False, + sender=None, + purpose=None, + mtype=None, + replies=False, + **kwargs, + ): + """Initialize Octopush Object.""" + super().__init__(**kwargs) + + # Store our API Login + self.api_login = validate_regex(api_login) + if not self.api_login or not is_email(self.api_login): + msg = "An invalid Octopush API Login ({}) was specified.".format( + api_login + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store our API Key + self.api_key = validate_regex(api_key) + if not self.api_key: + msg = "An invalid Octopush API Key ({}) was specified.".format( + api_key + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Prepare Batch Mode Flag + self.batch = batch + + # Prepare Replies Mode Flag + self.replies = replies + + # The type of the message + if mtype is None: + self.mtype = self.template_args["type"]["default"] + else: + mtype = str(mtype).lower().strip() + self.mtype = ( + next( + ( + value + for key, value in OCTOPUSH_TYPE_MAP.items() + if mtype.startswith(key) + ), + None, + ) + if mtype + else None + ) + + if self.mtype is None: + msg = "The Octopush type specified ({}) is invalid.".format(mtype) + self.logger.warning(msg) + raise TypeError(msg) + + self.purpose = ( + self.template_args["purpose"]["default"] + if purpose is None + else validate_regex(purpose) + ) + if not self.purpose: + msg = "The Octopush purpose specified ({}) is invalid.".format( + purpose + ) + self.logger.warning(msg) + raise TypeError(msg) + + self.purpose = self.purpose.lower() + if self.purpose not in OCTOPUSH_PURPOSES: + msg = "The Octopush purpose specified ({}) is invalid.".format( + purpose + ) + self.logger.warning(msg) + raise TypeError(msg) + + self.sender = None + if sender: + self.sender = validate_regex(sender) + if not self.sender: + msg = "An invalid Octopush sender ({}) was specified.".format( + sender + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Initialize numbers list + self.targets = [] + + # Validate targets and drop bad ones: + for target in parse_phone_no(targets): + result = is_phone_no(target) + if result: + # Store valid phone number in E.164 format + self.targets.append("+{}".format(result["full"])) + continue + + self.logger.warning( + "Dropped invalid phone ({}) specified.".format(target), + ) + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform Octopush Notification.""" + if not self.targets: + self.logger.warning("No Octopush targets to notify.") + return False + + # error tracking (used for function return) + has_error = False + + # Send in batches if identified to do so + batch_size = 1 if not self.batch else self.default_batch_size + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Accept": "application/json", + "Content-Type": "application/json; charset=utf-8", + "api-key": self.api_key, + "api-login": self.api_login, + "cache-control": "no-cache", + } + + for index in range(0, len(self.targets), batch_size): + recipients = [ + {"phone_number": phone_no} + for phone_no in self.targets[index : index + batch_size] + ] + + payload = { + "recipients": recipients, + "text": body, + "type": self.mtype, + "purpose": self.purpose, + "sender": self.sender if self.sender else self.app_id, + "with_replies": self.replies, + } + + p_targets = self.targets[index : index + batch_size] + verbose_dest = ( + ", ".join(p_targets) + if len(p_targets) <= 3 + else "{} recipients".format(len(p_targets)) + ) + + # Always call throttle before any remote server i/o is made + self.throttle() + + self.logger.debug( + "Octopush POST URL: {} (cert_verify={})".format( + self.notify_url, self.verify_certificate + ) + ) + self.logger.debug("Octopush Payload: {}".format(payload)) + + try: + r = requests.post( + self.notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.created: + status_str = NotifyOctopush.http_response_code_lookup( + r.status_code + ) + + self.logger.warning( + "Failed to send Octopush notification to {}: " + "{}{}error={}.".format( + verbose_dest, + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + + self.logger.debug( + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) + + has_error = True + continue + + self.logger.info( + "Sent Octopush notification to {}.".format(verbose_dest) + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Octopush:%s " + "notification.", + verbose_dest, + ) + self.logger.debug("Socket Exception: %s", str(e)) + + has_error = True + continue + + return not has_error + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique.""" + return ( + self.secure_protocol, + self.api_login, + self.api_key, + self.sender, + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + params = { + "batch": "yes" if self.batch else "no", + "replies": "yes" if self.replies else "no", + "type": self.mtype, + "purpose": self.purpose, + } + + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + return "{schema}://{sender}{api_login}/{api_key}/{targets}?{params}".format( + schema=self.secure_protocol, + sender="{}:".format(NotifyOctopush.quote(self.sender, safe="")) + if self.sender + else "", + api_login=self.pprint(self.api_login, privacy, safe="@"), + api_key=self.pprint( + self.api_key, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + targets="/".join( + [NotifyOctopush.quote(x, safe="+") for x in self.targets] + ), + params=NotifyOctopush.urlencode(params), + ) + + def __len__(self): + """Returns the number of targets associated with this notification.""" + batch_size = 1 if not self.batch else self.default_batch_size + targets = len(self.targets) + if batch_size > 1: + targets = int(targets / batch_size) + ( + 1 if targets % batch_size else 0 + ) + + return targets + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + return results + + tokens = NotifyOctopush.split_path(results["fullpath"]) + + if "key" in results["qsd"] and len(results["qsd"]["key"]): + results["api_key"] = NotifyOctopush.unquote(results["qsd"]["key"]) + elif tokens: + results["api_key"] = NotifyOctopush.unquote(tokens.pop(0)) + + # The remaining elements are the phone numbers we want to contact + results["targets"] = tokens + if "to" in results["qsd"] and len(results["qsd"]["to"]): + results["targets"] += NotifyOctopush.parse_phone_no( + results["qsd"]["to"] + ) + + if "login" in results["qsd"] and len(results["qsd"]["login"]): + results["api_login"] = NotifyOctopush.unquote( + results["qsd"]["login"] + ) + elif results["user"] or results["password"]: + results["api_login"] = "{}@{}".format( + NotifyOctopush.unquote(results["user"]) + if not results["password"] + else NotifyOctopush.unquote(results["password"]), + NotifyOctopush.unquote(results["host"]), + ) + + results["batch"] = parse_bool( + results["qsd"].get( + "batch", NotifyOctopush.template_args["batch"]["default"] + ) + ) + results["replies"] = parse_bool( + results["qsd"].get( + "replies", + NotifyOctopush.template_args["replies"]["default"], + ) + ) + + if "type" in results["qsd"] and len(results["qsd"]["type"]): + results["mtype"] = NotifyOctopush.unquote(results["qsd"]["type"]) + + if "purpose" in results["qsd"] and len(results["qsd"]["purpose"]): + results["purpose"] = NotifyOctopush.unquote( + results["qsd"]["purpose"] + ) + + if "sender" in results["qsd"] and len(results["qsd"]["sender"]): + results["sender"] = NotifyOctopush.unquote( + results["qsd"]["sender"] + ) + elif results["user"] and results["password"]: + results["sender"] = NotifyOctopush.unquote(results["user"]) + + return results diff --git a/libs/apprise/plugins/office365.py b/libs/apprise/plugins/office365.py index 32ce5f424..9a4f40e4b 100644 --- a/libs/apprise/plugins/office365.py +++ b/libs/apprise/plugins/office365.py @@ -33,11 +33,17 @@ # https://docs.microsoft.com/en-us/graph/api/user-sendmail\ # ?view=graph-rest-1.0&tabs=http # -# Note: One must set up Application Permissions (not Delegated Permissions) +# Org mode uses Application Permissions (not Delegated Permissions): # - Scopes required: Mail.Send # - For Large Attachments: Mail.ReadWrite # - For Email Lookups: User.Read.All # +# Personal mode uses Delegated Permissions for consumer accounts +# (@live.com, @hotmail.com, @outlook.com, etc.): +# - Scopes required: Mail.Send offline_access +# - Requires a pre-obtained refresh_token (via device code flow) +# - See the plugin documentation for setup instructions +# from datetime import datetime, timedelta import json import logging @@ -49,11 +55,131 @@ from .. import exception from ..common import NotifyFormat, NotifyType, PersistentStoreMode from ..locale import gettext_lazy as _ from ..url import PrivacyMode -from ..utils.parse import is_email, parse_emails, validate_regex +from ..utils.parse import is_email, parse_bool, parse_emails, validate_regex from ..utils.sanitize import sanitize_payload from .base import NotifyBase +class Office365Mode: + """Operating mode for the Office 365 plugin.""" + + # Organizational (app-only / daemon): client_credentials grant. + # Requires a Microsoft Entra ID tenant admin. + ORG = "org" + + # Personal consumer accounts: delegated refresh_token grant to + # the /consumers endpoint. Works with @live.com, @hotmail.com, + # @outlook.com, and other Microsoft consumer domains. + PERSONAL = "personal" + + +OFFICE365_MODES = ( + Office365Mode.ORG, + Office365Mode.PERSONAL, +) + +# Microsoft personal (consumer) account domains. Sources whose domain +# matches an entry here are automatically routed through the personal +# refresh_token OAuth flow rather than the organizational +# client_credentials flow. Use mode= to override auto-detection. +OFFICE365_PERSONAL_DOMAINS = frozenset( + ( + # outlook.com and regional variants + "outlook.com", + "outlook.at", + "outlook.be", + "outlook.ca", + "outlook.cl", + "outlook.co.nz", + "outlook.co.uk", + "outlook.com.ar", + "outlook.com.au", + "outlook.com.br", + "outlook.com.mx", + "outlook.cz", + "outlook.de", + "outlook.dk", + "outlook.es", + "outlook.fi", + "outlook.fr", + "outlook.hu", + "outlook.ie", + "outlook.in", + "outlook.it", + "outlook.jp", + "outlook.kr", + "outlook.lv", + "outlook.my", + "outlook.nl", + "outlook.ph", + "outlook.pt", + "outlook.rs", + "outlook.sa", + "outlook.sg", + "outlook.sk", + # hotmail.com and regional variants + "hotmail.com", + "hotmail.at", + "hotmail.be", + "hotmail.ca", + "hotmail.cl", + "hotmail.co.uk", + "hotmail.com.ar", + "hotmail.com.au", + "hotmail.com.br", + "hotmail.com.mx", + "hotmail.cz", + "hotmail.de", + "hotmail.dk", + "hotmail.es", + "hotmail.fi", + "hotmail.fr", + "hotmail.gr", + "hotmail.hu", + "hotmail.ie", + "hotmail.it", + "hotmail.nl", + "hotmail.no", + "hotmail.pt", + "hotmail.rs", + "hotmail.se", + # live.com and regional variants + "live.at", + "live.be", + "live.ca", + "live.cl", + "live.co.nz", + "live.co.uk", + "live.com", + "live.com.ar", + "live.com.au", + "live.com.mx", + "live.de", + "live.dk", + "live.es", + "live.fi", + "live.fr", + "live.gr", + "live.hu", + "live.ie", + "live.in", + "live.it", + "live.jp", + "live.mx", + "live.my", + "live.nl", + "live.no", + "live.ph", + "live.pt", + "live.rs", + "live.se", + "live.sg", + # other Microsoft consumer domains + "msn.com", + ) +) + + class NotifyOffice365(NotifyBase): """A wrapper for Office 365 Notifications.""" @@ -76,9 +202,19 @@ class NotifyOffice365(NotifyBase): # URL to Microsoft Graph Server graph_url = "https://graph.microsoft.com" - # Authentication URL + # Org mode authentication URL (client_credentials grant) auth_url = "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" + # Personal mode authentication URL (refresh_token grant). + # /consumers services MSA (personal Microsoft accounts) only. + personal_auth_url = ( + "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" + ) + + # Delegated scopes for personal mode. + # offline_access is required to receive a rotating refresh_token. + personal_scope = "https://graph.microsoft.com/Mail.Send offline_access" + # Support attachments attachment_support = True @@ -93,9 +229,9 @@ class NotifyOffice365(NotifyBase): # Currently (as of 2025.10.06) this was documented to be 3MB outlook_attachment_inline_max = 3145728 - # Use all the direct application permissions you have configured for your - # app. The endpoint should issue a token for the ones associated with the - # resource you want to use. + # Use all the direct application permissions you have configured for + # your app. The endpoint should issue a token for the ones associated + # with the resource you want to use. # see https://docs.microsoft.com/en-us/azure/active-directory/develop/\ # v2-permissions-and-consent#the-default-scope scope = ".default" @@ -105,9 +241,12 @@ class NotifyOffice365(NotifyBase): # Define object templates templates = ( - # Send as user (only supported method) + # Org mode (app-only / daemon auth): tenant required "{schema}://{source}/{tenant}/{client_id}/{secret}", "{schema}://{source}/{tenant}/{client_id}/{secret}/{targets}", + # Personal mode (delegated refresh_token auth): no tenant + "{schema}://{source}/{client_id}/{secret}", + "{schema}://{source}/{client_id}/{secret}/{targets}", ) # Define our template tokens @@ -155,6 +294,12 @@ class NotifyOffice365(NotifyBase): template_args = dict( NotifyBase.template_args, **{ + "oauth_id": { + "alias_of": "client_id", + }, + "oauth_secret": { + "alias_of": "secret", + }, "to": { "alias_of": "targets", }, @@ -166,37 +311,57 @@ class NotifyOffice365(NotifyBase): "name": _("Blind Carbon Copy"), "type": "list:string", }, - "oauth_id": { - "alias_of": "client_id", + "reply_to": { + "name": _("Reply To"), + "type": "list:string", }, - "oauth_secret": { - "alias_of": "secret", + "mode": { + "name": _("Mode"), + "type": "choice:string", + "values": OFFICE365_MODES, + "default": Office365Mode.ORG, + }, + "savesent": { + "name": _("Save to Sent Items"), + "type": "bool", + "default": True, }, }, ) def __init__( self, - tenant, - client_id, - secret, + tenant=None, + client_id=None, + secret=None, source=None, targets=None, cc=None, bcc=None, + reply_to=None, + mode=None, + savesent=None, **kwargs, ): """Initialize Office 365 Object.""" super().__init__(**kwargs) - # Tenant identifier - self.tenant = validate_regex( - tenant, *self.template_tokens["tenant"]["regex"] - ) - if not self.tenant: - msg = f"An invalid Office 365 Tenant({tenant}) was specified." - self.logger.warning(msg) - raise TypeError(msg) + # Resolve mode — explicit parameter wins, then auto-detect from domain + if mode and isinstance(mode, str): + _mode = mode.lower() + if _mode not in OFFICE365_MODES: + msg = f"The Office 365 mode specified ({mode}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) + self.mode = _mode + else: + _src = is_email(source) if source else None + self.mode = ( + Office365Mode.PERSONAL + if _src + and _src["domain"].lower() in OFFICE365_PERSONAL_DOMAINS + else Office365Mode.ORG + ) # Store our email/ObjectID Source self.source = source @@ -213,7 +378,7 @@ class NotifyOffice365(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - # Client Secret (associated with generated OAuth2 Login) + # Client Secret (org) or seed Refresh Token (personal) self.secret = validate_regex(secret) if not self.secret: msg = ( @@ -223,6 +388,27 @@ class NotifyOffice365(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + if self.mode == Office365Mode.ORG: + # Tenant identifier — required for org mode + self.tenant = validate_regex( + tenant, *self.template_tokens["tenant"]["regex"] + ) + if not self.tenant: + msg = f"An invalid Office 365 Tenant ({tenant}) was specified." + self.logger.warning(msg) + raise TypeError(msg) + + else: + # Personal mode requires no tenant + self.tenant = None + + # Define whether or not we should operate in batch mode + self.save_sent = ( + self.template_args["savesent"]["default"] + if savesent is None + else bool(savesent) + ) + # For tracking our email -> name lookups self.names = {} @@ -241,10 +427,12 @@ class NotifyOffice365(NotifyBase): result = is_email(recipient) if result: # Add our email to our target list - self.targets.append(( - result["name"] if result["name"] else False, - result["full_email"], - )) + self.targets.append( + ( + result["name"] if result["name"] else False, + result["full_email"], + ) + ) continue self.logger.warning( @@ -257,8 +445,8 @@ class NotifyOffice365(NotifyBase): self.logger.warning("No Target Office 365 Email Detected") else: - # If our target email list is empty we want to add ourselves to - # it + # If our target email list is empty we want to add ourselves + # to it self.targets.append((False, self.source)) # Validate recipients (cc:) and drop bad ones: @@ -294,23 +482,65 @@ class NotifyOffice365(NotifyBase): f"({recipient}) specified.", ) - # Our token is acquired upon a successful login + # Acquire Reply-To addresses + self.reply_to = set() + for recipient in parse_emails(reply_to): + email = is_email(recipient) + if email: + self.reply_to.add(email["full_email"]) + + # Index our name (if one exists) + self.names[email["full_email"]] = ( + email["name"] if email["name"] else False + ) + continue + + self.logger.warning( + f"Dropped invalid Reply-To email ({recipient}) specified.", + ) + + # Our access token is acquired upon a successful authentication self.token = None # Presume that our token has expired 'now' self.token_expiry = datetime.now() - # Our email source; we detect this if the source is an ObjectID - # If it is unknown we set this to None - # User is the email associated with the account - self.from_email = self.store.get("from") + # Set up sender identity. + # Personal mode: source must be an email — resolved directly. + # Org mode: source may be an Object ID — resolved via API on first + # send when necessary. result = is_email(self.source) - if result: + + if self.mode == Office365Mode.PERSONAL: + if not result: + msg = ( + "A valid source email address is required for personal " + f"mode; got ({self.source})." + ) + self.logger.warning(msg) + raise TypeError(msg) + self.from_email = result["full_email"] - self.from_name = result["name"] or self.store.get("name") + self.from_name = result["name"] or None + self.source_is_object_id = False else: - self.from_name = self.store.get("name") + # Org mode: use cached from_email; fall back to API lookup + self.from_email = self.store.get("from") + if result: + self.from_email = result["full_email"] + self.from_name = result["name"] or self.store.get("name") + else: + self.from_name = self.store.get("name") + + self.source_is_object_id = bool( + not result + and validate_regex( + self.source, + r"^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$", + "i", + ) + ) return @@ -332,7 +562,9 @@ class NotifyOffice365(NotifyBase): self.logger.warning("There are no Email recipients to notify") return False - if self.from_email is None: + # For org mode, perform from_email lookup when source is an Object ID. + # Personal mode always has from_email set in __init__. + if self.mode == Office365Mode.ORG and self.from_email is None: if not self.authenticate(): # We could not authenticate ourselves; we're done return False @@ -341,6 +573,13 @@ class NotifyOffice365(NotifyBase): url = f"https://graph.microsoft.com/v1.0/users/{self.source}" postokay, response = self._fetch(url=url, method="GET") if not postokay: + if getattr(self, "source_is_object_id", False): + self.logger.warning( + "The specifieg Object ID could not be resolved in " + "this tenant. Verify the Object ID exists in the " + "selected directory, or use a email address instead." + ) + return False self.logger.warning( "Could not acquire From email address; ensure " '"User.Read.All" Application scope is set!' @@ -356,8 +595,8 @@ class NotifyOffice365(NotifyBase): "Could not get From email from the Azure endpoint." ) - # Prevent re-occuring upstream fetches for info that isn't - # there + # Prevent re-occuring upstream fetches for info that + # isn't there self.from_email = False else: @@ -384,28 +623,45 @@ class NotifyOffice365(NotifyBase): }, }, # Below takes a string (not bool) of either 'true' or 'false' - "saveToSentItems": "true", + "saveToSentItems": "true" if self.save_sent else "false", } if self.from_email: # Apply from email if it is known - payload["message"].update({ - "from": { - "emailAddress": { - "address": self.from_email, - "name": self.from_name or self.app_id, - } - }, - }) + payload["message"].update( + { + "from": { + "emailAddress": { + "address": self.from_email, + "name": self.from_name or self.app_id, + } + }, + } + ) + + if self.reply_to: + reply_to_list = [] + for addr in self.reply_to: + payload_ = {"address": addr} + if self.names.get(addr): + payload_["name"] = self.names[addr] + reply_to_list.append({"emailAddress": payload_}) + payload["message"]["replyTo"] = reply_to_list # Create a copy of the email list emails = list(self.targets) # Define our URL to post to - url = f"{self.graph_url}/v1.0/users/{self.source}/sendMail" + if self.mode == Office365Mode.PERSONAL: + url = f"{self.graph_url}/v1.0/me/sendMail" + else: + url = f"{self.graph_url}/v1.0/users/{self.source}/sendMail" # Prepare our Draft URL - draft_url = f"{self.graph_url}/v1.0/users/{self.source}/messages" + if self.mode == Office365Mode.PERSONAL: + draft_url = f"{self.graph_url}/v1.0/me/messages" + else: + draft_url = f"{self.graph_url}/v1.0/users/{self.source}/messages" small_attachments = [] large_attachments = [] @@ -427,31 +683,35 @@ class NotifyOffice365(NotifyBase): if len(attachment) > self.outlook_attachment_inline_max: # Messages larger then xMB need to be uploaded after; a # draft email must be prepared; below is our session - large_attachments.append({ - "obj": attachment, - "name": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - }) + large_attachments.append( + { + "obj": attachment, + "name": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + } + ) continue try: # Prepare our Attachment in Base64 - small_attachments.append({ - "@odata.type": "#microsoft.graph.fileAttachment", - # Name of the attachment (as it should appear in email) - "name": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - # MIME type of the attachment - "contentType": "attachment.mimetype", - # Base64 Content - "contentBytes": attachment.base64(), - }) + small_attachments.append( + { + "@odata.type": "#microsoft.graph.fileAttachment", + # Name of attachment (as it appears in email) + "name": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + # MIME type of the attachment + "contentType": "attachment.mimetype", + # Base64 Content + "contentBytes": attachment.base64(), + } + ) except exception.AppriseException: # We could not access the attachment @@ -519,22 +779,24 @@ class NotifyOffice365(NotifyBase): self.logger.debug( "{}Email Cc: {}".format( "Draft" if large_attachments else "", - ", ".join([ - "{}{}".format( - ( - "" - if self.names.get(e) - else f"{self.names[e]}: " - ), - e, - ) - for e in cc - ]), + ", ".join( + [ + "{}{}".format( + ( + "" + if self.names.get(e) + else f"{self.names[e]}: " + ), + e, + ) + for e in cc + ] + ), ) ) if bcc: - # Prepare our CC list + # Prepare our BCC list payload["message"]["bccRecipients"] = [] for addr in bcc: payload_ = {"address": addr} @@ -549,17 +811,19 @@ class NotifyOffice365(NotifyBase): self.logger.debug( "{}Email Bcc: {}".format( "Draft" if large_attachments else "", - ", ".join([ - "{}{}".format( - ( - "" - if self.names.get(e) - else f"{self.names[e]}: " - ), - e, - ) - for e in bcc - ]), + ", ".join( + [ + "{}{}".format( + ( + "" + if self.names.get(e) + else f"{self.names[e]}: " + ), + e, + ) + for e in bcc + ] + ), ) ) @@ -609,18 +873,19 @@ class NotifyOffice365(NotifyBase): continue # Send off our draft - attach_url = ( - "https://graph.microsoft.com/v1.0/users/" - "{}/messages/{}/send" - ) - - attach_url = attach_url.format( - self.source, - message_id, - ) + if self.mode == Office365Mode.PERSONAL: + attach_url = ( + "https://graph.microsoft.com/v1.0" + f"/me/messages/{message_id}/send" + ) + else: + attach_url = ( + "https://graph.microsoft.com/v1.0/users" + f"/{self.source}/messages/{message_id}/send" + ) # Trigger our send - postokay, response = self._fetch(url=url) + postokay, response = self._fetch(url=attach_url) if not postokay: self.logger.warning( "Could not send drafted email id: {} ", message_id @@ -648,10 +913,17 @@ class NotifyOffice365(NotifyBase): return False # Our Session URL - url = ( - f"{self.graph_url}/v1.0/users/{self.source}/message/{message_id}" - + "/attachments/createUploadSession" - ) + if self.mode == Office365Mode.PERSONAL: + url = ( + f"{self.graph_url}/v1.0/me/message/{message_id}" + "/attachments/createUploadSession" + ) + else: + url = ( + f"{self.graph_url}/v1.0/users/{self.source}" + f"/message/{message_id}" + "/attachments/createUploadSession" + ) file_size = len(attachment) @@ -697,7 +969,9 @@ class NotifyOffice365(NotifyBase): headers = { "User-Agent": self.app_id, "Content-Length": str(len(chunk)), - "Content-Range": f"bytes {start_byte}-{end_byte}/{file_size}", + "Content-Range": ( + f"bytes {start_byte}-{end_byte}/{file_size}" + ), } # Upload the chunk @@ -724,6 +998,12 @@ class NotifyOffice365(NotifyBase): # If we reach here, we've either expired, or we need to authenticate # for the first time. + if self.mode == Office365Mode.PERSONAL: + return self._personal_authenticate() + return self._org_authenticate() + + def _org_authenticate(self): + """Acquires an access token via client_credentials (org mode).""" # Prepare our payload payload = { @@ -770,8 +1050,8 @@ class NotifyOffice365(NotifyBase): self.token = None try: - # Extract our time from our response and subtrace 10 seconds from - # it to give us some wiggle/grace people to re-authenticate if we + # Extract our time from our response and subtract 10 seconds from + # it to give us some wiggle/grace period to re-authenticate if we # need to self.token_expiry = datetime.now() + timedelta( seconds=int(response.get("expires_in")) - 10 @@ -780,8 +1060,7 @@ class NotifyOffice365(NotifyBase): except (ValueError, AttributeError, TypeError): # ValueError: expires_in wasn't an integer # TypeError: expires_in was None - # AttributeError: we could not extract anything from our response - # object. + # AttributeError: we could not extract anything from response return False # Go ahead and store our token if it's available @@ -790,6 +1069,47 @@ class NotifyOffice365(NotifyBase): # We're authenticated return bool(self.token) + def _personal_authenticate(self): + """Acquires an access token via refresh_token (personal mode).""" + + # Prefer the stored (most recently rotated) token over the seed + # value from the URL — ensures we always use the latest credential + refresh_token = self.store.get("refresh_token") or self.secret + + payload = { + "grant_type": "refresh_token", + "client_id": self.client_id, + "refresh_token": refresh_token, + "scope": self.personal_scope, + } + + postokay, response = self._fetch( + url=self.personal_auth_url, + payload=payload, + content_type="application/x-www-form-urlencoded", + ) + if not postokay: + return False + + self.token = None + + try: + self.token_expiry = datetime.now() + timedelta( + seconds=int(response.get("expires_in")) - 10 + ) + + except (ValueError, AttributeError, TypeError): + return False + + # Rotate the refresh token — store the newest one for next send. + # url() will emit this stored token so configs stay current. + new_refresh = response.get("refresh_token") + if new_refresh: + self.store.set("refresh_token", new_refresh) + + self.token = response.get("access_token") + return bool(self.token) + def _fetch( self, url, @@ -816,7 +1136,7 @@ class NotifyOffice365(NotifyBase): # Some Debug Logging if self.logger.isEnabledFor(logging.DEBUG): - # Due to attachments; output can be quite heavy and io intensive + # Due to attachments; output can be quite heavy and io intensive. # To accommodate this, we only show our debug payload information # if required. self.logger.debug( @@ -825,7 +1145,8 @@ class NotifyOffice365(NotifyBase): method, ) self.logger.debug( - "Office 365 Payload: %s", sanitize_payload(payload)) + "Office 365 Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -848,6 +1169,7 @@ class NotifyOffice365(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -855,7 +1177,6 @@ class NotifyOffice365(NotifyBase): requests.codes.created, requests.codes.accepted, ): - # We had a problem status_str = NotifyOffice365.http_response_code_lookup( r.status_code @@ -883,9 +1204,8 @@ class NotifyOffice365(NotifyBase): # }} # } - # Error 403; the below is returned if he User.Read.All - # Application scope is not set and a lookup is - # attempted. + # Error 403; the below is returned if User.Read.All was not + # granted and a user lookup was attempted. # { # "error": { # "code": "Authorization_RequestDenied", @@ -911,7 +1231,8 @@ class NotifyOffice365(NotifyBase): # } self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure return (False, content) @@ -944,6 +1265,13 @@ class NotifyOffice365(NotifyBase): Targets or end points should never be identified here. """ + if self.mode == Office365Mode.PERSONAL: + return ( + self.secure_protocol[0], + self.source, + self.client_id, + self.secret, + ) return ( self.secure_protocol[0], self.source, @@ -955,26 +1283,84 @@ class NotifyOffice365(NotifyBase): def url(self, privacy=False, *args, **kwargs): """Returns the URL built dynamically based on specified arguments.""" + # Define any URL parameters + params = { + "savesent": "yes" if self.save_sent else "no", + } + # Extend our parameters - params = self.url_parameters(privacy=privacy, *args, **kwargs) + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Include mode only for personal — org is the default and omitting + # it keeps existing org URLs clean + if self.mode == Office365Mode.PERSONAL: + params["mode"] = self.mode if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - "{}{}".format( - "" if not self.names.get(e) else f"{self.names[e]}:", e - ) - for e in self.cc - ]) + params["cc"] = ",".join( + [ + "{}{}".format( + "" if not self.names.get(e) else f"{self.names[e]}:", + e, + ) + for e in sorted(self.cc) + ] + ) if self.bcc: # Handle our Blind Carbon Copy Addresses - params["bcc"] = ",".join([ - "{}{}".format( - "" if not self.names.get(e) else f"{self.names[e]}:", e + params["bcc"] = ",".join( + [ + "{}{}".format( + "" if not self.names.get(e) else f"{self.names[e]}:", + e, + ) + for e in sorted(self.bcc) + ] + ) + + if self.reply_to: + params["reply_to"] = ",".join( + [ + "{}{}".format( + "" if not self.names.get(e) else f"{self.names[e]}:", + e, + ) + for e in sorted(self.reply_to) + ] + ) + + targets_str = "/".join( + [ + NotifyOffice365.quote( + "{}{}".format("" if not e[0] else f"{e[0]}:", e[1]), + safe="@", ) - for e in self.bcc - ]) + for e in self.targets + ] + ) + + if self.mode == Office365Mode.PERSONAL: + # Emit the most current refresh token — prefer the stored + # (rotated) value over the original seed so configs stay valid + current_secret = self.store.get("refresh_token") or self.secret + return ( + "{schema}://{source}/{client_id}/{secret}" + "/{targets}/?{params}".format( + schema=self.secure_protocol[0], + source=self.source, + client_id=self.pprint(self.client_id, privacy, safe=""), + secret=self.pprint( + current_secret, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + targets=targets_str, + params=NotifyOffice365.urlencode(params), + ) + ) return ( "{schema}://{source}/{tenant}/{client_id}/{secret}" @@ -988,13 +1374,7 @@ class NotifyOffice365(NotifyBase): secret=self.pprint( self.secret, privacy, mode=PrivacyMode.Secret, safe="" ), - targets="/".join([ - NotifyOffice365.quote( - "{}{}".format("" if not e[0] else f"{e[0]}:", e[1]), - safe="@", - ) - for e in self.targets - ]), + targets=targets_str, params=NotifyOffice365.urlencode(params), ) ) @@ -1005,10 +1385,12 @@ class NotifyOffice365(NotifyBase): @staticmethod def parse_url(url): - """Parses the URL and returns enough arguments that can allow us to re- - instantiate this object.""" + """Parses the URL and returns enough arguments that can allow us to + re-instantiate this object.""" - results = NotifyBase.parse_url(url, verify_host=False) + results = NotifyBase.parse_url( + url, verify_host=False, plus_to_space=True + ) if not results: # We're done early as we couldn't load the results return results @@ -1023,9 +1405,7 @@ class NotifyOffice365(NotifyBase): # Initialize our tenant results["tenant"] = None - # Initialize our email - results["email"] = None - + # Legacy source alias in query-string form # From Email if "from" in results["qsd"] and len(results["qsd"]["from"]): # Extract the sending account's information @@ -1042,17 +1422,35 @@ class NotifyOffice365(NotifyBase): # Object ID instead of email results["source"] = NotifyOffice365.unquote(results["host"]) - # Tenant - if "tenant" in results["qsd"] and len(results["qsd"]["tenant"]): - # Extract the Tenant from the argument - results["tenant"] = NotifyOffice365.unquote( - results["qsd"]["tenant"] + # Detect mode — explicit ?mode= wins, then auto-detect from domain + results["mode"] = None + if "mode" in results["qsd"] and results["qsd"]["mode"]: + _mode = results["qsd"]["mode"].lower() + if _mode in OFFICE365_MODES: + results["mode"] = _mode + + if results["mode"] is None: + _src = is_email(results.get("source", "")) + results["mode"] = ( + Office365Mode.PERSONAL + if _src + and _src["domain"].lower() in OFFICE365_PERSONAL_DOMAINS + else Office365Mode.ORG ) - elif entries: - results["tenant"] = NotifyOffice365.unquote(entries.pop(0)) + if results["mode"] == Office365Mode.ORG: + # Org mode: first path segment is the tenant + # Tenant + if "tenant" in results["qsd"] and len(results["qsd"]["tenant"]): + # Extract the Tenant from the argument + results["tenant"] = NotifyOffice365.unquote( + results["qsd"]["tenant"] + ) - # OAuth2 ID + elif entries: + results["tenant"] = NotifyOffice365.unquote(entries.pop(0)) + + # OAuth2 ID (common to both modes; next path segment after tenant) if "oauth_id" in results["qsd"] and len(results["qsd"]["oauth_id"]): # Extract the API Key from an argument results["client_id"] = NotifyOffice365.unquote( @@ -1060,7 +1458,7 @@ class NotifyOffice365(NotifyBase): ) elif entries: - # Get our client_id is the first entry on the path + # Get our client_id; it is the first entry on the path results["client_id"] = NotifyOffice365.unquote(entries.pop(0)) # @@ -1076,14 +1474,14 @@ class NotifyOffice365(NotifyBase): results["targets"].append(entry) continue - # If we reach here, the entry we just popped is part of the secret - # key, so put it back + # If we reach here, the entry we just popped is part of the + # secret key, so put it back entries.append(NotifyOffice365.quote(entry, safe="")) # We're done break - # OAuth2 Secret + # OAuth2 Secret / Refresh Token if "oauth_secret" in results["qsd"] and len( results["qsd"]["oauth_secret"] ): @@ -1100,8 +1498,8 @@ class NotifyOffice365(NotifyBase): [NotifyOffice365.unquote(x) for x in entries] ) - # Support the 'to' variable so that we can support targets this way too - # The 'to' makes it easier to use yaml configuration + # Support the 'to' variable so that we can support targets this way + # too. The 'to' makes it easier to use yaml configuration if "to" in results["qsd"] and len(results["qsd"]["to"]): results["targets"] += NotifyOffice365.parse_list( results["qsd"]["to"] @@ -1115,4 +1513,15 @@ class NotifyOffice365(NotifyBase): if "bcc" in results["qsd"] and len(results["qsd"]["bcc"]): results["bcc"] = results["qsd"]["bcc"] + # Handle Reply-To Addresses + if "reply_to" in results["qsd"] and len(results["qsd"]["reply_to"]): + results["reply_to"] = results["qsd"]["reply_to"] + + # Get Save Sent Items Flag + results["savesent"] = parse_bool( + results["qsd"].get( + "savesent", + NotifyOffice365.template_args["savesent"]["default"], + ) + ) return results diff --git a/libs/apprise/plugins/one_signal.py b/libs/apprise/plugins/one_signal.py index 5b42aad5b..1c4cca223 100644 --- a/libs/apprise/plugins/one_signal.py +++ b/libs/apprise/plugins/one_signal.py @@ -148,8 +148,17 @@ class NotifyOneSignal(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", + "template": { + "alias_of": "template", + }, + "subtitle": { + "name": _("Subtitle"), + "type": "string", + }, + "language": { + "name": _("Language"), + "type": "string", + "default": "en", }, "image": { "name": _("Include Image"), @@ -157,11 +166,6 @@ class NotifyOneSignal(NotifyBase): "default": True, "map_to": "include_image", }, - "batch": { - "name": _("Batch Mode"), - "type": "bool", - "default": False, - }, "contents": { "name": _("Enable Contents"), "type": "bool", @@ -174,17 +178,13 @@ class NotifyOneSignal(NotifyBase): "default": False, "map_to": "decode_tpl_args", }, - "template": { - "alias_of": "template", + "to": { + "alias_of": "targets", }, - "subtitle": { - "name": _("Subtitle"), - "type": "string", - }, - "language": { - "name": _("Language"), - "type": "string", - "default": "en", + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, }, }, ) @@ -298,7 +298,6 @@ class NotifyOneSignal(NotifyBase): if target.startswith( NotifyOneSignal.template_tokens["target_user"]["prefix"] ): - self.targets[OneSignalCategory.USER].append(target) self.logger.debug( "Detected OneSignal UserID:" @@ -309,7 +308,6 @@ class NotifyOneSignal(NotifyBase): if target.startswith( NotifyOneSignal.template_tokens["target_segment"]["prefix"] ): - self.targets[OneSignalCategory.SEGMENT].append(target) self.logger.debug( "Detected OneSignal Include Segment:" @@ -400,15 +398,19 @@ class NotifyOneSignal(NotifyBase): # Set our data if defined if self.custom_data: - payload.update({ - "custom_data": self.custom_data, - }) + payload.update( + { + "custom_data": self.custom_data, + } + ) # Set our postback data if defined if self.postback_data: - payload.update({ - "data": self.postback_data, - }) + payload.update( + { + "data": self.postback_data, + } + ) if title: # Display our title if defined @@ -421,11 +423,13 @@ class NotifyOneSignal(NotifyBase): ) if self.subtitle: - payload.update({ - "subtitle": { - self.language: self.subtitle, - }, - }) + payload.update( + { + "subtitle": { + self.language: self.subtitle, + }, + } + ) # Acquire our large_icon image URL (if set) image_url = ( @@ -468,6 +472,7 @@ class NotifyOneSignal(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -489,7 +494,8 @@ class NotifyOneSignal(NotifyBase): self.logger.debug( "Response Details:\r\n%r", - (r.content or b"")[:2000]) + (r.content or b"")[:2000], + ) has_error = True diff --git a/libs/apprise/plugins/opsgenie.py b/libs/apprise/plugins/opsgenie.py index 1c10aae64..26adbc695 100644 --- a/libs/apprise/plugins/opsgenie.py +++ b/libs/apprise/plugins/opsgenie.py @@ -50,7 +50,7 @@ from json import dumps, loads import requests -from ..common import NOTIFY_TYPES, NotifyType, PersistentStoreMode +from ..common import NotifyType, PersistentStoreMode from ..locale import gettext_lazy as _ from ..utils.parse import is_uuid, parse_bool, parse_list, validate_regex from .base import NotifyBase @@ -104,14 +104,6 @@ OPSGENIE_ACTIONS = ( OpsgenieAlertAction.NOTE, ) -# Map all support Apprise Categories to Opsgenie Categories -OPSGENIE_ALERT_MAP = { - NotifyType.INFO: OpsgenieAlertAction.CLOSE, - NotifyType.SUCCESS: OpsgenieAlertAction.CLOSE, - NotifyType.WARNING: OpsgenieAlertAction.NEW, - NotifyType.FAILURE: OpsgenieAlertAction.NEW, -} - # Regions class OpsgenieRegion: @@ -210,7 +202,7 @@ class NotifyOpsgenie(NotifyBase): # Defines our default message mapping opsgenie_message_map = { - # Add a note to existing alert + # Add a note to an existing alert NotifyType.INFO: OpsgenieAlertAction.NOTE, # Close existing alert NotifyType.SUCCESS: OpsgenieAlertAction.CLOSE, @@ -284,11 +276,6 @@ class NotifyOpsgenie(NotifyBase): "default": OpsgenieRegion.US, "map_to": "region_name", }, - "batch": { - "name": _("Batch Mode"), - "type": "bool", - "default": False, - }, "priority": { "name": _("Priority"), "type": "choice:int", @@ -307,15 +294,20 @@ class NotifyOpsgenie(NotifyBase): "name": _("Tags"), "type": "string", }, - "to": { - "alias_of": "targets", - }, "action": { "name": _("Action"), "type": "choice:string", "values": OPSGENIE_ACTIONS, "default": OPSGENIE_ACTIONS[0], }, + "to": { + "alias_of": "targets", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, + }, }, ) @@ -349,6 +341,12 @@ class NotifyOpsgenie(NotifyBase): """Initialize Opsgenie Object.""" super().__init__(**kwargs) + # Notify users that this plugin will require them to switch soon + self.logger.deprecate( + "Opsgenie will soon be depricated and moved to Jira; " + "visit https://atlassian.com/ for more details" + ) + # API Key (associated with project) self.apikey = validate_regex(apikey) if not self.apikey: @@ -403,7 +401,7 @@ class NotifyOpsgenie(NotifyBase): if mapping and isinstance(mapping, dict): for k_, v_ in mapping.items(): # Get our mapping - k = next((t for t in NOTIFY_TYPES if t.startswith(k_)), None) + k = next((t for t in NotifyType if t.startswith(k_)), None) if not k: msg = ( f"The Opsgenie mapping key specified ({k_}) " @@ -414,11 +412,7 @@ class NotifyOpsgenie(NotifyBase): v_lower = v_.lower() v = next( - ( - v - for v in OPSGENIE_ACTIONS[1:] - if v.startswith(v_lower) - ), + (v for v in OPSGENIE_ACTIONS[1:] if v.startswith(v_lower)), None, ) if not v: @@ -462,7 +456,6 @@ class NotifyOpsgenie(NotifyBase): if target.startswith( NotifyOpsgenie.template_tokens["target_team"]["prefix"] ): - self.targets.append( {"type": OpsgenieCategory.TEAM, "id": target[1:]} if is_uuid(target[1:]) @@ -472,7 +465,6 @@ class NotifyOpsgenie(NotifyBase): elif target.startswith( NotifyOpsgenie.template_tokens["target_schedule"]["prefix"] ): - self.targets.append( {"type": OpsgenieCategory.SCHEDULE, "id": target[1:]} if is_uuid(target[1:]) @@ -485,7 +477,6 @@ class NotifyOpsgenie(NotifyBase): elif target.startswith( NotifyOpsgenie.template_tokens["target_escalation"]["prefix"] ): - self.targets.append( {"type": OpsgenieCategory.ESCALATION, "id": target[1:]} if is_uuid(target[1:]) @@ -498,7 +489,6 @@ class NotifyOpsgenie(NotifyBase): elif target.startswith( NotifyOpsgenie.template_tokens["target_user"]["prefix"] ): - self.targets.append( {"type": OpsgenieCategory.USER, "id": target[1:]} if is_uuid(target[1:]) @@ -547,6 +537,7 @@ class NotifyOpsgenie(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # A Response might look like: @@ -582,7 +573,8 @@ class NotifyOpsgenie(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return (False, content.get("requestId")) @@ -604,7 +596,7 @@ class NotifyOpsgenie(NotifyBase): # Get our Opsgenie Action action = ( - OPSGENIE_ALERT_MAP[notify_type] + self.mapping[notify_type] if self.action == OpsgenieAlertAction.MAP else self.action ) @@ -658,7 +650,7 @@ class NotifyOpsgenie(NotifyBase): # limitation if len(payload["message"]) > self.opsgenie_body_minlen: payload["message"] = ( - f"{title_body[:self.opsgenie_body_minlen - 3]}..." + f"{title_body[: self.opsgenie_body_minlen - 3]}..." ) if self.__tags: @@ -813,16 +805,18 @@ class NotifyOpsgenie(NotifyBase): schema=self.secure_protocol, user=f"{self.user}@" if self.user else "", apikey=self.pprint(self.apikey, privacy, safe=""), - targets="/".join([ - NotifyOpsgenie.quote( - "{}{}".format( - map_[x["type"]], - x.get("id", x.get("name", x.get("username"))), + targets="/".join( + [ + NotifyOpsgenie.quote( + "{}{}".format( + map_[x["type"]], + x.get("id", x.get("name", x.get("username"))), + ) ) - ) - for x in self.targets - ]), - params=NotifyOpsgenie.urlencode(params), + for x in self.targets + ] + ), + params=NotifyOpsgenie.urlencode(params, safe=":"), ) def __len__(self): diff --git a/libs/apprise/plugins/pagerduty.py b/libs/apprise/plugins/pagerduty.py index 68517f579..ff1693b6a 100644 --- a/libs/apprise/plugins/pagerduty.py +++ b/libs/apprise/plugins/pagerduty.py @@ -362,9 +362,11 @@ class NotifyPagerDuty(NotifyBase): payload["payload"]["class"] = self.class_id if self.click: - payload["links"] = [{ - "href": self.click, - }] + payload["links"] = [ + { + "href": self.click, + } + ] # Acquire our image url if configured to do so image_url = ( @@ -372,10 +374,12 @@ class NotifyPagerDuty(NotifyBase): ) if image_url: - payload["images"] = [{ - "src": image_url, - "alt": notify_type.value, - }] + payload["images"] = [ + { + "src": image_url, + "alt": notify_type.value, + } + ] if self.details: payload["payload"]["custom_details"] = {} @@ -402,6 +406,7 @@ class NotifyPagerDuty(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -421,7 +426,8 @@ class NotifyPagerDuty(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/pagertree.py b/libs/apprise/plugins/pagertree.py index b1527e2ef..339b5ce63 100644 --- a/libs/apprise/plugins/pagertree.py +++ b/libs/apprise/plugins/pagertree.py @@ -275,6 +275,7 @@ class NotifyPagerTree(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -294,7 +295,8 @@ class NotifyPagerTree(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/parseplatform.py b/libs/apprise/plugins/parseplatform.py index 70f370866..22f6d85cc 100644 --- a/libs/apprise/plugins/parseplatform.py +++ b/libs/apprise/plugins/parseplatform.py @@ -236,6 +236,7 @@ class NotifyParsePlatform(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -251,7 +252,8 @@ class NotifyParsePlatform(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/plivo.py b/libs/apprise/plugins/plivo.py index 7967cb5d1..21fc7cb69 100644 --- a/libs/apprise/plugins/plivo.py +++ b/libs/apprise/plugins/plivo.py @@ -121,9 +121,6 @@ class NotifyPlivo(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "source", }, @@ -133,6 +130,9 @@ class NotifyPlivo(NotifyBase): "id": { "alias_of": "auth_id", }, + "to": { + "alias_of": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -176,7 +176,7 @@ class NotifyPlivo(NotifyBase): raise TypeError(msg) # Store our source; enforce E.164 format - self.source = f'+{result["full"]}' + self.source = f"+{result['full']}" # Parse our targets self.targets = [] @@ -187,7 +187,7 @@ class NotifyPlivo(NotifyBase): result = is_phone_no(target) if result: # store valid phone number; enforce E.164 format - self.targets.append(f'+{result["full"]}') + self.targets.append(f"+{result['full']}") continue self.logger.warning( @@ -257,6 +257,7 @@ class NotifyPlivo(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -284,7 +285,8 @@ class NotifyPlivo(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/popcorn_notify.py b/libs/apprise/plugins/popcorn_notify.py index 74cacac8d..1c1b14290 100644 --- a/libs/apprise/plugins/popcorn_notify.py +++ b/libs/apprise/plugins/popcorn_notify.py @@ -197,6 +197,7 @@ class NotifyPopcornNotify(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -220,7 +221,8 @@ class NotifyPopcornNotify(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -241,7 +243,7 @@ class NotifyPopcornNotify(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occured sending" - f" {len(self.targets[index:index + batch_size])} " + f" {len(self.targets[index : index + batch_size])} " "PopcornNotify notification(s)." ) self.logger.debug(f"Socket Exception: {e!s}") diff --git a/libs/apprise/plugins/postmark.py b/libs/apprise/plugins/postmark.py new file mode 100644 index 000000000..f05b7e6b3 --- /dev/null +++ b/libs/apprise/plugins/postmark.py @@ -0,0 +1,642 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Steps to get your Postmark Server API Token: +# 1. Visit https://account.postmarkapp.com/ and sign in. +# 2. Select the server you wish to send from (or create a new one). +# 3. In the server settings, click "API Tokens". +# 4. Copy the Server API Token shown on that page. +# +# Your sender address (From Email) must be a verified sender signature +# or belong to a verified sending domain in Postmark. Visit: +# https://account.postmarkapp.com/signature_domains +# +# Build your Apprise URL as follows: +# postmark://{apikey}:{from_email} +# postmark://{apikey}:{from_email}/{to_email} +# postmark://{apikey}:{from_email}/{to_email1}/{to_email2}/{to_emailN} +# +# Use the optional query parameters to add CC, BCC, Reply-To: +# postmark://{apikey}:{from_email}?bcc=bcc@example.com +# postmark://{apikey}:{from_email}?cc=cc@example.com +# postmark://{apikey}:{from_email}?reply=reply@example.com +# postmark://{apikey}:{from_email}?name=Display+Name +# +# API Reference: +# https://postmarkapp.com/developer/api/email-api + +from email.utils import formataddr +from json import dumps +import logging + +import requests + +from .. import exception +from ..common import NotifyFormat, NotifyType +from ..locale import gettext_lazy as _ +from ..utils.parse import is_email, parse_emails, validate_regex +from ..utils.sanitize import sanitize_payload +from .base import NotifyBase + +# Provide some known codes Postmark uses and what they translate to. +# Reference: https://postmarkapp.com/developer/api/overview#error-codes +POSTMARK_HTTP_ERROR_MAP = { + 401: "Unauthorized - Invalid or missing Server API Token.", + 422: "Unprocessable Entity - Invalid payload or configuration.", + 429: "Too Many Requests - Rate limit exceeded.", + 500: "Internal Server Error.", +} + + +class NotifyPostmark(NotifyBase): + """A wrapper for Postmark Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "Postmark" + + # The services URL + service_url = "https://postmarkapp.com/" + + # The default secure protocol + secure_protocol = "postmark" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/postmark/" + + # Default to HTML + notify_format = NotifyFormat.HTML + + # The Postmark email API endpoint + notify_url = "https://api.postmarkapp.com/email" + + # Support attachments + attachment_support = True + + # Postmark practical send rate is ~50 messages/second. + # 60 / 50 = 1.2 + request_rate_per_sec = 1.2 + + # The default subject to use if one is not specified + default_empty_subject = "" + + # Define object URL templates + templates = ( + "{schema}://{apikey}:{from_email}", + "{schema}://{apikey}:{from_email}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "apikey": { + "name": _("API Key"), + "type": "string", + "private": True, + "required": True, + }, + "from_email": { + "name": _("Source Email"), + "type": "string", + "required": True, + }, + "target_email": { + "name": _("Target Email"), + "type": "string", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "name": { + "name": _("From Name"), + "type": "string", + "map_to": "from_name", + }, + "reply": { + "name": _("Reply To Email"), + "type": "list:string", + "map_to": "reply_to", + }, + "to": { + "alias_of": "targets", + }, + "cc": { + "name": _("Carbon Copy"), + "type": "list:string", + }, + "bcc": { + "name": _("Blind Carbon Copy"), + "type": "list:string", + }, + }, + ) + + def __init__( + self, + apikey, + from_email, + targets=None, + cc=None, + bcc=None, + from_name=None, + reply_to=None, + **kwargs, + ): + """Initialize Notify Postmark Object.""" + super().__init__(**kwargs) + + # API Key (Server Token) + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = f"An invalid Postmark API Key ({apikey}) was specified." + self.logger.warning(msg) + raise TypeError(msg) + + # Validate our from email address + result = is_email(from_email) + if not result: + msg = ( + f"An invalid Postmark From email ({from_email}) was specified." + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store our from address as a (name, email) tuple + self.from_addr = ( + result["name"] if result["name"] is not None else False, + result["full_email"], + ) + + # from_name overrides any name embedded in the from email string + if from_name: + self.from_addr = (from_name, self.from_addr[1]) + + # For tracking email -> display name lookups + self.names = {} + + # Acquire Targets (To Emails) + self.targets = [] + + # Acquire Carbon Copies + self.cc = set() + + # Acquire Blind Carbon Copies + self.bcc = set() + + # Acquire Reply-To addresses + self.reply_to = set() + + # Validate recipients (to:) and drop bad ones: + if targets: + for recipient in parse_emails(targets): + result = is_email(recipient) + if result: + self.targets.append(result["full_email"]) + # Index name if one exists + self.names[result["full_email"]] = ( + result["name"] if result["name"] else False + ) + continue + + self.logger.warning( + "Dropped invalid Postmark To email " + f"({recipient}) specified.", + ) + + else: + # Default to the from address when no targets are specified + self.targets.append(self.from_addr[1]) + + # Validate recipients (cc:) and drop bad ones: + for recipient in parse_emails(cc): + result = is_email(recipient) + if result: + self.cc.add(result["full_email"]) + # Index name if one exists + self.names[result["full_email"]] = ( + result["name"] if result["name"] else False + ) + continue + + self.logger.warning( + "Dropped invalid Postmark Carbon Copy email " + f"({recipient}) specified.", + ) + + # Validate recipients (bcc:) and drop bad ones: + for recipient in parse_emails(bcc): + result = is_email(recipient) + if result: + self.bcc.add(result["full_email"]) + continue + + self.logger.warning( + "Dropped invalid Postmark Blind Carbon Copy " + f"email ({recipient}) specified.", + ) + + # Validate reply-to addresses and drop bad ones: + for recipient in parse_emails(reply_to): + result = is_email(recipient) + if result: + self.reply_to.add(result["full_email"]) + # Index name if one exists + self.names[result["full_email"]] = ( + result["name"] if result["name"] else False + ) + continue + + self.logger.warning( + "Dropped invalid Postmark Reply To email " + f"({recipient}) specified.", + ) + + return + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another simliar one. + + Targets or end points should never be identified here. + """ + return (self.secure_protocol, self.apikey, self.from_addr[1]) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # Our URL parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + # Include from display name if one is set + if self.from_addr[0]: + params["name"] = self.from_addr[0] + + if self.cc: + # Handle our Carbon Copy Addresses + params["cc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + charset="utf-8", + ).replace(",", "%2C") + for e in self.cc + ] + ) + + if self.bcc: + # Handle our Blind Carbon Copy Addresses + params["bcc"] = ",".join(self.bcc) + + if self.reply_to: + # Handle our Reply-To Addresses + params["reply"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + charset="utf-8", + ).replace(",", "%2C") + for e in self.reply_to + ] + ) + + # Determine whether to display target emails in the URL + has_targets = not ( + len(self.targets) == 1 and self.targets[0] == self.from_addr[1] + ) + + return "{schema}://{apikey}:{from_email}/{targets}?{params}".format( + schema=self.secure_protocol, + apikey=self.pprint(self.apikey, privacy, safe=""), + # never encode email since it plays a role in our hostname + from_email=self.from_addr[1], + targets=( + "" + if not has_targets + else "/".join( + [NotifyPostmark.quote(x, safe="@") for x in self.targets] + ) + ), + params=NotifyPostmark.urlencode(params), + ) + + def __len__(self): + """Returns the number of targets associated with this notification.""" + return max(len(self.targets), 1) + + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): + """Perform Postmark Notification.""" + + if not self.targets: + # There is no one to email; we're done + self.logger.warning( + "There are no Postmark email recipients to notify" + ) + return False + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + "Accept": "application/json", + "X-Postmark-Server-Token": self.apikey, + } + + # error tracking (used for function return) + has_error = False + + # Prepare the From field (with optional display name) + from_field = formataddr(self.from_addr, charset="utf-8") + + # Base payload template (shared across all targets) + payload_ = { + "From": from_field, + "Subject": (title if title else self.default_empty_subject), + } + + # Set body content based on the notify format + if self.notify_format == NotifyFormat.HTML: + payload_["HtmlBody"] = body + else: + payload_["TextBody"] = body + + if attach and self.attachment_support: + # Prepare attachment list + attachments = [] + + for no, attachment in enumerate(attach, start=1): + # Perform some simple error checking + if not attachment: + # We could not access the attachment + self.logger.error( + "Could not access Postmark attachment" + f" {attachment.url(privacy=True)}." + ) + return False + + try: + # Append base64-encoded attachment to list + attachments.append( + { + "Name": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + "Content": attachment.base64(), + "ContentType": attachment.mimetype, + } + ) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + "Could not access Postmark attachment" + f" {attachment.url(privacy=True)}." + ) + return False + + self.logger.debug( + "Appending Postmark attachment" + f" {attachment.url(privacy=True)}" + ) + + # Append our attachment list to the base payload + payload_["Attachments"] = attachments + + # Iterate over each recipient target + targets = list(self.targets) + while len(targets) > 0: + target = targets.pop(0) + + # Create a per-target copy of our base payload + payload = payload_.copy() + + # Unique cc/bcc/reply-to management -- remove target from each + cc = self.cc - self.bcc - {target} + bcc = self.bcc - {target} + reply_to = self.reply_to - {target} + + # Set our main recipient + payload["To"] = target + + if cc: + # Format CC addresses with optional display names + payload["Cc"] = ",".join( + [ + formataddr( + (self.names.get(a, False), a), + charset="utf-8", + ) + for a in cc + ] + ) + + if bcc: + # BCC addresses (plain, no display names) + payload["Bcc"] = ",".join(bcc) + + if reply_to: + # Format Reply-To addresses with optional display names + payload["ReplyTo"] = ",".join( + [ + formataddr( + (self.names.get(a, False), a), + charset="utf-8", + ) + for a in reply_to + ] + ) + + # Some Debug Logging + if self.logger.isEnabledFor(logging.DEBUG): + # Due to attachments, output can be quite heavy; only + # show the debug payload when debug logging is active. + self.logger.debug( + "Postmark POST URL:" + f" {self.notify_url} " + f"(cert_verify={self.verify_certificate!r})" + ) + self.logger.debug( + "Postmark Payload: %s", sanitize_payload(payload) + ) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + self.notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = NotifyPostmark.http_response_code_lookup( + r.status_code, POSTMARK_HTTP_ERROR_MAP + ) + + self.logger.warning( + "Failed to send Postmark notification to " + "{}: {}{}error={}.".format( + target, + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + + # Mark our failure + has_error = True + continue + + else: + self.logger.info( + f"Sent Postmark notification to {target}." + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Postmark " + f"notification to {target}." + ) + self.logger.debug(f"Socket Exception: {e!s}") + + # Mark our failure + has_error = True + continue + + return not has_error + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow us + to re-instantiate this object.""" + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Our URL looks like this: + # {schema}://{apikey}:{from_email}/{targets} + # + # which actually equates to: + # {schema}://{user}:{password}@{host}/{email1}/{email2}/etc + # ^ ^ ^ + # | | | + # apikey user domain + # (from email parts) + + # Handle apikey= query param override + if "apikey" in results["qsd"] and results["qsd"]["apikey"]: + results["apikey"] = NotifyPostmark.unquote( + results["qsd"]["apikey"] + ) + else: + # Fall back to the user field + results["apikey"] = NotifyPostmark.unquote(results["user"]) + + # Our targets list + results["targets"] = [] + + # Handle from= query param (alternative from address) + if "from" in results["qsd"] and results["qsd"]["from"]: + results["from_email"] = NotifyPostmark.unquote( + results["qsd"]["from"] + ) + # Treat the host as a target when from= is given explicitly + if results.get("host"): + results["targets"].append( + NotifyPostmark.unquote(results["host"]) + ) + + else: + # Reconstruct from_email from {password}@{host} + results["from_email"] = "{}@{}".format( + NotifyPostmark.unquote( + results["password"] + if results["password"] + else results["user"] + ), + NotifyPostmark.unquote(results["host"]), + ) + + # Handle from display name + if "name" in results["qsd"] and results["qsd"]["name"]: + results["from_name"] = NotifyPostmark.unquote( + results["qsd"]["name"] + ) + + # Acquire targets from the URL path + results["targets"].extend( + NotifyPostmark.split_path(results["fullpath"]) + ) + + # Support ?to= for additional targets + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyPostmark.parse_list( + results["qsd"]["to"] + ) + + # Handle Carbon Copy Addresses + if "cc" in results["qsd"] and results["qsd"]["cc"]: + results["cc"] = NotifyPostmark.parse_list(results["qsd"]["cc"]) + + # Handle Blind Carbon Copy Addresses + if "bcc" in results["qsd"] and results["qsd"]["bcc"]: + results["bcc"] = NotifyPostmark.parse_list(results["qsd"]["bcc"]) + + # Handle Reply-To Addresses + if "reply" in results["qsd"] and results["qsd"]["reply"]: + results["reply_to"] = results["qsd"]["reply"] + + return results diff --git a/libs/apprise/plugins/prowl.py b/libs/apprise/plugins/prowl.py index f45bd7f93..a825a1ca0 100644 --- a/libs/apprise/plugins/prowl.py +++ b/libs/apprise/plugins/prowl.py @@ -228,6 +228,7 @@ class NotifyProwl(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -242,7 +243,8 @@ class NotifyProwl(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/pushbullet.py b/libs/apprise/plugins/pushbullet.py index 3ac3af503..a71c780b1 100644 --- a/libs/apprise/plugins/pushbullet.py +++ b/libs/apprise/plugins/pushbullet.py @@ -158,7 +158,6 @@ class NotifyPushBullet(NotifyBase): # We need to upload our payload first so that we can source it # in remaining messages for no, attachment in enumerate(attach, start=1): - # Perform some simple error checking if not attachment: # We could not access the attachment @@ -343,7 +342,8 @@ class NotifyPushBullet(NotifyBase): # file handle is safely closed in `finally`; inline # open is intentional open(payload.path, "rb"), # noqa: SIM115 - )} + ) + } r = requests.post( url, @@ -353,6 +353,7 @@ class NotifyPushBullet(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -383,7 +384,8 @@ class NotifyPushBullet(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False, response diff --git a/libs/apprise/plugins/pushdeer.py b/libs/apprise/plugins/pushdeer.py index 617b8e8e3..f5f998d24 100644 --- a/libs/apprise/plugins/pushdeer.py +++ b/libs/apprise/plugins/pushdeer.py @@ -147,6 +147,7 @@ class NotifyPushDeer(NotifyBase): notify_url, data=payload, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -163,7 +164,8 @@ class NotifyPushDeer(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False diff --git a/libs/apprise/plugins/pushed.py b/libs/apprise/plugins/pushed.py index 28b2323f0..fc3aff180 100644 --- a/libs/apprise/plugins/pushed.py +++ b/libs/apprise/plugins/pushed.py @@ -224,7 +224,6 @@ class NotifyPushed(NotifyBase): if not self._send( payload=payload_, notify_type=notify_type, **kwargs ): - # toggle flag has_error = True @@ -240,7 +239,6 @@ class NotifyPushed(NotifyBase): if not self._send( payload=payload_, notify_type=notify_type, **kwargs ): - # toggle flag has_error = True @@ -275,6 +273,7 @@ class NotifyPushed(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -290,7 +289,8 @@ class NotifyPushed(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -330,15 +330,17 @@ class NotifyPushed(NotifyBase): app_secret=self.pprint( self.app_secret, privacy, mode=PrivacyMode.Secret, safe="" ), - targets="/".join([ - NotifyPushed.quote(x) - for x in chain( - # Channels are prefixed with a pound/hashtag symbol - [f"#{x}" for x in self.channels], - # Users are prefixed with an @ symbol - [f"@{x}" for x in self.users], - ) - ]), + targets="/".join( + [ + NotifyPushed.quote(x) + for x in chain( + # Channels are prefixed with a pound/hashtag symbol + [f"#{x}" for x in self.channels], + # Users are prefixed with an @ symbol + [f"@{x}" for x in self.users], + ) + ] + ), params=NotifyPushed.urlencode(params), ) diff --git a/libs/apprise/plugins/pushjet.py b/libs/apprise/plugins/pushjet.py index ccbc9265d..8f051b461 100644 --- a/libs/apprise/plugins/pushjet.py +++ b/libs/apprise/plugins/pushjet.py @@ -218,6 +218,7 @@ class NotifyPushjet(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -233,7 +234,8 @@ class NotifyPushjet(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/pushme.py b/libs/apprise/plugins/pushme.py index e4385588e..f0c7ed2be 100644 --- a/libs/apprise/plugins/pushme.py +++ b/libs/apprise/plugins/pushme.py @@ -140,6 +140,7 @@ class NotifyPushMe(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -154,7 +155,8 @@ class NotifyPushMe(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/pushover.py b/libs/apprise/plugins/pushover.py index 7f969f233..c90bef239 100644 --- a/libs/apprise/plugins/pushover.py +++ b/libs/apprise/plugins/pushover.py @@ -25,17 +25,41 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import base64 import contextlib -from itertools import chain +import gzip +import os as _os import re import requests +# Optional dependency for E2EE field encryption. +# When absent, encryption is silently skipped with a warning if the user +# explicitly configured a key + e2ee=yes. +try: + from cryptography.hazmat.primitives import ( + hashes as _crypto_hashes, + hmac as _crypto_hmac, + padding as _crypto_pad, + ) + from cryptography.hazmat.primitives.ciphers import ( + Cipher as _Cipher, + algorithms as _crypto_algos, + modes as _crypto_modes, + ) + + # E2EE support is available + PUSHOVER_E2EE_SUPPORT = True + +except ImportError: + # E2EE support unavailable; `pip install cryptography` to enable + PUSHOVER_E2EE_SUPPORT = False + from ..attachment.base import AttachBase from ..common import NotifyFormat, NotifyType from ..conversion import convert_between from ..locale import gettext_lazy as _ -from ..utils.parse import parse_list, validate_regex +from ..utils.parse import parse_bool, parse_list, validate_regex from .base import NotifyBase # Flag used as a placeholder to sending to all devices @@ -44,6 +68,10 @@ PUSHOVER_SEND_TO_ALL = "ALL_DEVICES" # Used to detect a Device VALIDATE_DEVICE = re.compile(r"^\s*(?P[a-z0-9_-]{1,25})\s*$", re.I) +# Used to detect a Group key (same format as user key: alphanumeric, prefixed +# with # or its URL-encoded equivalent %23) +VALIDATE_GROUP = re.compile(r"^\s*(%23|#)(?P[a-z0-9]+)\s*$", re.I) + # Priorities class PushoverPriority: @@ -138,6 +166,11 @@ PUSHOVER_HTTP_ERROR_MAP = { 401: "Unauthorized - Invalid Token.", } +# Validate the E2EE encryption key: exactly 64 hexadecimal characters +# representing a 256-bit AES key, matching the format required by the +# Pushover E2EE API: https://pushover.net/api#e2ee +VALIDATE_ENCRYPTION_KEY = re.compile(r"^[0-9a-f]{64}$", re.I) + class NotifyPushover(NotifyBase): """A wrapper for Pushover Notifications.""" @@ -145,6 +178,13 @@ class NotifyPushover(NotifyBase): # The default descriptive name associated with the Notification service_name = "Pushover" + # Optional dependency: cryptography is only needed for E2EE field + # encryption. The plugin works without it; encryption is skipped + # with a warning when the package is absent. + requirements = { + "packages_recommended": "cryptography", + } + # The services URL service_url = "https://pushover.net/" @@ -202,6 +242,12 @@ class NotifyPushover(NotifyBase): "regex": (r"^[a-z0-9_-]{1,25}$", "i"), "map_to": "targets", }, + "target_group": { + "name": _("Target Group"), + "type": "string", + "prefix": "#", + "map_to": "targets", + }, "targets": { "name": _("Targets"), "type": "list:string", @@ -235,8 +281,8 @@ class NotifyPushover(NotifyBase): "map_to": "supplemental_url_title", "type": "string", }, - "retry": { - "name": _("Retry"), + "interval": { + "name": _("Emergency Retry Interval"), "type": "int", "min": 30, "default": 900, # 15 minutes @@ -251,6 +297,20 @@ class NotifyPushover(NotifyBase): "to": { "alias_of": "targets", }, + "key": { + "name": _("Encryption Key"), + "type": "string", + "private": True, + # Maps to the encryption_key __init__ parameter + "map_to": "encryption_key", + }, + "e2ee": { + "name": _("E2EE"), + "type": "bool", + # Enabled by default when an encryption_key is provided; + # set to no to force plaintext even when a key is set + "default": True, + }, }, ) @@ -261,10 +321,12 @@ class NotifyPushover(NotifyBase): targets=None, priority=None, sound=None, - retry=None, + interval=None, expire=None, supplemental_url=None, supplemental_url_title=None, + encryption_key=None, + e2ee=None, **kwargs, ): """Initialize Pushover Object.""" @@ -284,26 +346,32 @@ class NotifyPushover(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - # Track our valid devices + # Track our valid devices and groups separately targets = parse_list(targets) # Track any invalid entries self.invalid_targets = [] + self.devices = [] + self.groups = [] - if len(targets) == 0: - self.targets = (PUSHOVER_SEND_TO_ALL,) + if not targets: + # No targets specified at all: send to all devices of user + self.devices = [PUSHOVER_SEND_TO_ALL] else: - self.targets = [] for target in targets: + result = VALIDATE_GROUP.match(target) + if result: + self.groups.append(result.group("group")) + continue + result = VALIDATE_DEVICE.match(target) if result: - # Store device information - self.targets.append(result.group("device")) + self.devices.append(result.group("device")) continue self.logger.warning( - f"Dropped invalid Pushover device ({target}) specified.", + f"Dropped invalid Pushover target ({target}) specified.", ) self.invalid_targets.append(target) @@ -337,21 +405,21 @@ class NotifyPushover(NotifyBase): # The following are for emergency alerts if self.priority == PushoverPriority.EMERGENCY: - - # How often to resend notification, in seconds - self.retry = self.template_args["retry"]["default"] + # How often to resend notification, in seconds (emergency only) + self.interval = self.template_args["interval"]["default"] with contextlib.suppress(ValueError, TypeError): - # Get our retry value - self.retry = int(retry) + self.interval = int(interval) - # How often to resend notification, in seconds + # How long before the emergency alert expires, in seconds self.expire = self.template_args["expire"]["default"] with contextlib.suppress(ValueError, TypeError): # Acquire our expiry value self.expire = int(expire) - if self.retry < 30: - msg = "Pushover retry must be at least 30 seconds." + if self.interval < 30: + msg = ( + "Pushover emergency interval must be at least 30 seconds." + ) self.logger.warning(msg) raise TypeError(msg) @@ -362,8 +430,84 @@ class NotifyPushover(NotifyBase): ) self.logger.warning(msg) raise TypeError(msg) + + # End-to-end encryption: validate and store the encryption key. + # The key must be exactly 64 hex characters (256-bit AES key). + if encryption_key: + # Normalise to lower-case hex string + _key = str(encryption_key).strip().lower() + if not VALIDATE_ENCRYPTION_KEY.match(_key): + msg = ( + "Pushover encryption_key must be exactly 64 hex " + "characters (256-bit AES key); " + "got {} chars".format(len(_key)) + ) + self.logger.warning(msg) + raise TypeError(msg) + + self.encryption_key = _key + else: + # No key -- E2EE is not possible + self.encryption_key = None + + # e2ee flag: defaults to True (encrypt when a key is available). + # Set to False/no to force plaintext even when a key is configured. + self.e2ee = ( + self.template_args["e2ee"]["default"] + if e2ee is None + else parse_bool(e2ee) + ) return + def _encrypt_field(self, plaintext, key_bytes): + """Encrypt a single Pushover message field for E2EE. + + Implements the field-level encryption scheme from + https://pushover.net/api#e2ee -- per field: + + 1. gzip-compress the plaintext string (UTF-8) + 2. AES-256-CBC-encrypt the compressed data (random 16-byte IV, + PKCS7 padding) using the caller-supplied 256-bit key + 3. Compute HMAC-SHA256 over (IV || ciphertext) with the same key + 4. Return base64(IV || ciphertext || HMAC) + + Raises on any crypto error; the caller must not silently fall + back to sending plaintext when encryption fails. + """ + + try: + # Compress the UTF-8 encoded plaintext + compressed = gzip.compress(plaintext.encode("utf-8")) + + # Generate a random 16-byte initialisation vector + iv = _os.urandom(16) + + # Pad the compressed data to a multiple of the AES block size + padder = _crypto_pad.PKCS7(128).padder() + padded = padder.update(compressed) + padder.finalize() + + # AES-256-CBC encryption + cipher = _Cipher( + _crypto_algos.AES(key_bytes), + _crypto_modes.CBC(iv), + ) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded) + encryptor.finalize() + + # HMAC-SHA256 over IV || ciphertext for integrity + h = _crypto_hmac.HMAC(key_bytes, _crypto_hashes.SHA256()) + h.update(iv + ciphertext) + mac = h.finalize() + + # Base64-encode the concatenated IV || ciphertext || HMAC + return base64.b64encode(iv + ciphertext + mac).decode("ascii") + + except Exception as err: + self.logger.debug( + "Pushover E2EE field encryption failed: {}".format(err) + ) + raise + def send( self, body, @@ -374,67 +518,142 @@ class NotifyPushover(NotifyBase): ): """Perform Pushover Notification.""" - if not self.targets: + if not self.devices and not self.groups: # There were no services to notify self.logger.warning("There were no Pushover targets to notify.") return False - # prepare JSON Object - payload = { + # Determine whether E2EE field encryption should be applied. + # Conditions: user supplied a key, e2ee flag is True, and the + # cryptography package is installed. + do_encrypt = bool(self.encryption_key and self.e2ee) + if do_encrypt and not PUSHOVER_E2EE_SUPPORT: + # Encryption was explicitly requested but the library is absent; + # fall back to plaintext with a clear warning + self.logger.warning( + "Pushover E2EE requested but the 'cryptography' package " + "is not installed; sending message unencrypted. " + "Install it with: pip install cryptography" + ) + do_encrypt = False + + # Build the base payload shared across all sends + base_payload = { "token": self.token, - "user": self.user_key, "priority": str(self.priority), "title": title if title else self.app_desc, "message": body, - "device": ",".join(self.targets), "sound": self.sound, } if self.supplemental_url: - payload["url"] = self.supplemental_url + base_payload["url"] = self.supplemental_url if self.supplemental_url_title: - payload["url_title"] = self.supplemental_url_title + base_payload["url_title"] = self.supplemental_url_title if self.notify_format == NotifyFormat.HTML: # https://pushover.net/api#html - payload["html"] = 1 + base_payload["html"] = 1 elif self.notify_format == NotifyFormat.MARKDOWN: - payload["message"] = convert_between( + base_payload["message"] = convert_between( NotifyFormat.MARKDOWN, NotifyFormat.HTML, body ) - payload["html"] = 1 + base_payload["html"] = 1 if self.priority == PushoverPriority.EMERGENCY: - payload.update({"retry": self.retry, "expire": self.expire}) + base_payload.update( + { + "retry": self.interval, + "expire": self.expire, + } + ) - if attach and self.attachment_support: - # Create a copy of our payload - payload_ = payload.copy() + # Apply field-level E2EE encryption when conditions are met. + # Encrypted fields: message, title, url, url_title. + # Reference: https://pushover.net/api#e2ee + if do_encrypt: + # Decode the hex key once for all fields in this send call + key_bytes = bytes.fromhex(self.encryption_key) - # Send with attachments - for no, attachment in enumerate(attach): - if no or not body: - # To handle multiple attachments, clean up our message - payload_["message"] = attachment.name + try: + # Encrypt each present field individually + base_payload["message"] = self._encrypt_field( + base_payload["message"], key_bytes + ) + base_payload["title"] = self._encrypt_field( + base_payload["title"], key_bytes + ) + if "url" in base_payload: + base_payload["url"] = self._encrypt_field( + base_payload["url"], key_bytes + ) + if "url_title" in base_payload: + base_payload["url_title"] = self._encrypt_field( + base_payload["url_title"], key_bytes + ) - if not self._send(payload_, attachment): - # Mark our failure - return False + # Signal to the Pushover API that fields are encrypted + base_payload["encrypted"] = 1 - # Clear our title if previously set - payload_["title"] = "" + except Exception: + # Any failure in the crypto path is fatal -- do not send + # silently degraded (unencrypted) content when the caller + # expected encryption. + self.logger.warning( + "Pushover E2EE encryption failed; notification not sent." + ) + return False - # No need to alarm for each consecutive attachment uploaded - # afterwards - payload_["sound"] = PushoverSound.NONE + # Build per-target payloads: + # - devices: one call with user=user_key, device=dev1,dev2,... + # - groups: one call per group with user=group_key + payloads = [] + if self.devices: + payloads.append( + { + **base_payload, + "user": self.user_key, + "device": ",".join(self.devices), + } + ) + for group_key in self.groups: + payloads.append( + { + **base_payload, + "user": group_key, + } + ) - else: - # Simple send - return self._send(payload) + has_error = False + for payload in payloads: + if attach and self.attachment_support: + # Create a copy of our payload + payload_ = payload.copy() - return True + # Send with attachments + for no, attachment in enumerate(attach): + if no or not body: + # To handle multiple attachments, clean up our message + payload_["message"] = attachment.name + + if not self._send(payload_, attachment): + # Mark our failure + has_error = True + + # Clear our title if previously set + payload_["title"] = "" + + # No need to alarm for each consecutive attachment + # uploaded afterwards + payload_["sound"] = PushoverSound.NONE + + else: + if not self._send(payload): + has_error = True + + return not has_error def _send(self, payload, attach=None): """Wrapper to the requests (post) object.""" @@ -469,7 +688,6 @@ class NotifyPushover(NotifyBase): if not ( file_size > 0 and file_size <= self.attach_max_size_bytes ): - # File size is no good self.logger.warning( f"Pushover attachment size ({file_size}B) exceeds" @@ -510,9 +728,11 @@ class NotifyPushover(NotifyBase): "attachment": ( attach.name, # file handle is safely closed in `finally`; inline - # open is intentional - open(attach.path, "rb"), # noqa: SIM115 - )} + # open is intentional; attach.open() dispatches to + # BytesIO for memory attachments + attach.open(), + ) + } r = requests.post( self.notify_url, @@ -522,6 +742,7 @@ class NotifyPushover(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -533,7 +754,7 @@ class NotifyPushover(NotifyBase): self.logger.warning( "Failed to send Pushover notification to {}: " "{}{}error={}.".format( - payload["device"], + payload.get("device") or f"group:{payload['user']}", status_str, ", " if status_str else "", r.status_code, @@ -541,23 +762,24 @@ class NotifyPushover(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False else: self.logger.info( "Sent Pushover notification to {}.".format( - payload["device"] + payload.get("device") or f"group:{payload['user']}" ) ) except requests.RequestException as e: self.logger.warning( - "A Connection error occurred sending Pushover:{} ".format( - payload["device"] + "A Connection error occurred sending Pushover" + ":{} notification.".format( + payload.get("device") or "group:{}".format(payload["user"]) ) - + "notification." ) self.logger.debug(f"Socket Exception: {e!s}") @@ -589,6 +811,14 @@ class NotifyPushover(NotifyBase): """ return (self.secure_protocol, self.user_key, self.token) + def __len__(self): + """Returns the number of HTTP requests this instance will make. + + Devices are batched into a single call; each group requires its own. + """ + # At least 1 if there are any valid targets + return max(1, (1 if self.devices else 0) + len(self.groups)) + def url(self, privacy=False, *args, **kwargs): """Returns the URL built dynamically based on specified arguments.""" @@ -599,32 +829,53 @@ class NotifyPushover(NotifyBase): if self.priority not in PUSHOVER_PRIORITIES else PUSHOVER_PRIORITIES[self.priority] ), + "sound": self.sound, } - # Only add expire and retry for emergency messages, - # pushover ignores for all other priorities + if self.supplemental_url: + params["url"] = self.supplemental_url + + if self.supplemental_url_title: + params["url_title"] = self.supplemental_url_title + + # Only add expire and interval for emergency messages; + # Pushover ignores them for all other priorities if self.priority == PushoverPriority.EMERGENCY: - params.update({"expire": self.expire, "retry": self.retry}) + params.update({"expire": self.expire, "interval": self.interval}) + + # Include E2EE parameters when a key is set. + # Use the short ?key= URL parameter for brevity. + if self.encryption_key: + params["key"] = ( + self.pprint(self.encryption_key, privacy, safe="") + if privacy + else self.encryption_key + ) + # Only emit e2ee=no when the user explicitly disabled it; + # the default (True) is implied when a key is present + if not self.e2ee: + params["e2ee"] = "no" # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - # Escape our devices - devices = "/".join([ - NotifyPushover.quote(x, safe="") - for x in chain(self.targets, self.invalid_targets) - ]) + # Assemble targets: devices (excluding sentinel), groups (%23-encoded), + # then any invalid entries so the URL round-trips faithfully + targets = "/".join( + [ + NotifyPushover.quote(d, safe="") + for d in self.devices + if d != PUSHOVER_SEND_TO_ALL + ] + + [NotifyPushover.quote(f"#{g}", safe="") for g in self.groups] + + [NotifyPushover.quote(x, safe="") for x in self.invalid_targets] + ) - if devices == PUSHOVER_SEND_TO_ALL: - # keyword is reserved for internal usage only; it's safe to remove - # it from the devices list - devices = "" - - return "{schema}://{user_key}@{token}/{devices}/?{params}".format( + return "{schema}://{user_key}@{token}/{targets}/?{params}".format( schema=self.secure_protocol, user_key=self.pprint(self.user_key, privacy, safe=""), token=self.pprint(self.token, privacy, safe=""), - devices=devices, + targets=targets, params=NotifyPushover.urlencode(params), ) @@ -632,6 +883,16 @@ class NotifyPushover(NotifyBase): def parse_url(url): """Parses the URL and returns enough arguments that can allow us to re- instantiate this object.""" + + # Encode any literal # in the path so group key prefixes survive + # Python's urlparse, which treats # as a fragment separator and + # silently drops everything after it. + if isinstance(url, str): + q_idx = url.find("?") + _path = url if q_idx < 0 else url[:q_idx] + _rest = "" if q_idx < 0 else url[q_idx:] + url = _path.replace("#", "%23") + _rest + results = NotifyBase.parse_url(url, verify_host=False) if not results: # We're done early as we couldn't load the results @@ -659,13 +920,15 @@ class NotifyPushover(NotifyBase): results["qsd"]["url"] ) if "url_title" in results["qsd"] and len(results["qsd"]["url_title"]): - results["supplemental_url_title"] = results["qsd"]["url_title"] + results["supplemental_url_title"] = NotifyPushover.unquote( + results["qsd"]["url_title"] + ) - # Get expire and retry + # Get expire and emergency retry interval if "expire" in results["qsd"] and len(results["qsd"]["expire"]): results["expire"] = results["qsd"]["expire"] - if "retry" in results["qsd"] and len(results["qsd"]["retry"]): - results["retry"] = results["qsd"]["retry"] + if "interval" in results["qsd"] and len(results["qsd"]["interval"]): + results["interval"] = results["qsd"]["interval"] # The 'to' makes it easier to use yaml configuration if "to" in results["qsd"] and len(results["qsd"]["to"]): @@ -673,7 +936,26 @@ class NotifyPushover(NotifyBase): results["qsd"]["to"] ) + # E2EE encryption key (64-char hex string); URL param is ?key= for + # brevity, mapped to the encryption_key __init__ parameter + if "key" in results["qsd"] and results["qsd"]["key"]: + results["encryption_key"] = NotifyPushover.unquote( + results["qsd"]["key"] + ) + + # E2EE flag -- only parse when present; absence preserves default + if "e2ee" in results["qsd"] and results["qsd"]["e2ee"]: + results["e2ee"] = results["qsd"]["e2ee"] + # Token results["token"] = NotifyPushover.unquote(results["host"]) return results + + @staticmethod + def runtime_deps(): + """Return optional runtime dependency package names. + + E2EE support requires the ``cryptography`` package. + """ + return ("cryptography",) diff --git a/libs/apprise/plugins/pushplus.py b/libs/apprise/plugins/pushplus.py index faf2f8c37..15fe9ffe7 100644 --- a/libs/apprise/plugins/pushplus.py +++ b/libs/apprise/plugins/pushplus.py @@ -26,23 +26,147 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -# Details at: -# https://www.pushplus.plus/doc/guide/api.html +# PushPlus is a Chinese notification platform that delivers messages via +# WeChat and several other channels. You can find its API documentation at: +# https://www.pushplus.plus/doc/guide/api.html +# +# To obtain your personal token: +# 1. Register or sign in at https://www.pushplus.plus/ +# 2. Copy the token shown on the dashboard under the "Push" section. +# +# Group (topic) sending is also supported. After creating a group under +# "Group Push" in the PushPlus console, use the group code as the topic: +# https://www.pushplus.plus/doc/guide/group.html +# +# Basic Apprise URL forms: +# +# Personal notification (WeChat default): +# pushplus://{token} +# +# Group topic -- one notification per topic: +# pushplus://{token}/{topic} +# pushplus://{token}/{topic1}/{topic2} +# +# Select a delivery channel (mode= and channel= are synonyms): +# pushplus://{token}?channel=mail +# pushplus://{token}?mode=cp +# +# Topic + delivery channel: +# pushplus://{token}/{topic}?channel=mail +# +# Webhook channel with a named endpoint -- two equivalent forms: +# pushplus://{token}?channel=webhook&name={webhook_name} +# pushplus://{webhook_name}@{token} +# +# When the schema://{name}@{token} form is used and no explicit channel= +# is given, the webhook channel is implied automatically. +# +# Native PushPlus API URL (also accepted by parse_native_url): +# https://www.pushplus.plus/send?token={token} +# +# Schema alias for WeCom users: +# wecom://{token} -- identical to pushplus://{token}?channel=cp +# +# For direct WeCom Application API notifications (without PushPlus as +# intermediary), use the wechat:// plugin instead. +# +# API References: +# https://www.pushplus.plus/doc/guide/api.html +# https://www.pushplus.plus/doc/guide/group.html import json import re import requests -from ..common import NotifyType +from ..common import NotifyFormat, NotifyType from ..locale import gettext_lazy as _ from ..url import PrivacyMode -from ..utils.parse import validate_regex +from ..utils.parse import parse_list, validate_regex from .base import NotifyBase +# PushPlus application-level response codes. +# The HTTP status is always 200; the real result lives in the JSON body. +# Reference: https://www.pushplus.plus/doc/guide/api.html +PUSHPLUS_RESPONSE_CODES = { + 200: "Request succeeded.", + 900: "System exception.", + 903: "Sending failed.", + 905: "Request parameter error.", + 907: "Token does not exist.", + 908: "User is blocked.", + 909: "Content requires review before sending.", + 912: "No available service package.", +} + +# Map Apprise's standard notify_format values to the PushPlus template +# identifiers. PushPlus uses these to render the body server-side before +# delivery -- the content itself does not change; only the rendering hint. +PUSHPLUS_FORMAT_MAP = { + NotifyFormat.HTML: "html", + NotifyFormat.MARKDOWN: "markdown", + NotifyFormat.TEXT: "txt", +} + +# Default PushPlus template when the format has no explicit mapping +PUSHPLUS_FORMAT_DEFAULT = "html" + + +class PushPlusChannel: + """Defines the PushPlus delivery channels. + + The channel controls where the rendered notification is delivered. + It is supplied as the channel= (or mode=) query parameter in the + Apprise URL, e.g. pushplus://{token}?channel=mail. + """ + + # Deliver via WeChat (PushPlus default) + WECHAT = "wechat" + + # Deliver via a configured webhook endpoint + WEBHOOK = "webhook" + + # Deliver via WeCom (WeChat Work / Enterprise WeChat). + # The API value "cp" is PushPlus's internal identifier for this channel. + # Both "cp" and the friendly alias "wecom" are accepted; both resolve here. + WECOM = "cp" + + # Deliver via email + MAIL = "mail" + + # Deliver via SMS + SMS = "sms" + + +# All valid PushPlus delivery channels (these are the API values) +PUSHPLUS_CHANNELS = ( + PushPlusChannel.WECHAT, + PushPlusChannel.WEBHOOK, + PushPlusChannel.WECOM, + PushPlusChannel.MAIL, + PushPlusChannel.SMS, +) + +# The PushPlus default delivery channel (WeChat) +PUSHPLUS_CHANNEL_DEFAULT = PushPlusChannel.WECHAT + +# A group topic has no special prefix; alphanumeric codes up to 50 chars. +IS_TOPIC = re.compile( + r"^(?P[a-z0-9_-]{1,50})$", + re.I, +) + +# Schema names that auto-select a specific delivery channel when used +# in place of the default "pushplus" schema. Mirrors the Kodi/XBMC pattern. +# wechat:// belongs to the separate direct WeCom Application API plugin. +PUSHPLUS_SCHEMA_MAP = { + # wecom:// forces channel=cp (WeCom / Enterprise WeChat) + "wecom": PushPlusChannel.WECOM, +} + class NotifyPushplus(NotifyBase): - """A wrapper for Pushplus Notifications.""" + """A wrapper for PushPlus Notifications.""" # The default descriptive name associated with the Notification service_name = _("Pushplus") @@ -50,17 +174,35 @@ class NotifyPushplus(NotifyBase): # The services URL service_url = "https://www.pushplus.plus/" - # The default secure protocol - secure_protocol = "pushplus" + # The default protocol; wecom:// is accepted as a schema alias. + # wechat:// belongs to the direct WeCom Application API plugin and + # is intentionally not listed here. + secure_protocol = ("pushplus", "wecom") # A URL that takes you to the setup/help of the specific protocol setup_url = "https://appriseit.com/services/pushplus/" - # URL used to send notifications with + # URL used to POST notifications notify_url = "https://www.pushplus.plus/send" - templates = ("{schema}://{token}",) + # Maximum body length documented by PushPlus + body_maxlen = 20000 + # Title is capped at a safe limit (not explicitly documented by PushPlus) + title_maxlen = 200 + + # Default to HTML since PushPlus renders HTML by default + notify_format = NotifyFormat.HTML + + # Define object URL templates + templates = ( + # No topics: personal notification with optional ?channel= override + "{schema}://{token}", + # Topics in path: one API call per topic + "{schema}://{token}/{targets}", + ) + + # Define our template tokens template_tokens = dict( NotifyBase.template_tokens, **{ @@ -69,103 +211,443 @@ class NotifyPushplus(NotifyBase): "type": "string", "private": True, "required": True, + # PushPlus tokens are 32-64 alphanumeric/underscore/dash chars "regex": (r"^[a-z0-9_-]{32,64}$", "i"), }, + # Group topics go directly in the URL path with no prefix + "targets": { + "name": _("Group Topics"), + "type": "list:string", + }, }, ) - def __init__(self, token, **kwargs): + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + # Allow the token to be supplied as a query parameter + "token": { + "alias_of": "token", + }, + # ?to= is the standard Apprise alias for targets (topics) + "to": { + "alias_of": "targets", + }, + # Delivery channel -- selects where the message is delivered. + # One of the PUSHPLUS_CHANNELS values (wechat is the default). + "channel": { + "name": _("Channel"), + "type": "choice:string", + "values": PUSHPLUS_CHANNELS, + "default": PUSHPLUS_CHANNEL_DEFAULT, + }, + # mode= is an alias for channel=; the two are fully synonymous. + "mode": { + "alias_of": "channel", + }, + # ?topic= backward-compat alias that maps to targets + "topic": { + "alias_of": "targets", + }, + # Webhook endpoint name; only meaningful when channel=webhook. + # The URL parameter is ?name= but the __init__ kwarg is webhook= + # to avoid shadowing URLBase.name. + "name": { + "name": _("Webhook Name"), + "type": "string", + "map_to": "webhook", + }, + }, + ) + + def __init__( + self, token, targets=None, channel=None, webhook=None, **kwargs + ): """Initialize Pushplus Object.""" super().__init__(**kwargs) + # Validate the required user token self.token = validate_regex( - token, *self.template_tokens["token"]["regex"] + token, + *self.template_tokens["token"]["regex"], ) if not self.token: - msg = f"The Pushplus token ({token}) is invalid." + msg = "The Pushplus token ({}) is invalid.".format(token) self.logger.warning(msg) raise TypeError(msg) - def url(self, privacy=False, *args, **kwargs): - """Returns the URL built dynamically based on specified arguments.""" - params = self.url_parameters(privacy=privacy, *args, **kwargs) - return ( - f"{self.secure_protocol}://" - f"{self.pprint(self.token, privacy, mode=PrivacyMode.Secret)}/" - f"?{self.urlencode(params)}" + # Resolve the delivery channel from the schema alias when the URL + # was entered as wechat:// or wecom:// instead of pushplus://. + # self.schema is set by NotifyBase from the parsed URL protocol. + schema_channel = PUSHPLUS_SCHEMA_MAP.get((self.schema or "").lower()) + + # Determine the active delivery channel: + # 1. Explicit channel= / mode= argument (already resolved by + # the caller since mode is an alias_of channel in template_args) + # 2. Schema-implied channel (wechat:// or wecom://) + # 3. Default (WeChat) + if channel: + # Normalise the 'wecom' friendly alias to the API value 'cp' + if channel.lower() == "wecom": + channel = PushPlusChannel.WECOM + self.channel = next( + (c for c in PUSHPLUS_CHANNELS if c == channel.lower()), + None, + ) + if not self.channel: + msg = "The Pushplus channel ({}) is not valid.".format(channel) + self.logger.warning(msg) + raise TypeError(msg) + + elif schema_channel: + # Schema-implied channel (wechat:// or wecom://) + self.channel = schema_channel + + else: + # Default to WeChat + self.channel = PUSHPLUS_CHANNEL_DEFAULT + + # Resolved group topics -- one API call is made per topic + self.topics = [] + + # Preserve unrecognised targets for round-trip fidelity in url() + self.invalid_targets = [] + + # Parse each target from the list -- only plain topics are valid here + for target in parse_list(targets): + result = IS_TOPIC.match(target) + if result: + self.topics.append(result.group("topic")) + continue + + # Unrecognised entry -- log and preserve + self.logger.warning("Dropped invalid Pushplus topic: %s", target) + self.invalid_targets.append(target) + + # Store the webhook name; only meaningful when channel=webhook. + # Kept as self.webhook (not self.name) to avoid shadowing URLBase.name. + # Arrives via the ?name= URL parameter (mapped to webhook= by map_to). + self.webhook = ( + webhook if isinstance(webhook, str) and webhook else None ) - @property - def url_identifier(self): - """Returns a unique identifier for this plugin instance.""" - return (self.secure_protocol, self.token) + return def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): - """Send a Pushplus Notification.""" - payload = { - "token": self.token, - "title": title if title else body, - "content": body, - } + """Perform PushPlus Notification.""" + # Prepare our headers headers = { "User-Agent": self.app_id, "Content-Type": "application/json", } - # Always call throttle before any remote server i/o is made - self.throttle() + # Derive the PushPlus rendering template from Apprise's standard + # notify_format. This is a server-side rendering hint only; the + # content payload does not change. + pp_template = PUSHPLUS_FORMAT_MAP.get( + self.notify_format, PUSHPLUS_FORMAT_DEFAULT + ) - try: - response = requests.post( + # When no topics are configured, fall back to a single personal send + # by iterating over a list containing None as the sole entry. + topics_to_notify = self.topics if self.topics else [None] + + # Track whether any individual send failed + has_error = False + + for topic in topics_to_notify: + # Build the payload for this particular topic + payload = { + # Authentication token + "token": self.token, + # Fall back to the body when no title is provided + "title": title if title else body, + # Notification body content + "content": body, + # Rendering template derived from notify_format + "template": pp_template, + # Delivery channel + "channel": self.channel, + } + + # Add the group topic when sending to a specific group + if topic: + payload["topic"] = topic + + # Add the webhook name when the webhook channel is selected + if self.channel == PushPlusChannel.WEBHOOK and self.webhook: + payload["webhook"] = self.webhook + + # Debug logging so the caller can inspect what will be sent + self.logger.debug( + "PushPlus POST URL: %s (cert_verify=%r)", self.notify_url, - headers=headers, - data=json.dumps(payload), - verify=self.verify_certificate, - timeout=self.request_timeout, + self.verify_certificate, + ) + self.logger.debug("PushPlus Payload: %r", payload) + + # Always throttle before each remote server I/O call + self.throttle() + + try: + r = requests.post( + self.notify_url, + headers=headers, + # Encode explicitly for non-ASCII (e.g. Chinese) chars + data=json.dumps(payload, ensure_ascii=False).encode( + "utf-8" + ), + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # HTTP-level failure -- log it and move on + status_str = NotifyPushplus.http_response_code_lookup( + r.status_code + ) + self.logger.warning( + "Failed to send PushPlus notification: " + "{}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + # Mark our failure and continue with the next topic + has_error = True + continue + + # PushPlus always returns HTTP 200; the real result is in + # the JSON body where code == 200 means success. + try: + content = json.loads(r.content) + except (AttributeError, TypeError, ValueError): + # ValueError = r.content is unparsable + # TypeError = r.content is None + # AttributeError = r is None + content = {} + + # Check the application-level status code + api_code = content.get("code") if content else None + if api_code != 200: + # Application-level failure + error_str = PUSHPLUS_RESPONSE_CODES.get( + api_code, + # Fall back to the msg field, then a generic string + ( + content.get("msg", "Unknown error") + if content + else "Unknown error" + ), + ) + self.logger.warning( + "Failed to send PushPlus notification: " + "code={}: {}.".format(api_code, error_str) + ) + self.logger.debug( + "Response Details:\r\n%r", + content if content else (r.content or b"")[:2000], + ) + # Mark our failure and continue with the next topic + has_error = True + continue + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending" + " PushPlus notification." + ) + self.logger.debug("Socket Exception: %s", str(e)) + # Mark our failure and continue with the next topic + has_error = True + continue + + # Notification delivered for this topic + self.logger.info( + "Sent PushPlus notification%s.", + " to topic {}".format(topic) if topic else "", ) - if response.status_code != requests.codes.ok: - self.logger.warning( - "Pushplus notification failed: %d - %s", - response.status_code, - response.text, + return not has_error + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another similar one. + + Targets or end points should never be identified here. + """ + # The token is the sole connection identity for PushPlus + return (self.secure_protocol[0], self.token) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # When channel=webhook with a named endpoint, use the compact + # {name}@pushplus://{token} form. Both ?channel=webhook and ?name= + # are implied by the user@ prefix and omitted from the query string. + webhook_prefix = ( + self.channel == PushPlusChannel.WEBHOOK and self.webhook + ) + + # Start with an empty params dict + params = {} + + # When not using the webhook prefix form, include ?channel= when it + # differs from the default. The schema alias (wechat:// / wecom://) + # is never emitted here -- we always normalise back to pushplus:// + # plus ?channel= for clarity. + if not webhook_prefix and self.channel != PUSHPLUS_CHANNEL_DEFAULT: + params["channel"] = self.channel + + # Merge in standard Apprise URL parameters (verify, format, etc.) + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Build the targets path from group topics and any invalid entries + targets = list(self.topics) + list(self.invalid_targets) + + # Masked token for the URL + token_str = self.pprint( + self.token, + privacy, + mode=PrivacyMode.Secret, + safe="", + ) + + # When using the webhook prefix form, the endpoint name goes in the + # user@ position (schema://name@token) -- channel=webhook and ?name= + # are both implied by the user@ presence and omitted from params. + if webhook_prefix: + name_str = NotifyPushplus.quote(self.webhook, safe="") + if targets: + return "{schema}://{name}@{token}/{targets}/?{params}".format( + schema=self.secure_protocol[0], + name=name_str, + token=token_str, + targets="/".join( + NotifyPushplus.quote(t, safe="") for t in targets + ), + params=NotifyPushplus.urlencode(params), ) - return False + return "{schema}://{name}@{token}/?{params}".format( + schema=self.secure_protocol[0], + name=name_str, + token=token_str, + params=NotifyPushplus.urlencode(params), + ) - except requests.RequestException as e: - self.logger.warning(f"Pushplus Exception: {e}") - return False + if targets: + # One or more topics: include them in the URL path + return "{schema}://{token}/{targets}/?{params}".format( + schema=self.secure_protocol[0], + token=token_str, + targets="/".join( + NotifyPushplus.quote(t, safe="") for t in targets + ), + params=NotifyPushplus.urlencode(params), + ) - self.logger.info("Pushplus notification sent successfully.") - return True + # No topics -- simple personal notification URL + return "{schema}://{token}/?{params}".format( + schema=self.secure_protocol[0], + token=token_str, + params=NotifyPushplus.urlencode(params), + ) @staticmethod def parse_url(url): - """Parses the URL and returns arguments to re-instantiate the - object.""" + """Parses the URL and returns enough arguments that can allow us to + re-instantiate this object.""" results = NotifyBase.parse_url(url, verify_host=False) if not results: + # We're done early as we couldn't load the results return results + # Prefer ?token= query parameter over the URL host field if "token" in results["qsd"] and results["qsd"]["token"]: + # Token was supplied as a query parameter results["token"] = NotifyPushplus.unquote(results["qsd"]["token"]) else: + # Token is the URL host results["token"] = NotifyPushplus.unquote(results["host"]) + # Collect group topics from the URL path + results["targets"] = NotifyPushplus.split_path(results["fullpath"]) + + # ?to= appends additional targets (comma/space delimited supported) + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyPushplus.parse_list( + results["qsd"]["to"] + ) + + # ?topic= backward-compat alias also appends topics + if "topic" in results["qsd"] and results["qsd"]["topic"]: + results["targets"] += NotifyPushplus.parse_list( + results["qsd"]["topic"] + ) + + # Extract the delivery channel from ?channel= or its alias ?mode= + # mode= takes lower priority; channel= wins if both are present. + if "mode" in results["qsd"] and results["qsd"]["mode"]: + results["channel"] = NotifyPushplus.unquote(results["qsd"]["mode"]) + if "channel" in results["qsd"] and results["qsd"]["channel"]: + results["channel"] = NotifyPushplus.unquote( + results["qsd"]["channel"] + ) + + # Extract the webhook name -- users specify it as ?name= in the URL. + # We store it internally as 'webhook' to avoid shadowing URLBase.name. + if "name" in results["qsd"] and results["qsd"]["name"]: + results["webhook"] = NotifyPushplus.unquote(results["qsd"]["name"]) + + # Support the {webhook_name}@pushplus://{token} short form. + # When user@ is present, it identifies the webhook endpoint name. + # If no channel was explicitly given, webhook is the implied channel. + if results.get("user"): + # Use user@ as the webhook name when ?name= was not also supplied + if "webhook" not in results: + results["webhook"] = NotifyPushplus.unquote(results["user"]) + # Imply webhook channel when no explicit channel was specified + if "channel" not in results: + results["channel"] = PushPlusChannel.WEBHOOK + return results @staticmethod def parse_native_url(url): - """Parse native Pushplus-style URL.""" - match = re.match( - r"^https://www\.pushplus\.plus/send\?token=([a-z0-9_-]+)$", + """Support native PushPlus API URLs of the form: + https://www.pushplus.plus/send?token=TOKEN[&other_params] + """ + result = re.match( + r"^https?://www\.pushplus\.plus/send" + r"(?:\?(?P[^#]+))?$", url, re.I, ) - if not match: - return None - - return NotifyPushplus.parse_url( - f"{NotifyPushplus.secure_protocol}://{match.group(1)}" - ) + if result: + params = result.group("params") or "" + # The token must be present as a query parameter + tok = re.search( + r"(?:(?:^|&))token=(?P[a-z0-9_-]+)", + params, + re.I, + ) + if tok: + # Re-build as an Apprise URL. Preserve all existing params + # so that topic, channel, and name all round-trip correctly. + return NotifyPushplus.parse_url( + "{schema}://{token}/?{params}".format( + schema=NotifyPushplus.secure_protocol[0], + token=tok.group("token"), + params=params, + ) + ) + return None diff --git a/libs/apprise/plugins/pushsafer.py b/libs/apprise/plugins/pushsafer.py index 19cb39db0..bc9e6c8cf 100644 --- a/libs/apprise/plugins/pushsafer.py +++ b/libs/apprise/plugins/pushsafer.py @@ -611,14 +611,16 @@ class NotifyPushSafer(NotifyBase): try: # Output must be in a DataURL format (that's what # PushSafer calls it): - attachments.append(( + attachments.append( ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - f"data:{attachment.mimetype};base64,{attachment.base64()}", - )) + ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + f"data:{attachment.mimetype};base64,{attachment.base64()}", + ) + ) except exception.AppriseException: # We could not access the attachment @@ -678,7 +680,6 @@ class NotifyPushSafer(NotifyBase): for c, attachment in enumerate( attachments[idx : idx + len(PICTURE_PARAMETER)] ): - # Get our attachment information filename, dataurl = attachment payload_.update({PICTURE_PARAMETER[c]: dataurl}) @@ -730,7 +731,8 @@ class NotifyPushSafer(NotifyBase): f" {notify_url} (cert_verify={self.verify_certificate!r})" ) self.logger.debug( - "PushSafer Payload: %s", sanitize_payload(payload)) + "PushSafer Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -750,6 +752,7 @@ class NotifyPushSafer(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -786,7 +789,8 @@ class NotifyPushSafer(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False, response diff --git a/libs/apprise/plugins/pushy.py b/libs/apprise/plugins/pushy.py index a91058a3b..d10d6aa92 100644 --- a/libs/apprise/plugins/pushy.py +++ b/libs/apprise/plugins/pushy.py @@ -245,6 +245,7 @@ class NotifyPushy(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Sample response @@ -273,7 +274,6 @@ class NotifyPushy(NotifyBase): if r.status_code != requests.codes.ok or not content.get( "success" ): - # We had a problem status_str = NotifyPushy.http_response_code_lookup( r.status_code, PUSHY_HTTP_ERROR_MAP @@ -290,7 +290,8 @@ class NotifyPushy(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) has_error = True continue @@ -337,15 +338,17 @@ class NotifyPushy(NotifyBase): return "{schema}://{apikey}/{targets}/?{params}".format( schema=self.secure_protocol, apikey=self.pprint(self.apikey, privacy, safe=""), - targets="/".join([ - NotifyPushy.quote(x, safe="@#") - for x in chain( - # Topics are prefixed with a pound/hashtag symbol - [f"#{x}" for x in self.topics], - # Devices - [f"@{x}" for x in self.devices], - ) - ]), + targets="/".join( + [ + NotifyPushy.quote(x, safe="@#") + for x in chain( + # Topics are prefixed with a pound/hashtag symbol + [f"#{x}" for x in self.topics], + # Devices + [f"@{x}" for x in self.devices], + ) + ] + ), params=NotifyPushy.urlencode(params), ) diff --git a/libs/apprise/plugins/qq.py b/libs/apprise/plugins/qq.py index 51a8c190f..b1962d9e4 100644 --- a/libs/apprise/plugins/qq.py +++ b/libs/apprise/plugins/qq.py @@ -121,6 +121,7 @@ class NotifyQQ(NotifyBase): data=payload, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if response.status_code != requests.codes.ok: diff --git a/libs/apprise/plugins/reddit.py b/libs/apprise/plugins/reddit.py index 5f1a595f8..bb53c6a62 100644 --- a/libs/apprise/plugins/reddit.py +++ b/libs/apprise/plugins/reddit.py @@ -178,9 +178,6 @@ class NotifyReddit(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "kind": { "name": _("Kind"), "type": "choice:string", @@ -227,6 +224,9 @@ class NotifyReddit(NotifyBase): "default": False, "map_to": "resubmit", }, + "to": { + "alias_of": "targets", + }, }, ) @@ -519,13 +519,17 @@ class NotifyReddit(NotifyBase): payload["flair_text"] = self.flair_text if kind == RedditMessageKind.LINK: - payload.update({ - "url": body, - }) + payload.update( + { + "url": body, + } + ) else: - payload.update({ - "text": body, - }) + payload.update( + { + "text": body, + } + ) postokay, _response = self._fetch(self.submit_url, payload=payload) # only toggle has_error flag if we had an error @@ -596,6 +600,7 @@ class NotifyReddit(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # We attempt to login again and retry the original request @@ -605,7 +610,6 @@ class NotifyReddit(NotifyBase): and self.__access_token and url != self.auth_url ): - # We had a problem status_str = NotifyReddit.http_response_code_lookup( r.status_code, REDDIT_HTTP_ERROR_MAP @@ -619,7 +623,8 @@ class NotifyReddit(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # We failed to authenticate with our token; login one more # time and retry this original request @@ -633,6 +638,7 @@ class NotifyReddit(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Get our JSON content if it's possible diff --git a/libs/apprise/plugins/resend.py b/libs/apprise/plugins/resend.py index d433b2d7d..08787eb8e 100644 --- a/libs/apprise/plugins/resend.py +++ b/libs/apprise/plugins/resend.py @@ -131,6 +131,26 @@ class NotifyResend(NotifyBase): template_args = dict( NotifyBase.template_args, **{ + "from": { + "name": _("From Address"), + "type": "string", + "map_to": "from_addr", + }, + "name": { + "name": _("From Name"), + "type": "string", + "map_to": "from_addr", + }, + "apikey": { + "name": _("API Key"), + "type": "string", + "map_to": "apikey", + }, + "reply": { + "name": _("Reply To"), + "type": "list:string", + "map_to": "reply_to", + }, "to": { "alias_of": "targets", }, @@ -142,27 +162,19 @@ class NotifyResend(NotifyBase): "name": _("Blind Carbon Copy"), "type": "list:string", }, - "reply": { - "name": _("Reply To"), - "type": "list:string", - "map_to": "reply_to", - }, - "from": { - "map_to": "from_addr", - }, - "name": { - "name": _("From Name"), - "map_to": "from_addr", - }, - "apikey": { - "map_to": "apikey", - }, }, ) def __init__( - self, apikey, from_addr, targets=None, cc=None, bcc=None, - reply_to=None, **kwargs): + self, + apikey, + from_addr, + targets=None, + cc=None, + bcc=None, + reply_to=None, + **kwargs, + ): """Initialize Notify Resend Object.""" super().__init__(**kwargs) @@ -213,7 +225,6 @@ class NotifyResend(NotifyBase): if targets: # Validate recipients (to:) and drop bad ones: for recipient in targets: - result = is_email(recipient) if result: self.targets.append(result["full_email"]) @@ -228,7 +239,6 @@ class NotifyResend(NotifyBase): # Validate recipients (cc:) and drop bad ones: for recipient in parse_emails(cc): - result = is_email(recipient) if result: self.cc.add(result["full_email"]) @@ -245,7 +255,6 @@ class NotifyResend(NotifyBase): # Validate recipients (bcc:) and drop bad ones: for recipient in parse_emails(bcc): - result = is_email(recipient) if result: self.bcc.add(result["full_email"]) @@ -293,15 +302,17 @@ class NotifyResend(NotifyBase): if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8", - ).replace(",", "%2C") - for e in self.cc - ]) + params["cc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.cc + ] + ) if len(self.bcc) > 0: # Handle our Blind Carbon Copy Addresses @@ -309,20 +320,23 @@ class NotifyResend(NotifyBase): if self.reply_to: # Handle our Reply-To Addresses - params["reply"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for its escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8", - ).replace(",", "%2C") - for e in self.reply_to - ]) + params["reply"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.reply_to + ] + ) # a simple boolean check as to whether we display our target emails # or not has_targets = not ( - len(self.targets) == 1 and self.targets[0] == self.from_addr[1]) + len(self.targets) == 1 and self.targets[0] == self.from_addr[1] + ) if self.from_addr[0] and self.from_addr[0] != self.app_id: # A custom name was provided @@ -367,8 +381,7 @@ class NotifyResend(NotifyBase): has_error = False # Prepare our from_name - self.from_addr[0] \ - if self.from_addr[0] is not False else self.app_id + self.from_addr[0] if self.from_addr[0] is not False else self.app_id payload_ = { "from": formataddr(self.from_addr, charset="utf-8"), @@ -396,16 +409,18 @@ class NotifyResend(NotifyBase): return False try: - attachments.append({ - "content": attachment.base64(), - "filename": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - "type": "application/octet-stream", - "disposition": "attachment", - }) + attachments.append( + { + "content": attachment.base64(), + "filename": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + "type": "application/octet-stream", + "disposition": "attachment", + } + ) except exception.AppriseException: # We could not access the attachment @@ -421,9 +436,11 @@ class NotifyResend(NotifyBase): ) # Append our attachments to the payload - payload_.update({ - "attachments": attachments, - }) + payload_.update( + { + "attachments": attachments, + } + ) targets = list(self.targets) while len(targets) > 0: @@ -441,15 +458,17 @@ class NotifyResend(NotifyBase): # Format our cc addresses to support the Name field cc = [ - formataddr((self.names.get(addr, False), addr), - charset="utf-8") + formataddr( + (self.names.get(addr, False), addr), charset="utf-8" + ) for addr in cc ] # Format our reply-to addresses to support the Name field reply_to = [ - formataddr((self.names.get(addr, False), addr), - charset="utf-8") + formataddr( + (self.names.get(addr, False), addr), charset="utf-8" + ) for addr in reply_to ] @@ -477,7 +496,8 @@ class NotifyResend(NotifyBase): f"(cert_verify={self.verify_certificate!r})" ) self.logger.debug( - "Resend Payload: %s", sanitize_payload(payload)) + "Resend Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -488,6 +508,7 @@ class NotifyResend(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -509,7 +530,8 @@ class NotifyResend(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -552,8 +574,7 @@ class NotifyResend(NotifyBase): # Prepare our API Key if "apikey" in results["qsd"] and len(results["qsd"]["apikey"]): - results["apikey"] = \ - NotifyResend.unquote(results["qsd"]["apikey"]) + results["apikey"] = NotifyResend.unquote(results["qsd"]["apikey"]) else: results["apikey"] = NotifyResend.unquote(results["user"]) @@ -567,21 +588,28 @@ class NotifyResend(NotifyBase): if results.get("host"): results["targets"].append( - NotifyResend.unquote(results["host"])) + NotifyResend.unquote(results["host"]) + ) else: # Prepare our From Email Address results["from_addr"] = "{}@{}".format( NotifyResend.unquote( results["password"] - if results["password"] else results["user"]), + if results["password"] + else results["user"] + ), NotifyResend.unquote(results["host"]), ) if "name" in results["qsd"] and len(results["qsd"]["name"]): - results["from_addr"] = formataddr(( - NotifyResend.unquote(results["qsd"]["name"]), - results["from_addr"]), charset="utf-8") + results["from_addr"] = formataddr( + ( + NotifyResend.unquote(results["qsd"]["name"]), + results["from_addr"], + ), + charset="utf-8", + ) # Acquire our targets results["targets"].extend(NotifyResend.split_path(results["fullpath"])) diff --git a/libs/apprise/plugins/revolt.py b/libs/apprise/plugins/revolt.py index 23b09a3d2..ba5152332 100644 --- a/libs/apprise/plugins/revolt.py +++ b/libs/apprise/plugins/revolt.py @@ -111,6 +111,7 @@ class NotifyRevolt(NotifyBase): "targets": { "name": _("Targets"), "type": "list:string", + "required": True, }, }, ) @@ -192,13 +193,17 @@ class NotifyRevolt(NotifyBase): ) if self.notify_format == NotifyFormat.MARKDOWN: - payload["embeds"] = [{ - "title": None if not title else title[0 : self.title_maxlen], - "description": body, - # Our color associated with our notification - "colour": self.color(notify_type), - "replies": None, - }] + payload["embeds"] = [ + { + "title": None + if not title + else title[0 : self.title_maxlen], + "description": body, + # Our color associated with our notification + "colour": self.color(notify_type), + "replies": None, + } + ] if image_url: payload["embeds"][0]["icon_url"] = image_url @@ -249,9 +254,7 @@ class NotifyRevolt(NotifyBase): # the same allowing this to role smoothly and set our throttle # accordingly wait = abs( - ( - self.ratelimit_reset - now + self.clock_skew - ).total_seconds() + (self.ratelimit_reset - now + self.clock_skew).total_seconds() ) # Default content response object @@ -267,6 +270,7 @@ class NotifyRevolt(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -299,11 +303,11 @@ class NotifyRevolt(NotifyBase): requests.codes.ok, requests.codes.no_content, ): - # Some details to debug by self.logger.debug( "Response Details:\r\n%r", - content if content else (r.content or b"")[:2000]) + content if content else (r.content or b"")[:2000], + ) # We had a problem status_str = NotifyBase.http_response_code_lookup( @@ -324,7 +328,6 @@ class NotifyRevolt(NotifyBase): r.status_code == requests.codes.too_many_requests and retries > 0 ): - # Try again return self._send( payload=payload, diff --git a/libs/apprise/plugins/ringcentral.py b/libs/apprise/plugins/ringcentral.py new file mode 100644 index 000000000..2205d4c37 --- /dev/null +++ b/libs/apprise/plugins/ringcentral.py @@ -0,0 +1,883 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Sign-up at https://ringcentral.com +# +# Create a server/bot app in the developer portal: +# https://developers.ringcentral.com/ +# +# Steps: +# 1. Log in and click "Create App". +# 2. Choose "REST API App" -> "Server/Bot (No UI)". +# 3. Under permissions enable "SMS" and "MMS" (MMS is needed for +# attachment support -- Apprise selects MMS automatically when +# attachments are present). +# 4. Copy the Client ID and Client Secret from the app credentials tab. +# +# Two authentication modes are supported: +# +# BASIC (password) mode -- use a RingCentral user password: +# ringc://SourcePhoneNo:Password@ClientID/ClientSecret +# ringc://SourcePhoneNo:Password@ClientID/ClientSecret/ToPhoneNo +# ringc://SourcePhoneNo:Password@ClientID/ClientSecret/To1/To2/ToN +# +# JWT mode -- use a JWT token generated in the developer portal: +# ringc://SourcePhoneNo:JWTToken@ClientID/ClientSecret +# ringc://SourcePhoneNo:JWTToken@ClientID/ClientSecret/ToPhoneNo +# +# Alternatively, supply credentials as query parameters: +# ringc://_?token=JWT&secret=ClientSecret&from=SourcePhoneNo +# +# When attachments are provided, Apprise automatically uses the MMS +# endpoint; otherwise SMS is used. +# +# API references: +# - https://developers.ringcentral.com/api-reference/SMS/createSMSMessage +# - https://developers.ringcentral.com/api-reference/MMS/createMMSMessage +# - https://developers.ringcentral.com/api-reference/OAuth-2.0/getToken + +import base64 +import contextlib +from json import dumps, loads +from time import time + +import requests + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from ..url import PrivacyMode +from ..utils.parse import is_phone_no, parse_phone_no, validate_regex +from .base import NotifyBase + + +class RingCentralAuthMode: + """Authentication modes supported by RingCentral.""" + + # Username + password credential flow + BASIC = "basic" + + # JWT bearer token credential flow + JWT = "jwt" + + +# Valid auth mode choices +RINGCENTRAL_AUTH_MODES = ( + RingCentralAuthMode.BASIC, + RingCentralAuthMode.JWT, +) + + +class RingCentralEnvironment: + """RingCentral API environment targets.""" + + # Live production environment + PRODUCTION = "prod" + + # Sandbox / devtest environment + SANDBOX = "sandbox" + + +# Valid environment choices +RINGCENTRAL_ENVIRONMENTS = ( + RingCentralEnvironment.PRODUCTION, + RingCentralEnvironment.SANDBOX, +) + +# Maps environment choice to the URL infix used by the API +RINGCENTRAL_ENV_URL_SUFFIX = { + RingCentralEnvironment.PRODUCTION: "", + RingCentralEnvironment.SANDBOX: ".devtest", +} + + +class NotifyRingCentral(NotifyBase): + """A wrapper for RingCentral Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "RingCentral" + + # The services URL + service_url = "https://ringcentral.com/" + + # The default protocol + secure_protocol = "ringc" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/ringcentral/" + + # Attachments are supported; MMS is selected automatically when present + attachment_support = True + + # SMS endpoint (environment is substituted at send time) + notify_url_sms = ( + "https://platform{environment}.ringcentral.com/" + "restapi/v1.0/account/~/extension/~/sms" + ) + + # MMS endpoint (used automatically when attachments are present) + notify_url_mms = ( + "https://platform{environment}.ringcentral.com/" + "restapi/v1.0/account/~/extension/~/mms" + ) + + # OAuth token endpoint + access_token_url = ( + "https://platform{environment}.ringcentral.com/restapi/oauth/token" + ) + + # Token revocation endpoint + revoke_token_url = ( + "https://platform{environment}.ringcentral.com/restapi/oauth/revoke" + ) + + # Access token lifetime in seconds (60 minutes) + access_token_ttl = 3600 + + # Refresh token lifetime in seconds (1 week) + refresh_token_ttl = 604800 + + # SMS body length limit + body_maxlen = 160 + + # Titles are not supported; prepend to body instead + title_maxlen = 0 + + # Define object URL templates + templates = ( + # source:token@client_id/secret (no targets -- loopback to source) + "{schema}://{from_phone}:{token}@{client_id}/{secret}/", + # source:token@client_id/secret/target[/target...] + "{schema}://{from_phone}:{token}@{client_id}/{secret}/{targets}", + # token@client_id/secret/source (source in path, JWT/password first) + "{schema}://{token}@{client_id}/{secret}/{from_phone}", + # token@client_id/secret/source/target[/target...] + "{schema}://{token}@{client_id}/{secret}/{from_phone}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "token": { + "name": _("Token / Password"), + "type": "string", + "required": True, + "private": True, + }, + "client_id": { + "name": _("Client ID"), + "type": "string", + "required": True, + "regex": (r"^[a-z0-9_-]+$", "i"), + "private": True, + }, + "secret": { + "name": _("Client Secret"), + "type": "string", + "required": True, + "regex": (r"^[a-z0-9_-]+$", "i"), + "private": True, + "map_to": "client_secret", + }, + "from_phone": { + "name": _("From Phone No"), + "type": "string", + "required": True, + "map_to": "source", + }, + "target_phone": { + "name": _("Target Phone No"), + "type": "string", + "prefix": "+", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "to": { + "alias_of": "targets", + }, + "from": { + "alias_of": "from_phone", + }, + "source": { + "alias_of": "from_phone", + }, + "env": { + "name": _("Environment"), + "type": "choice:string", + "values": RINGCENTRAL_ENVIRONMENTS, + "default": RingCentralEnvironment.PRODUCTION, + "map_to": "environment", + }, + "token": { + "alias_of": "token", + }, + "secret": { + "alias_of": "secret", + }, + "mode": { + "name": _("Authentication Mode"), + "type": "choice:string", + "values": RINGCENTRAL_AUTH_MODES, + "map_to": "mode", + }, + }, + ) + + def __init__( + self, + source, + targets=None, + environment=None, + token=None, + client_id=None, + client_secret=None, + mode=None, + **kwargs, + ): + """Initialize RingCentral Object.""" + super().__init__(**kwargs) + + # Internal OAuth state + self._access_token = None + self._expire_time = 0.0 + self._scope = None + self._owner = None + self._endpoint_id = None + + # Resolve authentication mode + if isinstance(mode, str): + _mode = mode.lower().strip() + match = ( + next( + (m for m in RINGCENTRAL_AUTH_MODES if m.startswith(_mode)), + None, + ) + if _mode + else None + ) + + if not match: + msg = ( + "An invalid RingCentral Authentication Mode " + "({}) was specified.".format(mode) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store resolved mode + self.mode = match + + else: + # Default to BASIC (password) auth + self.mode = RingCentralAuthMode.BASIC + + # Validate Client ID + self.client_id = validate_regex( + client_id, *self.template_tokens["client_id"]["regex"] + ) + if not self.client_id: + msg = ( + "An invalid RingCentral Client ID ({}) was specified.".format( + client_id + ) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Validate Client Secret + self.client_secret = validate_regex( + client_secret, *self.template_tokens["secret"]["regex"] + ) + if not self.client_secret: + msg = ( + "An invalid RingCentral Client Secret " + "({}) was specified.".format(client_secret) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Validate JWT token (only strict regex check in JWT mode) + if self.mode == RingCentralAuthMode.JWT: + self.token = validate_regex(token, r"^[a-z0-9._-]+$", "i") + if not self.token: + msg = ( + "An invalid RingCentral JWT Token " + "({}) was specified.".format(token) + ) + self.logger.warning(msg) + raise TypeError(msg) + else: + # BASIC mode -- token holds the user password (not validated) + self.token = token + + # Validate source (sender) phone number + result = is_phone_no(source) + if not result: + msg = ( + "The RingCentral source (From) phone # " + "({}) is invalid.".format(source) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store normalised source digits only + self.source = result["full"] + + # Resolve environment + _environment = ( + environment.lower().strip() + if isinstance(environment, str) + else RingCentralEnvironment.PRODUCTION + ) + match = ( + next( + ( + e + for e in RINGCENTRAL_ENVIRONMENTS + if e.startswith(_environment) + ), + None, + ) + if _environment + else None + ) + if not match: + msg = ( + "An invalid RingCentral environment " + "({}) was specified.".format(environment) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store resolved environment + self.environment = match + + # Parse target phone numbers, dropping invalid ones + self.targets = [] + for target in parse_phone_no(targets): + result = is_phone_no(target) + if not result: + self.logger.warning( + "Dropped invalid RingCentral phone # (%s) specified.", + target, + ) + continue + + # Store normalised digits + self.targets.append(result["full"]) + + return + + def login(self): + """Authenticate with the RingCentral OAuth token endpoint.""" + + if self._expire_time >= time(): + # Already authenticated + return True + + # Build the token request URL + url = self.access_token_url.format( + environment=RINGCENTRAL_ENV_URL_SUFFIX[self.environment], + ) + + # Encode client credentials for Basic auth header + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic {}".format( + str( + base64.b64encode( + bytes( + self.client_id + ":" + self.client_secret, + "utf8", + ) + ), + "utf8", + ) + ), + } + + # Clear any stale token state + self._access_token = None + self._scope = None + self._owner = None + self._endpoint_id = None + + # Build grant payload for the selected auth mode + if self.mode == RingCentralAuthMode.JWT: + payload = { + "grant_type": ("urn:ietf:params:oauth:grant-type:jwt-bearer"), + "assertion": self.token, + } + else: + # BASIC password grant + payload = { + "grant_type": "password", + "username": "+" + self.source, + "password": self.token, + "access_token_ttl": self.access_token_ttl, + "refresh_token_ttl": self.refresh_token_ttl, + } + + # Send authentication request (no throttle for auth calls) + status, response = self._send( + url, + payload, + headers, + name="auth.login", + throttle=False, + ) + + if status: + # Store token and expiry + self._access_token = response.get("access_token") + self._expire_time = time() + response.get("expires_in", 0) + self._scope = response.get("scope") + self._owner = response.get("owner_id") + self._endpoint_id = response.get("endpoint_id") + + # A 200 OK without an access_token is not a usable login + return bool(self._access_token) + + def logout(self): + """Revoke the current access token.""" + + if not self._access_token or self._expire_time < time(): + # Nothing to revoke + return + + # Build the revocation URL + url = self.revoke_token_url.format( + environment=RINGCENTRAL_ENV_URL_SUFFIX[self.environment], + ) + + # Encode client credentials + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic {}".format( + str( + base64.b64encode( + bytes( + self.client_id + ":" + self.client_secret, + "utf8", + ) + ), + "utf8", + ) + ), + } + + # Revocation payload + payload = {"token": self._access_token} + + # Send revocation request (no throttle for auth calls) + self._send( + url, + payload, + headers, + name="auth.logout", + throttle=False, + ) + + # Clear token state + self._access_token = None + self._expire_time = 0.0 + self._scope = None + self._owner = None + self._endpoint_id = None + + return + + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): + """Perform RingCentral Notification.""" + + # Error tracking across multiple targets + has_error = False + + # Authenticate before sending + if not self.login(): + self.logger.warning( + "RingCentral %s authentication failed.", self.mode + ) + return False + + # Bearer auth header token (reused across send calls) + auth_header = "Bearer {}".format(self._access_token) + + # Auto-select MMS when attachments are present, SMS otherwise + use_mms = bool(attach and self.attachment_support and len(attach)) + + # Build the endpoint URL for the selected message type + if use_mms: + notify_url = self.notify_url_mms.format( + environment=RINGCENTRAL_ENV_URL_SUFFIX[self.environment], + ) + else: + notify_url = self.notify_url_sms.format( + environment=RINGCENTRAL_ENV_URL_SUFFIX[self.environment], + ) + + # If no targets specified, send to own number (loopback test) + targets = list(self.targets) if self.targets else [self.source] + + for target in targets: + # Message metadata for this recipient + metadata = { + "from": {"phoneNumber": "+" + self.source}, + "to": [{"phoneNumber": "+" + target}], + "text": body, + } + + if use_mms: + # MMS: multipart/form-data -- JSON metadata + attachments + # Content-Type is set automatically by requests for multipart + headers = { + "Authorization": auth_header, + } + files = [ + ( + "json", + (None, dumps(metadata), "application/json"), + ), + ] + + # Track opened handles for guaranteed cleanup + handles = [] + attach_ok = True + + try: + # Build attachment parts; abort target on first failure + for attachment in attach: + # Verify the attachment is accessible + if not attachment: + self.logger.warning( + "Could not access RingCentral attachment %s.", + attachment.url(privacy=True), + ) + attach_ok = False + break + + # Open handle; guard against I/O errors + try: + handle = attachment.open() + except OSError as exc: + self.logger.warning( + "An I/O error occurred reading " + "RingCentral attachment %s.", + attachment.name, + ) + self.logger.debug("I/O Exception: %s", str(exc)) + attach_ok = False + break + + # Register handle for cleanup + handles.append(handle) + # Append the attachment part + files.append( + ( + "attachment", + ( + attachment.name, + handle, + attachment.mimetype, + ), + ) + ) + + if not attach_ok: + # Skip this target; mark overall failure + has_error = True + continue + + # Send MMS notification (multipart) + status, _ = self._send( + notify_url, None, headers, files=files + ) + + finally: + # Close all handles whether we succeeded or failed + for handle in handles: + handle.close() + + else: + # SMS: plain JSON payload + headers = { + "Content-Type": "application/json", + "Authorization": auth_header, + } + status, _ = self._send(notify_url, dumps(metadata), headers) + + if status: + self.logger.info( + "Sent RingCentral notification to %s.", target + ) + else: + # Mark failure and continue to remaining targets + has_error = True + + return not has_error + + def _send( + self, + url, + payload, + headers, + name="notification", + throttle=True, + files=None, + ): + """POST helper shared by login, logout, and send calls.""" + + headers.update( + { + # Minimum required headers + "User-Agent": self.app_id, + "Accept": "application/json", + } + ) + + self.logger.debug( + "RingCentral POST URL: %s (cert_verify=%s)", + url, + self.verify_certificate, + ) + self.logger.debug("RingCentral Payload: %s", payload) + + if throttle: + # Throttle notification calls; auth calls skip this + self.throttle() + + content = None + try: + r = requests.post( + url, + data=payload, + headers=headers, + files=files, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + self.logger.trace("RingCentral Response: %s", r.content) + + try: + content = loads(r.content) + + except (AttributeError, TypeError, ValueError): + # ValueError = r.content is unparsable + # TypeError = r.content is None + # AttributeError = r is None + content = {} + + if r.status_code != requests.codes.ok: + # We had a failure + status_str = NotifyRingCentral.http_response_code_lookup( + r.status_code + ) + + self.logger.warning( + "Failed to send RingCentral %s: %s%serror=%s.", + name, + status_str, + ", " if status_str else "", + r.status_code, + ) + self.logger.debug("Response Details:\r\n%s", r.content) + + else: + # We were successful + return (True, content) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending RingCentral %s.", + name, + ) + self.logger.debug("Socket Exception: %s", str(e)) + + return (False, content) + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique from + another similar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol, + self.client_id, + self.client_secret, + self.token, + self.source, + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # Define any URL parameters + params = { + "env": str(self.environment), + "mode": str(self.mode), + } + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + return ( + "{schema}://{source}:{token}@{client_id}" + "/{client_secret}/{targets}/?{params}".format( + schema=self.secure_protocol, + source=NotifyRingCentral.quote(self.source, safe=""), + token=self.pprint( + self.token, + privacy, + mode=( + PrivacyMode.Secret + if self.mode == RingCentralAuthMode.BASIC + else PrivacyMode.Outer + ), + safe="", + ), + client_id=self.pprint(self.client_id, privacy, safe=""), + client_secret=self.pprint( + self.client_secret, + privacy, + mode=PrivacyMode.Secret, + safe="", + ), + targets="/".join( + NotifyRingCentral.quote(x, safe="") for x in self.targets + ), + params=NotifyRingCentral.urlencode(params), + ) + ) + + def __len__(self): + """Returns the number of targets associated with this notification.""" + return len(self.targets) if self.targets else 1 + + def __del__(self): + """Destructor -- revoke the auth token on cleanup.""" + with contextlib.suppress(Exception): + self.logout() + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow + us to re-instantiate this object.""" + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Path elements: first segment is client_secret, rest are targets + results["targets"] = NotifyRingCentral.split_path(results["fullpath"]) + + # Host is the client_id + results["client_id"] = NotifyRingCentral.unquote(results["host"]) + + # First path segment is the client_secret + results["client_secret"] = ( + results["targets"].pop(0) if results["targets"] else None + ) + + # Determine source phone and token from URL user[:password] form + if not results.get("password"): + # ringc://token@client_id/secret/source_phone[/targets] + results["source"] = ( + results["targets"].pop(0) if results["targets"] else None + ) + results["token"] = NotifyRingCentral.unquote(results["user"]) + else: + # ringc://source_phone:token@client_id/secret[/targets] + results["source"] = NotifyRingCentral.unquote(results["user"]) + results["token"] = NotifyRingCentral.unquote(results["password"]) + + # Environment (from ?env= query parameter) + if "env" in results["qsd"] and results["qsd"]["env"]: + results["environment"] = NotifyRingCentral.unquote( + results["qsd"]["env"] + ) + + # ?token= overrides the token extracted from the URL path; + # must be applied before mode auto-detection so that a JWT + # supplied via query parameter is detected correctly + if "token" in results["qsd"] and results["qsd"]["token"]: + results["token"] = NotifyRingCentral.unquote( + results["qsd"]["token"] + ) + + # Auth mode: ?mode= wins; otherwise auto-detect from token length + if "mode" in results["qsd"] and results["qsd"]["mode"]: + results["mode"] = NotifyRingCentral.unquote(results["qsd"]["mode"]) + elif results.get("token") and len(results["token"]) > 60: + # JWT tokens are significantly longer than passwords + results["mode"] = RingCentralAuthMode.JWT + else: + results["mode"] = RingCentralAuthMode.BASIC + + # ?secret= overrides the client secret extracted from the path + if "secret" in results["qsd"] and results["qsd"]["secret"]: + results["client_secret"] = NotifyRingCentral.unquote( + results["qsd"]["secret"] + ) + + # ?from= / ?source= override source phone number + if "from" in results["qsd"] and results["qsd"]["from"]: + results["source"] = NotifyRingCentral.unquote( + results["qsd"]["from"] + ) + if "source" in results["qsd"] and results["qsd"]["source"]: + results["source"] = NotifyRingCentral.unquote( + results["qsd"]["source"] + ) + + # ?to= appends additional target phone numbers + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyRingCentral.parse_phone_no( + results["qsd"]["to"] + ) + + return results diff --git a/libs/apprise/plugins/rocketchat.py b/libs/apprise/plugins/rocketchat.py index 2d248a62a..835cc1472 100644 --- a/libs/apprise/plugins/rocketchat.py +++ b/libs/apprise/plugins/rocketchat.py @@ -138,6 +138,7 @@ class NotifyRocketChat(NotifyBase): }, "token": { "name": _("API Token"), + "type": "string", "map_to": "password", "private": True, }, @@ -265,10 +266,12 @@ class NotifyRocketChat(NotifyBase): if self.mode == RocketChatAuthMode.TOKEN: # Set our headers for further communication - self.headers.update({ - "X-User-Id": self.user, - "X-Auth-Token": self.password, - }) + self.headers.update( + { + "X-User-Id": self.user, + "X-Auth-Token": self.password, + } + ) # Validate recipients and drop bad ones: for recipient in parse_list(targets): @@ -385,17 +388,19 @@ class NotifyRocketChat(NotifyBase): if self.port is None or self.port == default_port else f":{self.port}" ), - targets="/".join([ - NotifyRocketChat.quote(x, safe="@#") - for x in chain( - # Channels are prefixed with a pound/hashtag symbol - [f"#{x}" for x in self.channels], - # Rooms are as is - self.rooms, - # Users - [f"@{x}" for x in self.users], - ) - ]), + targets="/".join( + [ + NotifyRocketChat.quote(x, safe="@#") + for x in chain( + # Channels are prefixed with a pound/hashtag symbol + [f"#{x}" for x in self.channels], + # Rooms are as is + self.rooms, + # Users + [f"@{x}" for x in self.users], + ) + ] + ), params=NotifyRocketChat.urlencode(params), ) @@ -456,7 +461,6 @@ class NotifyRocketChat(NotifyBase): if not self._send( payload, notify_type=notify_type, path=path, **kwargs ): - # toggle flag has_error = True @@ -490,7 +494,6 @@ class NotifyRocketChat(NotifyBase): payload["channel"] = channel if not self._send(payload, notify_type=notify_type, **kwargs): - # toggle flag has_error = True @@ -503,7 +506,6 @@ class NotifyRocketChat(NotifyBase): payload["roomId"] = room if not self._send(payload, notify_type=notify_type, **kwargs): - # toggle flag has_error = True @@ -544,10 +546,12 @@ class NotifyRocketChat(NotifyBase): headers = self.headers.copy() # Apply minimum headers - headers.update({ - "User-Agent": self.app_id, - "Content-Type": "application/json", - }) + headers.update( + { + "User-Agent": self.app_id, + "Content-Type": "application/json", + } + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -559,6 +563,7 @@ class NotifyRocketChat(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -577,7 +582,8 @@ class NotifyRocketChat(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -613,6 +619,7 @@ class NotifyRocketChat(NotifyBase): data=payload, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -684,6 +691,7 @@ class NotifyRocketChat(NotifyBase): headers=self.headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem diff --git a/libs/apprise/plugins/rsyslog.py b/libs/apprise/plugins/rsyslog.py index ef8a5789d..2b1fe35c7 100644 --- a/libs/apprise/plugins/rsyslog.py +++ b/libs/apprise/plugins/rsyslog.py @@ -246,8 +246,11 @@ class NotifyRSyslog(NotifyBase): ) priority = SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8 - payload = f"<{priority}>- {os.getpid()} {body}" \ - if self.log_pid else f"<{priority}>- {body}" + payload = ( + f"<{priority}>- {os.getpid()} {body}" + if self.log_pid + else f"<{priority}>- {body}" + ) # send UDP packet to upstream server self.logger.debug( diff --git a/libs/apprise/plugins/ryver.py b/libs/apprise/plugins/ryver.py index b3ec0aa4e..bcd4ea00d 100644 --- a/libs/apprise/plugins/ryver.py +++ b/libs/apprise/plugins/ryver.py @@ -244,6 +244,7 @@ class NotifyRyver(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -259,7 +260,8 @@ class NotifyRyver(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -270,8 +272,7 @@ class NotifyRyver(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending" - f" Ryver:{self.organization} " - + "notification." + f" Ryver:{self.organization} " + "notification." ) self.logger.debug(f"Socket Exception: {e!s}") return False diff --git a/libs/apprise/plugins/sendgrid.py b/libs/apprise/plugins/sendgrid.py index 556e4eee9..b966477ef 100644 --- a/libs/apprise/plugins/sendgrid.py +++ b/libs/apprise/plugins/sendgrid.py @@ -140,6 +140,12 @@ class NotifySendGrid(NotifyBase): template_args = dict( NotifyBase.template_args, **{ + "template": { + # Template ID + # The template ID is 64 characters with one dash (d-uuid) + "name": _("Template"), + "type": "string", + }, "to": { "alias_of": "targets", }, @@ -151,12 +157,6 @@ class NotifySendGrid(NotifyBase): "name": _("Blind Carbon Copy"), "type": "list:string", }, - "template": { - # Template ID - # The template ID is 64 characters with one dash (d-uuid) - "name": _("Template"), - "type": "string", - }, }, ) @@ -220,7 +220,6 @@ class NotifySendGrid(NotifyBase): # Validate recipients (to:) and drop bad ones: if targets: for recipient in parse_list(targets): - result = is_email(recipient) if result: self.targets.append(result["full_email"]) @@ -235,7 +234,6 @@ class NotifySendGrid(NotifyBase): # Validate recipients (cc:) and drop bad ones: for recipient in parse_list(cc): - result = is_email(recipient) if result: self.cc.add(result["full_email"]) @@ -247,7 +245,6 @@ class NotifySendGrid(NotifyBase): # Validate recipients (bcc:) and drop bad ones: for recipient in parse_list(bcc): - result = is_email(recipient) if result: self.bcc.add(result["full_email"]) @@ -328,7 +325,8 @@ class NotifySendGrid(NotifyBase): if not self.targets: # There is no one to email; we're done self.logger.warning( - "There are no SendGrid email recipients to notify") + "There are no SendGrid email recipients to notify" + ) return False headers = { @@ -342,10 +340,12 @@ class NotifySendGrid(NotifyBase): # A Simple Email Payload Template payload_ = { - "personalizations": [{ - # Placeholder - "to": [{"email": None}], - }], + "personalizations": [ + { + # Placeholder + "to": [{"email": None}], + } + ], "from": { "email": self.from_email, }, @@ -353,14 +353,16 @@ class NotifySendGrid(NotifyBase): # set a default with at least 1 character or SendGrid will deny # our request "subject": title if title else self.default_empty_subject, - "content": [{ - "type": ( - "text/plain" - if self.notify_format == NotifyFormat.TEXT - else "text/html" - ), - "value": body, - }], + "content": [ + { + "type": ( + "text/plain" + if self.notify_format == NotifyFormat.TEXT + else "text/html" + ), + "value": body, + } + ], } if attach and self.attachment_support: @@ -378,16 +380,18 @@ class NotifySendGrid(NotifyBase): return False try: - attachments.append({ - "content": attachment.base64(), - "filename": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - "type": "application/octet-stream", - "disposition": "attachment", - }) + attachments.append( + { + "content": attachment.base64(), + "filename": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + "type": "application/octet-stream", + "disposition": "attachment", + } + ) except exception.AppriseException: # We could not access the attachment @@ -403,9 +407,11 @@ class NotifySendGrid(NotifyBase): ) # Append our attachments to the payload - payload_.update({ - "attachments": attachments, - }) + payload_.update( + { + "attachments": attachments, + } + ) if self.template: payload_["template_id"] = self.template @@ -454,7 +460,8 @@ class NotifySendGrid(NotifyBase): f"(cert_verify={self.verify_certificate!r})" ) self.logger.debug( - "SendGrid Payload: %s", sanitize_payload(payload)) + "SendGrid Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -465,6 +472,7 @@ class NotifySendGrid(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -486,7 +494,8 @@ class NotifySendGrid(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/sendpulse.py b/libs/apprise/plugins/sendpulse.py index 5ddee7d13..aad9f108d 100644 --- a/libs/apprise/plugins/sendpulse.py +++ b/libs/apprise/plugins/sendpulse.py @@ -103,71 +103,78 @@ class NotifySendPulse(NotifyBase): ) # Define our template arguments - template_tokens = dict(NotifyBase.template_tokens, **{ - "user": { - "name": _("User Name"), - "type": "string", + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "user": { + "name": _("User Name"), + "type": "string", + "required": True, + }, + "host": { + "name": _("Domain"), + "type": "string", + "required": True, + }, + "client_id": { + "name": _("Client ID"), + "type": "string", + "required": True, + "private": True, + "regex": (r"^[A-Z0-9._-]+$", "i"), + }, + "client_secret": { + "name": _("Client Secret"), + "type": "string", + "required": True, + "private": True, + "regex": (r"^[A-Z0-9._-]+$", "i"), + }, + "target_email": { + "name": _("Target Email"), + "type": "string", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, }, - "host": { - "name": _("Domain"), - "type": "string", - "required": True, - }, - "client_id": { - "name": _("Client ID"), - "type": "string", - "required": True, - "private": True, - "regex": (r"^[A-Z0-9._-]+$", "i"), - }, - "client_secret": { - "name": _("Client Secret"), - "type": "string", - "required": True, - "private": True, - "regex": (r"^[A-Z0-9._-]+$", "i"), - }, - "target_email": { - "name": _("Target Email"), - "type": "string", - "map_to": "targets", - }, - "targets": { - "name": _("Targets"), - "type": "list:string", - }, - }) + ) # Define our template arguments - template_args = dict(NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", + template_args = dict( + NotifyBase.template_args, + **{ + "from": { + "name": _("From Email"), + "type": "string", + "map_to": "from_addr", + }, + "template": { + # The template ID is an integer + "name": _("Template ID"), + "type": "int", + }, + "id": { + "alias_of": "client_id", + }, + "secret": { + "alias_of": "client_secret", + }, + "to": { + "alias_of": "targets", + }, + "cc": { + "name": _("Carbon Copy"), + "type": "list:string", + }, + "bcc": { + "name": _("Blind Carbon Copy"), + "type": "list:string", + }, }, - "from": { - "name": _("From Email"), - "type": "string", - "map_to": "from_addr", - }, - "cc": { - "name": _("Carbon Copy"), - "type": "list:string", - }, - "bcc": { - "name": _("Blind Carbon Copy"), - "type": "list:string", - }, - "template": { - # The template ID is an integer - "name": _("Template ID"), - "type": "int", - }, - "id": { - "alias_of": "client_id", - }, - "secret": { - "alias_of": "client_secret", - } - }) + ) # Support Template Dynamic Variables (Substitutions) template_kwargs = { @@ -177,9 +184,18 @@ class NotifySendPulse(NotifyBase): }, } - def __init__(self, client_id, client_secret, from_addr=None, targets=None, - cc=None, bcc=None, template=None, template_data=None, - **kwargs): + def __init__( + self, + client_id, + client_secret, + from_addr=None, + targets=None, + cc=None, + bcc=None, + template=None, + template_data=None, + **kwargs, + ): """ Initialize Notify SendPulse Object """ @@ -194,10 +210,13 @@ class NotifySendPulse(NotifyBase): if self.user: if self.host: # Prepare the bases of our email - from_addr_ = [from_addr_[0], "{}@{}".format( - re.split(r"[\s@]+", self.user)[0], - self.host, - )] + from_addr_ = [ + from_addr_[0], + "{}@{}".format( + re.split(r"[\s@]+", self.user)[0], + self.host, + ), + ] else: result = is_email(self.user) @@ -205,15 +224,17 @@ class NotifySendPulse(NotifyBase): # Prepare the bases of our email and include domain self.host = result["domain"] from_addr_ = [ - result["name"] if result["name"] - else from_addr_[0], self.user] + result["name"] if result["name"] else from_addr_[0], + self.user, + ] if isinstance(from_addr, str): result = is_email(from_addr) if result: from_addr_ = ( result["name"] if result["name"] else from_addr_[0], - result["full_email"]) + result["full_email"], + ) else: # Only update the string but use the already detected info from_addr_[0] = from_addr @@ -223,7 +244,9 @@ class NotifySendPulse(NotifyBase): # Parse Source domain based on from_addr msg = "Invalid ~From~ email specified: {}".format( "{} <{}>".format(from_addr_[0], from_addr_[1]) - if from_addr_[0] else "{}".format(from_addr_[1])) + if from_addr_[0] + else "{}".format(from_addr_[1]) + ) self.logger.warning(msg) raise TypeError(msg) @@ -233,19 +256,24 @@ class NotifySendPulse(NotifyBase): # Client ID self.client_id = validate_regex( - client_id, *self.template_tokens["client_id"]["regex"]) + client_id, *self.template_tokens["client_id"]["regex"] + ) if not self.client_id: - msg = "An invalid SendPulse Client ID " \ - "({}) was specified.".format(client_id) + msg = "An invalid SendPulse Client ID ({}) was specified.".format( + client_id + ) self.logger.warning(msg) raise TypeError(msg) # Client Secret self.client_secret = validate_regex( - client_secret, *self.template_tokens["client_secret"]["regex"]) + client_secret, *self.template_tokens["client_secret"]["regex"] + ) if not self.client_secret: - msg = "An invalid SendPulse Client Secret " \ - "({}) was specified.".format(client_secret) + msg = ( + "An invalid SendPulse Client Secret " + "({}) was specified.".format(client_secret) + ) self.logger.warning(msg) raise TypeError(msg) @@ -267,14 +295,17 @@ class NotifySendPulse(NotifyBase): except (TypeError, ValueError): # Not a valid integer; ignore entry - err = "The SendPulse Template ID specified ({}) is invalid."\ - .format(template) + err = ( + "The SendPulse Template ID specified" + f" ({template}) is invalid." + ) self.logger.warning(err) raise TypeError(err) from None # Now our dynamic template data (if defined) - self.template_data = template_data \ - if isinstance(template_data, dict) else {} + self.template_data = ( + template_data if isinstance(template_data, dict) else {} + ) if targets: # Validate recipients (to:) and drop bad ones: @@ -287,8 +318,9 @@ class NotifySendPulse(NotifyBase): continue self.logger.warning( - "Dropped invalid To email " - "({}) specified.".format(recipient), + "Dropped invalid To email ({}) specified.".format( + recipient + ), ) else: @@ -297,7 +329,6 @@ class NotifySendPulse(NotifyBase): # Validate recipients (cc:) and drop bad ones: for recipient in parse_emails(cc): - result = is_email(recipient) if result: self.cc.add(result["full_email"]) @@ -306,13 +337,13 @@ class NotifySendPulse(NotifyBase): continue self.logger.warning( - "Dropped invalid Carbon Copy email " - "({}) specified.".format(recipient), + "Dropped invalid Carbon Copy email ({}) specified.".format( + recipient + ), ) # Validate recipients (bcc:) and drop bad ones: for recipient in parse_emails(bcc): - result = is_email(recipient) if result: self.bcc.add(result["full_email"]) @@ -350,23 +381,31 @@ class NotifySendPulse(NotifyBase): if len(self.cc) > 0: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8").replace(",", "%2C") - for e in self.cc]) + params["cc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.cc + ] + ) if len(self.bcc) > 0: # Handle our Blind Carbon Copy Addresses - params["bcc"] = ",".join([ - formataddr( - (self.names.get(e, False), e), - # Swap comma for it's escaped url code (if detected) since - # we're using that as a delimiter - charset="utf-8").replace(",", "%2C") - for e in self.bcc]) + params["bcc"] = ",".join( + [ + formataddr( + (self.names.get(e, False), e), + # Swap comma for its escaped url code (if + # detected) since we use it as a delimiter + charset="utf-8", + ).replace(",", "%2C") + for e in self.bcc + ] + ) if self.template: # Handle our Template ID if if was specified @@ -378,20 +417,25 @@ class NotifySendPulse(NotifyBase): # Append our template_data into our parameter list params.update( - {"+{}".format(k): v for k, v in self.template_data.items()}) + {"+{}".format(k): v for k, v in self.template_data.items()} + ) # a simple boolean check as to whether we display our target emails # or not - has_targets = \ - not (len(self.targets) == 1 and self.targets[0] == self.from_addr) + has_targets = not ( + len(self.targets) == 1 and self.targets[0] == self.from_addr + ) return "{schema}://{source}/{cid}/{secret}/{targets}?{params}".format( schema=self.secure_protocol, source=self.from_addr, cid=self.pprint(self.client_id, privacy, safe=""), secret=self.pprint(self.client_secret, privacy, safe=""), - targets="" if not has_targets else "/".join( - [NotifySendPulse.quote(x, safe="") for x in self.targets]), + targets="" + if not has_targets + else "/".join( + [NotifySendPulse.quote(x, safe="") for x in self.targets] + ), params=NotifySendPulse.urlencode(params), ) @@ -420,32 +464,42 @@ class NotifySendPulse(NotifyBase): # If we get here, we're authenticated try: - expires = \ - int(response.get("expires_in")) - self.token_expiry_edge + expires = int(response.get("expires_in")) - self.token_expiry_edge if expires <= self.token_expiry_edge: self.logger.error( - "SendPulse token expiry limit returned was invalid") + "SendPulse token expiry limit returned was invalid" + ) return False elif expires > self.token_expiry: self.logger.warning( - "SendPulse token expiry limit fixed to: {}s" - .format(self.token_expiry)) + "SendPulse token expiry limit fixed to: {}s".format( + self.token_expiry + ) + ) expires = self.token_expiry - self.token_expiry_edge except (AttributeError, TypeError, ValueError): # expires_in was not an integer self.logger.warning( "SendPulse token expiry limit presumed to be: {}s".format( - self.token_expiry)) + self.token_expiry + ) + ) expires = self.token_expiry - self.token_expiry_edge self.store.set("access_token", access_token, expires=expires) return access_token - def send(self, body, title="", notify_type=NotifyType.INFO, attach=None, - **kwargs): + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): """ Perform SendPulse Notification """ @@ -476,11 +530,16 @@ class NotifySendPulse(NotifyBase): # Prepare Email Message if self.notify_format == NotifyFormat.HTML: # HTML - payload_["email"].update({ - "text": convert_between( - NotifyFormat.HTML, NotifyFormat.TEXT, body), - "html": base64.b64encode(body.encode("utf-8")).decode("ascii"), - }) + payload_["email"].update( + { + "text": convert_between( + NotifyFormat.HTML, NotifyFormat.TEXT, body + ), + "html": base64.b64encode(body.encode("utf-8")).decode( + "ascii" + ), + } + ) else: # Text payload_["email"]["text"] = body @@ -495,36 +554,49 @@ class NotifySendPulse(NotifyBase): # We could not access the attachment self.logger.error( "Could not access SendPulse attachment {}.".format( - attachment.url(privacy=True))) + attachment.url(privacy=True) + ) + ) return False try: attachments[ - attachment.name if attachment.name - else f"file{no:03}.dat"] = attachment.base64() + attachment.name + if attachment.name + else f"file{no:03}.dat" + ] = attachment.base64() except exception.AppriseException: # We could not access the attachment self.logger.error( "Could not access SendPulse attachment {}.".format( - attachment.url(privacy=True))) + attachment.url(privacy=True) + ) + ) return False self.logger.debug( "Appending SendPulse attachment {}".format( - attachment.url(privacy=True))) + attachment.url(privacy=True) + ) + ) # Append our attachments to the payload - payload_["email"].update({ - "attachments_binary": attachments, - }) + payload_["email"].update( + { + "attachments_binary": attachments, + } + ) if self.template: - payload_["email"].update({ - "template": { - "id": self.template, - "variables": self.template_data, - }}) + payload_["email"].update( + { + "template": { + "id": self.template, + "variables": self.template_data, + } + } + ) targets = list(self.targets) while len(targets) > 0: @@ -537,15 +609,13 @@ class NotifySendPulse(NotifyBase): # below code prepares this by ensuring the target isn't in the cc # list or bcc list. It also makes sure the cc list does not contain # any of the bcc entries - cc = (self.cc - self.bcc - {target}) - bcc = (self.bcc - {target}) + cc = self.cc - self.bcc - {target} + bcc = self.bcc - {target} # # prepare our 'to' # - to = { - "email": target - } + to = {"email": target} if target in self.names: to["name"] = self.names[target] @@ -576,7 +646,8 @@ class NotifySendPulse(NotifyBase): # Perform our post success, _response = self._fetch( - self.notify_email_url, payload, target, retry=1) + self.notify_email_url, payload, target, retry=1 + ) if not success: has_error = True continue @@ -607,9 +678,11 @@ class NotifySendPulse(NotifyBase): # if required. self.logger.debug( f"SendPulse POST URL: {url}" - f"(cert_verify={self.verify_certificate!r})") + f"(cert_verify={self.verify_certificate!r})" + ) self.logger.debug( - "SendPulse Payload: %s", sanitize_payload(payload)) + "SendPulse Payload: %s", sanitize_payload(payload) + ) # Prepare our default response response = {} @@ -623,6 +696,7 @@ class NotifySendPulse(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -635,23 +709,26 @@ class NotifySendPulse(NotifyBase): # - AttributeError = r is None self.logger.warning("Invalid response from SendPulse server.") self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return (False, {}) # Reference status code status_code = r.status_code # Key likely expired, we'll reset it and try one more time - if status_code == requests.codes.unauthorized \ - and retry and self.login(): + if ( + status_code == requests.codes.unauthorized + and retry + and self.login() + ): return self._fetch(url, payload, target, retry=retry - 1) - if status_code not in ( - requests.codes.ok, requests.codes.accepted): + if status_code not in (requests.codes.ok, requests.codes.accepted): # We had a problem - status_str = \ - NotifySendPulse.http_response_code_lookup( - status_code) + status_str = NotifySendPulse.http_response_code_lookup( + status_code + ) if target: self.logger.warning( @@ -660,22 +737,26 @@ class NotifySendPulse(NotifyBase): target, status_str, ", " if status_str else "", - status_code)) + status_code, + ) + ) else: self.logger.warning( "SendPulse Authentication Request failed: " "{}{}error={}.".format( - status_str, - ", " if status_str else "", - status_code)) + status_str, ", " if status_str else "", status_code + ) + ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) else: if target: self.logger.info( - "Sent SendPulse notification to {}.".format(target)) + "Sent SendPulse notification to {}.".format(target) + ) else: self.logger.debug("SendPulse authentication successful") @@ -684,7 +765,8 @@ class NotifySendPulse(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending SendPulse " - "notification to {}.".format(target)) + "notification to {}.".format(target) + ) self.logger.debug("Socket Exception: {}".format(str(e))) return (False, response) @@ -721,18 +803,21 @@ class NotifySendPulse(NotifyBase): # | | # -from addr- if "from" in results["qsd"]: - results["from_addr"] = \ - NotifySendPulse.unquote(results["qsd"]["from"].rstrip()) + results["from_addr"] = NotifySendPulse.unquote( + results["qsd"]["from"].rstrip() + ) if is_email(results["from_addr"]): # Our hostname is free'd up to be interpreted as part of the # targets results["targets"].append( - NotifySendPulse.unquote(results["host"])) + NotifySendPulse.unquote(results["host"]) + ) results["host"] = "" - if "user" in results["qsd"] and \ - is_email(NotifySendPulse.unquote(results["user"])): + if "user" in results["qsd"] and is_email( + NotifySendPulse.unquote(results["user"]) + ): # Our hostname is free'd up to be interpreted as part of the # targets results["targets"].append(NotifySendPulse.unquote(results["host"])) @@ -744,8 +829,9 @@ class NotifySendPulse(NotifyBase): # check for our client id if "id" in results["qsd"] and len(results["qsd"]["id"]): # Store our Client ID - results["client_id"] = \ - NotifySendPulse.unquote(results["qsd"]["id"]) + results["client_id"] = NotifySendPulse.unquote( + results["qsd"]["id"] + ) elif results["targets"]: # Store our Client ID @@ -753,8 +839,9 @@ class NotifySendPulse(NotifyBase): if "secret" in results["qsd"] and len(results["qsd"]["secret"]): # Store our Client Secret - results["client_secret"] = \ - NotifySendPulse.unquote(results["qsd"]["secret"]) + results["client_secret"] = NotifySendPulse.unquote( + results["qsd"]["secret"] + ) elif results["targets"]: # Store our Client Secret @@ -763,22 +850,22 @@ class NotifySendPulse(NotifyBase): # The 'to' makes it easier to use yaml configuration if "to" in results["qsd"] and len(results["qsd"]["to"]): results["targets"].append( - NotifySendPulse.unquote(results["qsd"]["to"])) + NotifySendPulse.unquote(results["qsd"]["to"]) + ) # Handle Carbon Copy Addresses if "cc" in results["qsd"] and len(results["qsd"]["cc"]): - results["cc"] = \ - NotifySendPulse.unquote(results["qsd"]["cc"]) + results["cc"] = NotifySendPulse.unquote(results["qsd"]["cc"]) # Handle Blind Carbon Copy Addresses if "bcc" in results["qsd"] and len(results["qsd"]["bcc"]): - results["bcc"] = \ - NotifySendPulse.unquote(results["qsd"]["bcc"]) + results["bcc"] = NotifySendPulse.unquote(results["qsd"]["bcc"]) # Handle Blind Carbon Copy Addresses if "template" in results["qsd"] and len(results["qsd"]["template"]): - results["template"] = \ - NotifySendPulse.unquote(results["qsd"]["template"]) + results["template"] = NotifySendPulse.unquote( + results["qsd"]["template"] + ) # Add any template substitutions results["template_data"] = results["qsd+"] diff --git a/libs/apprise/plugins/serverchan.py b/libs/apprise/plugins/serverchan.py index 2d2d3fc54..b2775e36f 100644 --- a/libs/apprise/plugins/serverchan.py +++ b/libs/apprise/plugins/serverchan.py @@ -115,6 +115,7 @@ class NotifyServerChan(NotifyBase): data=payload, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -131,7 +132,8 @@ class NotifyServerChan(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False else: diff --git a/libs/apprise/plugins/ses.py b/libs/apprise/plugins/ses.py index 62aef2ccd..81cc01b7a 100644 --- a/libs/apprise/plugins/ses.py +++ b/libs/apprise/plugins/ses.py @@ -150,6 +150,15 @@ class NotifySES(NotifyBase): template_tokens = dict( NotifyBase.template_tokens, **{ + "token": { + # Session token for temporary/IAM credentials (optional). + # May be supplied in the URL password field + # (ses://user:{token}@host/...) or via ?token=. + "name": _("Session Token"), + "type": "string", + "private": True, + "map_to": "session_token", + }, "from_email": { "name": _("From Email"), "type": "string", @@ -202,23 +211,31 @@ class NotifySES(NotifyBase): "type": "string", "map_to": "from_name", }, - "cc": { - "name": _("Carbon Copy"), - "type": "list:string", - }, - "bcc": { - "name": _("Blind Carbon Copy"), - "type": "list:string", + "token": { + # ?token= kwarg mirrors the token template token + "alias_of": "token", }, "access": { "alias_of": "access_key_id", }, + "key": { + # Intuitive alias for access_key_id + "alias_of": "access_key_id", + }, "secret": { "alias_of": "secret_access_key", }, "region": { "alias_of": "region", }, + "cc": { + "name": _("Carbon Copy"), + "type": "list:string", + }, + "bcc": { + "name": _("Blind Carbon Copy"), + "type": "list:string", + }, }, ) @@ -233,11 +250,15 @@ class NotifySES(NotifyBase): targets=None, cc=None, bcc=None, + session_token=None, **kwargs, ): """Initialize Notify AWS SES Object.""" super().__init__(**kwargs) + # Store optional session token for temporary/IAM credentials + self.aws_session_token = session_token if session_token else None + # Store our AWS API Access Key self.aws_access_key_id = validate_regex(access_key_id) if not self.aws_access_key_id: @@ -326,10 +347,12 @@ class NotifySES(NotifyBase): for recipient in parse_emails(targets): result = is_email(recipient) if result: - self.targets.append(( - result["name"] if result["name"] else False, - result["full_email"], - )) + self.targets.append( + ( + result["name"] if result["name"] else False, + result["full_email"], + ) + ) continue self.logger.warning( @@ -585,6 +608,7 @@ class NotifySES(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -604,7 +628,8 @@ class NotifySES(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return (False, NotifySES.aws_response_to_dict(r.text)) @@ -656,51 +681,67 @@ class NotifySES(NotifyBase): ) # Similar to headers; but a subset. keys must be lowercase - signed_headers = OrderedDict([ - ("content-type", headers["Content-Type"]), - ("host", f"email.{self.aws_region_name}.amazonaws.com"), - ("x-amz-date", headers["X-Amz-Date"]), - ]) + signed_headers = OrderedDict( + [ + ("content-type", headers["Content-Type"]), + ("host", f"email.{self.aws_region_name}.amazonaws.com"), + ("x-amz-date", headers["X-Amz-Date"]), + ] + ) + + # Include session token in signed headers for temporary credentials; + # x-amz-security-token sorts after x-amz-date alphabetically and + # must appear after it to keep the canonical request valid. + if self.aws_session_token: + headers["X-Amz-Security-Token"] = self.aws_session_token + signed_headers["x-amz-security-token"] = self.aws_session_token # # Build Canonical Request Object # - canonical_request = "\n".join([ - # Method - "POST", - # URL - self.aws_canonical_uri, - # Query String (none set for POST) - "", - # Header Content (must include \n at end!) - # All entries except characters in amazon date must be - # lowercase - "\n".join([f"{k}:{v}" for k, v in signed_headers.items()]) + "\n", - # Header Entries (in same order identified above) - ";".join(signed_headers.keys()), - # Payload - sha256(payload.encode("utf-8")).hexdigest(), - ]) + canonical_request = "\n".join( + [ + # Method + "POST", + # URL + self.aws_canonical_uri, + # Query String (none set for POST) + "", + # Header Content (must include \n at end!) + # All entries except characters in amazon date must be + # lowercase + "\n".join([f"{k}:{v}" for k, v in signed_headers.items()]) + + "\n", + # Header Entries (in same order identified above) + ";".join(signed_headers.keys()), + # Payload + sha256(payload.encode("utf-8")).hexdigest(), + ] + ) # Prepare Unsigned Signature - to_sign = "\n".join([ - self.aws_auth_algorithm, - amzdate, - scope, - sha256(canonical_request.encode("utf-8")).hexdigest(), - ]) + to_sign = "\n".join( + [ + self.aws_auth_algorithm, + amzdate, + scope, + sha256(canonical_request.encode("utf-8")).hexdigest(), + ] + ) # Our Authorization header - headers["Authorization"] = ", ".join([ - ( - f"{self.aws_auth_algorithm} " - f"Credential={self.aws_access_key_id}/{scope}" - ), - "SignedHeaders={signed_headers}".format( - signed_headers=";".join(signed_headers.keys()), - ), - f"Signature={self.aws_auth_signature(to_sign, reference)}", - ]) + headers["Authorization"] = ", ".join( + [ + ( + f"{self.aws_auth_algorithm} " + f"Credential={self.aws_access_key_id}/{scope}" + ), + "SignedHeaders={signed_headers}".format( + signed_headers=";".join(signed_headers.keys()), + ), + f"Signature={self.aws_auth_signature(to_sign, reference)}", + ] + ) return headers @@ -833,13 +874,15 @@ class NotifySES(NotifyBase): if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - "{}{}".format( - "" if not e not in self.names else f"{self.names[e]}:", - e, - ) - for e in self.cc - ]) + params["cc"] = ",".join( + [ + "{}{}".format( + "" if not e not in self.names else f"{self.names[e]}:", + e, + ) + for e in self.cc + ] + ) if self.bcc: # Handle our Blind Carbon Copy Addresses @@ -859,11 +902,28 @@ class NotifySES(NotifyBase): len(self.targets) == 1 and self.targets[0][1] == self.from_addr ) + # Build the from-address URL segment; include the session token in + # the password position when present: + # ses://sender:{token}@example.com/... + if self.aws_session_token: + # Split from_addr into local and domain parts; + # from_addr always contains @ (enforced by __init__ validation) + fa_local, fa_domain = self.from_addr.split("@", 1) + from_url_part = "{}:{}@{}".format( + NotifySES.quote(fa_local, safe=""), + self.pprint(self.aws_session_token, privacy, safe="+=") + if privacy + else NotifySES.quote(self.aws_session_token, safe="+="), + NotifySES.quote(fa_domain, safe=""), + ) + else: + from_url_part = NotifySES.quote(self.from_addr, safe="@") + return ( "{schema}://{from_addr}/{key_id}/{key_secret}/{region}/" "{targets}/?{params}".format( schema=self.secure_protocol, - from_addr=NotifySES.quote(self.from_addr, safe="@"), + from_addr=from_url_part, key_id=self.pprint(self.aws_access_key_id, privacy, safe=""), key_secret=self.pprint( self.aws_secret_access_key, @@ -875,15 +935,17 @@ class NotifySES(NotifyBase): targets=( "" if not has_targets - else "/".join([ - NotifySES.quote( - "{}{}".format( - "" if not e[0] else f"{e[0]}:", e[1] - ), - safe="", - ) - for e in self.targets - ]) + else "/".join( + [ + NotifySES.quote( + "{}{}".format( + "" if not e[0] else f"{e[0]}:", e[1] + ), + safe="", + ) + for e in self.targets + ] + ) ), params=NotifySES.urlencode(params), ) @@ -925,7 +987,6 @@ class NotifySES(NotifyBase): # Section 1: Get Region and Access Secret index = 0 for index, entry in enumerate(entries, start=1): - # Are we at the region yet? result = IS_REGION.match(entry) if result: @@ -989,8 +1050,11 @@ class NotifySES(NotifyBase): else: results["secret_access_key"] = secret_access_key - # Handle access key id over-ride - if "access" in results["qsd"] and len(results["qsd"]["access"]): + # Handle access key id override; ?key= is the preferred alias, + # ?access= is retained for backwards compatibility + if "key" in results["qsd"] and len(results["qsd"]["key"]): + results["access_key_id"] = NotifySES.unquote(results["qsd"]["key"]) + elif "access" in results["qsd"] and len(results["qsd"]["access"]): results["access_key_id"] = NotifySES.unquote( results["qsd"]["access"] ) @@ -1005,5 +1069,16 @@ class NotifySES(NotifyBase): else: results["region_name"] = region_name + # Session token may come from the URL password field or the + # ?token= query parameter; ?token= takes priority when both + # are present. + results["session_token"] = None + if results.get("password"): + results["session_token"] = NotifySES.unquote(results["password"]) + if "token" in results["qsd"] and len(results["qsd"]["token"]): + results["session_token"] = NotifySES.unquote( + results["qsd"]["token"] + ) + # Return our result set return results diff --git a/libs/apprise/plugins/seven.py b/libs/apprise/plugins/seven.py index f14782162..384c2243c 100644 --- a/libs/apprise/plugins/seven.py +++ b/libs/apprise/plugins/seven.py @@ -87,6 +87,7 @@ class NotifySeven(NotifyBase): "targets": { "name": _("Targets"), "type": "list:string", + "required": True, }, }, ) @@ -95,9 +96,6 @@ class NotifySeven(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "source": { # Originating address,In cases where the rewriting of the # sender's address is supported or permitted by the SMS-C. @@ -117,6 +115,9 @@ class NotifySeven(NotifyBase): "default": False, }, "label": {"name": _("Label"), "type": "string"}, + "to": { + "alias_of": "targets", + }, }, ) @@ -222,6 +223,7 @@ class NotifySeven(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Sample output of a successful transmission # { @@ -266,7 +268,8 @@ class NotifySeven(NotifyBase): ) ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/sfr.py b/libs/apprise/plugins/sfr.py index 1d528195e..b2f9781f5 100644 --- a/libs/apprise/plugins/sfr.py +++ b/libs/apprise/plugins/sfr.py @@ -240,12 +240,14 @@ class NotifySFR(NotifyBase): targets = list(self.targets) # Construct the authentication JSON - auth_payload = json.dumps({ - "serviceId": self.user, - "servicePassword": self.password, - "spaceId": self.space_id, - "lang": self.lang, - }) + auth_payload = json.dumps( + { + "serviceId": self.user, + "servicePassword": self.password, + "spaceId": self.space_id, + "lang": self.lang, + } + ) base_payload = { # Can be 'SMS', 'SMSLong', 'SMSUnicode', or 'SMSUnicodeLong' @@ -291,6 +293,7 @@ class NotifySFR(NotifyBase): params=payload, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -323,7 +326,8 @@ class NotifySFR(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/signal_api.py b/libs/apprise/plugins/signal_api.py index a6ccc3006..c7bf817f0 100644 --- a/libs/apprise/plugins/signal_api.py +++ b/libs/apprise/plugins/signal_api.py @@ -145,19 +145,19 @@ class NotifySignalAPI(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "from_phone", }, - "batch": { - "name": _("Batch Mode"), + "status": { + "name": _("Show Status"), "type": "bool", "default": False, }, - "status": { - "name": _("Show Status"), + "to": { + "alias_of": "targets", + }, + "batch": { + "name": _("Batch Mode"), "type": "bool", "default": False, }, @@ -278,7 +278,8 @@ class NotifySignalAPI(NotifyBase): # Support Styled (Markdown formatting) text_mode = ( - "styled" if self.notify_format == NotifyFormat.MARKDOWN + "styled" + if self.notify_format == NotifyFormat.MARKDOWN else "normal" ) @@ -351,7 +352,8 @@ class NotifySignalAPI(NotifyBase): log_payload = dict(payload) log_payload.pop("recipients", None) self.logger.debug( - "Signal API Payload: %s", sanitize_payload(log_payload)) + "Signal API Payload: %s", sanitize_payload(log_payload) + ) self.logger.debug( "Signal API Recipients: %s", payload.get("recipients", []), @@ -367,6 +369,7 @@ class NotifySignalAPI(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -393,7 +396,8 @@ class NotifySignalAPI(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -414,7 +418,7 @@ class NotifySignalAPI(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occured sending" - f" {len(self.targets[index:index + batch_size])} Signal" + f" {len(self.targets[index : index + batch_size])} Signal" " API notification(s)." ) self.logger.debug(f"Socket Exception: {e!s}") diff --git a/libs/apprise/plugins/signl4.py b/libs/apprise/plugins/signl4.py index 567b11acb..587299a15 100644 --- a/libs/apprise/plugins/signl4.py +++ b/libs/apprise/plugins/signl4.py @@ -65,49 +65,53 @@ class NotifySIGNL4(NotifyBase): notify_url = "https://connect.signl4.com/webhook/{secret}/" # Define object templates - templates = ( - "{schema}://{secret}", - ) + templates = ("{schema}://{secret}",) # Define our template tokens - template_tokens = dict(NotifyBase.template_tokens, **{ - # SIGNL4 team or integration secret - "secret": { - "name": _("Secret"), - "type": "string", - "private": True, - "required": True + template_tokens = dict( + NotifyBase.template_tokens, + **{ + # SIGNL4 team or integration secret + "secret": { + "name": _("Secret"), + "type": "string", + "private": True, + "required": True, + }, }, - }) + ) # Define our template arguments - template_args = dict(NotifyBase.template_args, **{ - "service": { - "name": _("Service"), - "type": "string", + template_args = dict( + NotifyBase.template_args, + **{ + "service": { + "name": _("Service"), + "type": "string", + }, + "location": { + "name": _("Location"), + "type": "string", + }, + "alerting_scenario": { + "name": _("Alerting Scenario"), + "type": "string", + }, + "filtering": { + "name": _("Filtering"), + "type": "bool", + "default": False, + }, + "external_id": { + "name": _("External ID"), + "type": "string", + }, + "status": { + "name": _("Status"), + "type": "string", + }, }, - "location": { - "name": _("Location"), - "type": "string", - }, - "alerting_scenario": { - "name": _("Alerting Scenario"), - "type": "string", - }, - "filtering": { - "name": _("Filtering"), - "type": "bool", - "default": False, - }, - "external_id": { - "name": _("External ID"), - "type": "string", - }, - "status": { - "name": _("Status"), - "type": "string", - }, - }) + ) def __init__( self, @@ -118,7 +122,7 @@ class NotifySIGNL4(NotifyBase): filtering: Optional[bool] = None, external_id: Optional[str] = None, status: Optional[str] = None, - **kwargs: Any + **kwargs: Any, ) -> None: """ Initialize SIGNL4 Object @@ -128,8 +132,10 @@ class NotifySIGNL4(NotifyBase): # SIGNL4 team or integration secret self.secret = validate_regex(secret) if not self.secret: - msg = "An invalid SIGNL4 team or integration secret " \ - "({}) was specified.".format(secret) + msg = ( + "An invalid SIGNL4 team or integration secret " + "({}) was specified.".format(secret) + ) self.logger.warning(msg) raise TypeError(msg) @@ -197,7 +203,9 @@ class NotifySIGNL4(NotifyBase): self.logger.debug( "SIGNL4 POST URL: %s (cert_verify=%s)", - notify_url, self.verify_certificate) + notify_url, + self.verify_certificate, + ) self.logger.debug("SIGNL4 Payload: %r", payload) # Always call throttle before any remote server i/o is made @@ -210,24 +218,27 @@ class NotifySIGNL4(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( - requests.codes.ok, requests.codes.created, - requests.codes.accepted): + requests.codes.ok, + requests.codes.created, + requests.codes.accepted, + ): # We had a problem - status_str = \ - NotifySIGNL4.http_response_code_lookup( - r.status_code) + status_str = NotifySIGNL4.http_response_code_lookup( + r.status_code + ) self.logger.warning( - "Failed to send SIGNL4 notification: " - "{}{}error={}.".format( - status_str, - ", " if status_str else "", - r.status_code)) + "Failed to send SIGNL4 notification: {}{}error={}.".format( + status_str, ", " if status_str else "", r.status_code + ) + ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -238,7 +249,9 @@ class NotifySIGNL4(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending SIGNL4 " - "notification to %s", self.host) + "notification to %s", + self.host, + ) self.logger.debug("Socket Exception: %s", e) # Return; we're done @@ -254,7 +267,8 @@ class NotifySIGNL4(NotifyBase): here. """ return ( - self.secure_protocol, self.secret, + self.secure_protocol, + self.secret, ) def url(self, privacy=False, *args, **kwargs): @@ -293,7 +307,8 @@ class NotifySIGNL4(NotifyBase): schema=self.secure_protocol, # never encode hostname since we're expecting it to be a valid one secret=self.pprint( - self.secret, privacy, mode=PrivacyMode.Secret, safe=""), + self.secret, privacy, mode=PrivacyMode.Secret, safe="" + ), params=NotifySIGNL4.urlencode(params), ) @@ -310,41 +325,44 @@ class NotifySIGNL4(NotifyBase): return results # The "secret" makes it easier to use yaml configuration - if "secret" in results["qsd"] and \ - len(results["qsd"]["secret"]): - results["secret"] = \ - NotifySIGNL4.unquote(results["qsd"]["secret"]) + if "secret" in results["qsd"] and len(results["qsd"]["secret"]): + results["secret"] = NotifySIGNL4.unquote(results["qsd"]["secret"]) else: - results["secret"] = \ - NotifySIGNL4.unquote(results["host"]) + results["secret"] = NotifySIGNL4.unquote(results["host"]) if "service" in results["qsd"] and len(results["qsd"]["service"]): - results["service"] = \ - NotifySIGNL4.unquote(results["qsd"]["service"]) + results["service"] = NotifySIGNL4.unquote( + results["qsd"]["service"] + ) if "location" in results["qsd"] and len(results["qsd"]["location"]): - results["location"] = \ - NotifySIGNL4.unquote(results["qsd"]["location"]) + results["location"] = NotifySIGNL4.unquote( + results["qsd"]["location"] + ) - if "alerting_scenario" in results["qsd"] and \ - len(results["qsd"]["alerting_scenario"]): - results["alerting_scenario"] = \ - NotifySIGNL4.unquote(results["qsd"]["alerting_scenario"]) + if "alerting_scenario" in results["qsd"] and len( + results["qsd"]["alerting_scenario"] + ): + results["alerting_scenario"] = NotifySIGNL4.unquote( + results["qsd"]["alerting_scenario"] + ) if "filtering" in results["qsd"] and len(results["qsd"]["filtering"]): - results["filtering"] = \ - parse_bool( - NotifySIGNL4.unquote( - results["qsd"]["filtering"], - NotifySIGNL4.template_args["filtering"]["default"])) + results["filtering"] = parse_bool( + NotifySIGNL4.unquote( + results["qsd"]["filtering"], + NotifySIGNL4.template_args["filtering"]["default"], + ) + ) - if "external_id" in results["qsd"] and \ - len(results["qsd"]["external_id"]): - results["external_id"] = \ - NotifySIGNL4.unquote(results["qsd"]["external_id"]) + if "external_id" in results["qsd"] and len( + results["qsd"]["external_id"] + ): + results["external_id"] = NotifySIGNL4.unquote( + results["qsd"]["external_id"] + ) if "status" in results["qsd"] and len(results["qsd"]["status"]): - results["status"] = \ - NotifySIGNL4.unquote(results["qsd"]["status"]) + results["status"] = NotifySIGNL4.unquote(results["qsd"]["status"]) return results diff --git a/libs/apprise/plugins/simplepush.py b/libs/apprise/plugins/simplepush.py index 38f3acd12..a7a966115 100644 --- a/libs/apprise/plugins/simplepush.py +++ b/libs/apprise/plugins/simplepush.py @@ -168,10 +168,12 @@ class NotifySimplePush(NotifyBase): self._iv = urandom(algorithms.AES.block_size // 8) # convert vector into hex string (used in payload) - self._iv_hex = "".join([ - f"{ord(self._iv[idx:idx + 1]):02x}" - for idx in range(len(self._iv)) - ]).upper() + self._iv_hex = "".join( + [ + f"{ord(self._iv[idx : idx + 1]):02x}" + for idx in range(len(self._iv)) + ] + ).upper() # encrypted key and cache it self._key = bytes( @@ -227,16 +229,20 @@ class NotifySimplePush(NotifyBase): if self.password and self.user: body = self._encrypt(body) title = self._encrypt(title) - payload.update({ - "encrypted": "true", - "iv": self._iv_hex, - }) + payload.update( + { + "encrypted": "true", + "iv": self._iv_hex, + } + ) # prepare SimplePush Object - payload.update({ - "msg": body, - "title": title, - }) + payload.update( + { + "msg": body, + "title": title, + } + ) if self.event: # Store Event @@ -263,6 +269,7 @@ class NotifySimplePush(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Get our SimplePush response (if it's possible) @@ -293,7 +300,8 @@ class NotifySimplePush(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -369,3 +377,10 @@ class NotifySimplePush(NotifyBase): ) return results + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("cryptography",) diff --git a/libs/apprise/plugins/sinch.py b/libs/apprise/plugins/sinch.py index 45c7abe41..d0db996c6 100644 --- a/libs/apprise/plugins/sinch.py +++ b/libs/apprise/plugins/sinch.py @@ -147,9 +147,6 @@ class NotifySinch(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "from_phone", }, @@ -165,6 +162,9 @@ class NotifySinch(NotifyBase): "token": { "alias_of": "api_token", }, + "to": { + "alias_of": "targets", + }, }, ) @@ -325,6 +325,7 @@ class NotifySinch(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # The responsne might look like: @@ -379,7 +380,8 @@ class NotifySinch(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/slack.py b/libs/apprise/plugins/slack.py index 29d05148b..7e57a39d2 100644 --- a/libs/apprise/plugins/slack.py +++ b/libs/apprise/plugins/slack.py @@ -73,14 +73,17 @@ import contextlib from json import dumps, loads +from json.decoder import JSONDecodeError import re from time import time import requests +from ..apprise_attachment import AppriseAttachment from ..common import NotifyFormat, NotifyImageSize, NotifyType from ..locale import gettext_lazy as _ from ..utils.parse import is_email, parse_bool, parse_list, validate_regex +from ..utils.templates import TemplateType, apply_template from .base import NotifyBase # Extend HTTP Error Messages @@ -114,12 +117,22 @@ class SlackMode: # Our token looks like: xoxb-1234-1234-abc124 or BOT = "bot" + # Slack Workflow Builder webhook + # URL: https://hooks.slack.com/workflows/T.../F.../X.../Y... + WORKFLOW = "workflow" + + # Slack Workflow trigger webhook (newer format) + # URL: https://hooks.slack.com/triggers/T.../X.../Y... + WORKFLOW_TRIGGER = "trigger" + # Define our Slack Modes SLACK_MODES = ( SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV, SlackMode.BOT, + SlackMode.WORKFLOW, + SlackMode.WORKFLOW_TRIGGER, ) @@ -150,6 +163,11 @@ class NotifySlack(NotifyBase): webhook_url = "https://hooks.slack.com/services" webhook_gov_url = "https://hooks.slack-gov.com/services" + # Slack Workflow Builder webhook URL bases + # See: https://slack.com/help/articles/360041352714 + workflow_url = "https://hooks.slack.com/workflows" + workflow_trigger_url = "https://hooks.slack.com/triggers" + # Slack API URL (used with Bots) api_url = "https://slack.com/api/{}" @@ -159,6 +177,10 @@ class NotifySlack(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 35000 + # There is no reason we should exceed 35KB when reading in a JSON file. + # If it is more than this, then it is not accepted + max_slack_template_size = 35000 + # Default Notification Format notify_format = NotifyFormat.MARKDOWN @@ -170,7 +192,7 @@ class NotifySlack(NotifyBase): templates = ( # Webhook "{schema}://{token_a}/{token_b}/{token_c}", - "{schema}://{botname}@{token_a}/{token_b}{token_c}", + "{schema}://{botname}@{token_a}/{token_b}/{token_c}", "{schema}://{token_a}/{token_b}/{token_c}/{targets}", "{schema}://{botname}@{token_a}/{token_b}/{token_c}/{targets}", # Bot @@ -203,6 +225,7 @@ class NotifySlack(NotifyBase): "name": _("Token A"), "type": "string", "private": True, + "required": True, "regex": (r"^[A-Z0-9]+$", "i"), }, # Token required as part of the Webhook request @@ -211,6 +234,7 @@ class NotifySlack(NotifyBase): "name": _("Token B"), "type": "string", "private": True, + "required": True, "regex": (r"^[A-Z0-9]+$", "i"), }, # Token required as part of the Webhook request @@ -219,6 +243,7 @@ class NotifySlack(NotifyBase): "name": _("Token C"), "type": "string", "private": True, + "required": True, "regex": (r"^[A-Za-z0-9]+$", "i"), }, "target_encoded_id": { @@ -248,6 +273,13 @@ class NotifySlack(NotifyBase): "name": _("Targets"), "type": "list:string", }, + # Workflow Builder path: all URL segments joined with slashes + # e.g. T05XXXX/Ft07XXXX/XXXXXXXX/YYYYYYYY + "workflow_path": { + "name": _("Workflow Path"), + "type": "string", + "private": True, + }, }, ) @@ -275,9 +307,6 @@ class NotifySlack(NotifyBase): "default": False, "map_to": "use_blocks", }, - "to": { - "alias_of": "targets", - }, "timestamp": { "name": _("Include Timestamp"), "type": "bool", @@ -294,9 +323,25 @@ class NotifySlack(NotifyBase): "name": _("Token"), "alias_of": ("access_token", "token_a", "token_b", "token_c"), }, + "to": { + "alias_of": "targets", + }, + "template": { + "name": _("Template Path"), + "type": "string", + "private": True, + }, }, ) + # Define our token control + template_kwargs = { + "tokens": { + "name": _("Template Tokens"), + "prefix": ":", + }, + } + # Formatting requirements are defined here: # https://api.slack.com/docs/message-formatting _re_formatting_map = { @@ -349,27 +394,86 @@ class NotifySlack(NotifyBase): include_timestamp=None, use_blocks=None, mode=None, + template=None, + tokens=None, + workflow_path=None, **kwargs, ): """Initialize Slack Object.""" super().__init__(**kwargs) # Store our webhook mode - if mode and isinstance(mode, str): + # Track whether the caller supplied an explicit mode so we can + # decide later whether to validate exact segment count or + # auto-detect the workflow sub-mode from segment count. + _mode_explicit = bool(mode and isinstance(mode, str)) + if _mode_explicit: self.mode = next( (a for a in SLACK_MODES if a.startswith(mode)), None ) if self.mode not in SLACK_MODES: - msg = ( - f"The Slack mode specified ({mode}) is invalid." - ) + msg = f"The Slack mode specified ({mode}) is invalid." self.logger.warning(msg) raise TypeError(msg) + elif workflow_path: + # Mode will be determined by segment count below; + # use WORKFLOW as a sentinel so we enter the right branch. + self.mode = SlackMode.WORKFLOW + else: # Detect self.mode = SlackMode.BOT if access_token else SlackMode.WEBHOOK - if self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV): + if self.mode in ( + SlackMode.WORKFLOW, + SlackMode.WORKFLOW_TRIGGER, + ): + # Workflow Builder mode: no OAuth token, no webhook tokens + self.access_token = None + self.token_a = None + self.token_b = None + self.token_c = None + + # Parse workflow path segments from string or list + if isinstance(workflow_path, (list, tuple)): + self.workflow_path = [str(s) for s in workflow_path if s] + elif isinstance(workflow_path, str): + self.workflow_path = [s for s in workflow_path.split("/") if s] + else: + self.workflow_path = [] + + # Segment counts are fixed by Slack: + # /workflows/ URLs have exactly 4 segments + # /triggers/ URLs have exactly 3 segments + seg_count = len(self.workflow_path) + if _mode_explicit: + # Validate exact count for the declared mode + expected = 4 if self.mode == SlackMode.WORKFLOW else 3 + if seg_count != expected: + msg = ( + f"A Slack {self.mode!r} URL requires exactly" + f" {expected} path segments" + f" ({workflow_path!r} has {seg_count})." + ) + self.logger.warning(msg) + raise TypeError(msg) + else: + # Auto-detect sub-mode from segment count + if seg_count == 4: + self.mode = SlackMode.WORKFLOW + elif seg_count == 3: + self.mode = SlackMode.WORKFLOW_TRIGGER + else: + msg = ( + "A Slack Workflow URL requires exactly 3 or 4" + f" path segments ({workflow_path!r} has" + f" {seg_count})." + ) + self.logger.warning(msg) + raise TypeError(msg) + + elif self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV): + self.workflow_path = [] self.access_token = None self.token_a = validate_regex( token_a, *self.template_tokens["token_a"]["regex"] @@ -404,6 +508,7 @@ class NotifySlack(NotifyBase): self.logger.warning(msg) raise TypeError(msg) else: + self.workflow_path = [] self.token_a = None self.token_b = None self.token_c = None @@ -437,7 +542,14 @@ class NotifySlack(NotifyBase): # a flag lower to not set the channels self.channels.append( None - if self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV) + if self.mode + in ( + SlackMode.WEBHOOK, + SlackMode.WEBHOOK_GOV, + # Workflow mode has no channel concept + SlackMode.WORKFLOW, + SlackMode.WORKFLOW_TRIGGER, + ) else self.default_notification_channel ) @@ -447,23 +559,173 @@ class NotifySlack(NotifyBase): re.IGNORECASE, ) # Place a thumbnail image inline with the message body - self.include_image = \ - self.template_args["image"]["default"] \ - if include_image is None else include_image + self.include_image = ( + self.template_args["image"]["default"] + if include_image is None + else include_image + ) # Place a footer with each post - self.include_footer = \ - self.template_args["footer"]["default"] \ - if include_footer is None else include_footer + self.include_footer = ( + self.template_args["footer"]["default"] + if include_footer is None + else include_footer + ) # timestamp inclusion (only applicable if footer also defined - self.include_timestamp = \ - self.template_args["timestamp"]["default"] \ - if include_timestamp is None \ + self.include_timestamp = ( + self.template_args["timestamp"]["default"] + if include_timestamp is None else include_timestamp + ) + + # Our template object is just an AppriseAttachment object + self.template = AppriseAttachment(asset=self.asset) + if template: + # Add our definition to our template + self.template.add(template) + if not len(self.template): + # add() failed (unsupported schema, unparseable URL, etc.) + msg = f"The Slack template ({template!r}) could not be loaded." + self.logger.warning(msg) + raise TypeError(msg) + # Enforce maximum file size + self.template[0].max_file_size = self.max_slack_template_size + + # Template functionality + self.tokens = {} + if isinstance(tokens, dict): + self.tokens.update(tokens) + + elif tokens: + msg = ( + "The specified Slack Template Tokens " + f"({tokens}) are not identified as a dictionary." + ) + self.logger.warning(msg) + raise TypeError(msg) + + # A template always implies Block Kit mode; there is no meaningful + # use for templates outside of blocks mode. + if self.template: + self.use_blocks = True return + def gen_payload( + self, body, title="", notify_type=NotifyType.INFO, **kwargs + ): + """Generate our payload from an external Block Kit JSON template. + + Returns the validated attachment content dict (which must contain + a non-empty 'blocks' list with typed entries) to be placed into + the Slack attachments array. Returns False on any failure. + """ + # Acquire the first template attachment + template = self.template[0] + if not template: + # We could not access the attachment + self.logger.error( + "Could not access Slack template" + f" {template.url(privacy=True)}." + ) + return False + + # Acquire our to-be footer icon if configured to do so + image_url = ( + None if not self.include_image else self.image_url(notify_type) + ) + + # Take a copy of our token dictionary + tokens = self.tokens.copy() + + # Apply some defaults template values + tokens["app_body"] = body + tokens["app_title"] = title + tokens["app_type"] = notify_type.value + tokens["app_id"] = self.app_id + tokens["app_desc"] = self.app_desc + tokens["app_color"] = self.color(notify_type) + tokens["app_image_url"] = image_url + tokens["app_url"] = self.app_url + + # Templates are always Block Kit JSON; enforce JSON escaping + tokens["app_mode"] = TemplateType.JSON + + # Coerce all substitution values to str before JSON-escaping. + # apply_template's _escape_json() calls json.dumps(v)[1:-1] which + # is only correct for strings; None produces "ul" and other non- + # string types produce similarly corrupted output. app_mode is a + # TemplateType sentinel passed as a named parameter, not a + # substitution value, so it is left as-is. + safe_tokens = { + k: ( + v + if k == "app_mode" or isinstance(v, str) + else ("" if v is None else str(v)) + ) + for k, v in tokens.items() + } + + try: + with open(template.path) as fp: + content = apply_template(fp.read(), **safe_tokens) + + except OSError: + self.logger.error( + "Slack template" + f" {template.url(privacy=True)} could not be read." + ) + return False + + # Parse and validate as JSON + try: + content = loads(content) + + except JSONDecodeError as e: + self.logger.error( + "Slack template" + f" {template.url(privacy=True)} contains invalid JSON." + ) + self.logger.debug(f"JSONDecodeError: {e}") + return False + + # Template must parse to a JSON object, not an array or scalar + if not isinstance(content, dict): + self.logger.error( + "Slack template" + f" {template.url(privacy=True)} must be a JSON object" + " (got {}).".format(type(content).__name__) + ) + return False + + # 'blocks' must be a non-empty list (Block Kit requirement) + if ( + not isinstance(content.get("blocks"), list) + or not content["blocks"] + ): + self.logger.error( + "Slack template" + f" {template.url(privacy=True)} must contain" + " a non-empty 'blocks' list." + ) + return False + + # Every block must be a dict with a 'type' string (Block Kit) + if not all( + isinstance(b, dict) and isinstance(b.get("type"), str) + for b in content["blocks"] + ): + self.logger.error( + "Slack template" + f" {template.url(privacy=True)} contains" + " a block missing a 'type' string." + ) + return False + + # Return the validated attachment content dict + return content + def send( self, body, @@ -477,71 +739,124 @@ class NotifySlack(NotifyBase): # error tracking (used for function return) has_error = False + # + # Workflow Builder mode: fixed endpoint, no channel iteration + # + if self.mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER): + if self.template: + # Use external Block Kit JSON template + payload = self.gen_payload( + body, title, notify_type=notify_type, **kwargs + ) + if payload is False: + return False + + else: + # Default payload: combine title and body into a single + # 'text' field -- the workflow must accept this variable. + payload = { + "text": f"{title}: {body}" if title else body, + } + + # Construct the workflow POST URL from stored path segments + wf_base = ( + self.workflow_trigger_url + if self.mode is SlackMode.WORKFLOW_TRIGGER + else self.workflow_url + ) + url = "{}/{}".format(wf_base, "/".join(self.workflow_path)) + # _send() returns False on error, non-False on success + return self._send(url, payload) is not False + # # Prepare JSON Object (applicable to both WEBHOOK and BOT mode) # if self.use_blocks: - # Our slack format - slack_format = ( - "mrkdwn" - if self.notify_format == NotifyFormat.MARKDOWN - else "plain_text" - ) - - payload = { - "username": self.user if self.user else self.app_id, - "attachments": [{ - "blocks": [{ - "type": "section", - "text": {"type": slack_format, "text": body}, - }], - "color": self.color(notify_type), - }], - } - - # Slack only accepts non-empty header sections - if title: - payload["attachments"][0]["blocks"].insert( - 0, - { - "type": "header", - "text": { - "type": "plain_text", - "text": title, - "emoji": True, - }, - }, + if self.template: + # Template mode: generate blocks attachment from template + template_content = self.gen_payload( + body, title, notify_type=notify_type, **kwargs ) + if template_content is False: + # Template loading or parsing failed; abort early + return False - # Include the footer only if specified to do so - if self.include_footer: - - # Acquire our to-be footer icon if configured to do so - image_url = ( - None - if not self.include_image - else self.image_url(notify_type) - ) - - # Prepare our footer based on the block structure - footer = { - "type": "context", - "elements": [{"type": slack_format, "text": self.app_id}], + # Wrap the template attachment content in a full payload + payload = { + "username": self.user if self.user else self.app_id, + "attachments": [template_content], } - if image_url: - payload["icon_url"] = image_url + else: + # Our slack format + slack_format = ( + "mrkdwn" + if self.notify_format == NotifyFormat.MARKDOWN + else "plain_text" + ) - footer["elements"].insert( + payload = { + "username": self.user if self.user else self.app_id, + "attachments": [ + { + "blocks": [ + { + "type": "section", + "text": { + "type": slack_format, + "text": body, + }, + } + ], + "color": self.color(notify_type), + } + ], + } + + # Slack only accepts non-empty header sections + if title: + payload["attachments"][0]["blocks"].insert( 0, { - "type": "image", - "image_url": image_url, - "alt_text": notify_type, + "type": "header", + "text": { + "type": "plain_text", + "text": title, + "emoji": True, + }, }, ) - payload["attachments"][0]["blocks"].append(footer) + # Include the footer only if specified to do so + if self.include_footer: + # Acquire our to-be footer icon if configured to do so + image_url = ( + None + if not self.include_image + else self.image_url(notify_type) + ) + + # Prepare our footer based on the block structure + footer = { + "type": "context", + "elements": [ + {"type": slack_format, "text": self.app_id} + ], + } + + if image_url: + payload["icon_url"] = image_url + + footer["elements"].insert( + 0, + { + "type": "image", + "image_url": image_url, + "alt_text": notify_type, + }, + ) + + payload["attachments"][0]["blocks"].append(footer) else: # @@ -607,11 +922,13 @@ class NotifySlack(NotifyBase): "username": self.user if self.user else self.app_id, # Use Markdown language "mrkdwn": self.notify_format == NotifyFormat.MARKDOWN, - "attachments": [{ - "title": title, - "text": body, - "color": self.color(notify_type), - }], + "attachments": [ + { + "title": title, + "text": body, + "color": self.color(notify_type), + } + ], } # Acquire our to-be footer icon if configured to do so @@ -693,8 +1010,9 @@ class NotifySlack(NotifyBase): continue # Store oure content - channel, thread_ts = result.group("channel"), result.group( - "thread_ts" + channel, thread_ts = ( + result.group("channel"), + result.group("thread_ts"), ) if thread_ts: payload["thread_ts"] = thread_ts @@ -744,7 +1062,6 @@ class NotifySlack(NotifyBase): ): # Send our attachments (can only be done in bot mode) for no, attachment in enumerate(attach, start=1): - # Perform some simple error checking if not attachment: # We could not access the attachment @@ -791,10 +1108,12 @@ class NotifySlack(NotifyBase): # https://api.slack.com/methods/files.completeUploadExternal for channel_id in attach_channel_list: payload_ = { - "files": [{ - "id": file_id, - "title": attachment.name, - }], + "files": [ + { + "id": file_id, + "title": attachment.name, + } + ], "channel_id": channel_id, } url_ = self.api_url.format("files.completeUploadExternal") @@ -865,6 +1184,7 @@ class NotifySlack(NotifyBase): params=params, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Attachment posts return a JSON string @@ -888,7 +1208,6 @@ class NotifySlack(NotifyBase): if r.status_code != requests.codes.ok or not ( response and response.get("ok", False) ): - # We had a problem status_str = NotifySlack.http_response_code_lookup( r.status_code, SLACK_HTTP_ERROR_MAP @@ -901,7 +1220,8 @@ class NotifySlack(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -1017,10 +1337,11 @@ class NotifySlack(NotifyBase): "file": ( attach.name, # file handle is safely closed in `finally`; inline - # open is intentional - open(attach.path, "rb"), # noqa: SIM115 - ), - } + # open is intentional; attach.open() dispatches to + # BytesIO for memory attachments + attach.open(), + ), + } r = requests.request( http_method, @@ -1030,6 +1351,7 @@ class NotifySlack(NotifyBase): files=files, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, params=params if params else None, ) @@ -1059,6 +1381,13 @@ class NotifySlack(NotifyBase): and b"OK" in r.content ) ) + elif self.mode in ( + SlackMode.WORKFLOW, + SlackMode.WORKFLOW_TRIGGER, + ): + # Workflow trigger webhooks confirm success via HTTP 200; + # the response body may be empty or non-standard JSON. + status_okay = r.status_code == requests.codes.ok elif r.content == b"ok": # The text 'ok' is returned if this is a Webhook request # So the below captures that as well. @@ -1149,6 +1478,10 @@ class NotifySlack(NotifyBase): Targets or end points should never be identified here. """ + if self.mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER): + # Workflow mode is identified by its path segments + return (self.secure_protocol, self.mode, *self.workflow_path) + return ( self.secure_protocol, self.token_a, @@ -1169,9 +1502,17 @@ class NotifySlack(NotifyBase): "mode": self.mode, } + if self.template: + params["template"] = NotifySlack.quote( + self.template[0].url(), safe="" + ) + # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + # Store any template token entries if specified + params.update({f":{k}": v for k, v in self.tokens.items()}) + # Determine if there is a botname present botname = "" if self.user: @@ -1179,6 +1520,16 @@ class NotifySlack(NotifyBase): botname=NotifySlack.quote(self.user, safe=""), ) + if self.mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER): + return "{schema}://{path}/?{params}".format( + schema=self.secure_protocol, + path="/".join( + self.pprint(s, privacy, safe="") + for s in self.workflow_path + ), + params=NotifySlack.urlencode(params), + ) + if self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV): return ( "{schema}://{botname}{token_a}/{token_b}/{token_c}/" @@ -1221,22 +1572,41 @@ class NotifySlack(NotifyBase): # The first token is stored in the hostname token = NotifySlack.unquote(results["host"]) - # Get unquoted entries + # Get unquoted path entries entries = NotifySlack.split_path(results["fullpath"]) - # Verify if our token_a us a bot token or part of a webhook: - if token.startswith("xo"): + # Peek at mode early to guide path parsing. + # Resolve via the same prefix-matching used in __init__ so that + # abbreviated inputs (e.g. mode=tri or mode=work) are handled + # identically in both places. + _mode_raw = results["qsd"].get("mode", "") + _mode = ( + next( + (a for a in SLACK_MODES if a.startswith(_mode_raw)), + "", + ) + if _mode_raw + else "" + ) + + if _mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER): + # All path segments form the workflow_path + results["workflow_path"] = "/".join([token, *entries]) + results["targets"] = [] + + # Verify if our token is a bot token or part of a webhook: + elif token.startswith("xo"): # We're dealing with a bot results["access_token"] = token + results["targets"] = entries else: # We're dealing with a webhook results["token_a"] = token results["token_b"] = entries.pop(0) if entries else None results["token_c"] = entries.pop(0) if entries else None - - # assign remaining entries to the channels we wish to notify - results["targets"] = entries + # assign remaining entries to the channels we wish to notify + results["targets"] = entries # Support the token flag where you can set it to the bot token # or the webhook token (with slash delimiters) @@ -1280,28 +1650,42 @@ class NotifySlack(NotifyBase): ) # Get Image Flag - results["include_image"] = \ - parse_bool(results["qsd"].get( - "image", NotifySlack.template_args["image"]["default"])) + results["include_image"] = parse_bool( + results["qsd"].get( + "image", NotifySlack.template_args["image"]["default"] + ) + ) - results["include_timestamp"] = \ - parse_bool(results["qsd"].get( - "timestamp", - NotifySlack.template_args["timestamp"]["default"])) + results["include_timestamp"] = parse_bool( + results["qsd"].get( + "timestamp", NotifySlack.template_args["timestamp"]["default"] + ) + ) # Get Payload structure (use blocks?) if "blocks" in results["qsd"] and len(results["qsd"]["blocks"]): results["use_blocks"] = parse_bool(results["qsd"]["blocks"]) # Get Footer Flag - results["include_footer"] = \ - parse_bool(results["qsd"].get( - "footer", NotifySlack.template_args["footer"]["default"])) + results["include_footer"] = parse_bool( + results["qsd"].get( + "footer", NotifySlack.template_args["footer"]["default"] + ) + ) # Get Mode if "mode" in results["qsd"] and len(results["qsd"]["mode"]): results["mode"] = NotifySlack.unquote(results["qsd"]["mode"]) + # Template Handling + if "template" in results["qsd"] and results["qsd"]["template"]: + results["template"] = NotifySlack.unquote( + results["qsd"]["template"] + ) + + # Store our tokens + results["tokens"] = results["qsd:"] + return results @staticmethod @@ -1310,8 +1694,41 @@ class NotifySlack(NotifyBase): Supports: - https://hooks.slack.com/services/TOKEN_A/TOKEN_B/TOKEN_C - https://hooks.slack-gov.com/services/TOKEN_A/TOKEN_B/TOKEN_C + - https://hooks.slack.com/workflows/T.../F.../X.../Y... + - https://hooks.slack.com/triggers/T.../X.../Y... """ + # Workflow Builder and trigger webhook URLs + wf_result = re.match( + r"^https?://hooks\.slack\.com/" + r"(?Pworkflows|triggers)/" + r"(?P[A-Z0-9][A-Z0-9/_-]+)/?" + r"(?P\?.+)?$", + url, + re.I, + ) + if wf_result: + wf_type = wf_result.group("wf_type").lower() + # Map URL path type to Apprise mode string + mode = ( + SlackMode.WORKFLOW_TRIGGER + if wf_type == "triggers" + else SlackMode.WORKFLOW + ) + path = wf_result.group("path").strip("/") + params = wf_result.group("params") or "" + sep = "&" if params else "?" + return NotifySlack.parse_url( + "{schema}://{path}/{params}{sep}mode={mode}".format( + schema=NotifySlack.secure_protocol, + path=path, + params=params, + sep=sep, + mode=mode, + ) + ) + + # Standard incoming webhook and government webhook URLs result = re.match( r"^https?://(?Phooks\.slack(?P-gov)?\.com)/services/" r"(?P[A-Z0-9]+)/" @@ -1324,15 +1741,14 @@ class NotifySlack(NotifyBase): if result: params = ( - "" - if not result.group("params") - else result.group("params") + "" if not result.group("params") else result.group("params") ) if result.group("gov"): # provide gov parameters - params = ("?" if not params else "&") + \ - f"mode={SlackMode.WEBHOOK_GOV}" + params = ( + "?" if not params else "&" + ) + f"mode={SlackMode.WEBHOOK_GOV}" return NotifySlack.parse_url( "{schema}://{token_a}/{token_b}/{token_c}/{params}".format( diff --git a/libs/apprise/plugins/smpp.py b/libs/apprise/plugins/smpp.py index 02e515960..d0d887547 100644 --- a/libs/apprise/plugins/smpp.py +++ b/libs/apprise/plugins/smpp.py @@ -203,10 +203,12 @@ class NotifySMPP(NotifyBase): password=self.password, host=f"{self.host}:{self.port}" if self.port else self.host, source=self.source, - targets="/".join([ - NotifySMPP.quote(t, safe="") - for t in chain(self.targets, self._invalid_targets) - ]), + targets="/".join( + [ + NotifySMPP.quote(t, safe="") + for t in chain(self.targets, self._invalid_targets) + ] + ), params=self.urlencode(params), ) @@ -344,3 +346,10 @@ class NotifySMPP(NotifyBase): results["source"] = results["targets"].pop(0) return results + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("smpplib",) diff --git a/libs/apprise/plugins/smseagle.py b/libs/apprise/plugins/smseagle.py index 5a44dfbb3..681d634b8 100644 --- a/libs/apprise/plugins/smseagle.py +++ b/libs/apprise/plugins/smseagle.py @@ -180,16 +180,14 @@ class NotifySMSEagle(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "token": { "alias_of": "token", }, - "batch": { - "name": _("Batch Mode"), - "type": "bool", - "default": False, + "priority": { + "name": _("Priority"), + "type": "choice:int", + "values": SMSEAGLE_PRIORITIES, + "default": SMSEaglePriority.NORMAL, }, "status": { "name": _("Show Status"), @@ -206,11 +204,13 @@ class NotifySMSEagle(NotifyBase): "type": "bool", "default": False, }, - "priority": { - "name": _("Priority"), - "type": "choice:int", - "values": SMSEAGLE_PRIORITIES, - "default": SMSEaglePriority.NORMAL, + "to": { + "alias_of": "targets", + }, + "batch": { + "name": _("Batch Mode"), + "type": "bool", + "default": False, }, }, ) @@ -391,10 +391,12 @@ class NotifySMSEagle(NotifyBase): try: # Prepare our Attachment in Base64 - attachments.append({ - "content_type": attachment.mimetype, - "content": attachment.base64(), - }) + attachments.append( + { + "content_type": attachment.mimetype, + "content": attachment.base64(), + } + ) except exception.AppriseException: # We could not access the attachment @@ -508,7 +510,8 @@ class NotifySMSEagle(NotifyBase): f"(cert_verify={self.verify_certificate!r})" ) self.logger.debug( - "SMSEagle Payload: %s", sanitize_payload(payload)) + "SMSEagle Payload: %s", sanitize_payload(payload) + ) # Always call throttle before any remote server i/o is made self.throttle() @@ -519,6 +522,7 @@ class NotifySMSEagle(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -564,7 +568,6 @@ class NotifySMSEagle(NotifyBase): ) # pragma: no cover ) ): - # We had a problem status_str = ( content.get("result") @@ -615,7 +618,7 @@ class NotifySMSEagle(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occured sending" - f" {len(targets[index:index + batch_size])} SMSEagle" + f" {len(targets[index : index + batch_size])} SMSEagle" f" {category} notification(s)." ) self.logger.debug(f"Socket Exception: {e!s}") @@ -680,19 +683,21 @@ class NotifySMSEagle(NotifyBase): if self.port is None or self.port == default_port else f":{self.port}" ), - targets="/".join([ - NotifySMSEagle.quote(x, safe="#@") - for x in chain( - # Pass phones directly as is - self.target_phones, - # Contacts - [f"@{x}" for x in self.target_contacts], - # Groups - [f"#{x}" for x in self.target_groups], - # Pass along the same invalid entries as were provided - self.invalid_targets, - ) - ]), + targets="/".join( + [ + NotifySMSEagle.quote(x, safe="#@") + for x in chain( + # Pass phones directly as is + self.target_phones, + # Contacts + [f"@{x}" for x in self.target_contacts], + # Groups + [f"#{x}" for x in self.target_groups], + # Pass along the same invalid entries as were provided + self.invalid_targets, + ) + ] + ), params=NotifySMSEagle.urlencode(params), ) diff --git a/libs/apprise/plugins/smsmanager.py b/libs/apprise/plugins/smsmanager.py index 6c47d2129..0493a8aa2 100644 --- a/libs/apprise/plugins/smsmanager.py +++ b/libs/apprise/plugins/smsmanager.py @@ -130,9 +130,6 @@ class NotifySMSManager(NotifyBase): "key": { "alias_of": "apikey", }, - "to": { - "alias_of": "targets", - }, "from": { "name": _("From Phone No"), "type": "string", @@ -148,6 +145,9 @@ class NotifySMSManager(NotifyBase): "values": SMS_MANAGER_GATEWAYS, "default": SMS_MANAGER_GATEWAYS[0], }, + "to": { + "alias_of": "targets", + }, "batch": { "name": _("Batch Mode"), "type": "bool", @@ -295,6 +295,7 @@ class NotifySMSManager(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -317,7 +318,8 @@ class NotifySMSManager(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -369,9 +371,12 @@ class NotifySMSManager(NotifyBase): return "{schema}://{apikey}@{targets}?{params}".format( schema=self.secure_protocol[0], apikey=self.pprint(self.apikey, privacy, safe=""), - targets="/".join([ - NotifySMSManager.quote(f"{x}", safe="+") for x in self.targets - ]), + targets="/".join( + [ + NotifySMSManager.quote(f"{x}", safe="+") + for x in self.targets + ] + ), params=NotifySMSManager.urlencode(params), ) diff --git a/libs/apprise/plugins/smtp2go.py b/libs/apprise/plugins/smtp2go.py index 15da21535..c19d28844 100644 --- a/libs/apprise/plugins/smtp2go.py +++ b/libs/apprise/plugins/smtp2go.py @@ -96,8 +96,8 @@ class NotifySMTP2Go(NotifyBase): # Define object templates templates = ( - "{schema}://{user}@{host}:{apikey}/", - "{schema}://{user}@{host}:{apikey}/{targets}", + "{schema}://{user}@{host}/{apikey}/", + "{schema}://{user}@{host}/{apikey}/{targets}", ) # Define our template tokens @@ -227,10 +227,12 @@ class NotifySMTP2Go(NotifyBase): for recipient in parse_emails(targets): result = is_email(recipient) if result: - self.targets.append(( - result["name"] if result["name"] else False, - result["full_email"], - )) + self.targets.append( + ( + result["name"] if result["name"] else False, + result["full_email"], + ) + ) continue self.logger.warning( @@ -320,15 +322,17 @@ class NotifySMTP2Go(NotifyBase): try: # Format our attachment - attachments.append({ - "filename": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - "fileblob": attachment.base64(), - "mimetype": attachment.mimetype, - }) + attachments.append( + { + "filename": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + "fileblob": attachment.base64(), + "mimetype": attachment.mimetype, + } + ) except exception.AppriseException: # We could not access the attachment @@ -428,18 +432,16 @@ class NotifySMTP2Go(NotifyBase): f"(cert_verify={self.verify_certificate})" ) self.logger.debug( - "SMTP2Go Payload: %s", sanitize_payload(payload)) + "SMTP2Go Payload: %s", sanitize_payload(payload) + ) # For logging output of success and errors; we get a head count # of our outbound details: + _batch = self.targets[index : index + batch_size] verbose_dest = ( - ", ".join( - [x[1] for x in self.targets[index : index + batch_size]] - ) - if len(self.targets[index : index + batch_size]) <= 3 - else ( - f"{len(self.targets[index:index + batch_size])} recipients" - ) + ", ".join([x[1] for x in _batch]) + if len(_batch) <= 3 + else f"{len(_batch)} recipients" ) # Always call throttle before any remote server i/o is made @@ -451,6 +453,7 @@ class NotifySMTP2Go(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -470,7 +473,8 @@ class NotifySMTP2Go(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -484,8 +488,7 @@ class NotifySMTP2Go(NotifyBase): except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending" - f" SMTP2Go:{verbose_dest} " - + "notification." + f" SMTP2Go:{verbose_dest} " + "notification." ) self.logger.debug(f"Socket Exception: {e!s}") @@ -534,13 +537,15 @@ class NotifySMTP2Go(NotifyBase): if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - "{}{}".format( - "" if not e not in self.names else f"{self.names[e]}:", - e, - ) - for e in self.cc - ]) + params["cc"] = ",".join( + [ + "{}{}".format( + "" if not e not in self.names else f"{self.names[e]}:", + e, + ) + for e in self.cc + ] + ) if self.bcc: # Handle our Blind Carbon Copy Addresses @@ -560,13 +565,17 @@ class NotifySMTP2Go(NotifyBase): targets=( "" if not has_targets - else "/".join([ - NotifySMTP2Go.quote( - "{}{}".format("" if not e[0] else f"{e[0]}:", e[1]), - safe="", - ) - for e in self.targets - ]) + else "/".join( + [ + NotifySMTP2Go.quote( + "{}{}".format( + "" if not e[0] else f"{e[0]}:", e[1] + ), + safe="", + ) + for e in self.targets + ] + ) ), params=NotifySMTP2Go.urlencode(params), ) diff --git a/libs/apprise/plugins/sns.py b/libs/apprise/plugins/sns.py index c79003ec0..ef6093f38 100644 --- a/libs/apprise/plugins/sns.py +++ b/libs/apprise/plugins/sns.py @@ -66,6 +66,27 @@ AWS_HTTP_ERROR_MAP = { } +class SNSMode: + """Tracks the mode of operation for SNS Notifications.""" + + # SMS mode: the title is merged into the message body and the + # body is limited to 160 characters. This is the safe default + # and applies to phone targets and any mix of phones + topics. + SMS = "sms" + + # Topic mode: the title is sent as the SNS Subject field for + # topic targets; the body limit is 256 KB. Auto-selected when + # all targets are SNS topics (no phone numbers present). + TOPIC = "topic" + + +# All valid SNS operating modes +SNS_MODES = ( + SNSMode.SMS, + SNSMode.TOPIC, +) + + class NotifySNS(NotifyBase): """A wrapper for AWS SNS (Amazon Simple Notification)""" @@ -85,16 +106,20 @@ class NotifySNS(NotifyBase): # can occur in much shorter bursts request_rate_per_sec = 2.5 - # The maximum length of the body + # body_maxlen and title_maxlen are defined as @property methods below + # since the limits differ by mode: SMS mode caps the body at 160 + # characters and has no title field (title_maxlen = 0 causes the + # framework to fold the title into the body automatically); Topic + # mode allows up to 256 KB and passes the title as the SNS Subject + # field (title_maxlen = 100). # Source: https://docs.aws.amazon.com/sns/latest/api/API_Publish.html - body_maxlen = 160 - - # A title can not be used for SMS Messages. Setting this to zero will - # cause any title (if defined) to get placed into the message body. - title_maxlen = 0 # Define object templates templates = ( + # With session token (for temporary/IAM credentials) + "{schema}://{token}@{access_key_id}" + "/{secret_access_key}/{region}/{targets}", + # Without session token (standard long-term credentials) "{schema}://{access_key_id}/{secret_access_key}/{region}/{targets}", ) @@ -102,6 +127,13 @@ class NotifySNS(NotifyBase): template_tokens = dict( NotifyBase.template_tokens, **{ + "token": { + # Session token for temporary/IAM credentials (optional) + "name": _("Session Token"), + "type": "string", + "private": True, + "map_to": "session_token", + }, "access_key_id": { "name": _("Access Key ID"), "type": "string", @@ -149,15 +181,29 @@ class NotifySNS(NotifyBase): "to": { "alias_of": "targets", }, + "token": { + # ?token= kwarg mirrors the {token} URL path entry + "alias_of": "token", + }, "access": { "alias_of": "access_key_id", }, + "key": { + # Intuitive alias for access_key_id + "alias_of": "access_key_id", + }, "secret": { "alias_of": "secret_access_key", }, "region": { "alias_of": "region", }, + "mode": { + "name": _("Mode"), + "type": "choice:string", + "values": SNS_MODES, + "default": SNSMode.SMS, + }, }, ) @@ -167,11 +213,16 @@ class NotifySNS(NotifyBase): secret_access_key, region_name, targets=None, + session_token=None, + mode=None, **kwargs, ): """Initialize Notify AWS SNS Object.""" super().__init__(**kwargs) + # Store optional session token for temporary/IAM credentials + self.aws_session_token = session_token if session_token else None + # Store our AWS API Access Key self.aws_access_key_id = validate_regex(access_key_id) if not self.aws_access_key_id: @@ -235,6 +286,28 @@ class NotifySNS(NotifyBase): f"Dropped invalid phone/topic ({target}) specified.", ) + # Determine operating mode: explicit override wins, otherwise + # auto-detect from target composition + if mode and isinstance(mode, str): + # Resolve via prefix match (e.g. "t" -> "topic") + self.mode = next( + (m for m in SNS_MODES if m.startswith(mode.lower())), + None, + ) + if self.mode not in SNS_MODES: + msg = f"The AWS SNS mode specified ({mode}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) + + else: + # Auto-detect: topic mode when all targets are SNS topics + # (no phone numbers); SMS mode for phones or mixed sets + self.mode = ( + SNSMode.TOPIC + if self.topics and not self.phone + else SNSMode.SMS + ) + return def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): @@ -254,14 +327,21 @@ class NotifySNS(NotifyBase): topics = list(self.topics) while len(phone) > 0: - # Get Phone No no = phone.pop(0) + # In topic mode the framework passes the title through + # separately; SMS has no Subject field so the title is + # prepended to the body manually when present + if self.mode == SNSMode.TOPIC and title: + sms_body = "{}\r\n{}".format(title, body) + else: + sms_body = body + # Prepare SNS Message Payload payload = { "Action": "Publish", - "Message": body, + "Message": sms_body, "Version": "2010-03-31", "PhoneNumber": no, } @@ -272,7 +352,6 @@ class NotifySNS(NotifyBase): # Send all our defined topic id's while len(topics): - # Get Topic topic = topics.pop(0) @@ -303,6 +382,12 @@ class NotifySNS(NotifyBase): "Message": body, } + # In topic mode, use the title as the SNS Subject field + # when a title was provided; email subscribers will see + # it as the message subject line + if self.mode == SNSMode.TOPIC and title: + payload["Subject"] = title + # Send our payload to AWS (result, _) = self._post(payload=payload, to=topic) if not result: @@ -345,6 +430,7 @@ class NotifySNS(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -364,7 +450,8 @@ class NotifySNS(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return (False, NotifySNS.aws_response_to_dict(r.text)) @@ -416,55 +503,71 @@ class NotifySNS(NotifyBase): ) # Similar to headers; but a subset. keys must be lowercase - signed_headers = OrderedDict([ - ("content-type", headers["Content-Type"]), - ( - "host", - f"{self.aws_service_name}" - f".{self.aws_region_name}.amazonaws.com", - ), - ("x-amz-date", headers["X-Amz-Date"]), - ]) + signed_headers = OrderedDict( + [ + ("content-type", headers["Content-Type"]), + ( + "host", + f"{self.aws_service_name}" + f".{self.aws_region_name}.amazonaws.com", + ), + ("x-amz-date", headers["X-Amz-Date"]), + ] + ) + + # Include session token in signed headers for temporary credentials; + # x-amz-security-token sorts after x-amz-date alphabetically and + # must appear after it to keep the canonical request valid. + if self.aws_session_token: + headers["X-Amz-Security-Token"] = self.aws_session_token + signed_headers["x-amz-security-token"] = self.aws_session_token # # Build Canonical Request Object # - canonical_request = "\n".join([ - # Method - "POST", - # URL - self.aws_canonical_uri, - # Query String (none set for POST) - "", - # Header Content (must include \n at end!) - # All entries except characters in amazon date must be - # lowercase - "\n".join([f"{k}:{v}" for k, v in signed_headers.items()]) + "\n", - # Header Entries (in same order identified above) - ";".join(signed_headers.keys()), - # Payload - sha256(payload.encode("utf-8")).hexdigest(), - ]) + canonical_request = "\n".join( + [ + # Method + "POST", + # URL + self.aws_canonical_uri, + # Query String (none set for POST) + "", + # Header Content (must include \n at end!) + # All entries except characters in amazon date must be + # lowercase + "\n".join([f"{k}:{v}" for k, v in signed_headers.items()]) + + "\n", + # Header Entries (in same order identified above) + ";".join(signed_headers.keys()), + # Payload + sha256(payload.encode("utf-8")).hexdigest(), + ] + ) # Prepare Unsigned Signature - to_sign = "\n".join([ - self.aws_auth_algorithm, - amzdate, - scope, - sha256(canonical_request.encode("utf-8")).hexdigest(), - ]) + to_sign = "\n".join( + [ + self.aws_auth_algorithm, + amzdate, + scope, + sha256(canonical_request.encode("utf-8")).hexdigest(), + ] + ) # Our Authorization header - headers["Authorization"] = ", ".join([ - ( - f"{self.aws_auth_algorithm} " - f"Credential={self.aws_access_key_id}/{scope}" - ), - "SignedHeaders={signed_headers}".format( - signed_headers=";".join(signed_headers.keys()), - ), - f"Signature={self.aws_auth_signature(to_sign, reference)}", - ]) + headers["Authorization"] = ", ".join( + [ + ( + f"{self.aws_auth_algorithm} " + f"Credential={self.aws_access_key_id}/{scope}" + ), + "SignedHeaders={signed_headers}".format( + signed_headers=";".join(signed_headers.keys()), + ), + f"Signature={self.aws_auth_signature(to_sign, reference)}", + ] + ) return headers @@ -580,16 +683,46 @@ class NotifySNS(NotifyBase): self.aws_region_name, ) + @property + def title_maxlen(self): + """Maximum title length: 100 for topic mode, 0 for SMS.""" + # Return 100 in topic mode so the framework passes the title + # to send() separately; return 0 for SMS so the framework + # folds the title into the body automatically before send() + return 100 if self.mode == SNSMode.TOPIC else 0 + + @property + def body_maxlen(self): + """Maximum body length: 256 KB for topic mode, 160 for SMS.""" + # Topic publishes support up to 256 KB; SMS is capped at 160 + return 256000 if self.mode == SNSMode.TOPIC else 160 + def url(self, privacy=False, *args, **kwargs): """Returns the URL built dynamically based on specified arguments.""" # Our URL parameters params = self.url_parameters(privacy=privacy, *args, **kwargs) + # Always include mode so the round-trip preserves the setting + params["mode"] = self.mode + + # Prepare session token prefix when using temporary credentials; + # + and = are RFC 3986 sub-delimiters safe in the userinfo position + # and look nicer unencoded; / must stay encoded (%2F) because an + # unencoded slash terminates the URL authority and breaks parsing + token_prefix = ( + "{token}@".format( + token=self.pprint(self.aws_session_token, privacy, safe="+="), + ) + if self.aws_session_token + else "" + ) + return ( - "{schema}://{key_id}/{key_secret}/{region}/{targets}/" - "?{params}".format( + "{schema}://{token_prefix}{key_id}/{key_secret}" + "/{region}/{targets}/?{params}".format( schema=self.secure_protocol, + token_prefix=token_prefix, key_id=self.pprint(self.aws_access_key_id, privacy, safe=""), key_secret=self.pprint( self.aws_secret_access_key, @@ -598,15 +731,17 @@ class NotifySNS(NotifyBase): safe="", ), region=NotifySNS.quote(self.aws_region_name, safe=""), - targets="/".join([ - NotifySNS.quote(x) - for x in chain( - # Phone # are already prefixed with a plus symbol - self.phone, - # Topics are prefixed with a pound/hashtag symbol - [f"#{x}" for x in self.topics], - ) - ]), + targets="/".join( + [ + NotifySNS.quote(x) + for x in chain( + # Phone # are already prefixed with a plus symbol + self.phone, + # Topics are prefixed with a pound/hashtag symbol + [f"#{x}" for x in self.topics], + ) + ] + ), params=NotifySNS.urlencode(params), ) ) @@ -645,7 +780,6 @@ class NotifySNS(NotifyBase): # Section 1: Get Region and Access Secret index = 0 for i, entry in enumerate(entries): - # Are we at the region yet? result = IS_REGION.match(entry) if result: @@ -686,8 +820,11 @@ class NotifySNS(NotifyBase): else: results["secret_access_key"] = secret_access_key - # Handle access key id over-ride - if "access" in results["qsd"] and len(results["qsd"]["access"]): + # Handle access key id override; ?key= is the preferred alias, + # ?access= is retained for backwards compatibility + if "key" in results["qsd"] and len(results["qsd"]["key"]): + results["access_key_id"] = NotifySNS.unquote(results["qsd"]["key"]) + elif "access" in results["qsd"] and len(results["qsd"]["access"]): results["access_key_id"] = NotifySNS.unquote( results["qsd"]["access"] ) @@ -702,5 +839,21 @@ class NotifySNS(NotifyBase): else: results["region_name"] = region_name + # Session token: userinfo position ({token}@{host}) takes first pass; + # ?token= kwarg overrides it when both appear + results["session_token"] = None + if results.get("user"): + results["session_token"] = NotifySNS.unquote(results["user"]) + if "token" in results["qsd"] and len(results["qsd"]["token"]): + results["session_token"] = NotifySNS.unquote( + results["qsd"]["token"] + ) + + # Read mode parameter if supplied; None triggers auto-detection + # in __init__ based on target composition + results["mode"] = None + if "mode" in results["qsd"] and results["qsd"]["mode"]: + results["mode"] = NotifySNS.unquote(results["qsd"]["mode"]) + # Return our result set return results diff --git a/libs/apprise/plugins/sogs.py b/libs/apprise/plugins/sogs.py new file mode 100644 index 000000000..ea8b730c6 --- /dev/null +++ b/libs/apprise/plugins/sogs.py @@ -0,0 +1,589 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Session Open Group Server (SOGS) Notifications +# +# SOGS is the reference implementation of a Session Community Server. +# It hosts publicly accessible open-group rooms for the Session messaging app. +# Resources: +# - https://getsession.org/ +# - https://github.com/session-foundation/session-pysogs +# +# Setting up a bot account +# ------------------------- +# 1. Generate a 32-byte Ed25519 seed and note its 64-character hex encoding. +# In Python: +# import os; print(os.urandom(32).hex()) +# Keep this value secret -- it is your bot's seed. +# +# 2. Find the SOGS public_key from any Session group join link. +# The link looks like: +# https://open.getsession.org/discussion?public_key=a03c383c... +# The value after "public_key=" is the 64-hex-char public_key. +# +# 3. Find the room token -- the path segment of the join link above +# ("discussion" in the example). +# +# Apprise URL format +# ------------------- +# Secure (HTTPS): +# sessions://{public_key}:{seed}@{host}/{room} +# sessions://{public_key}:{seed}@{host}/{room1}/{room2} +# sessions://{public_key}:{seed}@{host}:{port}/{room} +# +# Insecure (HTTP): +# session://{public_key}:{seed}@{host}/{room} +# +# Query-string form (useful in config files): +# sessions://{host}/{room}?key={public_key}&seed={seed} +# sessions://{host}/{room}?public_key={public_key}&seed={seed} +# +# References: +# - SOGS API: https://github.com/session-foundation/session-pysogs +# - Session protocol: https://getsession.org/ +# - Auth spec: sogs/routes/auth.py in the session-pysogs repository + +from base64 import b64encode +import hashlib +from json import dumps, loads +import os +import re +import time + +import requests + +try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + ) + from cryptography.hazmat.primitives.serialization import ( + Encoding, + PublicFormat, + ) + + # We're good to go! + NOTIFY_SESSIONOGS_ENABLED = True + +except ImportError: + Ed25519PrivateKey = None + Encoding = None + PublicFormat = None + NOTIFY_SESSIONOGS_ENABLED = False + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from ..utils.parse import parse_list +from .base import NotifyBase + +# public_key: 64 hex characters (the SOGS server's Curve25519 public key). +# This is the value shown in Session group join links as ?public_key=... +IS_PUBLIC_KEY = re.compile(r"^[0-9a-f]{64}$", re.I) + +# Room token: alphanumeric, hyphens, underscores (as used by pysogs). +IS_ROOM_TOKEN = re.compile(r"^[a-z0-9_-]{1,64}$", re.I) + +# seed: 64 hex characters (32-byte Ed25519 seed that identifies the bot). +IS_SEED = re.compile(r"^[0-9a-f]{64}$", re.I) + +# HTTP error map for SOGS responses. +SOGS_HTTP_ERROR_MAP = { + 400: "Bad Request -- malformed message data or headers.", + 401: "Unauthorized -- invalid or missing X-SOGS-* auth headers.", + 403: "Forbidden -- the bot does not have write permission in this room.", + 404: "Not Found -- the room token does not exist on this server.", + 425: "Too Early -- timestamp out of range or nonce already used.", + 429: "Too Many Requests -- rate limit exceeded.", +} + + +def _encode_varint(n): + """Encode an integer as a protobuf-style base-128 varint.""" + out = bytearray() + while True: + b = n & 0x7F + n >>= 7 + if n: + out.append(b | 0x80) + else: + out.append(b) + break + return bytes(out) + + +def _ld_field(field_num, data): + """Encode a protobuf length-delimited field (wire type 2).""" + tag = _encode_varint((field_num << 3) | 2) + return tag + _encode_varint(len(data)) + data + + +def _build_session_message(text): + """Encode text as a Session protocol Content protobuf message. + + Builds: Content { dataMessage { body: text } } + and appends a minimal Session padding marker byte (0x80). + """ + # DataMessage.body is field 1, wire type 2 (length-delimited). + body_bytes = text.encode("utf-8") + data_message = _ld_field(1, body_bytes) + + # Content.dataMessage is field 1, wire type 2. + content = _ld_field(1, data_message) + + # Append the Session padding marker so the server can locate message + # boundaries correctly. + return content + b"\x80" + + +class NotifySessionOGS(NotifyBase): + """A wrapper for Session Open Group Server (SOGS) Notifications.""" + + # The default descriptive name associated with the Notification. + service_name = "Session Open Group Server" + + # The services URL. + service_url = "https://getsession.org/" + + # The default insecure protocol. + protocol = "session" + + # The default secure protocol. + secure_protocol = ("sessions", "sogs") + + # A URL that takes you to the setup/help of the specific protocol. + setup_url = "https://appriseit.com/services/sogs/" + + # SOGS message endpoint; actual request URL is built in _post(). + notify_url = "{schema}://{host}/room/{room}/message" + + # Titles are not a native Session concept; prepend to body if provided. + title_maxlen = 0 + + # 2000-character soft limit (Session app displays long messages fine). + body_maxlen = 2000 + + # Set our global enabled flag. + enabled = NOTIFY_SESSIONOGS_ENABLED + + # Require the cryptography library. + requirements = { + "packages_required": "cryptography", + } + + # URL templates for this plugin. + templates = ( + "{schema}://{user}:{password}@{host}/{targets}", + "{schema}://{user}:{password}@{host}:{port}/{targets}", + ) + + # Template tokens. + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "host": { + "name": _("Hostname"), + "type": "string", + "required": True, + }, + "port": { + "name": _("Port"), + "type": "int", + "min": 1, + "max": 65535, + }, + # URL user field carries the 64-hex public_key (Curve25519 pubkey). + "user": { + "name": _("Public Key"), + "type": "string", + "required": True, + "map_to": "public_key", + }, + # URL password field carries the 64-hex Ed25519 seed. + "password": { + "name": _("Seed"), + "type": "string", + "private": True, + "required": True, + "map_to": "seed", + }, + "targets": { + "name": _("Rooms"), + "type": "list:string", + }, + }, + ) + + # Template arguments. + template_args = dict( + NotifyBase.template_args, + **{ + # key= is the short query-string alias for the server's Curve25519 + # public key (URL user field). + "key": { + "alias_of": "user", + }, + # public_key= matches Session's own join-link terminology + # (?public_key=... in Session group join links). + "public_key": { + "alias_of": "user", + }, + # seed= is the query-string form of the bot's Ed25519 seed + # (URL password field). + "seed": { + "alias_of": "password", + }, + # to= is the project-wide convention for additional targets. + "to": { + "alias_of": "targets", + }, + }, + ) + + def __init__(self, public_key, seed, targets=None, **kwargs): + """Initialize Session Open Group Server Object.""" + super().__init__(**kwargs) + + # Raise a clear error when the cryptography library is absent so + # callers get an explicit message rather than a confusing + # AttributeError from None.from_private_bytes(). + if not NOTIFY_SESSIONOGS_ENABLED: + msg = ( + "The cryptography library is required for SOGS " + "notifications. Install it with: pip install cryptography" + ) + self.logger.warning(msg) + raise ImportError(msg) + + # Validate the public key (64-char hex Curve25519 public key). + _pk = (public_key or "").strip().lower() + if not IS_PUBLIC_KEY.match(_pk): + msg = ( + "The SOGS public_key must be exactly 64 hex " + f"characters ({_pk!r} is invalid)." + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store the validated public key. + self.public_key = _pk + + # Validate the bot seed (64-char hex Ed25519 seed). + _seed = (seed or "").strip().lower() + if not IS_SEED.match(_seed): + msg = ( + "The SOGS bot seed must be exactly 64 hex " + f"characters ({_seed!r} is invalid)." + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Store the validated seed hex string. + self.seed = _seed + + # Derive the Ed25519 signing key from the seed. + self._signing_key = Ed25519PrivateKey.from_private_bytes( + bytes.fromhex(self.seed) + ) + + # Derive the 32-byte Ed25519 public key for auth headers. + self._bot_pubkey_bytes = self._signing_key.public_key().public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + + # Parse and validate the room token list. + self.rooms = [] + self.invalid_rooms = [] + for token in parse_list(targets): + token = token.strip().lower() + if IS_ROOM_TOKEN.match(token): + self.rooms.append(token) + else: + self.logger.warning( + "SOGS ignoring invalid room token %r.", token + ) + self.invalid_rooms.append(token) + + # At least one valid room is required to send a notification. + if not self.rooms: + msg = "At least one valid SOGS room token must be specified." + self.logger.warning(msg) + raise TypeError(msg) + + def _sogs_auth_headers(self, method, path, body_bytes=None): + """ + Build the four X-SOGS-* authentication headers for a request. + + The signature covers: + SERVER_KEY || NONCE || TIMESTAMP || METHOD || PATH [|| HBODY] + where HBODY is the 64-byte blake2b hash of the request body when + the body is non-empty. The signing key is the bot's Ed25519 key. + """ + # Generate a fresh 16-byte random nonce for each request. + nonce = os.urandom(16) + + # Unix timestamp as a decimal string. + ts_str = str(int(time.time())) + + # Concatenate the fields to sign. + to_sign = ( + bytes.fromhex(self.public_key) + + nonce + + ts_str.encode() + + method.upper().encode() + + path.encode() + ) + + # Append the 64-byte blake2b hash of the body when a body is present. + if body_bytes: + to_sign += hashlib.blake2b(body_bytes, digest_size=64).digest() + + # Sign with the bot's Ed25519 key. + sig = self._signing_key.sign(to_sign) + + # Assemble headers. The Pubkey header is "00" (unblinded prefix) + # followed by the 32-byte Ed25519 pubkey as hex. + return { + "X-SOGS-Pubkey": "00" + self._bot_pubkey_bytes.hex(), + "X-SOGS-Nonce": b64encode(nonce).decode(), + "X-SOGS-Timestamp": ts_str, + "X-SOGS-Signature": b64encode(sig).decode(), + } + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform Session Open Group Server Notification.""" + + # Encode as a Session protocol protobuf message with padding. + # title_maxlen=0 ensures the framework already prepended any title. + msg_data = _build_session_message(body) + + # Sign the padded message bytes with the bot's Ed25519 key. + msg_sig = self._signing_key.sign(msg_data) + + # JSON payload for POST /room/{room}/message. + payload = { + "data": b64encode(msg_data).decode(), + "signature": b64encode(msg_sig).decode(), + } + + # Serialize the payload to bytes for body hashing. + body_bytes = dumps(payload).encode() + + # Send to each configured room in turn. + has_error = False + for room in self.rooms: + if not self._post(room, body_bytes): + has_error = True + + return not has_error + + def _post(self, room, body_bytes): + """POST a notification to a single SOGS room.""" + + # Build the request path. + path = f"/room/{room}/message" + + # Derive the base URL from the secure flag and host/port. + # Only include the port when it differs from the protocol default. + schema = "https" if self.secure else "http" + default_port = 443 if self.secure else 80 + host_str = ( + f"{self.host}:{self.port}" + if self.port and self.port != default_port + else self.host + ) + url = f"{schema}://{host_str}{path}" + + # Build the X-SOGS-* authentication headers. + auth_headers = self._sogs_auth_headers("POST", path, body_bytes) + + # Merge with the content-type header. + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + } + headers.update(auth_headers) + + self.logger.debug( + "SOGS POST URL: %s (cert_verify=%r)", url, self.verify_certificate + ) + self.logger.debug("SOGS Room: %s", room) + + # Throttle before every network request. + self.throttle() + + try: + r = requests.post( + url, + data=body_bytes, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + # Parse response body defensively. + try: + content = loads(r.content) + except (AttributeError, TypeError, ValueError): + content = {} + + if r.status_code not in ( + requests.codes.ok, + requests.codes.created, + ): + # Log any known status description. + status_str = NotifySessionOGS.http_response_code_lookup( + r.status_code, SOGS_HTTP_ERROR_MAP + ) + self.logger.warning( + "Failed to post to SOGS room %r: %s%serror=%s.", + room, + status_str, + ", " if status_str else "", + r.status_code, + ) + self.logger.debug("Response Details:\r\n%s", r.content) + return False + + self.logger.info("Sent SOGS notification to room %r.", room) + self.logger.debug("SOGS response: %s", content) + + except requests.RequestException as e: + self.logger.warning( + "A connection error occurred posting to SOGS room %r.", + room, + ) + self.logger.debug("Socket Exception: %s", str(e)) + return False + + return True + + @property + def url_identifier(self): + """ + Returns all of the identifiers that make this URL unique from + another similar one. + + Targets or end points should never be identified here. + """ + return ( + self.secure_protocol[0] if self.secure else self.protocol, + self.public_key, + self.host, + self.port if self.port else (443 if self.secure else 80), + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified arguments.""" + + # Collect standard URL parameters. + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + # Determine the schema and default port. + schema = self.secure_protocol[0] if self.secure else self.protocol + default_port = 443 if self.secure else 80 + + # public_key goes in the URL user field; seed in the password field + # (masked to {first}...{last} when privacy=True). + return "{schema}://{public_key}:{seed}@{host}{port}/{rooms}?{params}".format( + schema=schema, + public_key=NotifySessionOGS.quote(self.public_key, safe=""), + seed=self.pprint(self.seed, privacy, safe=""), + host=self.host, + port=( + "" + if not self.port or self.port == default_port + else f":{self.port}" + ), + rooms="/".join( + [NotifySessionOGS.quote(r, safe="") for r in self.rooms] + + [ + NotifySessionOGS.quote(r, safe="") + for r in self.invalid_rooms + ] + ), + params=NotifySessionOGS.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow us to + re-instantiate this object. + """ + results = NotifyBase.parse_url(url) + if not results: + return results + + # The URL user field carries the public_key (Curve25519 public key). + results["public_key"] = ( + NotifySessionOGS.unquote(results["user"]) + if results.get("user") + else None + ) + + # The URL password field carries the bot's Ed25519 seed. + results["seed"] = ( + NotifySessionOGS.unquote(results["password"]) + if results.get("password") + else None + ) + + # ?key= is the short query-string alias for the public_key. + if "key" in results["qsd"] and results["qsd"]["key"]: + results["public_key"] = NotifySessionOGS.unquote( + results["qsd"]["key"] + ) + + # ?public_key= matches Session's own join-link terminology and + # overrides ?key= when both are present. + if "public_key" in results["qsd"] and results["qsd"]["public_key"]: + results["public_key"] = NotifySessionOGS.unquote( + results["qsd"]["public_key"] + ) + + # ?seed= query param overrides the URL password field. + if "seed" in results["qsd"] and results["qsd"]["seed"]: + results["seed"] = NotifySessionOGS.unquote(results["qsd"]["seed"]) + + # All path segments are room tokens. + results["targets"] = NotifySessionOGS.split_path(results["fullpath"]) + + # ?to= is the project-wide alias for additional room tokens. + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifySessionOGS.parse_list( + results["qsd"]["to"] + ) + + return results + + @staticmethod + def runtime_deps(): + """ + Return a tuple of top-level Python package names that this + plugin imported as optional runtime dependencies. + """ + return ("cryptography",) diff --git a/libs/apprise/plugins/sparkpost.py b/libs/apprise/plugins/sparkpost.py index 5a135598d..e3b05c89d 100644 --- a/libs/apprise/plugins/sparkpost.py +++ b/libs/apprise/plugins/sparkpost.py @@ -147,8 +147,8 @@ class NotifySparkPost(NotifyBase): # Define object templates templates = ( - "{schema}://{user}@{host}:{apikey}/", - "{schema}://{user}@{host}:{apikey}/{targets}", + "{schema}://{user}@{host}/{apikey}/", + "{schema}://{user}@{host}/{apikey}/{targets}", ) # Define our template tokens @@ -316,10 +316,12 @@ class NotifySparkPost(NotifyBase): for recipient in parse_emails(targets): result = is_email(recipient) if result: - self.targets.append(( - result["name"] if result["name"] else False, - result["full_email"], - )) + self.targets.append( + ( + result["name"] if result["name"] else False, + result["full_email"], + ) + ) continue self.logger.warning( @@ -388,7 +390,8 @@ class NotifySparkPost(NotifyBase): f" {url} (cert_verify={self.verify_certificate})" ) self.logger.debug( - "SparkPost Payload: %s", sanitize_payload(payload)) + "SparkPost Payload: %s", sanitize_payload(payload) + ) wait = None @@ -407,7 +410,6 @@ class NotifySparkPost(NotifyBase): status_code = -1 while 1: # pragma: no branch - # Always call throttle before any remote server i/o is made self.throttle(wait=wait) try: @@ -417,6 +419,7 @@ class NotifySparkPost(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # A Good response (200) looks like this: @@ -440,7 +443,8 @@ class NotifySparkPost(NotifyBase): # } # with contextlib.suppress( - AttributeError, TypeError, ValueError): + AttributeError, TypeError, ValueError + ): # Load our JSON Object if we can # ValueError = r.content is Unparsable # TypeError = r.content is None @@ -472,7 +476,8 @@ class NotifySparkPost(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) if status_code == requests.codes.too_many_requests and retry: retry = retry - 1 @@ -565,15 +570,17 @@ class NotifySparkPost(NotifyBase): try: # Prepare API Upload Payload - payload["content"]["attachments"].append({ - "name": ( - attachment.name - if attachment.name - else f"file{no:03}.dat" - ), - "type": attachment.mimetype, - "data": attachment.base64(), - }) + payload["content"]["attachments"].append( + { + "name": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + "type": attachment.mimetype, + "data": attachment.base64(), + } + ) except exception.AppriseException: # We could not access the attachment @@ -661,14 +668,16 @@ class NotifySparkPost(NotifyBase): # Handle our bcc for addr in bcc: # Add our recipient to our list - payload["recipients"].append({ - "address": { - "email": addr, - "header_to": - # Take the first email in the To - self.targets[index : index + batch_size][0][1], - }, - }) + payload["recipients"].append( + { + "address": { + "email": addr, + "header_to": + # Take the first email in the To + self.targets[index : index + batch_size][0][1], + }, + } + ) if headers: payload["content"]["headers"] = headers @@ -717,13 +726,15 @@ class NotifySparkPost(NotifyBase): if self.cc: # Handle our Carbon Copy Addresses - params["cc"] = ",".join([ - "{}{}".format( - "" if not e not in self.names else f"{self.names[e]}:", - e, - ) - for e in self.cc - ]) + params["cc"] = ",".join( + [ + "{}{}".format( + "" if not e not in self.names else f"{self.names[e]}:", + e, + ) + for e in self.cc + ] + ) if self.bcc: # Handle our Blind Carbon Copy Addresses @@ -743,13 +754,17 @@ class NotifySparkPost(NotifyBase): targets=( "" if not has_targets - else "/".join([ - NotifySparkPost.quote( - "{}{}".format("" if not e[0] else f"{e[0]}:", e[1]), - safe="", - ) - for e in self.targets - ]) + else "/".join( + [ + NotifySparkPost.quote( + "{}{}".format( + "" if not e[0] else f"{e[0]}:", e[1] + ), + safe="", + ) + for e in self.targets + ] + ) ), params=NotifySparkPost.urlencode(params), ) diff --git a/libs/apprise/plugins/spike.py b/libs/apprise/plugins/spike.py index 22584c72f..4cd4e38a6 100644 --- a/libs/apprise/plugins/spike.py +++ b/libs/apprise/plugins/spike.py @@ -129,6 +129,7 @@ class NotifySpike(NotifyBase): data=json.dumps(payload), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if response.status_code != requests.codes.ok: self.logger.warning( diff --git a/libs/apprise/plugins/splunk.py b/libs/apprise/plugins/splunk.py index dddc70aaa..2ce310fac 100644 --- a/libs/apprise/plugins/splunk.py +++ b/libs/apprise/plugins/splunk.py @@ -281,11 +281,7 @@ class NotifySplunk(NotifyBase): v_upper = v_.upper() v = next( - ( - v - for v in SPLUNK_MESSAGE_TYPES - if v.startswith(v_upper) - ), + (v for v in SPLUNK_MESSAGE_TYPES if v.startswith(v_upper)), None, ) if not v: @@ -364,6 +360,7 @@ class NotifySplunk(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Sample Response # { @@ -384,7 +381,8 @@ class NotifySplunk(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/spugpush.py b/libs/apprise/plugins/spugpush.py index 3af8754f4..9662fb50f 100644 --- a/libs/apprise/plugins/spugpush.py +++ b/libs/apprise/plugins/spugpush.py @@ -123,6 +123,7 @@ class NotifySpugpush(NotifyBase): data=json.dumps(payload), verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if response.status_code != requests.codes.ok: diff --git a/libs/apprise/plugins/streamlabs.py b/libs/apprise/plugins/streamlabs.py index e5d9ffdc0..674776ef7 100644 --- a/libs/apprise/plugins/streamlabs.py +++ b/libs/apprise/plugins/streamlabs.py @@ -278,7 +278,6 @@ class NotifyStreamlabs(NotifyBase): "User-Agent": self.app_id, } if self.call == StrmlabsCall.ALERT: - data = { "access_token": self.access_token, "type": self.alert_type.lower(), @@ -297,6 +296,7 @@ class NotifyStreamlabs(NotifyBase): data=data, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -314,7 +314,8 @@ class NotifyStreamlabs(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False else: @@ -344,6 +345,7 @@ class NotifyStreamlabs(NotifyBase): data=data, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem diff --git a/libs/apprise/plugins/synology.py b/libs/apprise/plugins/synology.py index 1204595c2..ecfaa4594 100644 --- a/libs/apprise/plugins/synology.py +++ b/libs/apprise/plugins/synology.py @@ -289,6 +289,7 @@ class NotifySynology(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code < 200 or r.status_code >= 300: # We had a problem @@ -305,7 +306,8 @@ class NotifySynology(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False @@ -354,7 +356,7 @@ class NotifySynology(NotifyBase): # Update our fullpath to not include our token results["fullpath"] = results["fullpath"][ - len(results["token"]) + 1: + len(results["token"]) + 1 : ] # Set upload/file_url if not otherwise set diff --git a/libs/apprise/plugins/techuluspush.py b/libs/apprise/plugins/techuluspush.py index 1e37b2a84..f02a62a59 100644 --- a/libs/apprise/plugins/techuluspush.py +++ b/libs/apprise/plugins/techuluspush.py @@ -146,6 +146,7 @@ class NotifyTechulusPush(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -164,7 +165,8 @@ class NotifyTechulusPush(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False diff --git a/libs/apprise/plugins/telegram.py b/libs/apprise/plugins/telegram.py index d267bc490..8c5f8255d 100644 --- a/libs/apprise/plugins/telegram.py +++ b/libs/apprise/plugins/telegram.py @@ -603,7 +603,9 @@ class NotifyTelegram(NotifyBase): payload["message_thread_id"] = topic try: - with open(path, "rb") as f: + with ( + attach if isinstance(attach, AttachBase) else open(path, "rb") + ) as f: # Configure file payload (for upload) files = {key: (file_name, f)} @@ -619,6 +621,7 @@ class NotifyTelegram(NotifyBase): data=payload, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -637,7 +640,8 @@ class NotifyTelegram(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) return False @@ -684,6 +688,7 @@ class NotifyTelegram(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -826,7 +831,6 @@ class NotifyTelegram(NotifyBase): # Prepare Message Body if self.notify_format == NotifyFormat.MARKDOWN: - if ( body_format not in (None, NotifyFormat.MARKDOWN) and self.markdown_ver == TelegramMarkdownVersion.TWO @@ -842,11 +846,9 @@ class NotifyTelegram(NotifyBase): payload_["text"] = body else: # HTML - # Use Telegram's HTML mode payload_["parse_mode"] = "HTML" for r, v, m in self.__telegram_escape_html_entries: - if "html" in m: # Handle special cases where we need to alter new lines # for presentation purposes @@ -919,7 +921,6 @@ class NotifyTelegram(NotifyBase): payload=caption_payload, attach=attach, ): - has_error = True continue @@ -949,6 +950,7 @@ class NotifyTelegram(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -1019,16 +1021,19 @@ class NotifyTelegram(NotifyBase): # Send our attachments now (if specified and if it exists) for no, attachment in enumerate(attach, start=1): payload = payload if payload and no == 1 else {} - payload.update({ - "title": ( - attachment.name if attachment.name else f"file{no:03}.dat" - ) - }) + payload.update( + { + "title": ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ) + } + ) if not self.send_media( target, notify_type, payload=payload, attach=attachment ): - # We failed; don't continue has_error = True break @@ -1070,14 +1075,16 @@ class NotifyTelegram(NotifyBase): topic = topic_ if topic_ else self.topic targets.append( - "".join([ - ( - NotifyTelegram.quote(f"{chat_id}", safe="@") - if isinstance(chat_id, str) - else f"{chat_id}" - ), - "" if not topic else f":{topic}", - ]) + "".join( + [ + ( + NotifyTelegram.quote(f"{chat_id}", safe="@") + if isinstance(chat_id, str) + else f"{chat_id}" + ), + "" if not topic else f":{topic}", + ] + ) ) # No need to check the user token because the user automatically gets diff --git a/libs/apprise/plugins/threema.py b/libs/apprise/plugins/threema.py index 0f73c27c9..5b272f20a 100644 --- a/libs/apprise/plugins/threema.py +++ b/libs/apprise/plugins/threema.py @@ -123,9 +123,6 @@ class NotifyThreema(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "gateway_id", }, @@ -135,6 +132,9 @@ class NotifyThreema(NotifyBase): "secret": { "alias_of": "secret", }, + "to": { + "alias_of": "targets", + }, }, ) @@ -256,6 +256,7 @@ class NotifyThreema(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -275,7 +276,8 @@ class NotifyThreema(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/twilio.py b/libs/apprise/plugins/twilio.py index 82f7ab439..684710e93 100644 --- a/libs/apprise/plugins/twilio.py +++ b/libs/apprise/plugins/twilio.py @@ -181,9 +181,6 @@ class NotifyTwilio(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "from_phone", }, @@ -205,6 +202,9 @@ class NotifyTwilio(NotifyBase): "values": TWILIO_NOTIFICATION_METHODS, "default": TWILIO_NOTIFICATION_METHODS[0], }, + "to": { + "alias_of": "targets", + }, }, ) @@ -251,11 +251,12 @@ class NotifyTwilio(NotifyBase): # Set notification method if isinstance(method, str) and method: - self.method = next(( - a - for a in TWILIO_NOTIFICATION_METHODS - if a.startswith(method.lower()) - ), + self.method = next( + ( + a + for a in TWILIO_NOTIFICATION_METHODS + if a.startswith(method.lower()) + ), None, ) @@ -288,8 +289,10 @@ class NotifyTwilio(NotifyBase): ) # Check compatibility between notification method and mode - if self.method == TwilioNotificationMethod.CALL and \ - self.default_mode == TwilioMessageMode.WHATSAPP: + if ( + self.method == TwilioNotificationMethod.CALL + and self.default_mode == TwilioMessageMode.WHATSAPP + ): msg = ( "The notification method Call is not valid along " "message mode Whatsapp." @@ -353,9 +356,10 @@ class NotifyTwilio(NotifyBase): # We can't use WhatsApp using short-codes as our source or # for phone calls - if ((len(self.source) in (5, 6) - or self.method == TwilioNotificationMethod.CALL) - and mode is TwilioMessageMode.WHATSAPP): + if ( + len(self.source) in (5, 6) + or self.method == TwilioNotificationMethod.CALL + ) and mode is TwilioMessageMode.WHATSAPP: self.logger.warning( f"Dropped WhatsApp phone # ({entry}) because source" " provided is a short-code or because notification" @@ -438,6 +442,7 @@ class NotifyTwilio(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -478,7 +483,8 @@ class NotifyTwilio(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -504,9 +510,11 @@ class NotifyTwilio(NotifyBase): def body_maxlen(self): """The maximum allowable characters allowed in the body per message. It is dependent on the notification method.""" - return self.body_sms_maxlen \ - if self.method == TwilioNotificationMethod.SMS \ + return ( + self.body_sms_maxlen + if self.method == TwilioNotificationMethod.SMS else self.body_call_maxlen + ) @property def url_identifier(self): @@ -548,13 +556,19 @@ class NotifyTwilio(NotifyBase): ), safe="", ), - targets="/".join([ - NotifyTwilio.quote( - (x[1] if x[0] is TwilioMessageMode.TEXT else f"w:{x[1]}"), - safe="", - ) - for x in self.targets - ]), + targets="/".join( + [ + NotifyTwilio.quote( + ( + x[1] + if x[0] is TwilioMessageMode.TEXT + else f"w:{x[1]}" + ), + safe="", + ) + for x in self.targets + ] + ), params=NotifyTwilio.urlencode(params), ) diff --git a/libs/apprise/plugins/twist.py b/libs/apprise/plugins/twist.py index 2a03ddf34..fc35d4d4b 100644 --- a/libs/apprise/plugins/twist.py +++ b/libs/apprise/plugins/twist.py @@ -254,15 +254,17 @@ class NotifyTwist(NotifyBase): ), user=self.quote(self.user, safe=""), host=self.host, - targets="/".join([ - NotifyTwist.quote(x, safe="") - for x in chain( - # Channels are prefixed with a pound/hashtag symbol - [f"#{x}" for x in self.channels], - # Channel IDs - self.channel_ids, - ) - ]), + targets="/".join( + [ + NotifyTwist.quote(x, safe="") + for x in chain( + # Channels are prefixed with a pound/hashtag symbol + [f"#{x}" for x in self.channels], + # Channel IDs + self.channel_ids, + ) + ] + ), params=NotifyTwist.urlencode(params), ) ) @@ -659,6 +661,7 @@ class NotifyTwist(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Get our JSON content if it's possible @@ -695,13 +698,13 @@ class NotifyTwist(NotifyBase): and content.get("error_code") in (120, 200) and self.login() ): - r = fn( api_url, data=payload, headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Get our JSON content if it's possible @@ -730,7 +733,8 @@ class NotifyTwist(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure return (False, content) diff --git a/libs/apprise/plugins/twitter.py b/libs/apprise/plugins/twitter.py index bc795437e..4bd166baf 100644 --- a/libs/apprise/plugins/twitter.py +++ b/libs/apprise/plugins/twitter.py @@ -25,8 +25,28 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -# See https://developer.twitter.com/en/docs/direct-messages/\ -# sending-and-receiving/api-reference/new-event.html +# Migration guide from X API v1.1 to v2: +# https://docs.x.com/x-api/migrate/overview +# +# X API v2 Tweet endpoint: +# https://docs.x.com/x-api/posts/creation/quickstart/post-a-tweet +# +# X API v2 Direct Message endpoint: +# https://docs.x.com/x-api/direct-messages/manage-direct-messages/ +# api-reference/post-dm_conversations-dm_conversation_id-messages +# +# X API v2 User Lookup endpoint: +# https://docs.x.com/x-api/users/user-lookup/api-reference/get-users-by +# +# X API v2 Current User endpoint: +# https://docs.x.com/x-api/users/user-lookup/api-reference/get-users-me +# +# X API v2 Media upload (available on all access tiers, including Free): +# https://docs.x.com/x-api/media/upload-media +# +# Free-tier accounts can ONLY post tweets (mode=tweet). +# Sending Direct Messages (mode=dm, the default) requires a paid X API +# subscription (Basic or higher). import contextlib from copy import deepcopy from datetime import datetime, timezone @@ -49,10 +69,10 @@ IS_USER = re.compile(r"^\s*@?(?P[A-Z0-9_]+)$", re.I) class TwitterMessageMode: """Twitter Message Mode.""" - # DM (a Direct Message) + # DM (a Direct Message) -- requires Basic+ X API subscription DM = "dm" - # A Public Tweet + # A Public Tweet -- available on Free X API tier and above TWEET = "tweet" @@ -89,27 +109,32 @@ class NotifyTwitter(NotifyBase): # Twitter does have titles when creating a message title_maxlen = 0 - # Twitter API Reference To Acquire Someone's Twitter ID - twitter_lookup = "https://api.twitter.com/1.1/users/lookup.json" + # X API v2 Endpoints: - # Twitter API Reference To Acquire Current Users Information - twitter_whoami = ( - "https://api.twitter.com/1.1/account/verify_credentials.json" - ) + # X API v2 -- Post a Tweet + # Available on Free tier and above + twitter_tweet = "https://api.twitter.com/2/tweets" - # Twitter API Reference To Send A Private DM - twitter_dm = "https://api.twitter.com/1.1/direct_messages/events/new.json" + # X API v2 -- Current authenticated user info + twitter_whoami = "https://api.twitter.com/2/users/me" - # Twitter API Reference To Send A Public Tweet - twitter_tweet = "https://api.twitter.com/1.1/statuses/update.json" + # X API v2 -- Batch user lookup by username (?usernames=u1,u2,...) + twitter_lookup = "https://api.twitter.com/2/users/by" + + # X API v2 -- Direct Message to a participant (format with user_id) + # Requires Basic+ X API subscription + twitter_dm = "https://api.twitter.com/2/dm_conversations/with/{}/messages" + + # X API v2 Media Upload (available on all access tiers, including Free) + # media_category is required: "tweet_image" or "dm_image" + + # Twitter Media (Attachment) Upload Location + twitter_media = "https://api.x.com/2/media/upload" # it is documented on the site that the maximum images per tweet # is 4 (unless it's a GIF, then it's only 1) __tweet_non_gif_images_batch = 4 - # Twitter Media (Attachment) Upload Location - twitter_media = "https://upload.twitter.com/1.1/media/upload.json" - # Twitter is kind enough to return how many more requests we're allowed to # continue to make within it's header response as: # X-Rate-Limit-Reset: The epoc time (in seconds) we can expect our @@ -312,7 +337,6 @@ class NotifyTwitter(NotifyBase): # We need to upload our payload first so that we can source it # in remaining messages for no, attachment in enumerate(attach, start=1): - # Perform some simple error checking if not attachment: # We could not access the attachment @@ -335,12 +359,20 @@ class NotifyTwitter(NotifyBase): f"{attachment.url(privacy=True)}" ) + # v2 media upload requires media_category: + # "tweet_image" for public tweets, "dm_image" for DMs + media_category = ( + "tweet_image" + if self.mode == TwitterMessageMode.TWEET + else "dm_image" + ) + # Upload our image and get our id associated with it - # see: https://developer.twitter.com/en/docs/twitter-api/v1/\ - # media/upload-media/api-reference/post-media-upload + # see: https://docs.x.com/x-api/media/upload-media postokay, response = self._fetch( self.twitter_media, payload=attachment, + extra={"media_category": media_category}, ) if not postokay: @@ -352,9 +384,13 @@ class NotifyTwitter(NotifyBase): attachment.name if attachment.name else f"file{no:03}.dat" ) - if not ( - isinstance(response, dict) and response.get("media_id") - ): + # v2 response: {"data": {"id": "...", "media_key": "...", ...}} + try: + media_id = response["data"]["id"] + except (TypeError, KeyError): + media_id = None + + if not media_id: self.logger.debug( "Could not attach the file to Twitter: %s (mime=%s)", filename, @@ -362,27 +398,17 @@ class NotifyTwitter(NotifyBase): ) continue - # If we get here, our output will look something like this: - # { - # "media_id": 710511363345354753, - # "media_id_string": "710511363345354753", - # "media_key": "3_710511363345354753", - # "size": 11065, - # "expires_after_secs": 86400, - # "image": { - # "image_type": "image/jpeg", - # "w": 800, - # "h": 320 - # } - # } - - response.update({ - # Update our response to additionally include the - # attachment details - "file_name": filename, - "file_mime": attachment.mimetype, - "file_path": attachment.path, - }) + response.update( + { + # Normalize id for _send_tweet/_send_dm consumption + "media_id": media_id, + # Update our response to additionally include the + # attachment details + "file_name": filename, + "file_mime": attachment.mimetype, + "file_path": attachment.path, + } + ) # Save our pre-prepared payload for attachment posting attachments.append(response) @@ -405,13 +431,14 @@ class NotifyTwitter(NotifyBase): attachments=None, **kwargs, ): - """Twitter Public Tweet.""" + """Twitter Public Tweet via X API v2.""" # Error Tracking has_error = False + # Base payload; v2 uses "text" instead of the v1.1 "status" key payload = { - "status": body, + "text": body, } payloads = [] @@ -428,7 +455,8 @@ class NotifyTwitter(NotifyBase): batches = [] batch = [] for attachment in attachments: - batch.append(str(attachment["media_id"])) + # v2 media_ids must be strings + media_id = str(attachment["media_id"]) # Twitter supports batching images together. This allows # the batching of multiple images together. Twitter also @@ -444,33 +472,41 @@ class NotifyTwitter(NotifyBase): # If you passed in, image, image, gif, image. <- This would # produce 3 images (as the first 2 images could be lumped # together as a batch) - if ( - not re.match( - r"^image/(png|jpe?g)", attachment["file_mime"], re.I - ) - or len(batch) >= batch_size + if not re.match( + r"^image/(png|jpe?g)", attachment["file_mime"], re.I ): - batches.append(",".join(batch)) - batch = [] + # Non-batchable (e.g. GIF): flush any pending batch + # first, then emit this attachment alone + if batch: + batches.append(list(batch)) + batch = [] + batches.append([media_id]) + else: + # PNG/JPEG: accumulate until the batch is full + batch.append(media_id) + if len(batch) >= batch_size: + batches.append(list(batch)) + batch = [] if batch: - batches.append(",".join(batch)) + batches.append(list(batch)) for no, media_ids in enumerate(batches): payload_ = deepcopy(payload) - payload_["media_ids"] = media_ids + # v2 places media in a nested dict with a list of id strings + payload_["media"] = {"media_ids": media_ids} if no or not body: # strip text and replace it with the image representation - payload_["status"] = f"{no + 1:02d}/{len(batches):02d}" + payload_["text"] = f"{no + 1:02d}/{len(batches):02d}" payloads.append(payload_) for no, payload in enumerate(payloads, start=1): - # Send Tweet + # Send Tweet via v2 (JSON body required) postokay, response = self._fetch( self.twitter_tweet, payload=payload, - json=False, + json=True, ) if not postokay: @@ -478,12 +514,13 @@ class NotifyTwitter(NotifyBase): has_error = True errors = [] - with contextlib.suppress(KeyError, TypeError): + with contextlib.suppress(AttributeError, KeyError, TypeError): errors = [ - "Error Code {}: {}".format( - e.get("code", "unk"), e.get("message") + "Error {}: {}".format( + e.get("title", e.get("code", "unk")), + e.get("detail", e.get("message", "")), ) - for e in response["errors"] + for e in response.get("errors", []) ] for error in errors: @@ -495,10 +532,10 @@ class NotifyTwitter(NotifyBase): ) continue + # v2 response: {"data": {"id": "...", "text": "..."}} try: - url = "https://twitter.com/{}/status/{}".format( - response["user"]["screen_name"], response["id_str"] - ) + tweet_id = response["data"]["id"] + url = f"https://x.com/i/web/status/{tweet_id}" except (KeyError, TypeError): url = "unknown" @@ -523,26 +560,15 @@ class NotifyTwitter(NotifyBase): attachments=None, **kwargs, ): - """Twitter Direct Message.""" + """Twitter Direct Message via X API v2. + + Requires a Basic or higher X API subscription. + Free-tier accounts will receive a 403 error. + """ # Error Tracking has_error = False - payload = { - "event": { - "type": "message_create", - "message_create": { - "target": { - # This gets assigned - "recipient_id": None, - }, - "message_data": { - "text": body, - }, - }, - } - } - # Lookup our users (otherwise we look up ourselves) targets = ( self._whoami(lazy=self.cache) @@ -557,37 +583,32 @@ class NotifyTwitter(NotifyBase): ) return False + # Build our list of payloads; one per attachment (or a single message) payloads = [] if not attachments: - payloads.append(payload) + # Plain text DM + payloads.append({"text": body}) else: for no, attachment in enumerate(attachments): - payload_ = deepcopy(payload) - data = payload_["event"]["message_create"]["message_data"] - data["attachment"] = { - "type": "media", - "media": {"id": attachment["media_id"]}, - "additional_owners": ",".join( - [str(x) for x in targets.values()] + # v2 DM attachment format: attachments list with media_id + payload_ = { + "text": ( + body + if (not no and body) + else f"{no + 1:02d}/{len(attachments):02d}" ), + "attachments": [{"media_id": str(attachment["media_id"])}], } - if no or not body: - # strip text and replace it with the image representation - data["text"] = f"{no + 1:02d}/{len(attachments):02d}" payloads.append(payload_) for no, payload in enumerate(payloads, start=1): for screen_name, user_id in targets.items(): - # Assign our user - target = payload["event"]["message_create"]["target"] - target["recipient_id"] = user_id + # v2 DM endpoint embeds the recipient user_id in the URL + url = self.twitter_dm.format(user_id) # Send Twitter DM - postokay, _response = self._fetch( - self.twitter_dm, - payload=payload, - ) + postokay, _response = self._fetch(url, payload=payload) if not postokay: # Track our error @@ -602,29 +623,27 @@ class NotifyTwitter(NotifyBase): return not has_error def _whoami(self, lazy=True): - """Looks details of current authenticated user.""" + """Looks up details of the current authenticated user via v2.""" if lazy and self._whoami_cache is not None: # Use cached response return self._whoami_cache - # Contains a mapping of screen_name to id + # Contains a mapping of username to id results = {} - # Send Twitter DM + # Get current user info via v2 postokay, response = self._fetch( self.twitter_whoami, method="GET", - json=False, ) if postokay: + # v2 response: {"data": {"id": "...", "username": "...", ...}} try: - results[response["screen_name"]] = response["id"] - self._whoami_cache = { - response["screen_name"]: response["id"], - } - + data = response["data"] + results[data["username"]] = data["id"] + self._whoami_cache = {data["username"]: data["id"]} self._user_cache.update(results) except (TypeError, KeyError): @@ -632,17 +651,17 @@ class NotifyTwitter(NotifyBase): return results - def _user_lookup(self, screen_name, lazy=True): - """Looks up a screen name and returns the user id. + def _user_lookup(self, usernames, lazy=True): + """Looks up usernames and returns user IDs via v2. - the screen_name can be a list/set/tuple as well + usernames can be a list/set/tuple as well. """ - # Contains a mapping of screen_name to id + # Contains a mapping of username to id results = {} # Build a unique set of names - names = parse_list(screen_name) + names = parse_list(usernames) if lazy and self._user_cache: # Use cached response @@ -655,28 +674,31 @@ class NotifyTwitter(NotifyBase): # They're is nothing further to do return results - # Twitters API documents that it can lookup to 100 - # results at a time. - # https://developer.twitter.com/en/docs/accounts-and-users/\ - # follow-search-get-users/api-reference/get-users-lookup + # v2 supports up to 100 usernames per request + # https://docs.x.com/x-api/users/user-lookup/api-reference/ + # get-users-by for i in range(0, len(names), 100): - # Look up our names by their screen_name - postokay, response = self._fetch( + batch = names[i : i + 100] + + # Build query-string URL for GET /2/users/by?usernames=... + lookup_url = "{}?usernames={}".format( self.twitter_lookup, - payload={ - "screen_name": names[i : i + 100], - }, - json=False, + ",".join(NotifyTwitter.quote(n, safe="") for n in batch), ) - if not postokay or not isinstance(response, list): + postokay, response = self._fetch( + lookup_url, + method="GET", + ) + + if not postokay or not isinstance(response, dict): # Track our error continue - # Update our user index - for entry in response: + # v2 response: {"data": [{"id": "...", "username": "..."}, ...]} + for entry in response.get("data", []): with contextlib.suppress(TypeError, KeyError): - results[entry["screen_name"]] = entry["id"] + results[entry["username"]] = entry["id"] # Cache our response for future use; this saves on un-nessisary extra # hits against the Twitter API when we already know the answer @@ -684,7 +706,7 @@ class NotifyTwitter(NotifyBase): return results - def _fetch(self, url, payload=None, method="POST", json=True): + def _fetch(self, url, payload=None, method="POST", json=True, extra=None): """Wrapper to Twitter API requests object.""" headers = { @@ -705,13 +727,16 @@ class NotifyTwitter(NotifyBase): open(payload.path, "rb"), # noqa: SIM115 ), } + # extra holds additional form fields (e.g. media_category) + data = extra - elif json: - headers["Content-Type"] = "application/json" - data = dumps(payload) - - else: - data = payload + elif payload is not None: + if json: + headers["Content-Type"] = "application/json" + data = dumps(payload) + else: + # form-encoded or raw body (e.g. media upload helper) + data = payload auth = OAuth1( self.ckey, @@ -761,6 +786,7 @@ class NotifyTwitter(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -772,7 +798,7 @@ class NotifyTwitter(NotifyBase): # AttributeError = r is None content = {} - if r.status_code != requests.codes.ok: + if not (200 <= r.status_code < 300): # We had a problem status_str = NotifyTwitter.http_response_code_lookup( r.status_code @@ -785,7 +811,8 @@ class NotifyTwitter(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure return (False, content) @@ -880,10 +907,12 @@ class NotifyTwitter(NotifyBase): self.asecret, privacy, mode=PrivacyMode.Secret, safe="" ), targets=( - "/".join([ - NotifyTwitter.quote(f"@{target}", safe="@") - for target in self.targets - ]) + "/".join( + [ + NotifyTwitter.quote(f"@{target}", safe="@") + for target in self.targets + ] + ) if self.targets else "" ), diff --git a/libs/apprise/plugins/vapid/__init__.py b/libs/apprise/plugins/vapid/__init__.py index 15300a9a7..58209f15d 100644 --- a/libs/apprise/plugins/vapid/__init__.py +++ b/libs/apprise/plugins/vapid/__init__.py @@ -306,7 +306,6 @@ class NotifyVapid(NotifyBase): and self.store.mode != PersistentStoreMode.MEMORY and self.asset.pem_autogen ): - self.subfile = os.path.join( self.store.path, self.vapid_subscription_file ) @@ -426,6 +425,7 @@ class NotifyVapid(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -447,7 +447,8 @@ class NotifyVapid(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) has_error = True @@ -614,3 +615,10 @@ class NotifyVapid(NotifyBase): def public_key(self): """Returns our public key representation.""" return self.pem.x962_str + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("cryptography",) diff --git a/libs/apprise/plugins/viber.py b/libs/apprise/plugins/viber.py index 7b3c3801b..bf74c9ba2 100644 --- a/libs/apprise/plugins/viber.py +++ b/libs/apprise/plugins/viber.py @@ -72,43 +72,46 @@ class NotifyViber(NotifyBase): viber_sender_name_limit = 28 # Minimal URL; endpoint is fixed, token is the first path entry. - templates = ( - "{schema}://{token}/{targets}", + templates = ("{schema}://{token}/{targets}",) + + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "token": { + "name": _("Authentication Token"), + "type": "string", + "private": True, + "required": True, + }, + "targets": { + "name": _("Receiver IDs"), + "type": "list:string", + "required": True, + }, + }, ) - template_tokens = dict(NotifyBase.template_tokens, **{ - "token": { - "name": _("Authentication Token"), - "type": "string", - "private": True, - "required": True, + template_args = dict( + NotifyBase.template_args, + **{ + # Viber requires sender.name + "from": { + "name": _("Bot Name"), + "type": "string", + "map_to": "source", + }, + # Optional sender.avatar URL + "avatar": { + "name": _("Bot Avatar URL"), + "type": "string", + }, + "token": { + "alias_of": "token", + }, + # Allow targets to also come from query string + "to": {"alias_of": "targets"}, }, - "targets": { - "name": _("Receiver IDs"), - "type": "list:string", - }, - }) - - template_args = dict(NotifyBase.template_args, **{ - # Viber requires sender.name - "from": { - "name": _("Bot Name"), - "type": "string", - "map_to": "source", - }, - # Optional sender.avatar URL - "avatar": { - "name": _("Bot Avatar URL"), - "type": "string", - }, - "token": { - "alias_of": "token", - }, - # Allow targets to also come from query string - "to": { - "alias_of": "targets" - } - }) + ) def __init__( self, @@ -131,8 +134,9 @@ class NotifyViber(NotifyBase): if len(sourcev) > self.viber_sender_name_limit: self.logger.warning( f"Viber sender name exceeds {self.viber_sender_name_limit} " - "characters, truncating.") - sourcev = sourcev[:self.viber_sender_name_limit] + "characters, truncating." + ) + sourcev = sourcev[: self.viber_sender_name_limit] self.source: str = sourcev self.avatar: Optional[str] = (avatar or "").strip() or None @@ -163,12 +167,15 @@ class NotifyViber(NotifyBase): # Token in first path element token = self.pprint( - self.token, privacy, mode=PrivacyMode.Secret, safe="") + self.token, privacy, mode=PrivacyMode.Secret, safe="" + ) query = self.urlencode(params) return ( f"{self.secure_protocol}://{token}/" - + tgt + (f"?{query}" if query else "")) + + tgt + + (f"?{query}" if query else "") + ) @property def url_identifier(self) -> str: @@ -204,8 +211,10 @@ class NotifyViber(NotifyBase): "type": "text", "text": body, "sender": { - "name": self.source if self.source - else self.app_desc[:self.viber_sender_name_limit]}, + "name": self.source + if self.source + else self.app_desc[: self.viber_sender_name_limit] + }, } if self.avatar: @@ -226,6 +235,7 @@ class NotifyViber(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) # Viber returns the following on success: @@ -241,7 +251,8 @@ class NotifyViber(NotifyBase): content = {} self.logger.warning( "Invalid JSON response from Viber sending " - f"notification to {dest}") + f"notification to {dest}" + ) self.logger.debug("Response Details:\n%s", r.content) has_error = True continue @@ -251,9 +262,7 @@ class NotifyViber(NotifyBase): status_str = ( content.get("status_message") if content.get("status_message") - else self.http_response_code_lookup( - r.status_code - ) + else self.http_response_code_lookup(r.status_code) ) self.logger.warning( f"Failed to send Viber notification to {dest} - " @@ -302,12 +311,11 @@ class NotifyViber(NotifyBase): # Prepare a Full path to work with results["targets"] = [ NotifyViber.unquote(results["host"]), - *NotifyViber.split_path(results["fullpath"])] + *NotifyViber.split_path(results["fullpath"]), + ] if "token" in results["qsd"] and len(results["qsd"]["token"]): - results["token"] = NotifyViber.unquote( - results["qsd"]["token"] - ) + results["token"] = NotifyViber.unquote(results["qsd"]["token"]) else: results["token"] = results["targets"][0] @@ -320,14 +328,10 @@ class NotifyViber(NotifyBase): # Map 'from' -> source if "from" in results["qsd"] and len(results["qsd"]["from"]): - results["source"] = NotifyViber.unquote( - results["qsd"]["from"] - ) + results["source"] = NotifyViber.unquote(results["qsd"]["from"]) # Map avatar if "avatar" in results["qsd"] and len(results["qsd"]["avatar"]): - results["avatar"] = NotifyViber.unquote( - results["qsd"]["avatar"] - ) + results["avatar"] = NotifyViber.unquote(results["qsd"]["avatar"]) return results diff --git a/libs/apprise/plugins/voipms.py b/libs/apprise/plugins/voipms.py index 632975417..397f3e488 100644 --- a/libs/apprise/plugins/voipms.py +++ b/libs/apprise/plugins/voipms.py @@ -93,6 +93,7 @@ class NotifyVoipms(NotifyBase): "from_phone": { "name": _("From Phone No"), "type": "string", + "required": True, "regex": (r"^\+?[0-9\s)(+-]+$", "i"), "map_to": "source", }, @@ -249,10 +250,12 @@ class NotifyVoipms(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) with contextlib.suppress( - AttributeError, TypeError, ValueError): + AttributeError, TypeError, ValueError + ): # Load our JSON object if valid # ValueError = r.content is Unparsable # TypeError = r.content is None @@ -276,7 +279,8 @@ class NotifyVoipms(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -343,10 +347,12 @@ class NotifyVoipms(NotifyBase): password=self.pprint(self.password, privacy, safe=""), from_phone=self.voip_ms_country_code + self.pprint(self.source, privacy, safe=""), - targets="/".join([ - self.voip_ms_country_code + NotifyVoipms.quote(x, safe="") - for x in self.targets - ]), + targets="/".join( + [ + self.voip_ms_country_code + NotifyVoipms.quote(x, safe="") + for x in self.targets + ] + ), params=NotifyVoipms.urlencode(params), ) diff --git a/libs/apprise/plugins/vonage.py b/libs/apprise/plugins/vonage.py index 70907236f..fcdc54994 100644 --- a/libs/apprise/plugins/vonage.py +++ b/libs/apprise/plugins/vonage.py @@ -115,9 +115,6 @@ class NotifyVonage(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "from_phone", }, @@ -139,6 +136,9 @@ class NotifyVonage(NotifyBase): "min": 20000, "max": 604800000, }, + "to": { + "alias_of": "targets", + }, }, ) @@ -265,6 +265,7 @@ class NotifyVonage(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: @@ -284,7 +285,8 @@ class NotifyVonage(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/plugins/webexteams.py b/libs/apprise/plugins/webexteams.py index 690e6ec84..223444c34 100644 --- a/libs/apprise/plugins/webexteams.py +++ b/libs/apprise/plugins/webexteams.py @@ -28,33 +28,56 @@ # At the time I created this plugin, their website had lots of issues with the # Firefox Browser. I fell back to Chrome and had no problems. -# To use this plugin, you need to first access https://teams.webex.com and -# make yourself an account if you don't already have one. You'll want to -# create at least one 'space' before getting the 'incoming webhook'. +# There are 2 ways to use this plugin... # -# Next you'll need to install the 'Incoming webhook' plugin found under -# the 'other' category here: https://apphub.webex.com/integrations/ - -# These links may not always work as time goes by and websites always -# change, but at the time of creating this plugin this was a direct link -# to it: https://apphub.webex.com/integrations/incoming-webhooks-cisco-systems - -# If you're logged in, you'll be able to click on the 'Connect' button. From -# there you'll need to accept the permissions it will ask of you. Give the -# webhook a name such as 'apprise'. -# When you're complete, you will recieve a URL that looks something like this: -# https://api.ciscospark.com/v1/webhooks/incoming/\ -# Y3lzY29zcGkyazovL3VzL1dFQkhPT0sajkkzYWU4fTMtMGE4Yy00 +# Method 1: Via Webhook (default mode): +# Visit https://teams.webex.com and make yourself an account if you don't +# already have one. You'll want to create at least one 'space' before +# getting the 'incoming webhook'. # -# The last part of the URL is all you need to be interested in. Think of this -# url as: -# https://api.ciscospark.com/v1/webhooks/incoming/{token} +# Next you'll need to install the 'Incoming webhook' plugin found under +# the 'other' category here: https://apphub.webex.com/integrations/ # -# You will need to assemble all of your URLs for this plugin to work as: -# wxteams://{token} +# These links may not always work as time goes by and websites always +# change, but at the time of creating this plugin this was a direct link +# to it: +# https://apphub.webex.com/integrations/incoming-webhooks-cisco-systems +# +# If you're logged in, you'll be able to click on the 'Connect' button. +# From there you'll need to accept the permissions it will ask of you. +# Give the webhook a name such as 'apprise'. +# When you're complete, you will recieve a URL that looks something like: +# https://api.ciscospark.com/v1/webhooks/incoming/\ +# Y3lzY29zcGkyazovL3VzL1dFQkhPT0sajkkzYWU4fTMtMGE4Yy00 +# +# The last part of the URL is all you need to be interested in. Think of +# this url as: +# https://api.ciscospark.com/v1/webhooks/incoming/{token} +# +# You will need to assemble all of your URLs for this plugin to work as: +# wxteams://{token} +# +# +# Method 2: Via Bot/API (supports file attachments): +# Visit https://developer.webex.com/my-apps and create a new Bot. +# After creating the bot, you'll receive a Bot Access Token. +# +# You will also need to know the Room ID(s) you want to post to. +# Room IDs can be retrieved from the Webex API: +# https://developer.webex.com/docs/api/v1/rooms/list-rooms +# +# Assemble your Apprise URL as: +# wxteams://{access_token}/{room_id} +# wxteams://{access_token}/{room_id1}/{room_id2} +# +# The plugin auto-detects the mode: +# - If the token is 80-160 lowercase alphanumeric chars -> Webhook mode +# - Otherwise -> Bot mode (requires at least one room ID) +# You may also force the mode with ?mode=webhook or ?mode=bot. # # Resources # - https://developer.webex.com/docs/api/basics - markdown/post syntax +# - https://developer.webex.com/docs/api/v1/messages/create-a-message # - https://developer.cisco.com/ecosystem/webex/apps/\ # incoming-webhooks-cisco-systems/ - Simple webhook example @@ -65,7 +88,7 @@ import requests from ..common import NotifyFormat, NotifyType from ..locale import gettext_lazy as _ -from ..utils.parse import validate_regex +from ..utils.parse import parse_list, validate_regex from .base import NotifyBase # Extend HTTP Error Messages @@ -78,6 +101,25 @@ WEBEX_HTTP_ERROR_MAP = { } +class WebexTeamsMode: + """Tracks the mode of which we're using Webex Teams.""" + + # We're dealing with an incoming webhook + # Token is 80-160 lowercase alphanumeric chars + WEBHOOK = "webhook" + + # We're dealing with a Bot/API access token (supports attachments) + # Token is a Bearer access token from the Webex developer portal + BOT = "bot" + + +# Define our Webex Teams Modes +WEBEX_TEAMS_MODES = ( + WebexTeamsMode.WEBHOOK, + WebexTeamsMode.BOT, +) + + class NotifyWebexTeams(NotifyBase): """A wrapper for Webex Teams Notifications.""" @@ -93,11 +135,19 @@ class NotifyWebexTeams(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = "https://appriseit.com/services/wxteams/" - # Webex Teams uses the http protocol with JSON requests + # Webex Teams Webhook URL notify_url = "https://api.ciscospark.com/v1/webhooks/incoming/" - # The maximum allowable characters allowed in the body per message - body_maxlen = 1000 + # Webex Teams Bot API URL (used in Bot mode) + api_url = "https://webexapis.com/v1/messages" + + # Bot mode supports attachments + attachment_support = True + + # Do not set body_maxlen as it is set in a property value below + # since the length varies depending on whether we are using a + # webhook (1000 chars) or the bot API (7439 chars) + # body_maxlen = see below @property defined # We don't support titles for Webex notifications title_maxlen = 0 @@ -105,20 +155,35 @@ class NotifyWebexTeams(NotifyBase): # Default to markdown; fall back to text notify_format = NotifyFormat.MARKDOWN - # Define object templates - templates = ("{schema}://{token}",) + # Define object URL templates + templates = ( + # Webhook mode (existing) + "{schema}://{token}", + # Bot mode (access_token in host, one or more room IDs in path) + "{schema}://{access_token}/{targets}", + ) # Define our template tokens template_tokens = dict( NotifyBase.template_tokens, **{ "token": { - "name": _("Token"), + "name": _("Webhook Token"), "type": "string", "private": True, "required": True, "regex": (r"^[a-z0-9]{80,160}$", "i"), }, + "access_token": { + "name": _("Bot Access Token"), + "type": "string", + "private": True, + "required": True, + }, + "targets": { + "name": _("Room IDs"), + "type": "list:string", + }, }, ) @@ -128,32 +193,113 @@ class NotifyWebexTeams(NotifyBase): "token": { "alias_of": "token", }, + "mode": { + "name": _("Mode"), + "type": "choice:string", + "values": WEBEX_TEAMS_MODES, + # mode is auto-detected if not specified + }, + "to": { + "alias_of": "targets", + }, }, ) - def __init__(self, token, **kwargs): + def __init__( + self, + token=None, + access_token=None, + targets=None, + mode=None, + **kwargs, + ): """Initialize Webex Teams Object.""" super().__init__(**kwargs) - # The token associated with the account - self.token = validate_regex( - token, *self.template_tokens["token"]["regex"] - ) - if not self.token: - msg = f"The Webex Teams token specified ({token}) is invalid." - self.logger.warning(msg) - raise TypeError(msg) + # Resolve mode: explicit override wins, otherwise auto-detect + if mode and isinstance(mode, str): + self.mode = next( + (m for m in WEBEX_TEAMS_MODES if m.startswith(mode)), None + ) + if self.mode not in WEBEX_TEAMS_MODES: + msg = f"The Webex Teams mode specified ({mode}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) - def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + else: + # Auto-detect: webhook tokens are 80-160 lowercase alphanumeric + _candidate = access_token or token + if token and validate_regex( + token, *self.template_tokens["token"]["regex"] + ): + self.mode = WebexTeamsMode.WEBHOOK + + elif _candidate: + # Non-webhook token length/format -> assume BOT + self.mode = WebexTeamsMode.BOT + + else: + # No usable token at all; will fail below + self.mode = WebexTeamsMode.WEBHOOK + + if self.mode == WebexTeamsMode.WEBHOOK: + self.access_token = None + self.targets = [] + + # Webhook token: prefer 'token', fall back to 'access_token' + _tok = token or access_token + self.token = validate_regex( + _tok, *self.template_tokens["token"]["regex"] + ) + if not self.token: + msg = ( + "The Webex Teams webhook token" + f" specified ({_tok}) is invalid." + ) + self.logger.warning(msg) + raise TypeError(msg) + + else: # WebexTeamsMode.BOT + self.token = None + + # Bot access token: prefer 'access_token', fall back to 'token' + _at = access_token or token + if not _at: + msg = "A Webex Teams bot access token must be specified." + self.logger.warning(msg) + raise TypeError(msg) + + self.access_token = _at + + self.targets = parse_list(targets) + + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): """Perform Webex Teams Notification.""" + if self.mode == WebexTeamsMode.WEBHOOK: + if attach and self.attachment_support: + self.logger.warning( + "Webex Teams Webhooks do not support" + " attachments; use bot mode." + ) + return self._send_webhook(body) + + return self._send_bot(body, attach=attach) + + def _send_webhook(self, body): + """Post via incoming webhook (no attachment support).""" - # Setup our headers headers = { "User-Agent": self.app_id, "Content-Type": "application/json", } - # Prepare our URL url = f"{self.notify_url}/{self.token}" payload = { @@ -165,7 +311,7 @@ class NotifyWebexTeams(NotifyBase): } self.logger.debug( - "Webex Teams POST URL:" + "Webex Teams Webhook POST URL:" f" {url} (cert_verify={self.verify_certificate!r})" ) self.logger.debug(f"Webex Teams Payload: {payload!s}") @@ -179,41 +325,234 @@ class NotifyWebexTeams(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, requests.codes.no_content, ): - # We had a problem status_str = NotifyWebexTeams.http_response_code_lookup( - r.status_code + r.status_code, WEBEX_HTTP_ERROR_MAP ) self.logger.warning( "Failed to send Webex Teams notification: " "{}{}error={}.".format( - status_str, ", " if status_str else "", r.status_code + status_str, + ", " if status_str else "", + r.status_code, ) ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) return False - else: - self.logger.info("Sent Webex Teams notification.") + self.logger.info("Sent Webex Teams notification.") except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending Webex Teams notification." ) self.logger.debug(f"Socket Exception: {e!s}") - return False return True + def _send_bot(self, body, attach=None): + """Post via Bot/API to one or more rooms (supports attachments).""" + + if not self.targets: + self.logger.warning( + "Webex Teams Bot mode has no room IDs to notify, aborting." + ) + return False + + has_error = False + for room_id in self.targets: + if not self._post_to_room(body, room_id, attach=attach): + has_error = True + + return not has_error + + def _post_to_room(self, body, room_id, attach=None): + """Send a single message (and optional attachments) to a room.""" + + headers = { + "User-Agent": self.app_id, + "Authorization": f"Bearer {self.access_token}", + } + + text_key = ( + "markdown" + if self.notify_format == NotifyFormat.MARKDOWN + else "text" + ) + + has_attachment = attach and self.attachment_support and len(attach) > 0 + + if not has_attachment: + # --- Text-only message sent as JSON --- + headers["Content-Type"] = "application/json" + payload = { + "roomId": room_id, + text_key: body, + } + + self.logger.debug( + "Webex Teams Bot POST URL:" + f" {self.api_url}" + f" (cert_verify={self.verify_certificate!r})" + ) + self.logger.debug(f"Webex Teams Bot Payload: {payload!s}") + + self.throttle() + try: + r = requests.post( + self.api_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + if r.status_code != requests.codes.ok: + status_str = NotifyWebexTeams.http_response_code_lookup( + r.status_code, WEBEX_HTTP_ERROR_MAP + ) + self.logger.warning( + "Failed to send Webex Teams Bot" + " notification:" + " {}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + return False + + self.logger.info( + f"Sent Webex Teams Bot notification to room {room_id}." + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending" + " Webex Teams Bot notification." + ) + self.logger.debug(f"Socket Exception: {e!s}") + return False + + return True + + # --- Multipart attachment(s) --- + for no, attachment in enumerate(attach, start=1): + if not attachment: + self.logger.error( + "Could not access Webex Teams attachment" + f" {attachment.url(privacy=True)}." + ) + return False + + self.logger.debug( + "Posting Webex Teams attachment" + f" {attachment.url(privacy=True)}" + ) + + try: + # open() returns a BytesIO for memory attachments; the + # context manager guarantees the handle is closed on exit + with attachment.open() as fp: + files = { + "files": ( + ( + attachment.name + if attachment.name + else f"file{no:03}.dat" + ), + fp, + attachment.mimetype, + ), + } + + data = {"roomId": room_id} + # Include message body only with the first attachment + if no == 1: + data[text_key] = body + + self.logger.debug( + "Webex Teams Bot attachment POST URL:" + f" {self.api_url}" + f" (cert_verify={self.verify_certificate!r})" + ) + + self.throttle() + r = requests.post( + self.api_url, + data=data, + headers=headers, + files=files, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + status_str = NotifyWebexTeams.http_response_code_lookup( + r.status_code, WEBEX_HTTP_ERROR_MAP + ) + self.logger.warning( + "Failed to send Webex Teams attachment" + " {}: {}{}error={}.".format( + attachment.name, + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + return False + + self.logger.info( + "Sent Webex Teams attachment" + f" {attachment.name} to room {room_id}." + ) + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred posting" + " Webex Teams attachment." + ) + self.logger.debug(f"Socket Exception: {e!s}") + return False + + except OSError as e: + self.logger.warning( + "An I/O error occurred while reading {}.".format( + attachment.name if attachment.name else "attachment" + ) + ) + self.logger.debug(f"I/O Exception: {e!s}") + return False + + return True + + @property + def body_maxlen(self): + """The maximum allowable characters allowed in the body per message. + Webhook mode is limited to 1000 chars; the Bot API allows 7439.""" + return 1000 if self.mode == WebexTeamsMode.WEBHOOK else 7439 + @property def url_identifier(self): """Returns all of the identifiers that make this URL unique from @@ -221,44 +560,82 @@ class NotifyWebexTeams(NotifyBase): Targets or end points should never be identified here. """ - return (self.secure_protocol[0], self.token) + if self.mode == WebexTeamsMode.WEBHOOK: + return (self.secure_protocol[0], self.token) + + # BOT mode + return (self.secure_protocol[0], self.access_token) def url(self, privacy=False, *args, **kwargs): """Returns the URL built dynamically based on specified arguments.""" - # Our URL parameters - params = self.url_parameters(privacy=privacy, *args, **kwargs) + params = {"mode": self.mode} + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - return "{schema}://{token}/?{params}".format( + if self.mode == WebexTeamsMode.WEBHOOK: + return "{schema}://{token}/?{params}".format( + schema=self.secure_protocol[0], + token=self.pprint(self.token, privacy, safe=""), + params=NotifyWebexTeams.urlencode(params), + ) + + # BOT mode + return "{schema}://{token}/{targets}/?{params}".format( schema=self.secure_protocol[0], - token=self.pprint(self.token, privacy, safe=""), + token=self.pprint(self.access_token, privacy, safe=""), + targets="/".join( + [NotifyWebexTeams.quote(r, safe="") for r in self.targets] + ), params=NotifyWebexTeams.urlencode(params), ) @staticmethod def parse_url(url): - """Parses the URL and returns enough arguments that can allow us to re- - instantiate this object.""" + """Parses the URL and returns enough arguments that can allow us + to re-instantiate this object.""" results = NotifyBase.parse_url(url, verify_host=False) if not results: - # We're done early as we couldn't load the results return results - # Set our token if found as an argument - if "token" in results["qsd"] and len(results["qsd"]["token"]): - results["token"] = \ - NotifyWebexTeams.unquote(results["qsd"]["token"]) + # Explicit mode parameter wins + if "mode" in results["qsd"] and results["qsd"]["mode"]: + results["mode"] = NotifyWebexTeams.unquote(results["qsd"]["mode"]) + # Pull additional room-ID path entries (bot mode) + entries = NotifyWebexTeams.split_path(results["fullpath"]) + + # Support ?to= for room IDs + if "to" in results["qsd"] and results["qsd"]["to"]: + entries += NotifyWebexTeams.split_path( + NotifyWebexTeams.unquote(results["qsd"]["to"]) + ) + + # Support ?token= for the primary token/access_token + if "token" in results["qsd"] and results["qsd"]["token"]: + host = NotifyWebexTeams.unquote(results["qsd"]["token"]) else: - # The first token is stored in the hostname - results["token"] = NotifyWebexTeams.unquote(results["host"]) + host = NotifyWebexTeams.unquote(results["host"]) + + # Determine whether this is webhook or bot mode. + # Path entries mean bot mode (room IDs present). + # Explicit mode= has already been stored above. + explicit_mode = results.get("mode", "") + if explicit_mode == WebexTeamsMode.BOT or ( + entries and explicit_mode != WebexTeamsMode.WEBHOOK + ): + results["access_token"] = host + results["targets"] = entries + else: + results["token"] = host return results @staticmethod def parse_native_url(url): """ - Support https://api.ciscospark.com/v1/webhooks/incoming/WEBHOOK_TOKEN + Support: + https://api.ciscospark.com/v1/webhooks/incoming/WEBHOOK_TOKEN + https://webexapis.com/v1/webhooks/incoming/WEBHOOK_TOKEN """ result = re.match( diff --git a/libs/apprise/plugins/wechat.py b/libs/apprise/plugins/wechat.py new file mode 100644 index 000000000..e10809b2b --- /dev/null +++ b/libs/apprise/plugins/wechat.py @@ -0,0 +1,649 @@ +# +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# WeCom (WeChat Work / Enterprise WeChat) Application Message API. +# +# This plugin sends notifications directly to users, departments, or +# tags within a WeCom enterprise organisation using the WeCom +# Application API. No third-party intermediary is required. +# +# Setup: +# 1. Sign in to the WeCom admin console at +# https://work.weixin.qq.com/wework_admin/frame#/index +# 2. Go to "Applications & Mini Programs" -> "Applications" and +# create or select a self-built application. +# 3. Copy the AgentID shown on the application details page. +# 4. Go to "My Enterprise" -> "Enterprise Information" and note +# the CorpID. +# 5. On the application page click "View" next to Secret and copy it. +# +# Basic Apprise URL forms: +# +# Send to all users in the organisation: +# wechat://{corpid}:{corpsecret}@{agentid}/@all +# +# Send to specific user(s) by WeCom user ID (@ prefix optional +# on input, always emitted in generated URLs): +# wechat://{corpid}:{corpsecret}@{agentid}/@{userid} +# wechat://{corpid}:{corpsecret}@{agentid}/@{user1}/@{user2} +# +# Send to a department by its numeric ID (# encoded as %23 in URLs): +# wechat://{corpid}:{corpsecret}@{agentid}/%23{deptid} +# +# Send to a tag by its numeric ID (prefix with +): +# wechat://{corpid}:{corpsecret}@{agentid}/+{tagid} +# +# Mixed recipients: +# wechat://{corpid}:{corpsecret}@{agentid}/@{user}/%23{dept}/+{tag} +# +# Notes: +# - # must be URL-encoded as %23 in URL paths to avoid being +# treated as a fragment separator. +# - At least one recipient (user, department, or tag) is required. +# - Use @all as a user target to send to the entire organisation. +# - Use ?to= to supply comma-separated extra targets as a query +# parameter alternative. +# +# API References: +# https://developer.work.weixin.qq.com/document/path/90235 +# https://developer.work.weixin.qq.com/document/path/90236 + +import json +import re + +import requests + +from ..common import NotifyFormat, NotifyType, PersistentStoreMode +from ..locale import gettext_lazy as _ +from ..utils.parse import parse_list, validate_regex +from .base import NotifyBase + +# WeCom API error code map. +# Reference: https://developer.work.weixin.qq.com/document/path/90313 +WECHAT_ERROR_CODES = { + 0: "Request succeeded.", + 40001: "Invalid credential or access token expired.", + 40003: "Invalid user ID.", + 40014: "Invalid access token.", + 42001: "Access token expired.", + 48001: "API not authorized; check application permissions.", + 60020: "IP address not in the allowlist.", + 81013: "All recipients are invalid or unauthorized.", +} + +# Error codes that indicate the cached access token must be discarded +# so the next send() call will fetch a fresh one. +WECHAT_TOKEN_ERROR_CODES = frozenset((40001, 40014, 42001)) + +# Validates a WeCom user ID. The optional leading @ is stripped by +# the regex so both "johndoe" and "@johndoe" are accepted. The +# special @all broadcast keyword is matched as "all"; the caller +# re-attaches the @ before storing. url() always emits the @ prefix. +IS_USER = re.compile( + r"^@?(?P[A-Za-z0-9][A-Za-z0-9_@.\-]*)$", +) + +# Validates a department target: # prefix followed by a numeric ID. +# The # will arrive already decoded from %23 by the URL path parser. +IS_DEPT = re.compile(r"^#(?P[0-9]+)$") + +# Validates a tag target: + prefix followed by a numeric ID. +IS_TAG = re.compile(r"^\+(?P[0-9]+)$") + + +class NotifyWeChat(NotifyBase): + """A wrapper for WeCom (WeChat Work) Application Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "WeChat (WeCom)" + + # The services URL + service_url = "https://work.weixin.qq.com/" + + # The default secure protocol + secure_protocol = "wechat" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/wechat/" + + # WeCom access token endpoint (GET request) + token_url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" + + # WeCom message send endpoint (POST); access_token is a query param + notify_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send" + + # WeCom text and markdown bodies are limited to 2048 bytes + body_maxlen = 2048 + + # WeCom application messages have no native separate title field + title_maxlen = 0 + + # Default to plain text; markdown is also natively supported + notify_format = NotifyFormat.TEXT + + # Enable the persistent store so the access token survives across + # plugin instantiations and process restarts + storage_mode = PersistentStoreMode.AUTO + + # Cache the access token for slightly less than its 2-hour lifetime + # to avoid racing the expiry boundary + default_cache_expiry_sec = 7200 - 300 # 1 h 55 min + + # URL templates + templates = ( + # No path targets: recipients supplied via ?to= or @all + "{schema}://{user}:{password}@{host}", + # One or more targets in the URL path + "{schema}://{user}:{password}@{host}/{targets}", + ) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + # Corp ID sits in the URL user field + "user": { + "name": _("Corp ID"), + "type": "string", + "required": True, + "map_to": "corpid", + }, + # App Secret sits in the URL password field + "password": { + "name": _("App Secret"), + "type": "string", + "private": True, + "required": True, + "map_to": "corpsecret", + }, + # Agent ID sits in the URL host field; always numeric + "host": { + "name": _("Agent ID"), + "type": "string", + "required": True, + "regex": (r"^[0-9]+$", ""), + "map_to": "agentid", + }, + # A WeCom user ID (@ prefix optional on input, + # always emitted by url()) + "target_user": { + "name": _("Target User"), + "type": "string", + "prefix": "@", + "map_to": "targets", + }, + # A WeCom department ID (numeric; prefix with #) + "target_department": { + "name": _("Target Department"), + "type": "string", + "prefix": "#", + "map_to": "targets", + }, + # A WeCom tag ID (numeric; prefix with +) + "target_tag": { + "name": _("Target Tag"), + "type": "string", + "prefix": "+", + "map_to": "targets", + }, + "targets": { + "name": _("Targets"), + "type": "list:string", + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + # ?to= is the standard Apprise comma-separated recipient alias + "to": { + "alias_of": "targets", + }, + }, + ) + + def __init__(self, corpid, corpsecret, agentid, targets=None, **kwargs): + """Initialize WeChat (WeCom Application) Object.""" + super().__init__(**kwargs) + + # Validate the Corp ID + self.corpid = validate_regex(corpid) + if not self.corpid: + msg = "A WeChat (WeCom) Corp ID must be specified." + self.logger.warning(msg) + raise TypeError(msg) + + # Validate the App Secret + self.corpsecret = validate_regex(corpsecret) + if not self.corpsecret: + msg = "A WeChat (WeCom) App Secret must be specified." + self.logger.warning(msg) + raise TypeError(msg) + + # Validate the Agent ID (must be a non-negative integer string) + self.agentid = validate_regex( + str(agentid) if agentid is not None else "", + *self.template_tokens["host"]["regex"], + ) + if not self.agentid: + msg = ( + "The WeChat (WeCom) Agent ID ({}) is invalid;" + " it must be a non-negative integer.".format(agentid) + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Parsed and validated recipient lists + self.users = [] + self.departments = [] + self.tag_ids = [] + + # Preserve unrecognised targets for round-trip URL fidelity + self.invalid_targets = [] + + # Classify each entry from the targets list + for target in parse_list(targets): + # Department ID: # prefix + numeric + result = IS_DEPT.match(target) + if result: + self.departments.append(result.group("dept")) + continue + + # Tag ID: + prefix + numeric + result = IS_TAG.match(target) + if result: + self.tag_ids.append(result.group("tag")) + continue + + # User ID or @all broadcast. + # The regex strips the optional leading @ so both "johndoe" + # and "@johndoe" match. "all" (captured from "@all" or bare + # "all") is re-normalised to "@all" for the WeCom API. + result = IS_USER.match(target) + if result: + user = result.group("user") + self.users.append("@all" if user == "all" else user) + continue + + # Unrecognised -- log and preserve for round-trip + self.logger.warning( + "Invalid/unrecognized WeChat (WeCom) target preserved" + " for round-trip fidelity: %s", + target, + ) + self.invalid_targets.append(target) + + return + + def _get_access_token(self): + """Return the cached WeCom access token, fetching a fresh one + if the cache is empty or the token has expired.""" + + # Check the persistent store for a cached token + token = self.store.get("access_token") + if token: + # Return the cached token + return token + + # Build the query parameters for the token request + params = { + "corpid": self.corpid, + "corpsecret": self.corpsecret, + } + + self.logger.debug( + "WeChat (WeCom) token GET URL: %s (cert_verify=%r)", + self.token_url, + self.verify_certificate, + ) + + # Always throttle before any remote server I/O + self.throttle() + + try: + r = requests.get( + self.token_url, + params=params, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # HTTP-level failure + status_str = NotifyWeChat.http_response_code_lookup( + r.status_code + ) + self.logger.warning( + "Failed to fetch WeChat access token: " + "{}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + # Return None to indicate failure + return None + + # Parse the JSON response body + try: + content = json.loads(r.content) + except (AttributeError, TypeError, ValueError): + content = {} + + # Check the WeCom application-level error code + errcode = content.get("errcode", 0) if content else -1 + if errcode != 0: + errmsg = WECHAT_ERROR_CODES.get( + errcode, + content.get("errmsg", "Unknown error") + if content + else "Unknown error", + ) + self.logger.warning( + "WeChat access token request failed: " + "errcode={}: {}.".format(errcode, errmsg) + ) + # Return None to indicate failure + return None + + # Extract the access token from the response + token = content.get("access_token") if content else None + if not token: + self.logger.warning( + "WeChat access token response contained no token." + ) + # Return None to indicate failure + return None + + # Derive the cache TTL: expires_in minus a grace period to + # avoid racing the expiry boundary + expires_in = content.get("expires_in", 7200) if content else 7200 + ttl = max(60, expires_in - 300) + + # Cache the token for reuse across plugin instances + self.store.set("access_token", token, expires=ttl) + + # Return the freshly fetched token + return token + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred fetching the WeChat access token." + ) + self.logger.debug("Socket Exception: %s", str(e)) + # Return None to indicate failure + return None + + def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs): + """Perform WeChat (WeCom Application) Notification.""" + + # Compile the recipient lists + users = list(self.users) + departments = list(self.departments) + tags = list(self.tag_ids) + + # At least one valid recipient is required to proceed + if not users and not departments and not tags: + self.logger.warning( + "No WeChat (WeCom) recipients configured; aborting." + ) + return False + + # Obtain (or reuse from cache) a valid access token + token = self._get_access_token() + if not token: + return False + + # Determine the WeCom msgtype from the active notify_format + if self.notify_format == NotifyFormat.MARKDOWN: + msgtype = "markdown" + else: + # Both TEXT and HTML fall back to plain text + msgtype = "text" + + # Build the message content wrapper + msgbody = {"content": body} + + # Assemble the full request payload + payload = { + "agentid": int(self.agentid), + "msgtype": msgtype, + msgtype: msgbody, + } + + # Add recipient fields -- only include lists that are non-empty + if users: + payload["touser"] = "|".join(users) + if departments: + payload["toparty"] = "|".join(departments) + if tags: + payload["totag"] = "|".join(tags) + + # Prepare the request headers + headers = { + "User-Agent": self.app_id, + "Content-Type": "application/json", + } + + # Append the access token as a query parameter on the send URL + send_url = "{}?access_token={}".format(self.notify_url, token) + + self.logger.debug( + "WeChat (WeCom) POST URL: %s (cert_verify=%r)", + self.notify_url, + self.verify_certificate, + ) + self.logger.debug("WeChat (WeCom) Payload: %r", payload) + + # Always throttle before any remote server I/O + self.throttle() + + try: + r = requests.post( + send_url, + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + + if r.status_code != requests.codes.ok: + # HTTP-level failure + status_str = NotifyWeChat.http_response_code_lookup( + r.status_code + ) + self.logger.warning( + "Failed to send WeChat (WeCom) notification: " + "{}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + # Return; we're done + return False + + # Parse the JSON response body + try: + content = json.loads(r.content) + except (AttributeError, TypeError, ValueError): + content = {} + + # Check the WeCom application-level error code + errcode = content.get("errcode", 0) if content else -1 + if errcode != 0: + # Token error codes mean the cached token has expired; + # evict it so the next call fetches a fresh one + if errcode in WECHAT_TOKEN_ERROR_CODES: + self.store.set("access_token", None, expires=1) + + errmsg = WECHAT_ERROR_CODES.get( + errcode, + content.get("errmsg", "Unknown error") + if content + else "Unknown error", + ) + self.logger.warning( + "Failed to send WeChat (WeCom) notification: " + "errcode={}: {}.".format(errcode, errmsg) + ) + self.logger.debug( + "Response Details:\r\n%r", + content if content else (r.content or b"")[:2000], + ) + # Return; we're done + return False + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending" + " WeChat (WeCom) notification." + ) + self.logger.debug("Socket Exception: %s", str(e)) + # Return; we're done + return False + + self.logger.info("Sent WeChat (WeCom) notification.") + return True + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique + from another similar one. + + Targets or end points should never be identified here. + """ + # Corp + secret + agentid together uniquely identify the app + return ( + self.secure_protocol, + self.corpid, + self.corpsecret, + self.agentid, + ) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified + arguments.""" + + params = {} + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Re-assemble the targets list, restoring each type's prefix. + # Department IDs use # which must be percent-encoded in paths. + targets = [] + for user in self.users: + if user.startswith("@"): + # @all and any other @-prefixed broadcast keyword; + # emit as-is since the @ is already present + targets.append(NotifyWeChat.quote(user, safe="@")) + else: + # Regular user ID; always emit with @ prefix so the + # URL is self-describing and consistent with the + # target_user prefix declaration + targets.append("@{}".format(NotifyWeChat.quote(user, safe=""))) + for dept in self.departments: + # Encode # as %23 so it is not treated as a URL fragment + targets.append("%23{}".format(dept)) + for tag in self.tag_ids: + targets.append("+{}".format(tag)) + # Always include invalid entries so the URL round-trips without + # silent data loss + for inv in self.invalid_targets: + targets.append(NotifyWeChat.quote(inv, safe="")) + + default_schema = self.secure_protocol + + if targets: + return ( + "{schema}://{corpid}:{corpsecret}@{agentid}" + "/{targets}/?{params}".format( + schema=default_schema, + corpid=NotifyWeChat.quote(self.corpid, safe=""), + corpsecret=self.pprint(self.corpsecret, privacy, safe=""), + agentid=self.agentid, + targets="/".join(targets), + params=NotifyWeChat.urlencode(params), + ) + ) + + return "{schema}://{corpid}:{corpsecret}@{agentid}/?{params}".format( + schema=default_schema, + corpid=NotifyWeChat.quote(self.corpid, safe=""), + corpsecret=self.pprint(self.corpsecret, privacy, safe=""), + agentid=self.agentid, + params=NotifyWeChat.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow + us to re-instantiate this object.""" + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # Corp ID comes from the URL user field + results["corpid"] = NotifyWeChat.unquote(results["user"]) + + # App Secret comes from the URL password field + results["corpsecret"] = NotifyWeChat.unquote(results["password"]) + + # Agent ID comes from the URL host field + results["agentid"] = NotifyWeChat.unquote(results["host"]) + + # Collect targets from the URL path + results["targets"] = NotifyWeChat.split_path(results["fullpath"]) + + # Support ?to= for additional comma-separated targets + if "to" in results["qsd"] and results["qsd"]["to"]: + results["targets"] += NotifyWeChat.parse_list(results["qsd"]["to"]) + + return results + + @staticmethod + def parse_native_url(url): + """WeCom does not expose a shareable single-URL credential form, + so native URL detection is not supported.""" + return None diff --git a/libs/apprise/plugins/wecombot.py b/libs/apprise/plugins/wecombot.py index f94b3e616..4b4a68331 100644 --- a/libs/apprise/plugins/wecombot.py +++ b/libs/apprise/plugins/wecombot.py @@ -187,6 +187,7 @@ class NotifyWeComBot(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -202,7 +203,8 @@ class NotifyWeComBot(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Return; we're done return False diff --git a/libs/apprise/plugins/whatsapp.py b/libs/apprise/plugins/whatsapp.py index 025dc4d06..8b6a687b6 100644 --- a/libs/apprise/plugins/whatsapp.py +++ b/libs/apprise/plugins/whatsapp.py @@ -28,17 +28,28 @@ # API Source: # https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages # -# 1. Register a developer account with Meta: -# https://developers.facebook.com/docs/whatsapp/cloud-api/get-started -# 2. Enable 2 Factor Authentication (2FA) with your account (if not done -# already) -# 3. Create a App using WhatsApp Product. There are 2 to create an app from -# Do NOT chose the WhatsApp Webhook one (choose the other) +# The setup is split across two Meta portals: +# - https://business.facebook.com (Business Manager — System Users & tokens) +# - https://developers.facebook.com (Developer Dashboard — App & Phone ID) # -# When you click on the API Setup section of your new app you need to record -# both the access token and the From Phone Number ID. Note that this not the -# from phone number itself, but it's ID. It's displayed below and contains -# way more numbers then your typical phone number +# 1. Create a Meta Business Manager account at https://business.facebook.com +# (or use an existing one). WhatsApp Business Accounts (WABAs) and System +# Users live here. +# 2. Create a Meta Developer account at https://developers.facebook.com and +# create a new App. Add WhatsApp as a product. If prompted, select the +# "Business" app type and choose "Connect to Customers (WhatsApp)" as the +# use case (you may need to click "Customise Use Case" on the home page). +# 3. In Business Manager, go to Settings > Users > System Users, create a +# System User (Admin or Employee role), click "Add Assets", assign your +# WhatsApp app, and grant the whatsapp_business_messaging permission. +# Then click "Generate Token" and copy it — this is your permanent token. +# 4. Switch back to the Developer Dashboard, open your app, then go to +# WhatsApp > API Setup (or Getting Started) to find your From Phone Number +# ID. This is NOT your actual phone number; it is a separate numeric ID +# (roughly 14 digits) assigned by Meta to the sender number. +# 5. During sandbox testing, verify any recipient phone number through Meta's +# interface. For production, your business must be verified and placed on +# the appropriate messaging tier. from json import dumps, loads import re @@ -47,9 +58,19 @@ import requests from ..common import NotifyType from ..locale import gettext_lazy as _ -from ..utils.parse import is_phone_no, parse_phone_no, validate_regex +from ..utils.parse import ( + is_phone_no, + parse_phone_no, + validate_regex, +) from .base import NotifyBase +# Matches the numeric-only portion of a WhatsApp Cloud API group ID after any +# '#' prefix and '@g.us' JID suffix have been stripped. E.164 phone numbers +# are capped at 15 digits; group IDs are 18-20 digits, so requiring 16+ gives +# an unambiguous separation with no overlap. +IS_GROUP_ID = re.compile(r"^[0-9]{16,}$") + class NotifyWhatsApp(NotifyBase): """A wrapper for WhatsApp Notifications.""" @@ -69,8 +90,9 @@ class NotifyWhatsApp(NotifyBase): # 60/300 = 0.2 request_rate_per_sec = 0.20 - # Facebook Graph version - fb_graph_version = "v17.0" + # Facebook Graph version; bump this when Meta deprecates the current one. + # Release schedule: https://developers.facebook.com/docs/graph-api/changelog + fb_graph_version = "v21.0" # A URL that takes you to the setup/help of the specific protocol setup_url = "https://appriseit.com/services/whatsapp/" @@ -115,6 +137,12 @@ class NotifyWhatsApp(NotifyBase): "required": True, "regex": (r"^[0-9]+$", "i"), }, + "language": { + "name": _("Language"), + "type": "string", + "default": "en_US", + "regex": (r"^[^0-9\s]+$", "i"), + }, "target_phone": { "name": _("Target Phone No"), "type": "string", @@ -122,15 +150,22 @@ class NotifyWhatsApp(NotifyBase): "regex": (r"^[0-9\s)(+-]+$", "i"), "map_to": "targets", }, + # WhatsApp group IDs are purely numeric and 16+ digits long — + # safely above the 15-digit E.164 phone number maximum. + # The '#' prefix in the Apprise URL is recommended for clarity; + # bare 16+ digit strings are also accepted. + # The '@g.us' API JID suffix is added automatically at send time. + "target_group": { + "name": _("Target Group ID"), + "type": "string", + "prefix": "#", + "regex": (r"^[0-9]{16,}$", "i"), + "map_to": "targets", + }, "targets": { "name": _("Targets"), "type": "list:string", - }, - "language": { - "name": _("Language"), - "type": "string", - "default": "en_US", - "regex": (r"^[^0-9\s]+$", "i"), + "required": True, }, }, ) @@ -139,9 +174,6 @@ class NotifyWhatsApp(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": { - "alias_of": "targets", - }, "from": { "alias_of": "from_phone_id", }, @@ -154,6 +186,9 @@ class NotifyWhatsApp(NotifyBase): "lang": { "alias_of": "language", }, + "to": { + "alias_of": "targets", + }, }, ) @@ -237,20 +272,47 @@ class NotifyWhatsApp(NotifyBase): # self.template = None - # Parse our targets + # Parse our targets (phone numbers and/or group IDs). + # + # parse_phone_no does the heavy lifting: it handles formatted phone + # strings ("+1 (555) 987-6543", dashes, spaces, etc.) and, with the + # default store_unparseable=True, passes through anything it cannot + # identify as a phone so we can inspect it below. + # A '#' prefix on a group ID is silently stripped by the phone regex + # (the digits still match); a '@g.us' JID suffix causes the whole + # token to be kept as-is (the '@' breaks the phone pattern). + # + # For each token after parse_phone_no: + # is_phone_no() succeeds -> valid phone, stored as E.164 '+XXX' + # is_phone_no() fails -> strip '@g.us', run IS_GROUP_ID: + # 16+ pure digits -> group ('#digits') + # anything else -> warn and drop self.targets = [] for target in parse_phone_no(targets): - # Validate targets and drop bad ones: + # Validate as a phone number first result = is_phone_no(target) - if not result: - self.logger.warning( - f"Dropped invalid phone # ({target}) specified.", - ) + if result: + # Valid phone number; store in E.164 format + self.targets.append("+{}".format(result["full"])) continue - # store valid phone number - self.targets.append("+{}".format(result["full"])) + # Not a valid phone — check whether it is a group ID. + # Strip both the '#' prefix (retained when parse_phone_no uses + # its unparseable-store path rather than the phone regex) and the + # '@g.us' JID suffix (the native Meta API group ID format). + gid = ( + re.sub(r"@g\.us$", "", target, flags=re.I).strip().lstrip("#") + ) + if IS_GROUP_ID.match(gid): + # Store with '#' prefix; '@g.us' is re-added at send time + self.targets.append(f"#{gid}") + continue + + # Genuinely invalid — warn and drop + self.logger.warning( + f"Dropped invalid WhatsApp target ({target}) specified.", + ) self.template_mapping = {} if template_mapping: @@ -336,7 +398,7 @@ class NotifyWhatsApp(NotifyBase): payload = { "messaging_product": "whatsapp", - # The To gets populated in the loop below + # 'to' and 'recipient_type' are set per-target in the loop below "to": None, } @@ -344,29 +406,36 @@ class NotifyWhatsApp(NotifyBase): # # Send Message # - payload.update({ - "recipient_type": "individual", - "type": "text", - "text": {"body": body}, - }) + payload.update( + { + # recipient_type is overridden per-target below + "recipient_type": "individual", + "type": "text", + "text": {"body": body}, + } + ) else: # # Send Template # - payload.update({ - "type": "template", - "template": { - "name": self.template, - "language": {"code": self.language}, - }, - }) + payload.update( + { + "type": "template", + "template": { + "name": self.template, + "language": {"code": self.language}, + }, + } + ) if self.components: - payload["template"]["components"] = [{ - "type": "body", - "parameters": [], - }] + payload["template"]["components"] = [ + { + "type": "body", + "parameters": [], + } + ] for key in self.component_keys: if isinstance(self.components[key], dict): # Manual Assignment @@ -376,14 +445,16 @@ class NotifyWhatsApp(NotifyBase): continue # Mapping of body and/or notify type - payload["template"]["components"][0]["parameters"].append({ - "type": "text", - "text": ( - body - if self.components[key] == "body" - else notify_type.value - ), - }) + payload["template"]["components"][0]["parameters"].append( + { + "type": "text", + "text": ( + body + if self.components[key] == "body" + else notify_type.value + ), + } + ) # Create a copy of the targets list targets = list(self.targets) @@ -392,8 +463,16 @@ class NotifyWhatsApp(NotifyBase): # Get our target to notify target = targets.pop(0) - # Prepare our user - payload["to"] = target + # Group targets are stored with a '#' prefix; phone numbers + # are stored in E.164 format ('+' prefix). + if target.startswith("#"): + # Reconstruct the full Meta API group ID (digits + @g.us) + payload["to"] = "{}@g.us".format(target[1:]) + payload["recipient_type"] = "group" + else: + # Individual phone number + payload["to"] = target + payload["recipient_type"] = "individual" # Some Debug Logging self.logger.debug( @@ -411,6 +490,7 @@ class NotifyWhatsApp(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( @@ -457,7 +537,8 @@ class NotifyWhatsApp(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True @@ -506,24 +587,19 @@ class NotifyWhatsApp(NotifyBase): # Append our payload extras into our parameters params.update({f":{k}": v for k, v in self.template_mapping.items()}) - return ( - "{schema}://{template}{token}@{from_id}/{targets}/?{params}" - .format( - schema=self.secure_protocol, - from_id=self.pprint(self.from_phone_id, privacy, safe=""), - token=self.pprint(self.token, privacy, safe=""), - template=( - "" - if not self.template - else "{}:".format( - NotifyWhatsApp.quote(self.template, safe="") - ) - ), - targets="/".join( - [NotifyWhatsApp.quote(x, safe="") for x in self.targets] - ), - params=NotifyWhatsApp.urlencode(params), - ) + return "{schema}://{template}{token}@{from_id}/{targets}/?{params}".format( + schema=self.secure_protocol, + from_id=self.pprint(self.from_phone_id, privacy, safe=""), + token=self.pprint(self.token, privacy, safe=""), + template=( + "" + if not self.template + else "{}:".format(NotifyWhatsApp.quote(self.template, safe="")) + ), + targets="/".join( + [NotifyWhatsApp.quote(x, safe="") for x in self.targets] + ), + params=NotifyWhatsApp.urlencode(params), ) def __len__(self): diff --git a/libs/apprise/plugins/workflows.py b/libs/apprise/plugins/workflows.py index 84e5ff33a..974c475bc 100644 --- a/libs/apprise/plugins/workflows.py +++ b/libs/apprise/plugins/workflows.py @@ -74,6 +74,7 @@ class APIVersion: """ Define API Versions """ + WORKFLOW = "2016-06-01" POWER_AUTOMATE = "2022-03-01-preview" @@ -258,6 +259,11 @@ class NotifyWorkflows(NotifyBase): if template: # Add our definition to our template self.template.add(template) + if not len(self.template): + # add() failed (unsupported schema, unparseable URL, etc.) + msg = "The Workflows template specified could not be loaded." + self.logger.warning(msg) + raise TypeError(msg) # Enforce maximum file size self.template[0].max_file_size = self.max_workflows_template_size @@ -266,12 +272,12 @@ class NotifyWorkflows(NotifyBase): # - If using power_automate, the API version required is different. default_api_version = ( APIVersion.POWER_AUTOMATE - if self.power_automate else APIVersion.WORKFLOW) + if self.power_automate + else APIVersion.WORKFLOW + ) self.api_version = ( - version - if version is not None - else default_api_version + version if version is not None else default_api_version ) # Template functionality @@ -304,30 +310,36 @@ class NotifyWorkflows(NotifyBase): body_content = [] if image_url: - body_content.append({ - "type": "Image", - "url": image_url, - "height": "32px", - "altText": notify_type.value, - }) + body_content.append( + { + "type": "Image", + "url": image_url, + "height": "32px", + "altText": notify_type.value, + } + ) if title: - body_content.append({ - "type": "TextBlock", - "text": f"{title}", - "style": "heading", - "weight": "Bolder", - "size": "Large", - "id": "title", - }) + body_content.append( + { + "type": "TextBlock", + "text": f"{title}", + "style": "heading", + "weight": "Bolder", + "size": "Large", + "id": "title", + } + ) - body_content.append({ - "type": "TextBlock", - "text": body, - "style": "default", - "wrap": self.wrap, - "id": "body", - }) + body_content.append( + { + "type": "TextBlock", + "text": body, + "style": "default", + "wrap": self.wrap, + "id": "body", + } + ) if not self.template: # By default we use a generic working payload if there was @@ -335,18 +347,22 @@ class NotifyWorkflows(NotifyBase): schema = "http://adaptivecards.io/schemas/adaptive-card.json" payload = { "type": "message", - "attachments": [{ - "contentType": "application/vnd.microsoft.card.adaptive", - "contentUrl": None, - "content": { - "$schema": schema, - "type": "AdaptiveCard", - "version": self.adaptive_card_version, - "body": body_content, - # Additionally - "msteams": {"width": "full"}, - }, - }], + "attachments": [ + { + "contentType": ( + "application/vnd.microsoft.card.adaptive" + ), + "contentUrl": None, + "content": { + "$schema": schema, + "type": "AdaptiveCard", + "version": self.adaptive_card_version, + "body": body_content, + # Additionally + "msteams": {"width": "full"}, + }, + } + ], } return payload @@ -377,24 +393,84 @@ class NotifyWorkflows(NotifyBase): # Enforce Application mode tokens["app_mode"] = TemplateType.JSON + # Coerce all substitution values to str before JSON-escaping. + # apply_template's _escape_json() calls json.dumps(v)[1:-1] which + # is only correct for strings; None produces "ul" and other non- + # string types produce similarly corrupted output. app_mode is a + # TemplateType sentinel passed as a named parameter, not a + # substitution value, so it is left as-is. + safe_tokens = { + k: ( + v + if k == "app_mode" or isinstance(v, str) + else ("" if v is None else str(v)) + ) + for k, v in tokens.items() + } + try: with open(template.path) as fp: - content = json.loads(apply_template(fp.read(), **tokens)) + content = json.loads(apply_template(fp.read(), **safe_tokens)) except OSError: self.logger.error( - f"MSTeam template {template.url(privacy=True)} could not be" - " read." + "Workflow template" + f" {template.url(privacy=True)} could not be read." ) - return None + return False except JSONDecodeError as e: self.logger.error( - f"MSTeam template {template.url(privacy=True)} contains" - " invalid JSON." + "Workflow template" + f" {template.url(privacy=True)} contains invalid JSON." ) self.logger.debug(f"JSONDecodeError: {e}") - return None + return False + + # Validate the required payload structure for Workflows/Power Automate + + # Template must parse to a JSON object, not an array or scalar + if not isinstance(content, dict): + self.logger.error( + "Workflow template" + f" {template.url(privacy=True)} must be a JSON object" + " (got {}).".format(type(content).__name__) + ) + return False + + # Root payload must be a Workflows/Power Automate 'message' type + if content.get("type") != "message": + self.logger.error( + "Workflow template" + f" {template.url(privacy=True)} must have" + " 'type': 'message'" + " (got {!r}).".format(content.get("type")) + ) + return False + + # Payload must carry a non-empty attachments list + if ( + not isinstance(content.get("attachments"), list) + or not content["attachments"] + ): + self.logger.error( + "Workflow template" + f" {template.url(privacy=True)} must contain" + " a non-empty 'attachments' list." + ) + return False + + # Each attachment must be a dict with a contentType string + if not all( + isinstance(a, dict) and isinstance(a.get("contentType"), str) + for a in content["attachments"] + ): + self.logger.error( + "Workflow template" + f" {template.url(privacy=True)} contains an" + " attachment missing a 'contentType' string." + ) + return False return content @@ -416,9 +492,7 @@ class NotifyWorkflows(NotifyBase): # The URL changes depending on whether we're using power automate or # not path = ( - "/powerautomate/automations/direct" - if self.power_automate - else "" + "/powerautomate/automations/direct" if self.power_automate else "" ) notify_url = ( @@ -456,6 +530,7 @@ class NotifyWorkflows(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code not in ( requests.codes.ok, @@ -474,7 +549,8 @@ class NotifyWorkflows(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # We failed return False @@ -524,10 +600,12 @@ class NotifyWorkflows(NotifyBase): ) # Store our version if it differs from default - if (self.api_version != APIVersion.WORKFLOW - and not self.power_automate) or ( - self.api_version != APIVersion.POWER_AUTOMATE - and self.power_automate): + if ( + self.api_version != APIVersion.WORKFLOW and not self.power_automate + ) or ( + self.api_version != APIVersion.POWER_AUTOMATE + and self.power_automate + ): # But only do so if we're not using power automate with the # default version for that. params["ver"] = self.api_version @@ -573,9 +651,8 @@ class NotifyWorkflows(NotifyBase): results["qsd"].get( "powerautomate", results["qsd"].get( - "pa", - NotifyWorkflows.template_args["pa"]["default"] - ) + "pa", NotifyWorkflows.template_args["pa"]["default"] + ), ) ) @@ -667,15 +744,12 @@ class NotifyWorkflows(NotifyBase): if result: # Determine if we're using power automate or not power_automate = ( - "&pa=yes" - if result.group("power_automate") - else "" + "&pa=yes" if result.group("power_automate") else "" ) # Construct our URL return NotifyWorkflows.parse_url( - "{schema}://{host}{port}/{workflow}/{params}{pa}" - .format( + "{schema}://{host}{port}/{workflow}/{params}{pa}".format( schema=NotifyWorkflows.secure_protocol[0], host=result.group("host"), port=( diff --git a/libs/apprise/plugins/wxpusher.py b/libs/apprise/plugins/wxpusher.py index aa36476a5..0f09e7ea1 100644 --- a/libs/apprise/plugins/wxpusher.py +++ b/libs/apprise/plugins/wxpusher.py @@ -138,6 +138,7 @@ class NotifyWxPusher(NotifyBase): "targets": { "name": _("Targets"), "type": "list:string", + "required": True, }, }, ) @@ -251,6 +252,7 @@ class NotifyWxPusher(NotifyBase): headers=headers, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) try: @@ -268,7 +270,6 @@ class NotifyWxPusher(NotifyBase): and content and content.get("code") == 1000 ): - # We're good! self.logger.info( "Sent WxPusher notification to %d targets.", @@ -307,7 +308,8 @@ class NotifyWxPusher(NotifyBase): self.logger.debug( "Response Details:\r\n%r", - content if content else (r.content or b"")[:2000]) + content if content else (r.content or b"")[:2000], + ) # Mark our failure return False diff --git a/libs/apprise/plugins/xmpp/adapter.py b/libs/apprise/plugins/xmpp/adapter.py index b1e0ebf92..acf5e85c9 100644 --- a/libs/apprise/plugins/xmpp/adapter.py +++ b/libs/apprise/plugins/xmpp/adapter.py @@ -63,6 +63,15 @@ except ImportError: FuturesTimeoutError = Exception # type: ignore[misc] +class XMPPChannelBindingError(Exception): + """SASL SCRAM-PLUS channel-binding authentication failure. + + Raised when the server rejects authentication because TLS + channel-binding data (tls-unique or tls-exporter) was unavailable + or mismatched. Callers may retry with SCRAM-PLUS disabled. + """ + + @dataclass(frozen=True, slots=True) class XMPPConfig: """Connection configuration.""" @@ -73,6 +82,11 @@ class XMPPConfig: password: str secure: str = SecureXMPPMode.STARTTLS verify_certificate: bool = True + # When False, SCRAM-PLUS channel-binding mechanisms are excluded + # from SASL negotiation. Useful when the server or Python ssl + # layer cannot provide valid channel-binding data, which causes + # "Invalid channel binding" authentication failures. + use_channel_binding: bool = True # --------------------------------------------------------------------------- @@ -157,10 +171,14 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: password: str, *, oneshot: bool, - targets: Optional[list[str]] = None, + logger: Optional[logging.Logger] = None, + targets: Optional[list[(str, str)]] = None, subject: str = "", body: str = "", before_message: Optional[Callable[[], None]] = None, + # Multi-User Chat + want_muc: bool = False, + nick: str = "", want_roster: bool = False, roster_timeout: float = 0.0, session_started_evt: Optional[asyncio.Event] = None, @@ -168,6 +186,10 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: ) -> None: super().__init__(jid, password) + # Store the logger so _log_task can find it + global LOGGING_ID + self.logger = logger or logging.getLogger(LOGGING_ID) + # Behaviour self._oneshot = bool(oneshot) @@ -181,11 +203,19 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: self._want_roster = bool(want_roster) self._roster_timeout = float(roster_timeout) + # Multi-User Chat + self._want_muc = bool(want_muc) + self._nick = nick + if self._want_muc: + with contextlib.suppress(Exception): + self.register_plugin("xep_0045") + # Keepalive coordination (keepalive mode only) self._session_started_evt = session_started_evt # State self._auth_failed = False + self._channel_binding_failed = False self.add_event_handler("session_start", self._on_session_start) self.add_event_handler("failed_auth", self._failed_auth) @@ -214,15 +244,29 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: # One-shot mode sends messages immediately on session_start and # then disconnects. Keepalive mode just signals readiness. if self._oneshot: - for target in self._targets: + for mtype, target in self._targets: if self._before_message: self._before_message() + if mtype == "groupchat": + nick = ( + self._nick + or getattr(self.boundjid, "user", "") + or "apprise" + ) + muc_coro = self.plugin["xep_0045"].join_muc( + target, nick + ) + try: + await asyncio.wait_for(muc_coro, timeout=5.0) + except Exception: + _close_awaitable(muc_coro) + self.send_message( mto=target, msubject=self._subject, mbody=self._body, - mtype="chat", + mtype=mtype, ) finally: @@ -235,6 +279,13 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: def _failed_auth(self, *args: object, **kwargs: object) -> None: # Authentication failure is always a hard failure. self._auth_failed = True + # Detect channel-binding rejections so the caller can surface + # a targeted hint without embedding Apprise-specific messages + # deep in the adapter layer. + with contextlib.suppress(Exception): + stanza_text = str(args[0]).lower() if args else "" + if "channel" in stanza_text and "binding" in stanza_text: + self._channel_binding_failed = True if self._session_started_evt is not None: self._session_started_evt.clear() self.disconnect() @@ -248,11 +299,7 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: """ coro = self._session_start(*args, **kwargs) - # One-shot mode: let Slixmpp schedule the coroutine itself. - if self._oneshot: - return coro - - # Keepalive mode: schedule on the assigned loop. + # Schedule on the event loop for both one-shot and keepalive. loop = getattr(self, "loop", None) # If the loop is missing or already closing, we MUST close the @@ -293,6 +340,38 @@ def _get_client_subclass(base_cls: type[Any]) -> type[Any]: def _build_client(*args: Any, **kwargs: Any) -> Any: return _get_client_subclass(slixmpp.ClientXMPP)(*args, **kwargs) + +def _disable_channel_binding(client: Any) -> None: + """Patch the slixmpp feature_mechanisms plugin on *client* so that + SASL mechanism selection never considers -PLUS (channel-binding) + variants. + + Works by wrapping _send_auth() -- which is called after the server's + mechanism list is stored in plugin.mech_list -- to strip every entry + whose name ends with '-PLUS' before the SASL choose() call picks + a mechanism. + + The whole operation is wrapped in contextlib.suppress so it degrades + gracefully when the plugin is unavailable (e.g., in tests that use a + fake ClientXMPP without plugin support). + """ + with contextlib.suppress(Exception): + # Access the feature_mechanisms plugin on the slixmpp client + plugin = client["feature_mechanisms"] + orig = plugin._send_auth + + def _no_plus(_orig=orig) -> None: + # Remove -PLUS mechs from the candidate list so choose() + # never attempts SCRAM channel binding + plugin.mech_list = { + m for m in plugin.mech_list if not m.endswith("-PLUS") + } + return _orig() + + # Replace _send_auth before SASL negotiation starts + plugin._send_auth = _no_plus + + # --------------------------------------------------------------------------- # Adapter # --------------------------------------------------------------------------- @@ -321,24 +400,36 @@ class SlixmppAdapter: def __init__( self, config: XMPPConfig, - targets: list[str], + targets: list[(str, str)], subject: str, body: str, timeout: float = 30.0, roster: bool = False, before_message: Optional[Callable[[], None]] = None, keepalive: bool = False, + want_muc: bool = False, + default_nickname: Optional[str] = None, ) -> None: - self.config, self.targets, self.subject, self.body = \ - config, targets, subject, body + self.config, self.targets, self.subject, self.body = ( + config, + targets, + subject, + body, + ) self.timeout = max(5.0, float(timeout)) - self.roster, self.before_message, self.keepalive = \ - roster, before_message, keepalive + self.roster, self.before_message, self.keepalive = ( + roster, + before_message, + keepalive, + ) + self._want_muc = want_muc global LOGGING_ID self.logger = logging.getLogger(LOGGING_ID) + self.nickname = default_nickname or "apprise" + bridge_slixmpp_logging() # Keepalive internals (only used when keepalive=True) @@ -474,6 +565,8 @@ class SlixmppAdapter: return False shared: dict[str, Any] = {"loop": None, "client": None} + # Shared flag: True when a channel-binding failure was detected. + cb_failed: list[bool] = [False] def runner() -> None: loop: Optional[asyncio.AbstractEventLoop] = None @@ -486,21 +579,28 @@ class SlixmppAdapter: shared["loop"] = loop targets = ( - list(self.targets) if self.targets else [self.config.jid]) + list(self.targets) + if self.targets + else [("chat", self.config.jid)] + ) roster_timeout = ( max(2.0, min(10.0, self.timeout / 3.0)) - if self.roster else 0.0 + if self.roster + else 0.0 ) client = _build_client( jid=self.config.jid, password=self.config.password, oneshot=True, + logger=self.logger, targets=targets, subject=self.subject, body=self.body, before_message=self.before_message, + want_muc=self._want_muc, + nick=self.nickname, want_roster=self.roster, roster_timeout=roster_timeout, session_started_evt=None, @@ -508,6 +608,10 @@ class SlixmppAdapter: shared["client"] = client + # Disable channel binding if requested + if not self.config.use_channel_binding: + _disable_channel_binding(client) + # Prevent Slixmpp from owning loop lifecycle with contextlib.suppress(Exception): client.loop = loop # type: ignore[assignment] @@ -543,7 +647,7 @@ class SlixmppAdapter: timeout=connect_timeout, ) ) - if not ok: + if ok is False: self.logger.warning("XMPP connect failed.") with contextlib.suppress(Exception): client.disconnect() @@ -585,6 +689,9 @@ class SlixmppAdapter: # Disconnect happened, success depends on auth state result[0] = not bool(getattr(client, "_auth_failed", False)) + # Flag channel-binding failures for upstream handling. + if getattr(client, "_channel_binding_failed", False): + cb_failed[0] = True except Exception as e: # pragma: no cover self.logger.warning("XMPP send failed.") @@ -602,7 +709,8 @@ class SlixmppAdapter: if not done.wait(timeout=self.timeout): self.logger.warning( - "XMPP send timed out after %.2fs.", self.timeout) + "XMPP send timed out after %.2fs.", self.timeout + ) result[0] = False loop_obj = shared.get("loop") @@ -622,6 +730,10 @@ class SlixmppAdapter: t.join(timeout=0.25) + # Re-raise channel-binding failures so the caller can log a hint. + if cb_failed[0]: + raise XMPPChannelBindingError() + return bool(result[0]) # ----------------------------------------------------------------------- @@ -674,19 +786,24 @@ class SlixmppAdapter: connect_lock = asyncio.Lock() # type: ignore[union-attr] roster_timeout = ( - max(2.0, min(10.0, self.timeout / 3.0)) - if self.roster else 0.0 + max(2.0, min(10.0, self.timeout / 3.0)) if self.roster else 0.0 ) client = _build_client( jid=self.config.jid, password=self.config.password, oneshot=False, + logger=self.logger, + want_muc=self._want_muc, want_roster=self.roster, roster_timeout=roster_timeout, session_started_evt=session_started, ) + # Disable channel binding if requested + if not self.config.use_channel_binding: + _disable_channel_binding(client) + with contextlib.suppress(Exception): client.loop = loop # type: ignore[assignment] @@ -709,6 +826,11 @@ class SlixmppAdapter: with contextlib.suppress(Exception): client.register_plugin("xep_0199", {"keepalive": True}) + if self._want_muc: + # Multi-User Chat + with contextlib.suppress(Exception): + client.register_plugin("xep_0045") + with self._state_lock: if self._closing: return @@ -748,6 +870,9 @@ class SlixmppAdapter: if self._connect_lock is None or self._session_started is None: return False + # If channel binding failed, surface a targeted exception. + if bool(getattr(self._client, "_channel_binding_failed", False)): + raise XMPPChannelBindingError() # If auth already failed, do not pretend a connection is ready. if bool(getattr(self._client, "_auth_failed", False)): return False @@ -803,12 +928,15 @@ class SlixmppAdapter: return False + # If channel binding failed, surface a targeted exception. + if getattr(self._client, "_channel_binding_failed", False): + raise XMPPChannelBindingError() # If auth failed during startup, treat as failure. return not bool(getattr(self._client, "_auth_failed", False)) async def _send_keepalive_async( self, - targets: list[str], + targets: list[(str, str)], subject: str, body: str, ) -> bool: @@ -816,22 +944,35 @@ class SlixmppAdapter: return False ok = await self._connect_if_required() - if not ok: + if ok is False: return False # Auth failed after connect, do not send. if bool(getattr(self._client, "_auth_failed", False)): return False - send_targets = targets if targets else [self.config.jid] + send_targets = targets if targets else [("chat", self.config.jid)] try: - for target in send_targets: + for mtype, target in send_targets: + if mtype == "groupchat": + nick = ( + getattr(self._client.boundjid, "user", "") + or self.nickname + ) + muc_coro = self._client.plugin["xep_0045"].join_muc( + target, nick + ) + try: + await asyncio.wait_for(muc_coro, timeout=5.0) + except Exception: + _close_awaitable(muc_coro) + self._client.send_message( mto=target, msubject=subject, mbody=body, - mtype="chat", + mtype=mtype, ) return True @@ -843,7 +984,7 @@ class SlixmppAdapter: def send_message( self, - targets: Optional[list[str]] = None, + targets: Optional[list[(str, str)]] = None, subject: Optional[str] = None, body: Optional[str] = None, ) -> bool: @@ -883,6 +1024,10 @@ class SlixmppAdapter: ) return bool(fut.result(timeout=self.timeout)) + except XMPPChannelBindingError: + # Re-raise so base.py can log a user-facing hint. + raise + except FuturesTimeoutError: self.logger.warning( "XMPP keepalive send timed out after %.2fs", self.timeout diff --git a/libs/apprise/plugins/xmpp/base.py b/libs/apprise/plugins/xmpp/base.py index e0f796acb..50fa2d9b1 100644 --- a/libs/apprise/plugins/xmpp/base.py +++ b/libs/apprise/plugins/xmpp/base.py @@ -35,9 +35,14 @@ from typing import Any, Optional from ...common import NotifyType from ...locale import gettext_lazy as _ from ...url import PrivacyMode -from ...utils.parse import parse_bool, parse_list +from ...utils.parse import parse_bool, parse_list, validate_regex from ..base import NotifyBase -from .adapter import SLIXMPP_SUPPORT_AVAILABLE, SlixmppAdapter, XMPPConfig +from .adapter import ( + SLIXMPP_SUPPORT_AVAILABLE, + SlixmppAdapter, + XMPPChannelBindingError, + XMPPConfig, +) from .common import SECURE_MODES, SecureXMPPMode # A pragmatic, "hardened" JID validator intended for Apprise URLs. @@ -50,7 +55,7 @@ from .common import SECURE_MODES, SecureXMPPMode # This does not try to fully implement RFC 7622. The goal is to catch bad # inputs early and reliably while still supporting common JID patterns. IS_JID = re.compile( - r"^\s*(?P[^@\s/]+)((@|%40)" + r"^\s*(?P#|%23)?(?P[^@\s/]+)((@|%40)" r"(?P[^@\s/]+))?(?:(/|%2F)(?P[^%/\s]+)" r"((/|%2F).*)?)?\s*$" ) @@ -114,6 +119,17 @@ class NotifyXMPP(NotifyBase): "private": True, "required": True, }, + "target_user": { + "name": _("Target User"), + "type": "string", + "map_to": "targets", + }, + "target_channels": { + "name": _("Target Channel"), + "type": "string", + "prefix": "#", + "map_to": "targets", + }, "targets": { "name": _("Targets"), "type": "list:string", @@ -124,7 +140,11 @@ class NotifyXMPP(NotifyBase): template_args = dict( NotifyBase.template_args, **{ - "to": {"alias_of": "targets"}, + "xmpp": { + "name": _("XMPP Server"), + "type": "string", + "map_to": "xmpp_host", + }, "mode": { "name": _("Secure Mode"), "type": "choice:string", @@ -147,49 +167,78 @@ class NotifyXMPP(NotifyBase): "type": "bool", "default": False, }, + "to": {"alias_of": "targets"}, + "name": { + "name": _("MUC Nickname"), + "type": "string", + }, + "scramplus": { + "name": _("SCRAM-PLUS Channel Binding"), + "type": "bool", + "default": True, + }, }, ) def __init__( self, - targets: Optional[list[str]] = None, + targets: Optional[list[(str, str)]] = None, secure_mode: Optional[str] = None, roster: Optional[bool] = None, subject: Optional[bool] = None, keepalive: Optional[bool] = None, + name: Optional[str] = None, + xmpp_host: Optional[str] = None, + scramplus: Optional[bool] = None, **kwargs: Any, ) -> None: super().__init__(**kwargs) + # xmpp_host allows the connection host to differ from the JID domain. + # Mirrors the smtp= / smtp_host pattern in the email plugin. + self.xmpp_host = ( + xmpp_host.strip() + if isinstance(xmpp_host, str) and xmpp_host.strip() + else "" + ) + try: - self.jid = self.normalize_jid(self.user or "", self.host) + self.jid, _ = self.normalize_jid(self.user or "", self.host) except ValueError: msg = f"An invalid XMPP JID ({self.user}) was specified." self.logger.warning(msg) raise TypeError(msg) from None - self.targets: list[str] = [] + self.targets: list[(str, str)] = [] + # Flag for tracking if we want Multi-User Chat function enabled + self.want_muc = False + for target in parse_list(targets): try: - jid = self.normalize_jid(target or "", self.host) + jid, is_muc = self.normalize_jid(target or "", self.host) + mtype = "groupchat" if is_muc else "chat" + if is_muc: + self.want_muc = True except ValueError: self.logger.warning( - "Dropped invalid XMPP target (%s).", target) + "Dropped invalid XMPP target (%s).", target + ) continue - self.targets.append(jid) + self.targets.append((mtype, jid)) if isinstance(secure_mode, str) and secure_mode.strip(): self.secure_mode = secure_mode.strip().lower() self.secure_mode = next( - (k for k in SECURE_MODES - if k.startswith(self.secure_mode)), None + (k for k in SECURE_MODES if k.startswith(self.secure_mode)), + None, ) if self.secure_mode not in SECURE_MODES: msg = ( "The XMPP secure mode specified " - f"({secure_mode}) is invalid.") + f"({secure_mode}) is invalid." + ) self.logger.warning(msg) raise TypeError(msg) @@ -203,12 +252,14 @@ class NotifyXMPP(NotifyBase): # Prepare our roster check self.roster = ( self.template_args["roster"]["default"] - if roster is None else bool(roster) + if roster is None + else bool(roster) ) self.subject = ( self.template_args["subject"]["default"] - if subject is None else bool(subject) + if subject is None + else bool(subject) ) self.keepalive = ( @@ -217,6 +268,17 @@ class NotifyXMPP(NotifyBase): else bool(keepalive) ) + # SCRAM-PLUS: controls whether SASL SCRAM-PLUS mechanisms are + # attempted. Set to False when the server returns "Invalid + # channel binding" during authentication; use ?scramplus=no. + self.scramplus = ( + self.template_args["scramplus"]["default"] + if scramplus is None + else parse_bool( + scramplus, default=self.template_args["scramplus"]["default"] + ) + ) + if self.secure and self.secure_mode == SecureXMPPMode.NONE: self.secure_mode = self.template_args["mode"]["default"] self.logger.warning( @@ -233,6 +295,12 @@ class NotifyXMPP(NotifyBase): ) self.secure = True + # MUC nickname: alphanumeric + underscore; falls back to the JID + # username, then the app_id as a last resort + self.name = validate_regex(name, r"^[a-zA-Z0-9_]+$") if name else None + if self.name is None: + self.name = self.user or self.app_id + # Keepalive adapter (created lazily) self._adapter: Optional[SlixmppAdapter] = None @@ -251,7 +319,11 @@ class NotifyXMPP(NotifyBase): """Return the pieces that uniquely identify this configuration.""" return ( self.secure_protocol if self.secure else self.protocol, - self.host, self.user, self.password, self.port, + self.host, + self.xmpp_host, + self.user, + self.password, + self.port, ) def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str: @@ -263,8 +335,17 @@ class NotifyXMPP(NotifyBase): "roster": "yes" if self.roster else "no", "subject": "yes" if self.subject else "no", "keepalive": "yes" if self.keepalive else "no", + "scramplus": "yes" if self.scramplus else "no", } + # Only include name when it differs from the default + # (JID user / app_id) + if self.name != (self.user or self.app_id): + params["name"] = self.name + + if self.xmpp_host and self.xmpp_host != self.host: + params["xmpp"] = self.xmpp_host + # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) @@ -286,7 +367,11 @@ class NotifyXMPP(NotifyBase): # Targets can contain '/' as a resource separator, so ensure it is # always percent-encoded in the path (otherwise Apprise will split it). - targets = "/".join(self.quote(t, safe="") for t in self.targets) + # Use %23 for the MUC '#' prefix so it is not misread as a fragment. + targets = "/".join( + ("%23" if mode == "groupchat" else "") + self.quote(jid, safe="") + for (mode, jid) in self.targets + ) return "{schema}://{auth}{host}{port}/{targets}?{params}".format( schema=schema, @@ -313,10 +398,11 @@ class NotifyXMPP(NotifyBase): config = XMPPConfig( jid=self.jid, password=self.password or "", - host=self.host, + host=self.xmpp_host or self.host, port=self.port if self.port else default_port, secure=self.secure_mode, verify_certificate=self.verify_certificate, + use_channel_binding=self.scramplus, ) self.logger.debug( @@ -336,30 +422,44 @@ class NotifyXMPP(NotifyBase): subject = title if self.subject else "" - if self.keepalive and self._adapter: - # Reuse existing adapter - return self._adapter.send_message( - targets=self.targets, - subject=subject, - body=body, + try: + if self.keepalive and self._adapter: + # Reuse existing adapter + return self._adapter.send_message( + targets=self.targets, + subject=subject, + body=body, + ) + + adapter_kwargs = { + "config": config, + "targets": self.targets, + "subject": subject, + "body": body, + "timeout": self.socket_connect_timeout, + "roster": self.roster, + "keepalive": self.keepalive, + "want_muc": self.want_muc, + "default_nickname": self.name, + } + if not self.keepalive: + # One-shot mode: Create, process, and discard + return SlixmppAdapter(**adapter_kwargs).process() + + # Keepalive mode, reuse a single adapter instance + self._adapter = SlixmppAdapter(**adapter_kwargs) + return self._adapter.send_message() + + except XMPPChannelBindingError: + # The server rejected SASL SCRAM-PLUS channel binding. + # Log a targeted hint so the user knows what to fix. + self.logger.warning( + "XMPP authentication failed: the server rejected " + "SASL SCRAM-PLUS channel binding. Add " + "?scramplus=no to your Apprise URL to disable " + "SCRAM-PLUS mechanism negotiation." ) - - adapter_kwargs = { - "config": config, - "targets": self.targets, - "subject": subject, - "body": body, - "timeout": self.socket_connect_timeout, - "roster": self.roster, - "keepalive": self.keepalive, - } - if not self.keepalive: - # One-shot mode: Create, process, and discard - return SlixmppAdapter(**adapter_kwargs).process() - - # Keepalive mode, reuse a single adapter instance - self._adapter = SlixmppAdapter(**adapter_kwargs) - return self._adapter.send_message() + return False @property def title_maxlen(self) -> Optional[int]: @@ -370,9 +470,8 @@ class NotifyXMPP(NotifyBase): return 0 if not self.subject else super().title_maxlen - # We don't support titles for SMSEagle notifications @staticmethod - def normalize_jid(value: str, default_host: str) -> str: + def normalize_jid(value: str, default_host: str) -> tuple[str, bool]: """Normalize and validate a JID. Behaviour: @@ -386,21 +485,18 @@ class NotifyXMPP(NotifyBase): optional '/resource' suffix. """ raw = (value or "").strip() - if not raw: - raise ValueError("Empty JID") - results = IS_JID.match(raw) if not results: raise ValueError("Invalid JID") - host = default_host \ - if not results.group("domain") else results.group("domain") + is_muc = bool(results.group("is_room")) + host = results.group("domain") or default_host jid = f"{results.group('local')}@{host}" if results.group("resource"): jid = f"{jid}/{results.group('resource')}" - return jid + return jid, is_muc @staticmethod def parse_url(url: str) -> Optional[dict[str, Any]]: @@ -412,7 +508,8 @@ class NotifyXMPP(NotifyBase): # Targets from path results["targets"] = [ NotifyXMPP.unquote(t) - for t in NotifyXMPP.split_path(results.get("fullpath"))] + for t in NotifyXMPP.split_path(results.get("fullpath")) + ] qd = results.get("qsd", {}) @@ -435,4 +532,23 @@ class NotifyXMPP(NotifyBase): if "keepalive" in results["qsd"] and len(results["qsd"]["keepalive"]): results["keepalive"] = parse_bool(results["qsd"]["keepalive"]) + if "name" in results["qsd"] and len(results["qsd"]["name"]): + results["name"] = NotifyXMPP.unquote(results["qsd"]["name"]) + + if "xmpp" in results["qsd"] and len(results["qsd"]["xmpp"]): + results["xmpp_host"] = NotifyXMPP.unquote(results["qsd"]["xmpp"]) + + if "scramplus" in results["qsd"] and len(results["qsd"]["scramplus"]): + results["scramplus"] = parse_bool( + results["qsd"]["scramplus"], + default=NotifyXMPP.template_args["scramplus"]["default"], + ) + return results + + @staticmethod + def runtime_deps(): + """Return a tuple of top-level Python package names that this plugin + imported as optional runtime dependencies. + """ + return ("slixmpp",) diff --git a/libs/apprise/plugins/xmpp/common.py b/libs/apprise/plugins/xmpp/common.py index b37bcb220..5fc27b005 100644 --- a/libs/apprise/plugins/xmpp/common.py +++ b/libs/apprise/plugins/xmpp/common.py @@ -32,6 +32,7 @@ class SecureXMPPMode: """ Defines our modes """ + NONE = "none" TLS = "tls" STARTTLS = "starttls" @@ -40,20 +41,20 @@ class SecureXMPPMode: SECURE_MODES = { SecureXMPPMode.STARTTLS: { "default_port": 5222, - "enable_plaintext": False, - "enable_starttls": True, - "enable_direct_tls": False, + "enable_plaintext": False, + "enable_starttls": True, + "enable_direct_tls": False, }, SecureXMPPMode.TLS: { "default_port": 5223, - "enable_plaintext": False, - "enable_starttls": False, - "enable_direct_tls": True, + "enable_plaintext": False, + "enable_starttls": False, + "enable_direct_tls": True, }, SecureXMPPMode.NONE: { "default_port": 5222, - "enable_plaintext": True, - "enable_starttls": False, - "enable_direct_tls": False, + "enable_plaintext": True, + "enable_starttls": False, + "enable_direct_tls": False, }, } diff --git a/libs/apprise/plugins/zoom.py b/libs/apprise/plugins/zoom.py new file mode 100644 index 000000000..b17d7f0ce --- /dev/null +++ b/libs/apprise/plugins/zoom.py @@ -0,0 +1,397 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Steps to set up a Zoom Team Chat Incoming Webhook: +# 1. Sign in to https://marketplace.zoom.us and search for +# "Incoming Webhook". +# 2. Click "Add" to install the Incoming Webhook app to your account. +# 3. In any Zoom Team Chat channel, type the slash command: +# /inc connect +# Follow the prompts to complete the connection. You will receive: +# - Endpoint URL: +# https://inbots.zoom.us/incoming/hook/WEBHOOK_ID +# - Verification Token: VERIFICATION_TOKEN +# +# Assemble your Apprise URL using both values: +# +# zoom://WEBHOOK_ID/VERIFICATION_TOKEN +# +# By default, messages are sent in "full" structured format, which renders +# the notification title as a heading and the body as the message text. +# To send a plain-text message instead, append ?mode=simple: +# +# zoom://WEBHOOK_ID/VERIFICATION_TOKEN?mode=simple +# +# The native webhook URL is also supported when a ?token= parameter is +# appended containing the verification token: +# +# https://inbots.zoom.us/incoming/hook/WEBHOOK_ID?token=TOKEN +# +# References: +# - https://marketplace.zoom.us/apps/eH_dLuquRd-VYcOsNGy-hQ +# - https://support.zoom.com/hc/en/article?id=zm_kb&\ +# sysparm_article=KB0067640 + +from json import dumps +import re + +import requests + +from ..common import NotifyType +from ..locale import gettext_lazy as _ +from ..utils.parse import validate_regex +from .base import NotifyBase + +# Extend HTTP Error Messages +ZOOM_HTTP_ERROR_MAP = { + 401: "Unauthorized - Invalid verification token.", + 404: "Webhook not found; check the Webhook ID.", + 429: "Rate limit exceeded; too many consecutive requests.", + 500: "Zoom internal server error.", + 503: "Service unavailable; try again later.", +} + + +class ZoomMode: + """Tracks the notification mode for Zoom Team Chat.""" + + # Plain-text message; sent without JSON wrapping + SIMPLE = "simple" + + # Structured message with head/body sections (supports title) + FULL = "full" + + +# Valid Zoom notification modes +ZOOM_MODES = ( + ZoomMode.SIMPLE, + ZoomMode.FULL, +) + +# Default notification mode +ZOOM_MODE_DEFAULT = ZoomMode.FULL + + +class NotifyZoom(NotifyBase): + """A wrapper for Zoom Team Chat Notifications.""" + + # The default descriptive name associated with the Notification + service_name = "Zoom" + + # The services URL + service_url = "https://zoom.us/" + + # The default secure protocol + secure_protocol = "zoom" + + # A URL that takes you to the setup/help of the specific protocol + setup_url = "https://appriseit.com/services/zoom/" + + # Zoom Team Chat incoming webhooks do not support file attachments + attachment_support = False + + # Zoom incoming-webhook base URL + zoom_webhook_url = "https://inbots.zoom.us/incoming/hook/{}" + + # The maximum body length for a Zoom notification + body_maxlen = 4000 + + # Maximum length for the head.text field (full mode only) + title_maxlen = 250 + + # In simple mode the title is prepended into the body string, so the + # combined payload must stay within body_maxlen. Setting this flag + # ensures Apprise enforces the combined limit across both modes. + overflow_amalgamate_title = True + + # Define object URL templates + templates = ("{schema}://{webhook_id}/{token}",) + + # Define our template tokens + template_tokens = dict( + NotifyBase.template_tokens, + **{ + "webhook_id": { + "name": _("Webhook ID"), + "type": "string", + "private": True, + "required": True, + }, + "token": { + "name": _("Verification Token"), + "type": "string", + "private": True, + "required": True, + }, + }, + ) + + # Define our template arguments + template_args = dict( + NotifyBase.template_args, + **{ + "mode": { + "name": _("Mode"), + "type": "choice:string", + "values": ZOOM_MODES, + "default": ZOOM_MODE_DEFAULT, + }, + # token= is already defined in template_tokens; this + # alias_of entry simply advertises that ?token= is also + # accepted as a query-string override on the URL. + "token": { + "alias_of": "token", + }, + }, + ) + + def __init__(self, webhook_id, token, mode=None, **kwargs): + """Initialize Zoom Object.""" + super().__init__(**kwargs) + + # Validate our webhook ID + self.webhook_id = validate_regex(webhook_id, r"^[A-Za-z0-9_-]+$") + if not self.webhook_id: + msg = ( + "A Zoom webhook ID must be specified and contain" + " only alphanumeric, hyphen, or underscore" + " characters." + ) + self.logger.warning(msg) + raise TypeError(msg) + + # Validate our verification token + self.token = validate_regex(token) + if not self.token: + msg = "A Zoom verification token must be specified." + self.logger.warning(msg) + raise TypeError(msg) + + # Validate notification mode + if mode is None: + # Default to full structured mode + self.mode = ZOOM_MODE_DEFAULT + + else: + # Reject empty or whitespace-only strings before prefix matching; + # an empty string would match every mode (false positive). + normalized_mode = ( + mode.strip().lower() if isinstance(mode, str) else "" + ) + if not normalized_mode: + self.mode = None + else: + # Allow partial prefix matching (e.g. "sim" -> "simple") + self.mode = next( + (m for m in ZOOM_MODES if m.startswith(normalized_mode)), + None, + ) + if not self.mode: + msg = "The Zoom mode ({}) is invalid.".format(mode) + self.logger.warning(msg) + raise TypeError(msg) + + def send( + self, + body, + title="", + notify_type=NotifyType.INFO, + attach=None, + **kwargs, + ): + """Perform Zoom Team Chat Notification.""" + + # Build the base webhook endpoint URL + base_url = self.zoom_webhook_url.format( + NotifyZoom.quote(self.webhook_id, safe="") + ) + + if self.mode == ZoomMode.FULL: + # Build the structured content object + content = {} + + # Include the head section only when a title is present + if title: + content["head"] = {"text": title} + + # Always include the body section + content["body"] = [ + { + "type": "message", + "text": body, + } + ] + + # Append the ?format=full query parameter + url = "{}?format=full".format(base_url) + + # Deliver the structured payload as JSON + return self._fetch( + url, + payload=dumps({"content": content}), + content_type="application/json", + ) + + # Simple mode: plain text, title colon-prepended if provided + text = "{}: {}".format(title, body) if title else body + + # Deliver plain text directly + return self._fetch(base_url, payload=text) + + def _fetch(self, url, payload, content_type=None): + """Wrapper to a Zoom webhook POST request.""" + + # Prepare our headers + headers = { + "User-Agent": self.app_id, + "Authorization": self.token, + } + + # Set Content-Type when a structured payload is provided + if content_type: + headers["Content-Type"] = content_type + + self.logger.debug( + "Zoom POST URL: {} (cert_verify={!r})".format( + url, self.verify_certificate + ) + ) + self.logger.debug("Zoom Payload: {!r}".format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + url, + data=payload, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.redirects, + ) + if r.status_code not in ( + requests.codes.ok, + requests.codes.no_content, + ): + # Report the error + status_str = NotifyZoom.http_response_code_lookup( + r.status_code, ZOOM_HTTP_ERROR_MAP + ) + self.logger.warning( + "Failed to send Zoom notification: {}{}error={}.".format( + status_str, + ", " if status_str else "", + r.status_code, + ) + ) + self.logger.debug( + "Response Details:\r\n%r", + (r.content or b"")[:2000], + ) + return False + + self.logger.info("Sent Zoom notification.") + + except requests.RequestException as e: + self.logger.warning( + "A Connection error occurred sending Zoom notification." + ) + self.logger.debug("Socket Exception: {}".format(str(e))) + return False + + return True + + @property + def url_identifier(self): + """Returns all of the identifiers that make this URL unique + from another simliar one. + + Targets or end points should never be identified here. + """ + return (self.secure_protocol, self.webhook_id, self.token, self.mode) + + def url(self, privacy=False, *args, **kwargs): + """Returns the URL built dynamically based on specified + arguments.""" + + # Always include mode so the round-trip is stable + params = {"mode": self.mode} + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + return "{schema}://{webhook_id}/{token}/?{params}".format( + schema=self.secure_protocol, + webhook_id=self.pprint(self.webhook_id, privacy, safe=""), + token=self.pprint(self.token, privacy, safe=""), + params=NotifyZoom.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """Parses the URL and returns enough arguments that can allow + us to re-instantiate this object.""" + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + return results + + # The host holds the webhook ID + results["webhook_id"] = NotifyZoom.unquote(results["host"]) + + # The first path segment carries the verification token + path = NotifyZoom.split_path(results["fullpath"]) + if path: + results["token"] = NotifyZoom.unquote(path.pop(0)) + + # Support ?token= override (useful with native URLs) + if "token" in results["qsd"] and results["qsd"]["token"]: + results["token"] = NotifyZoom.unquote(results["qsd"]["token"]) + + # Support ?mode= parameter + if "mode" in results["qsd"] and results["qsd"]["mode"]: + results["mode"] = NotifyZoom.unquote(results["qsd"]["mode"]) + + return results + + @staticmethod + def parse_native_url(url): + """Support https://inbots.zoom.us/incoming/hook/WEBHOOK_ID""" + result = re.match( + r"^https?://inbots\.zoom\.us/incoming/hook/" + r"(?P[A-Za-z0-9_-]+)/?" + r"(?P\?.+)?$", + url, + re.I, + ) + if result: + return NotifyZoom.parse_url( + "{schema}://{webhook_id}/{params}".format( + schema=NotifyZoom.secure_protocol, + webhook_id=result.group("webhook_id"), + params=result.group("params") or "", + ) + ) + return None diff --git a/libs/apprise/plugins/zulip.py b/libs/apprise/plugins/zulip.py index e392b3264..388415d43 100644 --- a/libs/apprise/plugins/zulip.py +++ b/libs/apprise/plugins/zulip.py @@ -297,6 +297,7 @@ class NotifyZulip(NotifyBase): auth=auth, verify=self.verify_certificate, timeout=self.request_timeout, + allow_redirects=self.redirects, ) if r.status_code != requests.codes.ok: # We had a problem @@ -315,7 +316,8 @@ class NotifyZulip(NotifyBase): ) self.logger.debug( - "Response Details:\r\n%r", (r.content or b"")[:2000]) + "Response Details:\r\n%r", (r.content or b"")[:2000] + ) # Mark our failure has_error = True diff --git a/libs/apprise/tag.py b/libs/apprise/tag.py new file mode 100644 index 000000000..191d43127 --- /dev/null +++ b/libs/apprise/tag.py @@ -0,0 +1,167 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import re + +# Matches: [priority:]tagname[:retry] +# - priority is a non-negative integer prefix (e.g. "2:endpoint") +# - tagname is a-z, 0-9, underscore, hyphen +# - retry is a non-negative integer suffix (e.g. "endpoint:3", CLI only) +_RE_TAG = re.compile( + r"^(?:(?P[0-9]+):)?(?P[a-z0-9][a-z0-9_-]*)(?::(?P[0-9]+))?$", + re.IGNORECASE, +) + + +class AppriseTag: + """Wraps a tag string and carries optional priority and retry metadata. + + The tag name is the canonical identity; two AppriseTag objects (or an + AppriseTag and a plain str) are considered equal when their tag names + match, regardless of priority or retry. Hash is likewise based solely + on the tag name so that set intersection and membership tests work + transparently against sets that contain plain strings. + + Supported string formats: + tagname -> priority=0, retry=None + priority:tagname -> priority=N, retry=None (config / YAML) + tagname:retry -> priority=0, retry=N (CLI runtime override) + priority:tagname:retry -> priority=N, retry=N + """ + + __slots__ = ("_tag", "has_priority", "priority", "retry") + + def __init__(self, tag, priority=0, retry=None, has_priority=False): + """Initialise an AppriseTag directly from its constituent parts. + + Prefer AppriseTag.parse() when constructing from a raw string because + it handles the "priority:tag:retry" tokenisation automatically. + + Args: + tag (str): The bare tag name. Always stored lowercased. + priority (int, optional): Numeric dispatch priority. Lower numbers + are dispatched first (0 = highest urgency). Defaults to 0. + retry (int or None, optional): Call-level retry count carried by + this tag token, or None when none was specified. + has_priority (bool, optional): True when the priority was + explicitly written in the source string (e.g. "0:endpoint"), + False when it was absent and the default of 0 was assumed. + This flag drives the exclusive-vs-escalation dispatch decision. + """ + self._tag = str(tag).lower().strip() + self.priority = max(0, int(priority)) if priority is not None else 0 + self.retry = int(retry) if retry is not None else None + self.has_priority = bool(has_priority) + + # ------------------------------------------------------------------ + # Construction helpers + # ------------------------------------------------------------------ + + @classmethod + def parse(cls, value): + """Return an AppriseTag parsed from a string (or the same object + if already an AppriseTag). + + Unrecognised strings fall back to a zero-priority, no-retry tag + whose name is the entire input lowercased. + """ + if isinstance(value, cls): + return value + m = _RE_TAG.match(str(value).strip()) + if m: + return cls( + tag=m.group("tag"), + priority=int(m.group("priority") or 0), + retry=( + int(m.group("retry")) + if m.group("retry") is not None + else None + ), + has_priority=m.group("priority") is not None, + ) + # Fallback: store raw value as name (no priority/retry) + return cls(tag=str(value).strip().lower()) + + # ------------------------------------------------------------------ + # String representation + # ------------------------------------------------------------------ + + def __str__(self): + """Return the bare lowercase tag name.""" + return self._tag + + def __repr__(self): + """Return a developer-readable representation including non-default + priority and retry values so they are visible in tracebacks.""" + parts = [repr(self._tag)] + if self.priority: + parts.append(f"priority={self.priority}") + if self.retry is not None: + parts.append(f"retry={self.retry}") + return "AppriseTag({})".format(", ".join(parts)) + + # ------------------------------------------------------------------ + # Identity (tag-name only -- priority/retry are metadata, not identity) + # ------------------------------------------------------------------ + + def __hash__(self): + """Hash based solely on the lowercased tag name. + + This must equal hash(tag_name_string) so that AppriseTag objects + can be mixed with plain strings inside sets and dictionaries without + any special handling in is_exclusive_match or elsewhere. + """ + return hash(self._tag) + + def __eq__(self, other): + """Compare identity by tag name only. + + An AppriseTag is considered equal to a plain string when their + lowercased tag names match, enabling transparent membership tests + such as ``"endpoint" in {AppriseTag("endpoint")}``. + """ + if isinstance(other, AppriseTag): + return self._tag == other._tag + if isinstance(other, str): + return self._tag == other.lower() + return NotImplemented + + def __lt__(self, other): + """Compare tag names lexicographically to allow sorting. + + Used when a deterministic ordering of tags is needed (e.g. for + reproducible log output). + """ + if isinstance(other, AppriseTag): + return self._tag < other._tag + if isinstance(other, str): + return self._tag < other.lower() + return NotImplemented + + def __bool__(self): + """Return False for an empty tag name, True otherwise.""" + return bool(self._tag) diff --git a/libs/apprise/url.py b/libs/apprise/url.py index d597682ac..c2afabeab 100644 --- a/libs/apprise/url.py +++ b/libs/apprise/url.py @@ -36,6 +36,7 @@ from xml.sax.saxutils import escape as sax_escape from .asset import AppriseAsset from .locale import gettext_lazy as _ from .logger import logger +from .tag import AppriseTag from .utils.parse import ( parse_bool, parse_list, @@ -119,6 +120,12 @@ class URLBase: # Secure sites should be verified against a Certificate Authority verify_certificate = True + # By default, HTTP redirects are followed, matching the behaviour of the + # underlying requests library. Set to False to prevent redirect-chain + # credential forwarding to destinations not explicitly configured by the + # operator. + redirects = True + # Logging to our global logger logger = logger @@ -147,6 +154,16 @@ class URLBase: # runtime. "_lookup_default": "verify_certificate", }, + "redirect": { + "name": _("Follow Redirects"), + # Whether to follow HTTP 3xx redirects + "type": "bool", + # Provide a default + "default": redirects, + # look up default using the following parent class value at + # runtime. + "_lookup_default": "redirects", + }, "rto": { "name": _("Socket Read Timeout"), "type": "float", @@ -210,6 +227,14 @@ class URLBase: kwargs.get("verify", URLBase.verify_certificate) ) + # Redirect support; default to enabled to match the behaviour of the + # underlying requests library. The asset-level http_redirects flag + # acts as the global default so operators can disable redirect + # following across all plugins without touching every individual URL. + self.redirects = parse_bool( + kwargs.get("redirect", self.asset.http_redirects) + ) + # Schema self.schema = kwargs.get("schema", "unknown").lower() @@ -252,8 +277,8 @@ class URLBase: self.socket_read_timeout = float(kwargs.get("rto")) except (TypeError, ValueError): self.logger.warning( - "Invalid socket read timeout (rto) was specified {}" - .format(kwargs.get("rto")) + "Invalid socket read timeout (rto)" + " was specified {}".format(kwargs.get("rto")) ) if "cto" in kwargs: @@ -262,15 +287,21 @@ class URLBase: except (TypeError, ValueError): self.logger.warning( - "Invalid socket connect timeout (cto) was specified {}" - .format(kwargs.get("cto")) + "Invalid socket connect timeout (cto)" + " was specified {}".format(kwargs.get("cto")) ) if "tag" in kwargs: - # We want to associate some tags with our notification service. - # the code below gets the 'tag' argument if defined, otherwise - # it just falls back to whatever was already defined globally - self.tags = set(parse_list(kwargs.get("tag"), self.tags)) + # Parse all tags as AppriseTag objects so priority metadata is + # preserved. Existing tags on the class (if any) are also + # converted so the set stays homogeneous. + existing = { + t if isinstance(t, AppriseTag) else AppriseTag.parse(str(t)) + for t in self.tags + } + self.tags = existing | { + AppriseTag.parse(t) for t in parse_list(kwargs.get("tag")) + } # Tracks the time any i/o was made to the remote server. This value # is automatically set and controlled through the throttle() call. @@ -408,7 +439,6 @@ class URLBase: self.__cached_url_identifier = None elif self.url_identifier in (None, True): - # Prepare our object engine = hash_engine( self.asset.storage_salt @@ -460,28 +490,32 @@ class URLBase: elif isinstance(self.url_identifier, (list, tuple, set)): self.__cached_url_identifier = hash_engine( self.asset.storage_salt - + b"".join([ - ( - x - if isinstance(x, bytes) - else str(x).encode(self.asset.encoding) - ) - for x in self.url_identifier - ]), + + b"".join( + [ + ( + x + if isinstance(x, bytes) + else str(x).encode(self.asset.encoding) + ) + for x in self.url_identifier + ] + ), **kwargs, ).hexdigest() elif isinstance(self.url_identifier, dict): self.__cached_url_identifier = hash_engine( self.asset.storage_salt - + b"".join([ - ( - x - if isinstance(x, bytes) - else str(x).encode(self.asset.encoding) - ) - for x in self.url_identifier.values() - ]), + + b"".join( + [ + ( + x + if isinstance(x, bytes) + else str(x).encode(self.asset.encoding) + ) + for x in self.url_identifier.values() + ] + ), **kwargs, ).hexdigest() @@ -820,6 +854,12 @@ class URLBase: if self.verify_certificate != URLBase.verify_certificate: params["verify"] = "yes" if self.verify_certificate else "no" + # Redirect following -- compare against the asset default, not the + # class constant, so that a per-URL override is preserved when the + # asset global has been changed (round-trip idempotency). + if self.redirects != self.asset.http_redirects: + params["redirect"] = "yes" if self.redirects else "no" + return params @staticmethod @@ -850,6 +890,19 @@ class URLBase: # enabled results["verify"] = True + if qsd_exists and "redirect" in results["qsd"]: + # Pulled from URL String + results["redirect"] = parse_bool( + results["qsd"].get("redirect", True) + ) + + elif "redirect" in results: + # Pulled from YAML Configuration + results["redirect"] = parse_bool(results.get("redirect", True)) + + # When redirect= is not specified, leave it absent so that URLBase + # __init__ falls back to asset.http_redirects as the global default. + # Password overrides if "pass" in results: results["password"] = results["pass"] diff --git a/libs/apprise/utils/cwe312.py b/libs/apprise/utils/cwe312.py index f65cd6d9e..193f15b21 100644 --- a/libs/apprise/utils/cwe312.py +++ b/libs/apprise/utils/cwe312.py @@ -155,10 +155,12 @@ def cwe312_url(url): # Apply our full path scan in all cases results["fullpath"] = ( "/" - + "/".join([ - cwe312_word(x) - for x in re.split(r"[\\/]+", results["fullpath"].lstrip("/")) - ]) + + "/".join( + [ + cwe312_word(x) + for x in re.split(r"[\\/]+", results["fullpath"].lstrip("/")) + ] + ) if results["fullpath"] else "" ) @@ -182,28 +184,30 @@ def cwe312_url(url): params = "" if results["qsd"]: params = "?{}".format( - "&".join([ - "{}={}".format( - k, - cwe312_word( - v, - force=( - k - in ( - "password", - "secret", - "pass", - "token", - "key", - "id", - "apikey", - "to", - ) + "&".join( + [ + "{}={}".format( + k, + cwe312_word( + v, + force=( + k + in ( + "password", + "secret", + "pass", + "token", + "key", + "id", + "apikey", + "to", + ) + ), ), - ), - ) - for k, v in results["qsd"].items() - ]) + ) + for k, v in results["qsd"].items() + ] + ) ) return "{schema}://{auth}{hostname}{port}{fullpath}{params}".format( diff --git a/libs/apprise/utils/format.py b/libs/apprise/utils/format.py index 945d2ec85..8e7468cce 100644 --- a/libs/apprise/utils/format.py +++ b/libs/apprise/utils/format.py @@ -147,7 +147,7 @@ def smart_split( start = 0 length = len(text) - while start < length: # pragma: no branch + while start < length: # pragma: no branch remaining = length - start if remaining <= limit: result.append(text[start:]) diff --git a/libs/apprise/utils/json.py b/libs/apprise/utils/json.py new file mode 100644 index 000000000..6cc7cb8cf --- /dev/null +++ b/libs/apprise/utils/json.py @@ -0,0 +1,56 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import base64 +from datetime import datetime +import json + +from ..common import AWARE_DATE_ISO_FORMAT, NAIVE_DATE_ISO_FORMAT +from ..locale import LazyTranslation + + +class AppriseJSONEncoder(json.JSONEncoder): + """A JSON Encoder for handling Apprise internals.""" + + def default(self, entry): + if isinstance(entry, datetime): + return entry.strftime( + AWARE_DATE_ISO_FORMAT + if entry.tzinfo is not None + else NAIVE_DATE_ISO_FORMAT + ) + + elif isinstance(entry, bytes): + return base64.b64encode(entry).decode("utf-8") + + elif isinstance(entry, (set, frozenset, tuple)): + return list(entry) + + elif isinstance(entry, LazyTranslation): + return str(entry) + + return super().default(entry) diff --git a/libs/apprise/utils/logic.py b/libs/apprise/utils/logic.py index 6defcd0a4..de05faeb8 100644 --- a/libs/apprise/utils/logic.py +++ b/libs/apprise/utils/logic.py @@ -27,9 +27,41 @@ from itertools import chain from .. import common +from ..tag import AppriseTag from .parse import parse_list +def _token_matches_data(tok, data, match_all): + """Return True if filter token *tok* matches any entry in *data*. + + When the token carries an explicit priority (e.g. "3:endpoint"), only data + entries that are AppriseTag objects with that exact priority match. When + the token has no priority prefix, any data entry with the same tag name + matches regardless of its stored priority. + """ + ft = AppriseTag.parse(tok) + tag_name = str(ft) + + if tag_name == match_all: + return True + + if not ft.has_priority: + # Name-only: works transparently for both str and AppriseTag in data + # because AppriseTag.__hash__ == hash(tag_name) and __eq__ handles str. + return tag_name in data + + # Priority-exact: must find a matching AppriseTag with the same priority. + # Plain-string entries in data (backward compat) fall back to name-only. + for item in data: + if isinstance(item, AppriseTag): + if str(item) == tag_name and item.priority == ft.priority: + return True + else: + if str(item).lower() == tag_name: + return True + return False + + def is_exclusive_match( logic, data, @@ -50,6 +82,10 @@ def is_exclusive_match( logic=[('tagA', 'tagC'), 'tagB'] = (tagA and tagC) or tagB logic=[('tagB', 'tagC')] = tagB and tagC + Filter tokens may carry an explicit priority prefix (e.g. "3:endpoint"). + When present, only server tags that are AppriseTag objects with that exact + priority will match. Tokens without a priority prefix match any priority. + If `match_always` is not set to None, then its value is added as an 'or' to all specified logic searches. """ @@ -68,7 +104,7 @@ def is_exclusive_match( return False if match_always: - # Add our match_always to our logic searching if secified + # Add our match_always to our logic searching if specified logic = chain(logic, [match_always]) # Track what we match against; but by default we do not match @@ -81,18 +117,13 @@ def is_exclusive_match( # Garbage entry in our logic found return False - # treat these entries as though all elements found - # must exist in the notification service - entries = set(parse_list(entry)) - if not entries: - # We got a bogus set of tags to parse - # If there is no logic to apply then we're done early; we only - # match if there is also no data to match against + # All tokens within an entry are AND-ed: every token must match data. + raw = parse_list(entry) + if not raw: + # We got a bogus set of tags to parse; match only if data is empty return not data - if len(entries.intersection(data.union({match_all}))) == len(entries): - # our set contains all of the entries found - # in our notification data set + if all(_token_matches_data(tok, data, match_all) for tok in raw): matched = True break diff --git a/libs/apprise/utils/parse.py b/libs/apprise/utils/parse.py index 2dd6304d6..6ce2c3231 100644 --- a/libs/apprise/utils/parse.py +++ b/libs/apprise/utils/parse.py @@ -100,6 +100,19 @@ PHONE_NO_DETECTION_RE = re.compile( r"\s*([+(\s]*[0-9][0-9()\s-]+[0-9])(?=$|[\s,+(]+[0-9])", re.I ) +IS_DOMAIN_SERVICE_TARGET = re.compile( + r"\s*((?P[a-z0-9_-]+)\.)?(?P[a-z0-9_-]+)" + r"(:(?P[a-z0-9_,-]+))?", + re.I, +) + +DOMAIN_SERVICE_TARGET_DETECTION_RE = re.compile( + r"\s*((?:[a-z0-9_-]+\.)?[a-z0-9_-]+" + r"(?::(?:[a-z0-9_-]+(?:,+[a-z0-9_-]+)+?))?)" + r"(?=$|(?:\s|,+\s|\s,+)+(?:[a-z0-9_-]+\.)?[a-z0-9_-]+)", + re.I, +) + # Support for prefix: (string followed by colon) infront of phone no PHONE_NO_WPREFIX_DETECTION_RE = re.compile( r"\s*((?:[a-z]+:)?[+(\s]*[0-9][0-9()\s-]+[0-9])" @@ -262,6 +275,44 @@ def is_uuid(uuid): return bool(match) +def is_domain_service_target(entry, domain="notify"): + """Determine if the specified entry a domain.service:target type + + Expects a string containing the following formats: + - service + - service:target + - service:target1,target2 + - domain.service:target + - domain.service:target1,target2 + + Args: + entry (str): The string you want to check. + + Returns: + bool: Returns False if the entry specified is domain.service:target + """ + + try: + result = IS_DOMAIN_SERVICE_TARGET.match(entry) + if not result: + # not parseable content as it does not even conform closely to a + # domain.service:target + return False + + except TypeError: + # not parseable content + return False + + return { + # Store domain or set default if not acquired + "domain": result.group("domain") if result.group("domain") else domain, + # store service + "service": result.group("service"), + # store targets if defined + "targets": parse_list(result.group("targets")), + } + + def is_phone_no(phone, min_len=10): """Determine if the specified entry is a phone number. @@ -358,7 +409,7 @@ def is_call_sign(callsign): callsign (str): The string you want to check. Returns: - bool: Returns False if the address specified is not a phone number + bool: Returns False if the enry specified is not a callsign """ try: @@ -763,7 +814,6 @@ def parse_url( if isinstance(port, int) and not ( not strict_port or (strict_port and port > 0 and port <= 65535) ): - # An invalid port was specified return None @@ -850,6 +900,48 @@ def parse_bool(arg, default=False): return bool(arg) +def parse_domain_service_targets( + *args, store_unparseable=True, domain="notify", **kwargs +): + """ + Takes a string containing the following formats separated by space + - service + - service:target + - service:target1,target2 + - domain.service:target + - domain.service:target1,target2 + + If no domain is parsed, the default domain is returned. + + Targets can be comma separated (if multiple are to be defined) + """ + + result = [] + for arg in args: + if isinstance(arg, str) and arg: + _result = DOMAIN_SERVICE_TARGET_DETECTION_RE.findall(arg) + if _result: + result += _result + + elif not _result and store_unparseable: + # we had content passed into us that was lost because it was + # so poorly formatted that it didn't even come close to + # meeting the regular expression we defined. We intentially + # keep it as part of our result set so that parsing done + # at a higher level can at least report this to the end user + # and hopefully give them some indication as to what they + # may have done wrong. + result += list(filter(bool, re.split(STRING_DELIMITERS, arg))) + + elif isinstance(arg, (set, list, tuple)): + # Use recursion to handle the list of phone numbers + result += parse_domain_service_targets( + *arg, store_unparseable=store_unparseable, domain=domain + ) + + return result + + def parse_phone_no(*args, store_unparseable=True, prefix=False, **kwargs): """Takes a string containing phone numbers separated by comma's and/or spaces and returns a list.""" @@ -937,7 +1029,10 @@ def parse_emails(*args, store_unparseable=True, **kwargs): elif isinstance(arg, (set, list, tuple)): # Use recursion to handle the list of Emails - result += parse_emails(*arg, store_unparseable=store_unparseable) + result += parse_emails( + *arg, + store_unparseable=store_unparseable, + ) return result @@ -955,7 +1050,6 @@ def url_assembly(encode=False, **kwargs): # Determine Authentication auth = "" if kwargs.get("user") is not None and kwargs.get("password") is not None: - auth = "{user}:{password}@".format( user=quote_(kwargs.get("user"), safe=""), password=quote_(kwargs.get("password"), safe=""), @@ -1013,8 +1107,18 @@ def urlencode(query, doseq=False, safe="", encoding=None, errors=None): """ # Tidy query by eliminating any records set to None query_ = {k: v for (k, v) in query.items() if v is not None} + # Use quote (not quote_plus) so spaces become %20 rather than +. + # Apprise's parse_qsd intentionally leaves + raw (it is a reserved + # character in tokens/passwords), so values encoded with + can never + # round-trip cleanly. %20 is decoded correctly by unquote() and is + # safe across all Apprise plugin URL round-trips. return _urlencode( - query_, doseq=doseq, safe=safe, encoding=encoding, errors=errors + query_, + doseq=doseq, + safe=safe, + encoding=encoding, + errors=errors, + quote_via=quote, ) @@ -1082,7 +1186,8 @@ def parse_list(*args, cast=None, allow_whitespace=True, sort=True): elif isinstance(arg, (set, list, tuple)): result += parse_list( - *arg, allow_whitespace=allow_whitespace, sort=sort) + *arg, allow_whitespace=allow_whitespace, sort=sort + ) # # filter() eliminates any empty entries @@ -1095,8 +1200,11 @@ def parse_list(*args, cast=None, allow_whitespace=True, sort=True): sorted(filter(bool, list(set(result)))) if allow_whitespace else sorted( - [x.strip() for x in filter( - bool, list(set(result))) if x.strip()] + [ + x.strip() + for x in filter(bool, list(set(result))) + if x.strip() + ] ) ) return ( diff --git a/libs/apprise/utils/pgp.py b/libs/apprise/utils/pgp.py index 8ca21a423..d0b6e3ce6 100644 --- a/libs/apprise/utils/pgp.py +++ b/libs/apprise/utils/pgp.py @@ -34,6 +34,31 @@ from ..asset import AppriseAsset from ..exception import ApprisePluginException from ..logger import logger + +def _ensure_imghdr_shim(): + """Install a minimal imghdr shim when the module is absent. + + pgpy 0.6.0 imports imghdr, which was removed from the standard + library in Python 3.13 (PEP 594). pgpy's only use of imghdr is + ImageEncoding.encodingof(), which Apprise never calls. Returning + None from what() is the same safe fallback the library uses + internally for non-JPEG data. + """ + try: + import imghdr # noqa: F401 + except ImportError: + import sys + import types + + # Build a minimal stand-in with the single function pgpy calls + shim = types.ModuleType("imghdr") + shim.what = lambda file=None, h=None: None + sys.modules["imghdr"] = shim + + +# Install shim before pgpy attempts its top-level import of imghdr +_ensure_imghdr_shim() + try: import pgpy @@ -60,13 +85,28 @@ class ApprisePGPController: # If it is more than this, then it is not accepted max_pgp_public_key_size = 8000 + # Private keys can be materially larger than public keys, especially for + # 4096-bit RSA or keys carrying multiple subkeys and UIDs; 32K is + # generous enough to accommodate all realistic cases without being + # unbounded. + max_pgp_private_key_size = 32000 + def __init__( - self, path, pub_keyfile=None, email=None, asset=None, **kwargs + self, + path, + pub_keyfile=None, + prv_keyfile=None, + email=None, + asset=None, + wkd=None, + **kwargs, ): """Path should be the directory keys can be written and read from such as .store.path. - Optionally additionally specify a keyfile to explicitly open + Optionally additionally specify a pub_keyfile and/or prv_keyfile to + use explicit key files, and/or an AppriseWKDController to enable Web + Key Directory key discovery when no local key is found. """ # PGP hash @@ -78,6 +118,9 @@ class ApprisePGPController: # Our email self.email = email + # Optional WKD controller for automatic key discovery + self.wkd = wkd + # Prepare our Asset Object self.asset = ( asset if isinstance(asset, AppriseAsset) else AppriseAsset() @@ -98,6 +141,21 @@ class ApprisePGPController: else: self._pub_keyfile = None + if prv_keyfile: + # Create an Attachment for the private key as well so that remote + # paths and other Apprise-supported sources are supported + self._prv_keyfile = AppriseAttachment(asset=self.asset) + + # Add our definition to our pgp_key reference + self._prv_keyfile.add(prv_keyfile) + + # Enforce a private-key-specific size limit; private keys can be + # larger than public keys (4096-bit RSA, multiple subkeys/UIDs) + self._prv_keyfile[0].max_file_size = self.max_pgp_private_key_size + + else: + self._prv_keyfile = None + def keygen(self, email=None, name=None, force=False): """Generates a set of keys based on email configured.""" @@ -213,6 +271,61 @@ class ApprisePGPController: ) return True + def _pub_key_candidates(self, *emails): + """Returns the ordered list of candidate public-key filenames to + search, highest priority first. + + Shared by public_keyfile() and the diagnostic warning in public_key() + so both always reflect exactly the same search order. + """ + + # Base candidates, lowest priority first + fnames = [ + "pgp-public.asc", + "pgp-pub.asc", + "public.asc", + "pub.asc", + ] + + # Merge the controller's own email with any caller-supplied addresses. + # Two filenames are prepended per address: localpart shorthand is + # inserted at index 0 first, then the full address is also inserted + # at index 0 -- so the full address lands ahead of the localpart in + # the list (full address has higher priority as it is more specific). + all_emails = [self.email, *emails] if self.email else list(emails) + for em in all_emails: + # Localpart shorthand (e.g. "chris-pub.asc") + fnames.insert(0, f"{em.split('@')[0].lower()}-pub.asc") + # Full lowercase email (e.g. "chris@nuxref.com-pub.asc") + fnames.insert(0, f"{em.lower()}-pub.asc") + + return fnames + + def _prv_key_candidates(self): + """Returns the ordered list of candidate private-key filenames to + search, highest priority first. + + Shared by private_keyfile() and the diagnostic warning in private_key() + so both always reflect exactly the same search order. + """ + + # Base candidates, lowest priority first + fnames = [ + "pgp-private.asc", + "pgp-prv.asc", + "private.asc", + "prv.asc", + ] + + # Prefer keys named after the sender's email address when known + if self.email: + # Localpart shorthand (e.g. "chris-prv.asc") + fnames.insert(0, f"{self.email.split('@')[0].lower()}-prv.asc") + # Full lowercase email (e.g. "chris@nuxref.com-prv.asc") + fnames.insert(0, f"{self.email.lower()}-prv.asc") + + return fnames + def public_keyfile(self, *emails): """Returns the first match of a useable public key based emails provided.""" @@ -239,24 +352,9 @@ class ApprisePGPController: # No path return None - fnames = [ - "pgp-public.asc", - "pgp-pub.asc", - "public.asc", - "pub.asc", - ] - - if self.email: - # Include our email in the list - emails = [self.email, *emails] - - for email in emails: - entry = email.split("@")[0].lower() - fnames.insert(0, f"{entry}-pub.asc") - - # Lowercase email (Highest Priority) - entry = email.lower() - fnames.insert(0, f"{entry}-pub.asc") + # Use shared candidate list so the search order is always consistent + # with any diagnostic messages that reference the same list + fnames = self._pub_key_candidates(*emails) return next( ( @@ -267,11 +365,256 @@ class ApprisePGPController: None, ) + def private_keyfile(self): + """Returns the path to the private key file if one can be found. + + Returns the explicit path when prv_keyfile was provided, looks for a + matching auto-generated key in self.path otherwise. Returns False + when an explicit keyfile was given but could not be accessed, and + None when no key file could be located at all. + """ + + if self._prv_keyfile is not None: + # Use the explicitly-provided private key attachment + pgp_key = self._prv_keyfile[0] + if not pgp_key: + # We could not access the attachment + logger.error( + "Could not access PGP Private Key" + f" {pgp_key.url(privacy=True)}." + ) + return False + + return pgp_key.path + + elif not self.path: + # No storage path; cannot search for auto-generated keys + return None + + # Use shared candidate list so the search order is always consistent + # with any diagnostic messages that reference the same list + fnames = self._prv_key_candidates() + + return next( + ( + os.path.join(self.path, fname) + for fname in fnames + if os.path.isfile(os.path.join(self.path, fname)) + ), + None, + ) + + def private_key(self): + """Loads and returns the PGP private key object. + + Reads from the explicit prv_keyfile if one was provided, otherwise + scans the persistent storage path for an auto-generated private key. + Returns None when no usable private key could be found or loaded. + Passphrase-protected keys are not supported and will be rejected. + """ + + # Locate the private key file + path = self.private_keyfile() + if not path: + if path is False: + # An explicit pgpprv= file was given but could not be accessed; + # keep as WARNING because the user made an explicit choice that + # failed -- this is always actionable regardless of plugin + logger.warning("PGP Private Key could not be accessed") + + elif self.path: + # path is None: storage was searched but nothing matched. + # Only hash + filename is shown -- never an absolute path. + ns = os.path.basename(self.path) + candidates = self._prv_key_candidates() + shown = ", ".join(f"'{ns}/{fn}'" for fn in candidates[:4]) + extra = ", ..." if len(candidates) > 4 else "" + logger.debug( + "No PGP private key found; searched: %s%s", + shown, + extra, + ) + + else: + # No storage path at all -- nothing was searched + logger.debug("No PGP private key found") + + return None + + # Build a cache key distinct from public-key entries for the same path + cache_key = hashlib.sha1( + ("prv:" + os.path.abspath(path)).encode("utf-8") + ).hexdigest() + + # Return the cached key when it is still valid + if cache_key in self.__key_lookup: + entry = self.__key_lookup[cache_key] + if entry["expires"] > datetime.now(timezone.utc): + return entry["private_key"] + + # Expired -- remove and re-load below + del self.__key_lookup[cache_key] + + try: + with open(path) as key_file: + private_key, _ = pgpy.PGPKey.from_blob(key_file.read()) + + except NameError: + # PGPy not installed + logger.debug("PGPy not installed; skipping PGP signing: %s", path) + return None + + except FileNotFoundError: + # File was found but disappeared before we could open it + logger.debug("PGP Private Key file not found: %s", path) + return None + + except OSError as e: + logger.warning("Error accessing PGP Private Key file %s", path) + logger.debug(f"I/O Exception: {e}") + return None + + except Exception: + # Malformed or non-PGP file content + logger.warning( + "PGP Private Key file could not be parsed: %s", path + ) + return None + + if private_key.is_protected: + # Apprise does not support passphrase-protected private keys + # because it runs unattended; a protected key would block sending + logger.warning( + "PGP Private Key is passphrase-protected; " + "Apprise does not support passphrase-protected keys" + ) + return None + + # Cache the successfully loaded key + self.__key_lookup[cache_key] = { + "private_key": private_key, + "expires": datetime.now(timezone.utc) + timedelta(seconds=86400), + } + + return private_key + + def sign(self, message): + """Creates a detached PGP signature for the given message string. + + Returns a (signature_str, micalg) tuple on success where + signature_str is the armored PGP signature block and micalg is the + MIME hash algorithm label (e.g. 'pgp-sha256') for the + Content-Type header of the multipart/signed container. + Returns None when signing is not possible. + """ + + # Load our private key + private_key = self.private_key() + if not private_key: + logger.debug("PGP signing skipped: no usable private key") + return None + + try: + # Create a detached signature over the message text + sig = private_key.sign(message) + + # Map the hash algorithm to the MIME micalg label + micalg = "pgp-" + sig.hash_algorithm.name.lower() + + return (str(sig), micalg) + + except NameError: + # PGPy not installed; must come before pgpy.errors.PGPError so + # that evaluating that except clause doesn't itself raise NameError + logger.debug("PGPy not installed; skipping PGP signing") + + except pgpy.errors.PGPError: + # Key may lack the Sign capability or be otherwise incompatible + logger.debug("PGP signing failed; key may lack Sign capability") + + return None + + def _fetch_wkd_key(self, *emails): + """Attempt a Web Key Directory lookup for each email in turn. + + Returns a pgpy.PGPKey on the first successful fetch, or None if + WKD is not configured, no emails are provided, or all lookups + fail. + """ + + if self.wkd is None: + # No WKD controller configured + return None + + # Use only the supplied recipient emails when provided. Fall back + # to self.email only when no recipient emails were supplied + # (for example, an explicit self-send). Otherwise a missed + # recipient WKD lookup could incorrectly encrypt to the sender's + # key, leaving the intended recipient unable to decrypt. + candidates = ( + list(emails) if emails else ([self.email] if self.email else []) + ) + + for email in candidates: + if not email: + continue + + # Derive a stable cache key from the email address + cache_key = hashlib.sha1( + ("wkd:" + email.lower()).encode("utf-8") + ).hexdigest() + + # Return the previously parsed key when still valid + if cache_key in self.__key_lookup: + entry = self.__key_lookup[cache_key] + if entry["expires"] > datetime.now(timezone.utc): + return entry["public_key"] + # Expired; remove so we re-fetch below + del self.__key_lookup[cache_key] + + # Fetch raw binary key material from WKD + key_bytes = self.wkd.fetch(email) + if not key_bytes: + continue + + try: + public_key, _ = pgpy.PGPKey.from_blob(key_bytes) + + except NameError: + # pgpy not installed + logger.debug( + "PGPy not installed; skipping WKD key for %s", + email, + ) + continue + + except Exception: + # Malformed or unsupported key data from WKD + logger.debug("WKD key parse failed for %s; skipping", email) + continue + + # Cache the successfully parsed key + self.__key_lookup[cache_key] = { + "public_key": public_key, + "expires": datetime.now(timezone.utc) + + timedelta(seconds=86400), + } + + logger.debug("Loaded PGP public key via WKD for %s", email) + return public_key + + return None + def public_key(self, *emails, autogen=None): """Opens a spcified pgp public file and returns the key from it which is used to encrypt the message.""" path = self.public_keyfile(*emails) if not path: + # Try Web Key Directory before falling back to autogen + wkd_key = self._fetch_wkd_key(*emails) + if wkd_key is not None: + return wkd_key + if ( autogen if autogen is not None else self.asset.pgp_autogen ) and self.keygen(*emails): @@ -280,7 +623,24 @@ class ApprisePGPController: # We should get a hit now return self.public_key(*emails) - logger.warning("No PGP Public Key could be loaded") + # All discovery methods exhausted (local file, WKD, autogen). + # Log at DEBUG so the caller's warning (with plugin-specific hints) + # is the only user-visible message. Only the namespace hash + + # filename is shown -- never an absolute path -- so no sensitive + # filesystem layout is revealed. + if self.path: + ns = os.path.basename(self.path) + candidates = self._pub_key_candidates(*emails) + shown = ", ".join(f"'{ns}/{fn}'" for fn in candidates[:4]) + extra = ", ..." if len(candidates) > 4 else "" + logger.debug( + "No PGP public key found; searched: %s%s", + shown, + extra, + ) + else: + logger.debug("No PGP public key found") + return None # Persistent Storage Key @@ -308,6 +668,11 @@ class ApprisePGPController: logger.debug(f"I/O Exception: {e}") return None + except Exception: + # Malformed or non-PGP file content (e.g. pgpy.errors.PGPError) + logger.warning("PGP Public Key file could not be parsed: %s", path) + return None + self.__key_lookup[key] = { "public_key": public_key, "expires": datetime.now(timezone.utc) + timedelta(seconds=86400), @@ -315,11 +680,16 @@ class ApprisePGPController: return public_key # Encrypt message using the recipient's public key - def encrypt(self, message, *emails): - """If provided a path to a pgp-key, content is encrypted.""" + def encrypt(self, message, *emails, autogen=None): + """If provided a path to a pgp-key, content is encrypted. - # Acquire our key - public_key = self.public_key(*emails) + Pass autogen=False to suppress key auto-generation during the + public-key lookup. This is used in sign mode for opportunistic + encryption: only encrypt when a pre-existing key is found. + """ + + # Acquire our key; autogen controls whether a missing key is created + public_key = self.public_key(*emails, autogen=autogen) if not public_key: # Encryption not possible return False @@ -347,6 +717,10 @@ class ApprisePGPController: if value["expires"] > datetime.now(timezone.utc) } + # Also prune the WKD in-memory cache when a controller is set + if self.wkd is not None: + self.wkd.prune() + @property def pub_keyfile(self): """Returns the Public Keyfile Path if set otherwise it returns None @@ -361,3 +735,18 @@ class ApprisePGPController: else self._pub_keyfile[0].path ) ) + + @property + def prv_keyfile(self): + """Returns the Private Keyfile Path if set, otherwise returns None. + Returns False when an explicit keyfile was provided but could not + be accessed.""" + return ( + None + if self._prv_keyfile is None + else ( + False + if not self._prv_keyfile[0] + else self._prv_keyfile[0].path + ) + ) diff --git a/libs/apprise/utils/sanitize.py b/libs/apprise/utils/sanitize.py index 4627c0269..9bebc9259 100644 --- a/libs/apprise/utils/sanitize.py +++ b/libs/apprise/utils/sanitize.py @@ -103,7 +103,8 @@ class SanitizeOptions: def sanitize_payload( - value: Any, *, options: Optional[SanitizeOptions] = None) -> Any: + value: Any, *, options: Optional[SanitizeOptions] = None +) -> Any: """ This function is intended for DEBUG and TRACE logging only. @@ -201,8 +202,8 @@ def sanitize_payload( tail = obj[-opts.preview :] if length >= opts.preview else obj items_seen += 1 return ( - f"") + f"" + ) return _summarize_str(obj) # Bytes: always summarize. diff --git a/libs/apprise/utils/socket.py b/libs/apprise/utils/socket.py index 326a0b8c4..1304f4311 100644 --- a/libs/apprise/utils/socket.py +++ b/libs/apprise/utils/socket.py @@ -38,9 +38,7 @@ from typing import Optional, Union from ..exception import AppriseException, AppriseInvalidData from ..logger import logger -TimeoutType = Optional[ - Union[float, tuple[Optional[float], Optional[float]]] -] +TimeoutType = Optional[Union[float, tuple[Optional[float], Optional[float]]]] class AppriseSocketError(AppriseException): @@ -85,8 +83,9 @@ class SocketTransport: self.verify = bool(verify) self.retries = retries - self._connect_timeout, self._read_timeout = \ - self._coerce_timeout(timeout) + self._connect_timeout, self._read_timeout = self._coerce_timeout( + timeout + ) self._sock: Optional[socket.socket] = None self._rfile = None @@ -102,7 +101,8 @@ class SocketTransport: @staticmethod def _coerce_timeout( - timeout: TimeoutType) -> tuple[Optional[float], Optional[float]]: + timeout: TimeoutType, + ) -> tuple[Optional[float], Optional[float]]: """ Coerce requests-style timeout into (connect_timeout, read_timeout). """ @@ -188,7 +188,8 @@ class SocketTransport: return None try: r, _, x = select.select( - [self._sock], [], [self._sock], float(timeout)) + [self._sock], [], [self._sock], float(timeout) + ) except OSError: self.close() @@ -205,8 +206,9 @@ class SocketTransport: if self._sock is None: return None try: - _, w, x = \ - select.select([], [self._sock], [self._sock], float(timeout)) + _, w, x = select.select( + [], [self._sock], [self._sock], float(timeout) + ) except OSError: self.close() return None @@ -235,7 +237,8 @@ class SocketTransport: if self.bind_addr is not None or self.bind_port is not None: sock.bind( - (self.bind_addr or "127.0.0.1", int(self.bind_port or 0))) + (self.bind_addr or "127.0.0.1", int(self.bind_port or 0)) + ) if self._connect_timeout is not None: sock.settimeout(self._connect_timeout) @@ -396,9 +399,7 @@ class SocketTransport: if not self._had_io: return False - logger.warning( - "Socket %s failed, reconnecting and retrying", action - ) + logger.warning("Socket %s failed, reconnecting and retrying", action) logger.debug("Socket %s exception: %s", action, exc) try: @@ -442,8 +443,9 @@ class SocketTransport: attempts = max(0, retry_count) + 1 # Derive wait timeout (None means wait indefinitely) - wait_timeout = \ + wait_timeout = ( self._read_timeout if timeout is None else float(timeout) + ) # We manage readiness via select, socket stays non-blocking self._sock.setblocking(False) @@ -457,11 +459,15 @@ class SocketTransport: data = self._sock.recv(int(max_bytes)) if data == b"": raise AppriseSocketError( - "Connection lost during read") + "Connection lost during read" + ) self._had_io = True return data - except (BlockingIOError, ssl.SSLWantReadError, - ssl.SSLWantWriteError): + except ( + BlockingIOError, + ssl.SSLWantReadError, + ssl.SSLWantWriteError, + ): return b"" # blocking=True path: wait for readability, then recv @@ -487,13 +493,16 @@ class SocketTransport: data = self._sock.recv(int(max_bytes)) if data == b"": raise AppriseSocketError( - "Connection lost during read") + "Connection lost during read" + ) self._had_io = True return data - except (ssl.SSLWantReadError, ssl.SSLWantWriteError, - BlockingIOError): - + except ( + ssl.SSLWantReadError, + ssl.SSLWantWriteError, + BlockingIOError, + ): if wait_timeout is None: continue @@ -507,37 +516,46 @@ class SocketTransport: logger.debug("Socket read exception: %s", e) # Only close on hard errors; WANT_READ/WRITE handled above - if isinstance(e, OSError) \ - and not isinstance(e, ssl.SSLWantReadError) \ - and not isinstance(e, ssl.SSLWantWriteError): + if ( + isinstance(e, OSError) + and not isinstance(e, ssl.SSLWantReadError) + and not isinstance(e, ssl.SSLWantWriteError) + ): self.close() err: Exception = e # Reconnect only if we've had prior useful I/O if self._attempt_reconnect( - retries=attempts, - action="read", - exc=err, + retries=attempts, + action="read", + exc=err, ): # In blocking mode with no timeout (wait indefinitely), # perform an immediate read attempt after reconnect. # This avoids relying solely on can_read(), and it keeps # edge cases (like stale sockets) recoverable. - if blocking and wait_timeout is None \ - and self._sock is not None: + if ( + blocking + and wait_timeout is None + and self._sock is not None + ): try: data = self._sock.recv(int(max_bytes)) if data == b"": raise AppriseSocketError( - "Connection lost during read") + "Connection lost during read" + ) self._had_io = True return data - except (BlockingIOError, ssl.SSLWantReadError, - ssl.SSLWantWriteError): + except ( + BlockingIOError, + ssl.SSLWantReadError, + ssl.SSLWantWriteError, + ): # No data yet; fall back to retry loop pass @@ -588,9 +606,7 @@ class SocketTransport: self._read_timeout if timeout is None else float(timeout) ) deadline = ( - None - if op_timeout is None - else (time.monotonic() + op_timeout) + None if op_timeout is None else (time.monotonic() + op_timeout) ) try: @@ -600,9 +616,7 @@ class SocketTransport: if deadline is not None: remaining = deadline - time.monotonic() if remaining <= 0: - raise AppriseSocketError( - "Timed out during write" - ) + raise AppriseSocketError("Timed out during write") writable = self.can_write(remaining) if not writable: raise AppriseSocketError( diff --git a/libs/apprise/utils/time.py b/libs/apprise/utils/time.py index a1231f8c6..02ad73a0a 100644 --- a/libs/apprise/utils/time.py +++ b/libs/apprise/utils/time.py @@ -47,7 +47,14 @@ def zoneinfo(name: str) -> Optional[ZoneInfo]: # Windows-safe: accept UTC family even without tzdata if raw.lower() in { - "utc", "z", "gmt", "etc/utc", "etc/gmt", "gmt0", "utc0"}: + "utc", + "z", + "gmt", + "etc/utc", + "etc/gmt", + "gmt0", + "utc0", + }: return _tz.utc # Try exact match first @@ -59,6 +66,7 @@ def zoneinfo(name: str) -> Optional[ZoneInfo]: # Try case-insensitive match across available keys from zoneinfo import available_timezones + lowered = name.lower().strip() for zone in available_timezones(): full_zone = zone.lower() @@ -66,7 +74,6 @@ def zoneinfo(name: str) -> Optional[ZoneInfo]: return ZoneInfo(zone) with contextlib.suppress(IndexError): - # Break our zones and enforce limit zones = full_zone.split("/")[1:3] diff --git a/libs/apprise/utils/wkd.py b/libs/apprise/utils/wkd.py new file mode 100644 index 000000000..0aeec2e28 --- /dev/null +++ b/libs/apprise/utils/wkd.py @@ -0,0 +1,367 @@ +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2026, Chris Caron +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Web Key Directory (WKD) support for automatic OpenPGP key discovery. +# +# WKD allows an email client to automatically fetch a recipient's public +# PGP key by querying a well-known HTTPS endpoint on the recipient's mail +# domain. No manual key exchange is required: if the recipient's provider +# publishes their key via WKD, Apprise can encrypt outbound email without +# any user-visible configuration. +# +# Two URL forms are tried, in order (RFC 9080 / draft-koch-openpgp-webkey- +# service): +# +# Subdomain method (preferred): +# https://openpgpkey.{domain}/.well-known/openpgpkey/{domain}/hu/{hash} +# ?l={localpart} +# +# Direct method (fallback): +# https://{domain}/.well-known/openpgpkey/hu/{hash}?l={localpart} +# +# The {hash} is a z-base32-encoded SHA-1 digest of the lower-cased local +# part of the email address. +# +# References: +# https://wiki.gnupg.org/WKD +# https://datatracker.ietf.org/doc/html/rfc9080 + +from datetime import datetime, timedelta, timezone +import hashlib +from urllib.parse import quote + +import requests + +from ..asset import AppriseAsset +from ..exception import ApprisePluginException +from ..logger import logger +from .parse import is_hostname + + +class AppriseWKDException(ApprisePluginException): + """Raised when a WKD operation fails in an unrecoverable way.""" + + def __init__(self, message, error_code=610): + super().__init__(message, error_code=error_code) + + +class AppriseWKDController: + """Web Key Directory controller for automatic OpenPGP key discovery. + + Fetches binary OpenPGP public key material for a given email address + by querying the two WKD URL forms defined in RFC 9080. Results are + cached in memory to avoid redundant network requests within a single + Apprise session. + + This class is intentionally decoupled from pgpy: it returns raw bytes + and leaves key parsing to the caller (ApprisePGPController). That + separation makes it reusable for any future consumer that works with + OpenPGP key material. + """ + + # z-base32 encoding alphabet (RFC 6189, used by the WKD hash) + _ZB32 = "ybndrfg8ejkmcpqxot1uwisza345h769" + + # Upper bound on accepted WKD response size (64 KiB) + max_response_size = 65536 + + # How long fetched keys are kept in the in-memory cache (24 hours) + default_cache_expiry_sec = 60 * 60 * 24 + + def __init__( + self, + asset=None, + verify_certificate=True, + request_timeout=(4, 4), + allow_redirects=True, + ): + """Initialise the WKD controller. + + Args: + asset: Optional AppriseAsset used for the User-Agent string. + verify_certificate: Whether TLS certificates are verified. + request_timeout: (connect, read) timeout tuple passed to + requests.get(). + allow_redirects: Whether HTTP redirects are followed. WKD + deployments commonly redirect (e.g. shared hosting), so + this defaults to True. Set to False to require a direct + response from the WKD host. + """ + + # Prepare our Asset Object + self.asset = ( + asset if isinstance(asset, AppriseAsset) else AppriseAsset() + ) + + # TLS verification flag + self.verify_certificate = verify_certificate + + # (connect, read) timeouts + self.request_timeout = request_timeout + + # Whether to follow HTTP redirects + self.allow_redirects = allow_redirects + + # In-memory cache: lower-cased email -> {data, expires} + self._cache = {} + + @classmethod + def zb32_encode(cls, data): + """Return the z-base32 encoding of *data* (bytes). + + z-base32 is a human-oriented base-32 encoding (RFC 6189) used by + WKD to build the hash component of the lookup URL. Each group of + five bits maps to one character in cls._ZB32. + + For a SHA-1 digest (20 bytes = 160 bits) this always produces + exactly 32 characters with no padding. + """ + + result = [] + acc = 0 + bits = 0 + + # Accumulate bits and emit one character per 5-bit group + for byte in data: + acc = (acc << 8) | byte + bits += 8 + while bits >= 5: + bits -= 5 + result.append(cls._ZB32[(acc >> bits) & 0x1F]) + + # Emit any leftover bits, left-shifted to fill a 5-bit slot + if bits: + result.append(cls._ZB32[(acc << (5 - bits)) & 0x1F]) + + return "".join(result) + + @classmethod + def wkd_urls(cls, email): + """Return the (subdomain_url, direct_url) pair for *email*. + + Returns (None, None) when *email* is not a valid address. + """ + + # Validate and split the email address + if not isinstance(email, str) or not email or "@" not in email: + return None, None + + try: + # Split before lowercasing so the original local-part case + # is preserved for the ?l= query parameter (RFC 9080 requires + # the unchanged local-part; only the hash uses lowercase) + local_orig, domain = email.split("@", 1) + local = local_orig.lower() + domain = domain.lower() + except (ValueError, AttributeError): + return None, None + + if not local or not domain: + return None, None + + # Reject domains that fail hostname validation. Without this, a + # crafted email like user@legit.com@attacker.test splits to + # domain="legit.com@attacker.test", causing the WKD request to + # reach attacker.test with legit.com as URL credentials. + if not is_hostname(domain, ipv4=False, ipv6=False, underscore=False): + return None, None + + # SHA-1 of the lower-cased local part, then z-base32 encoded + hash_val = cls.zb32_encode( + hashlib.sha1(local.encode("utf-8")).digest() + ) + + # URL-encode the original (unchanged) local part for ?l= per RFC 9080 + quoted_local = quote(local_orig) + + # Subdomain method (preferred per RFC 9080) + subdomain_url = ( + "https://openpgpkey.{domain}" + "/.well-known/openpgpkey/{domain}" + "/hu/{h}?l={l}" + ).format(domain=domain, h=hash_val, l=quoted_local) + + # Direct method (fallback) + direct_url = ( + "https://{domain}/.well-known/openpgpkey/hu/{h}?l={l}" + ).format(domain=domain, h=hash_val, l=quoted_local) + + return subdomain_url, direct_url + + def fetch(self, email): + """Return binary OpenPGP key material for *email* via WKD. + + Tries the subdomain method first, then the direct method. + Returns bytes on success, or None when no key is found or the + email address is invalid. + + Results are cached in memory for default_cache_expiry_sec seconds + so repeated calls within a session avoid unnecessary round-trips. + """ + + # Reject obviously invalid input + if not isinstance(email, str) or not email or "@" not in email: + return None + + # Normalise for cache lookups + email_key = email.lower().strip() + + # Return cached entry when it has not expired + cached = self._cache.get(email_key) + if cached and cached["expires"] > datetime.now(timezone.utc): + logger.debug("WKD cache hit for %s", email_key) + return cached["data"] + + # Build the two WKD URLs; pass the stripped original (not the + # lowercased key) so wkd_urls() can preserve the local-part case + # in the ?l= parameter as required by RFC 9080 + subdomain_url, direct_url = self.wkd_urls(email.strip()) + if not subdomain_url: + # wkd_urls() returned None, None -- email was malformed + return None + + # Try subdomain method first, direct method as fallback + for url in (subdomain_url, direct_url): + key_data = self._get(url) + if key_data is not None: + # Store in cache before returning + self._cache[email_key] = { + "data": key_data, + "expires": datetime.now(timezone.utc) + + timedelta(seconds=self.default_cache_expiry_sec), + } + logger.debug( + "WKD key fetched for %s (%d bytes)", + email_key, + len(key_data), + ) + return key_data + + logger.debug("No WKD key found for %s", email_key) + return None + + def _get(self, url): + """Perform a single GET request and return the response body. + + Returns bytes when the server responds with HTTP 200 and a + non-empty body within max_response_size, or None for any other + outcome (network error, non-200 status, empty or oversized body). + """ + + logger.debug("WKD GET %s", url) + + try: + with requests.get( + url, + headers={"User-Agent": self.asset.app_id}, + verify=self.verify_certificate, + timeout=self.request_timeout, + allow_redirects=self.allow_redirects, + # Stream so we can abort early without buffering everything + stream=True, + ) as r: + if r.status_code != requests.codes.ok: + logger.debug( + "WKD returned HTTP %d for %s", r.status_code, url + ) + return None + + # Verify the final URL after any redirects is still HTTPS. + # Following a redirect to HTTP would expose the key material + # to interception and break the WKD trust model. + if not r.url.lower().startswith("https://"): + logger.debug( + "WKD redirect ended on non-HTTPS URL for %s; skipping", + url, + ) + return None + + # Reject early when the server advertises an oversized body + # before we read a single byte + content_length = r.headers.get("Content-Length") + if content_length is not None: + try: + if int(content_length) > self.max_response_size: + logger.warning( + "WKD response too large (%s bytes) for" + " %s; skipping", + content_length, + url, + ) + return None + except (TypeError, ValueError): + # Malformed Content-Length; let the chunk + # accumulation guard catch any actual oversize + pass + + # Consume the body in 8 KiB chunks, checking cumulative + # size after each chunk so we abort before the full body + # is in memory + content = bytearray() + for chunk in r.iter_content(chunk_size=8192): + if not chunk: + continue + content.extend(chunk) + if len(content) > self.max_response_size: + logger.warning( + "WKD response too large (%d bytes) for" + " %s; skipping", + len(content), + url, + ) + return None + + content = bytes(content) + + except Exception as exc: + # Connection refused, DNS failure, TLS error, timeout mid- + # stream, disk full, connection reset, IncompleteRead, etc. + logger.debug("WKD request failed for %s: %s", url, str(exc)) + return None + + if not content: + logger.debug("WKD returned empty body for %s", url) + return None + + # Reject obvious non-PGP responses (e.g. an HTML error page served + # with HTTP 200 by a CDN or parked domain at the subdomain endpoint). + # Every OpenPGP binary packet has bit 7 set in its first byte; ASCII- + # armoured keys start with "-----". HTML/JSON/text does not match + # either pattern, so we can skip the direct-method fallback slot from + # being silently eaten by a bad subdomain response. + if not (content[0] & 0x80 or content.startswith(b"-----")): + logger.debug("WKD response for %s is not PGP data; skipping", url) + return None + + return content + + def prune(self): + """Remove expired entries from the in-memory cache.""" + now = datetime.now(timezone.utc) + self._cache = { + k: v for k, v in self._cache.items() if v["expires"] > now + } diff --git a/libs/attr/__init__.pyi b/libs/attr/__init__.pyi index 8d78fa19a..758decf8f 100644 --- a/libs/attr/__init__.pyi +++ b/libs/attr/__init__.pyi @@ -308,7 +308,7 @@ def attrs( match_args: bool = ..., unsafe_hash: bool | None = ..., ) -> Callable[[_C], _C]: ... -def fields(cls: type[AttrsInstance]) -> Any: ... +def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ... def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... def validate(inst: AttrsInstance) -> None: ... def resolve_types( diff --git a/libs/attr/_make.py b/libs/attr/_make.py index d24d9ba98..4b32d6a71 100644 --- a/libs/attr/_make.py +++ b/libs/attr/_make.py @@ -463,6 +463,15 @@ def _transform_attrs( attrs = base_attrs + own_attrs + # Resolve default field alias before executing field_transformer, so that + # the transformer receives fully populated Attribute objects with usable + # alias values. + for a in attrs: + if not a.alias: + # Evolve is very slow, so we hold our nose and do it dirty. + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + _OBJ_SETATTR.__get__(a)("alias_is_default", True) + if field_transformer is not None: attrs = tuple(field_transformer(cls, attrs)) @@ -480,13 +489,12 @@ def _transform_attrs( if had_default is False and a.default is not NOTHING: had_default = True - # Resolve default field alias after executing field_transformer. - # This allows field_transformer to differentiate between explicit vs - # default aliases and supply their own defaults. + # Resolve default field alias for any new attributes that the + # field_transformer may have added without setting an alias. for a in attrs: if not a.alias: - # Evolve is very slow, so we hold our nose and do it dirty. _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + _OBJ_SETATTR.__get__(a)("alias_is_default", True) # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! @@ -569,7 +577,7 @@ def _frozen_delattrs(self, name): """ Attached to frozen classes as __delattr__. """ - if isinstance(self, BaseException) and name in ("__notes__",): + if isinstance(self, BaseException) and name == "__notes__": BaseException.__delattr__(self, name) return @@ -1096,9 +1104,7 @@ class _ClassBuilder: return self def add_replace(self): - self._cls_dict["__replace__"] = self._add_method_dunders( - lambda self, **changes: evolve(self, **changes) - ) + self._cls_dict["__replace__"] = self._add_method_dunders(evolve) return self def add_match_args(self): @@ -1884,16 +1890,16 @@ def _add_repr(cls, ns=None, attrs=None): def fields(cls): """ - Return the tuple of *attrs* attributes for a class. + Return the tuple of *attrs* attributes for a class or instance. The tuple also allows accessing the fields by their names (see below for examples). Args: - cls (type): Class to introspect. + cls (type): Class or instance to introspect. Raises: - TypeError: If *cls* is not a class. + TypeError: If *cls* is neither a class nor an *attrs* instance. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. @@ -1904,12 +1910,17 @@ def fields(cls): .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields by name. .. versionchanged:: 23.1.0 Add support for generic classes. + .. versionchanged:: 26.1.0 Add support for instances. """ generic_base = get_generic_base(cls) if generic_base is None and not isinstance(cls, type): - msg = "Passed object must be a class." - raise TypeError(msg) + type_ = type(cls) + if getattr(type_, "__attrs_attrs__", None) is None: + msg = "Passed object must be a class or attrs instance." + raise TypeError(msg) + + return fields(type_) attrs = getattr(cls, "__attrs_attrs__", None) @@ -2016,7 +2027,7 @@ def _make_init_script( attr_dict[a.name] = a if a.on_setattr is not None: - if frozen is True: + if frozen is True and a.on_setattr is not setters.NO_OP: msg = "Frozen classes can't use on_setattr." raise ValueError(msg) @@ -2429,6 +2440,8 @@ class Attribute: - ``name`` (`str`): The name of the attribute. - ``alias`` (`str`): The __init__ parameter name of the attribute, after any explicit overrides and default private-attribute-name handling. + - ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically + generated (``True``) or explicitly provided by the user (``False``). - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The @@ -2454,6 +2467,7 @@ class Attribute: equality checks and hashing anymore. .. versionadded:: 21.1.0 *eq_key* and *order_key* .. versionadded:: 22.2.0 *alias* + .. versionadded:: 26.1.0 *alias_is_default* For the full version history of the fields, see `attr.ib`. """ @@ -2478,6 +2492,7 @@ class Attribute: "inherited", "on_setattr", "alias", + "alias_is_default", ) def __init__( @@ -2500,6 +2515,7 @@ class Attribute: order_key=None, on_setattr=None, alias=None, + alias_is_default=None, ): eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq_key or eq, order_key or order, True @@ -2534,6 +2550,10 @@ class Attribute: bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) bound_setattr("alias", alias) + bound_setattr( + "alias_is_default", + alias is None if alias_is_default is None else alias_is_default, + ) def __setattr__(self, name, value): raise FrozenInstanceError @@ -2569,6 +2589,7 @@ class Attribute: ca.order_key, ca.on_setattr, ca.alias, + ca.alias is None, ) # Don't use attrs.evolve since fields(Attribute) doesn't work @@ -2587,6 +2608,20 @@ class Attribute: new._setattrs(changes.items()) + if "alias" in changes and "alias_is_default" not in changes: + # Explicit alias provided -- no longer the default. + _OBJ_SETATTR.__get__(new)("alias_is_default", False) + elif ( + "name" in changes + and "alias" not in changes + # Don't auto-generate alias if the user picked picked the old one. + and self.alias_is_default + ): + # Name changed, alias was auto-generated -- update it. + _OBJ_SETATTR.__get__(new)( + "alias", _default_init_alias_for(new.name) + ) + return new # Don't use _add_pickle since fields(Attribute) doesn't work @@ -2603,6 +2638,17 @@ class Attribute: """ Play nice with pickle. """ + if len(state) < len(self.__slots__): + # Pre-26.1.0 pickle without alias_is_default -- infer it + # heuristically. + state_dict = dict(zip(self.__slots__, state)) + alias_is_default = state_dict.get( + "alias" + ) is None or state_dict.get("alias") == _default_init_alias_for( + state_dict["name"] + ) + state = (*state, alias_is_default) + self._setattrs(zip(self.__slots__, state)) def _setattrs(self, name_values_pairs): @@ -2626,7 +2672,7 @@ _a = [ name=name, default=NOTHING, validator=None, - repr=True, + repr=(name != "alias_is_default"), cmp=None, eq=True, order=False, @@ -3087,9 +3133,7 @@ class Converter: value, field ) else: - self.__call__ = lambda value, instance, field: self.converter( - value, instance, field - ) + self.__call__ = self.converter rt = ex.get_return_type() if rt is not None: diff --git a/libs/attr/exceptions.py b/libs/attr/exceptions.py index 3b7abb815..a207df402 100644 --- a/libs/attr/exceptions.py +++ b/libs/attr/exceptions.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import ClassVar - class FrozenError(AttributeError): """ @@ -16,8 +14,10 @@ class FrozenError(AttributeError): .. versionadded:: 20.1.0 """ - msg = "can't set attribute" - args: ClassVar[tuple[str]] = [msg] + def __init__(self): + msg = "can't set attribute" + super().__init__(msg) + self.msg = msg class FrozenInstanceError(FrozenError): diff --git a/libs/attr/validators.py b/libs/attr/validators.py index 837e003b6..0b1a29443 100644 --- a/libs/attr/validators.py +++ b/libs/attr/validators.py @@ -79,12 +79,14 @@ def disabled(): This context manager is not thread-safe! .. versionadded:: 21.3.0 + .. versionchanged:: 26.1.0 The contextmanager is nestable. """ + prev = get_run_validators() set_run_validators(False) try: yield finally: - set_run_validators(True) + set_run_validators(prev) @attrs(repr=False, slots=True, unsafe_hash=True) diff --git a/libs/attr/validators.pyi b/libs/attr/validators.pyi index 36a7e800c..18fb112c8 100644 --- a/libs/attr/validators.pyi +++ b/libs/attr/validators.pyi @@ -54,7 +54,7 @@ def optional( validator: ( _ValidatorType[_T] | list[_ValidatorType[_T]] - | tuple[_ValidatorType[_T]] + | tuple[_ValidatorType[_T], ...] ), ) -> _ValidatorType[_T | None]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... diff --git a/libs/attrs-25.4.0.dist-info/INSTALLER b/libs/attrs-26.1.0.dist-info/INSTALLER similarity index 100% rename from libs/attrs-25.4.0.dist-info/INSTALLER rename to libs/attrs-26.1.0.dist-info/INSTALLER diff --git a/libs/attrs-25.4.0.dist-info/METADATA b/libs/attrs-26.1.0.dist-info/METADATA similarity index 67% rename from libs/attrs-25.4.0.dist-info/METADATA rename to libs/attrs-26.1.0.dist-info/METADATA index 51128bb96..5cf16a040 100644 --- a/libs/attrs-25.4.0.dist-info/METADATA +++ b/libs/attrs-26.1.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: attrs -Version: 25.4.0 +Version: 26.1.0 Summary: Classes Without Boilerplate Project-URL: Documentation, https://www.attrs.org/ Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html @@ -50,13 +50,15 @@ Especially those generously supporting us at the *The Organization* tier and hig import pathlib, tomllib for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]: - print(f'') + print(f'') ]]] --> - - - - - + + + + + + +

@@ -174,59 +176,21 @@ Save time, reduce risk, and improve code health, while paying the maintainers of ### Backwards-incompatible Changes -- Class-level `kw_only=True` behavior is now consistent with `dataclasses`. - - Previously, a class that sets `kw_only=True` makes all attributes keyword-only, including those from base classes. - If an attribute sets `kw_only=False`, that setting is ignored, and it is still made keyword-only. - - Now, only the attributes defined in that class that doesn't explicitly set `kw_only=False` are made keyword-only. - - This shouldn't be a problem for most users, unless you have a pattern like this: - - ```python - @attrs.define(kw_only=True) - class Base: - a: int - b: int = attrs.field(default=1, kw_only=False) - - @attrs.define - class Subclass(Base): - c: int - ``` - - Here, we have a `kw_only=True` *attrs* class (`Base`) with an attribute that sets `kw_only=False` and has a default (`Base.b`), and then create a subclass (`Subclass`) with required arguments (`Subclass.c`). - Previously this would work, since it would make `Base.b` keyword-only, but now this fails since `Base.b` is positional, and we have a required positional argument (`Subclass.c`) following another argument with defaults. - [#1457](https://github.com/python-attrs/attrs/issues/1457) +- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`. + The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`). + [#1509](https://github.com/python-attrs/attrs/issues/1509) ### Changes -- Values passed to the `__init__()` method of `attrs` classes are now correctly passed to `__attrs_pre_init__()` instead of their default values (in cases where *kw_only* was not specified). - [#1427](https://github.com/python-attrs/attrs/issues/1427) -- Added support for Python 3.14 and [PEP 749](https://peps.python.org/pep-0749/). - [#1446](https://github.com/python-attrs/attrs/issues/1446), - [#1451](https://github.com/python-attrs/attrs/issues/1451) -- `attrs.validators.deep_mapping()` now allows to leave out either *key_validator* xor *value_validator*. - [#1448](https://github.com/python-attrs/attrs/issues/1448) -- `attrs.validators.deep_iterator()` and `attrs.validators.deep_mapping()` now accept lists and tuples for all validators and wrap them into a `attrs.validators.and_()`. - [#1449](https://github.com/python-attrs/attrs/issues/1449) -- Added a new **experimental** way to inspect classes: - - `attrs.inspect(cls)` returns the _effective_ class-wide parameters that were used by *attrs* to construct the class. - - The returned class is the same data structure that *attrs* uses internally to decide how to construct the final class. - [#1454](https://github.com/python-attrs/attrs/issues/1454) -- Fixed annotations for `attrs.field(converter=...)`. - Previously, a `tuple` of converters was only accepted if it had exactly one element. - [#1461](https://github.com/python-attrs/attrs/issues/1461) -- The performance of `attrs.asdict()` has been improved by 45–260%. - [#1463](https://github.com/python-attrs/attrs/issues/1463) -- The performance of `attrs.astuple()` has been improved by 49–270%. - [#1469](https://github.com/python-attrs/attrs/issues/1469) -- The type annotation for `attrs.validators.or_()` now allows for different types of validators. - - This was only an issue on Pyright. - [#1474](https://github.com/python-attrs/attrs/issues/1474) +- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator. + [#1496](https://github.com/python-attrs/attrs/issues/1496) +- The `attrs.validators.disabled()` contextmanager can now be nested. + [#1513](https://github.com/python-attrs/attrs/issues/1513) +- Frozen classes can set `on_setattr=attrs.setters.NO_OP` in addition to `None`. + [#1515](https://github.com/python-attrs/attrs/issues/1515) +- It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`. + [#1529](https://github.com/python-attrs/attrs/issues/1529) diff --git a/libs/attrs-25.4.0.dist-info/RECORD b/libs/attrs-26.1.0.dist-info/RECORD similarity index 71% rename from libs/attrs-25.4.0.dist-info/RECORD rename to libs/attrs-26.1.0.dist-info/RECORD index e902531fa..6170952ed 100644 --- a/libs/attrs-25.4.0.dist-info/RECORD +++ b/libs/attrs-26.1.0.dist-info/RECORD @@ -1,32 +1,32 @@ attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057 -attr/__init__.pyi,sha256=IZkzIjvtbRqDWGkDBIF9dd12FgDa379JYq3GHnVOvFQ,11309 +attr/__init__.pyi,sha256=pVGImAUVovq2_TYl_r_HIYnGlyOaoCuEhxo-EvsnnSc,11325 attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117 attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368 attr/_compat.py,sha256=x0g7iEUOnBVJC72zyFCgb1eKqyxS-7f2LGnNyZ_r95s,2829 attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843 attr/_funcs.py,sha256=Ix5IETTfz5F01F-12MF_CSFomIn2h8b67EVVz2gCtBE,16479 -attr/_make.py,sha256=NRJDGS8syg2h3YNflVNoK2FwR3CpdSZxx8M6lacwljA,104141 +attr/_make.py,sha256=H7OH2eWS5CnBzLUjNFE1WymfPrmF1r8fv2RPdt9MuYA,106129 attr/_next_gen.py,sha256=BQtCUlzwg2gWHTYXBQvrEYBnzBUrDvO57u0Py6UCPhc,26274 attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469 attr/_version_info.py,sha256=w4R-FYC3NK_kMkGUWJlYP4cVAlH9HRaC-um3fcjYkHM,2222 attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209 attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861 attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643 -attr/exceptions.py,sha256=HRFq4iybmv7-DcZwyjl6M1euM2YeJVK_hFxuaBGAngI,1977 +attr/exceptions.py,sha256=b4vMbnoQ3VpwWZhqrYi_ssXVCK8o2c4HQSS09cSUM9o,1990 attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539 attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795 attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208 attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617 attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584 -attr/validators.py,sha256=1BnYGTuYvSucGEI4ju-RPNJteVzG0ZlfWpJiWoSFHQ8,21458 -attr/validators.pyi,sha256=ftmW3m4KJ3pQcIXAj-BejT7BY4ZfqrC1G-5W7XvoPds,4082 -attrs-25.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -attrs-25.4.0.dist-info/METADATA,sha256=2Rerxj7agcMRxiwdkt6lC2guqHAmkGKCH13nWWK7ZoQ,10473 -attrs-25.4.0.dist-info/RECORD,, -attrs-25.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -attrs-25.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 -attrs-25.4.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109 +attr/validators.py,sha256=m3QRzZTANr4f2C4eVdUoFg11NgXWak8Wat4qQTGhvcs,21553 +attr/validators.pyi,sha256=gM1ZmHaBckyYWI2EirpRNzqm3B19cw5Iq6B4Kno9YCM,4087 +attrs-26.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +attrs-26.1.0.dist-info/METADATA,sha256=TNQOaQ8jvzfLytNO_WdY4GLfHfB8hoM_fjzpW_H6OMw,8754 +attrs-26.1.0.dist-info/RECORD,, +attrs-26.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attrs-26.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +attrs-26.1.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109 attrs/__init__.py,sha256=RxaAZNwYiEh-fcvHLZNpQ_DWKni73M_jxEPEftiq1Zc,1183 attrs/__init__.pyi,sha256=2gV79g9UxJppGSM48hAZJ6h_MHb70dZoJL31ZNJeZYI,9416 attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76 diff --git a/libs/attrs-25.4.0.dist-info/REQUESTED b/libs/attrs-26.1.0.dist-info/REQUESTED similarity index 100% rename from libs/attrs-25.4.0.dist-info/REQUESTED rename to libs/attrs-26.1.0.dist-info/REQUESTED diff --git a/libs/attrs-25.4.0.dist-info/WHEEL b/libs/attrs-26.1.0.dist-info/WHEEL similarity index 67% rename from libs/attrs-25.4.0.dist-info/WHEEL rename to libs/attrs-26.1.0.dist-info/WHEEL index ae8ec1bda..bd8422674 100644 --- a/libs/attrs-25.4.0.dist-info/WHEEL +++ b/libs/attrs-26.1.0.dist-info/WHEEL @@ -1,4 +1,4 @@ Wheel-Version: 1.0 -Generator: hatchling 1.28.0 +Generator: hatchling 1.30.1 Root-Is-Purelib: true Tag: py3-none-any diff --git a/libs/attrs-25.4.0.dist-info/licenses/LICENSE b/libs/attrs-26.1.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/attrs-25.4.0.dist-info/licenses/LICENSE rename to libs/attrs-26.1.0.dist-info/licenses/LICENSE diff --git a/libs/beautifulsoup4-4.14.3.dist-info/INSTALLER b/libs/beautifulsoup4-4.15.0.dist-info/INSTALLER similarity index 100% rename from libs/beautifulsoup4-4.14.3.dist-info/INSTALLER rename to libs/beautifulsoup4-4.15.0.dist-info/INSTALLER diff --git a/libs/beautifulsoup4-4.14.3.dist-info/METADATA b/libs/beautifulsoup4-4.15.0.dist-info/METADATA similarity index 99% rename from libs/beautifulsoup4-4.14.3.dist-info/METADATA rename to libs/beautifulsoup4-4.15.0.dist-info/METADATA index 7fd97b769..43c537f62 100644 --- a/libs/beautifulsoup4-4.14.3.dist-info/METADATA +++ b/libs/beautifulsoup4-4.15.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: beautifulsoup4 -Version: 4.14.3 +Version: 4.15.0 Summary: Screen-scraping library Project-URL: Download, https://www.crummy.com/software/BeautifulSoup/bs4/download/ Project-URL: Homepage, https://www.crummy.com/software/BeautifulSoup/bs4/ diff --git a/libs/beautifulsoup4-4.14.3.dist-info/RECORD b/libs/beautifulsoup4-4.15.0.dist-info/RECORD similarity index 52% rename from libs/beautifulsoup4-4.14.3.dist-info/RECORD rename to libs/beautifulsoup4-4.15.0.dist-info/RECORD index 6d84e6774..01d9dadfe 100644 --- a/libs/beautifulsoup4-4.14.3.dist-info/RECORD +++ b/libs/beautifulsoup4-4.15.0.dist-info/RECORD @@ -1,23 +1,23 @@ -beautifulsoup4-4.14.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -beautifulsoup4-4.14.3.dist-info/METADATA,sha256=Ac93vA8Xp9FtgOcKXFM8ESfVdztimUfJ3WUpVlhKtsY,3812 -beautifulsoup4-4.14.3.dist-info/RECORD,, -beautifulsoup4-4.14.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -beautifulsoup4-4.14.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 -beautifulsoup4-4.14.3.dist-info/licenses/AUTHORS,sha256=uYkjiRjh_aweRnF8tAW2PpJJeickE68NmJwd9siry28,2201 -beautifulsoup4-4.14.3.dist-info/licenses/LICENSE,sha256=VbTY1LHlvIbRDvrJG3TIe8t3UmsPW57a-LnNKtxzl7I,1441 -bs4/__init__.py,sha256=E7wiVp7oQK0JhdAYxpehZa8drv3W_sJv5oeTFiBfR5o,44386 +beautifulsoup4-4.15.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +beautifulsoup4-4.15.0.dist-info/METADATA,sha256=sviIEDJZ8WB8hfOxFjvhvEn8mUGjJDw7iaIQdb_G1Ps,3812 +beautifulsoup4-4.15.0.dist-info/RECORD,, +beautifulsoup4-4.15.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +beautifulsoup4-4.15.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +beautifulsoup4-4.15.0.dist-info/licenses/AUTHORS,sha256=uYkjiRjh_aweRnF8tAW2PpJJeickE68NmJwd9siry28,2201 +beautifulsoup4-4.15.0.dist-info/licenses/LICENSE,sha256=VbTY1LHlvIbRDvrJG3TIe8t3UmsPW57a-LnNKtxzl7I,1441 +bs4/__init__.py,sha256=kOAH-CqfxbzyKo3zs5W3vFJF5U1lzWj9UcoSpBRxjU4,44562 bs4/_deprecation.py,sha256=niHJCk37APg8KEuFOa57ZXaxLdBmc_2V6uuaJqu7r30,2408 bs4/_typing.py,sha256=zNcx7R1yCTK8WwtumP28hc7CJ3pMyZXj_VAeYaNXMZA,7549 bs4/_warnings.py,sha256=ZuOETgcnEbZgw2N0nnNXn6wvtrn2ut7AF0d98bvkMFc,4711 bs4/builder/__init__.py,sha256=Rl4qjOXvdyyyjayOFqbkgoUoo81IgoyKD-RwWeVK59g,31194 bs4/builder/_html5lib.py,sha256=hL6xUk4_I2i5CMguFoYFlrI26cY4Dut7fOEQrUctHIM,23607 -bs4/builder/_htmlparser.py,sha256=CnULPQV2rm4vLojJABpQ7Xm9diddnEZx2Wcz_VTC1Mg,17445 +bs4/builder/_htmlparser.py,sha256=SrjQvuL8SKOrXyjzNhE1XTBJpGslpE1LK-3B1vm7_r0,19866 bs4/builder/_lxml.py,sha256=ks1e8boA_nOA2oomAhxeudccR6ThbEE-EllFqHRoPLA,18969 bs4/css.py,sha256=_m_l_4SGWHnY620VJ21j_qQH1RX3p91sYVemgKxaLsM,12713 -bs4/dammit.py,sha256=ZJWa9K32X6N2imFHleqUq0ekf592weU1lvULN_WYWYk,57024 +bs4/dammit.py,sha256=0mFwW_-Hklybot_Mg3goTqYYfKfluKJT_WJU4ojbQF4,57485 bs4/diagnose.py,sha256=at98iuxyOrqec4V8iwkTIbNUqBCsq9Lr3fDAQx2129Y,7846 -bs4/element.py,sha256=oXmj7LG_2NpsDK90mq73q0PMK0FjFBIGSeTTJLVwwTc,120237 +bs4/element.py,sha256=oQm8ZK9P1RN7TnOawlS5wdUYwss2OR0h5ZET6VuGJkE,125805 bs4/exceptions.py,sha256=Q9FOadNe8QRvzDMaKSXe2Wtl8JK_oAZW7mbFZBVP_GE,951 -bs4/filter.py,sha256=rw8ZNhTDLEJVCEiSifou5tZR_3zBLeuvAyouY82qU_E,29201 +bs4/filter.py,sha256=d9wtWHkg8cx3bU0BPjLeGgFpHVORzX6HAnftB6rsXqM,29180 bs4/formatter.py,sha256=uBT0k6W8O5kJ9PCuJYjra97yoUqC-dlM9D_v-oRM0r8,10478 bs4/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/libs/beautifulsoup4-4.14.3.dist-info/REQUESTED b/libs/beautifulsoup4-4.15.0.dist-info/REQUESTED similarity index 100% rename from libs/beautifulsoup4-4.14.3.dist-info/REQUESTED rename to libs/beautifulsoup4-4.15.0.dist-info/REQUESTED diff --git a/libs/beautifulsoup4-4.14.3.dist-info/WHEEL b/libs/beautifulsoup4-4.15.0.dist-info/WHEEL similarity index 67% rename from libs/beautifulsoup4-4.14.3.dist-info/WHEEL rename to libs/beautifulsoup4-4.15.0.dist-info/WHEEL index ae8ec1bda..bd8422674 100644 --- a/libs/beautifulsoup4-4.14.3.dist-info/WHEEL +++ b/libs/beautifulsoup4-4.15.0.dist-info/WHEEL @@ -1,4 +1,4 @@ Wheel-Version: 1.0 -Generator: hatchling 1.28.0 +Generator: hatchling 1.30.1 Root-Is-Purelib: true Tag: py3-none-any diff --git a/libs/beautifulsoup4-4.14.3.dist-info/licenses/AUTHORS b/libs/beautifulsoup4-4.15.0.dist-info/licenses/AUTHORS similarity index 100% rename from libs/beautifulsoup4-4.14.3.dist-info/licenses/AUTHORS rename to libs/beautifulsoup4-4.15.0.dist-info/licenses/AUTHORS diff --git a/libs/beautifulsoup4-4.14.3.dist-info/licenses/LICENSE b/libs/beautifulsoup4-4.15.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/beautifulsoup4-4.14.3.dist-info/licenses/LICENSE rename to libs/beautifulsoup4-4.15.0.dist-info/licenses/LICENSE diff --git a/libs/bs4/__init__.py b/libs/bs4/__init__.py index 19b7c0ed0..5b41ee74e 100644 --- a/libs/bs4/__init__.py +++ b/libs/bs4/__init__.py @@ -15,8 +15,8 @@ documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/ """ __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.14.3" -__copyright__ = "Copyright (c) 2004-2025 Leonard Richardson" +__version__ = "4.15.0" +__copyright__ = "Copyright (c) 2004-2026 Leonard Richardson" # Use of this source code is governed by the MIT license. __license__ = "MIT" @@ -534,6 +534,11 @@ class BeautifulSoup(Tag): self.reset() self._feed() + @property + def _is_root(self): + """Yes, a BeautifulSoup object is the root of its parse tree. Used by the _root_object internal property.""" + return True + @classmethod @_deprecated( replaced_by="nothing (private method, will be removed)", version="4.13.0" diff --git a/libs/bs4/builder/_htmlparser.py b/libs/bs4/builder/_htmlparser.py index 165a3d89c..84ef6f1ac 100644 --- a/libs/bs4/builder/_htmlparser.py +++ b/libs/bs4/builder/_htmlparser.py @@ -10,6 +10,7 @@ __all__ = [ ] from html.parser import HTMLParser +import re from typing import ( Any, @@ -137,7 +138,12 @@ class BeautifulSoupHTMLParser(HTMLParser, DetectsXMLParsedAsHTML): # know that this is an empty-element tag, and we want to call # handle_endtag ourselves. self.handle_starttag(tag, attrs, handle_empty_element=False) - self.handle_endtag(tag) + + # Similarly, we set `check_already_closed` when calling + # handle_endtag. Since we know the start event is identical to + # the end event, we don't want handle_endtag() to cross off + # any previous end events for tags of this name. + self.handle_endtag(tag, check_already_closed=False) def handle_starttag( self, @@ -223,6 +229,64 @@ class BeautifulSoupHTMLParser(HTMLParser, DetectsXMLParsedAsHTML): """Handle some textual data that shows up between tags.""" self.soup.handle_data(data) + _DECIMAL_REFERENCE_WITH_FOLLOWING_DATA = re.compile("^([0-9]+)(.*)") + _HEX_REFERENCE_WITH_FOLLOWING_DATA = re.compile("^([0-9a-f]+)(.*)") + + @classmethod + def _dereference_numeric_character_reference(cls, name:str) -> Tuple[str, bool, str]: + """Convert a numeric character reference into an actual character. + + :param name: The number of the character reference, as + obtained by html.parser + + :return: A 3-tuple (dereferenced, replacement_added, + extra_data). `dereferenced` is the dereferenced character + reference, or the empty string if there was no + reference. `replacement_added` is True if the reference + could only be dereferenced by replacing content with U+FFFD + REPLACEMENT CHARACTER. `extra_data` is a portion of data + following the character reference, which was deemed to be + normal data and not part of the reference at all. + """ + dereferenced:str = "" + replacement_added:bool = False + extra_data:str = "" + + base:int = 10 + reg = cls._DECIMAL_REFERENCE_WITH_FOLLOWING_DATA + if name.startswith("x") or name.startswith("X"): + # Hex reference + name = name[1:] + base = 16 + reg = cls._HEX_REFERENCE_WITH_FOLLOWING_DATA + + real_name:Optional[int] = None + try: + real_name = int(name, base) + except ValueError: + # This is either bad data that starts with what looks like + # a numeric character reference, or a real numeric + # reference that wasn't terminated by a semicolon. + # + # The fix to https://bugs.python.org/issue13633 made it + # our responsibility to handle the extra data. + # + # To preserve the old behavior, we extract the numeric + # portion of the incoming "reference" and treat that as a + # numeric reference. All subsequent data will be processed + # as string data. + match = reg.search(name) + if match is not None: + real_name = int(match.groups()[0], base) + extra_data = match.groups()[1] + + if real_name is None: + dereferenced = "" + extra_data = name + else: + dereferenced, replacement_added = UnicodeDammit.numeric_character_reference(real_name) + return dereferenced, replacement_added, extra_data + def handle_charref(self, name: str) -> None: """Handle a numeric character reference by converting it to the corresponding Unicode character and treating it as textual @@ -230,22 +294,13 @@ class BeautifulSoupHTMLParser(HTMLParser, DetectsXMLParsedAsHTML): :param name: Character number, possibly in hexadecimal. """ - # TODO: This was originally a workaround for a bug in - # HTMLParser. (http://bugs.python.org/issue13633) The bug has - # been fixed, but removing this code still makes some - # Beautiful Soup tests fail. This needs investigation. - real_name:int - if name.startswith("x"): - real_name = int(name.lstrip("x"), 16) - elif name.startswith("X"): - real_name = int(name.lstrip("X"), 16) - else: - real_name = int(name) - - data, replacement_added = UnicodeDammit.numeric_character_reference(real_name) + dereferenced, replacement_added, extra_data = self._dereference_numeric_character_reference(name) if replacement_added: self.soup.contains_replacement_characters = True - self.handle_data(data) + if dereferenced is not None: + self.handle_data(dereferenced) + if extra_data is not None: + self.handle_data(extra_data) def handle_entityref(self, name: str) -> None: """Handle a named entity reference by converting it to the diff --git a/libs/bs4/dammit.py b/libs/bs4/dammit.py index 52f0babd3..4746cfc77 100644 --- a/libs/bs4/dammit.py +++ b/libs/bs4/dammit.py @@ -95,60 +95,68 @@ encoding_res[str] = { } -class EntitySubstitution(object): - """The ability to substitute XML or HTML entities for certain characters.""" +class EntitySubstitutionMeta(type): + """Provides lazy access to some data structures and regular + expressions used by EntitySubstitution which have a measurable + startup cost. + """ + # Trigger for + _CLASS_VARIABLES_POPULATED: bool = False - #: A map of named HTML entities to the corresponding Unicode string. - #: - #: :meta hide-value: - HTML_ENTITY_TO_CHARACTER: Dict[str, str] - - #: A map of Unicode strings to the corresponding named HTML entities; - #: the inverse of HTML_ENTITY_TO_CHARACTER. - #: - #: :meta hide-value: - CHARACTER_TO_HTML_ENTITY: Dict[str, str] - - #: A regular expression that matches any character (or, in rare - #: cases, pair of characters) that can be replaced with a named - #: HTML entity. - #: - #: :meta hide-value: - CHARACTER_TO_HTML_ENTITY_RE: Pattern[str] - - #: A very similar regular expression to - #: CHARACTER_TO_HTML_ENTITY_RE, but which also matches unescaped - #: ampersands. This is used by the 'html' formatted to provide - #: backwards-compatibility, even though the HTML5 spec allows most - #: ampersands to go unescaped. - #: - #: :meta hide-value: - CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE: Pattern[str] - - @classmethod - def _populate_class_variables(cls) -> None: - """Initialize variables used by this class to manage the plethora of - HTML5 named entities. - - This function sets the following class variables: - - CHARACTER_TO_HTML_ENTITY - A mapping of Unicode strings like "⦨" to - entity names like "angmsdaa". When a single Unicode string has - multiple entity names, we try to choose the most commonly-used - name. - - HTML_ENTITY_TO_CHARACTER: A mapping of entity names like "angmsdaa" to - Unicode strings like "⦨". - - CHARACTER_TO_HTML_ENTITY_RE: A regular expression matching (almost) any - Unicode string that corresponds to an HTML5 named entity. - - CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE: A very similar - regular expression to CHARACTER_TO_HTML_ENTITY_RE, but which - also matches unescaped ampersands. This is used by the 'html' - formatted to provide backwards-compatibility, even though the HTML5 - spec allows most ampersands to go unescaped. + @property + def HTML_ENTITY_TO_CHARACTER(self) -> Dict[str, str]: + """A mapping of entity names like "angmsdaa" to Unicode + strings like "⦨". """ + if not self._CLASS_VARIABLES_POPULATED: + self._populate_class_variables() + return self._HTML_ENTITY_TO_CHARACTER + _HTML_ENTITY_TO_CHARACTER: Dict[str, str] + + @property + def CHARACTER_TO_HTML_ENTITY(self) -> Dict[str, str]: + """A mapping of Unicode strings like "⦨" to entity names like + "angmsdaa". When a single Unicode string has multiple entity + names, we try to choose the most commonly-used name. + """ + if not self._CLASS_VARIABLES_POPULATED: + self._populate_class_variables() + return self._CHARACTER_TO_HTML_ENTITY + _CHARACTER_TO_HTML_ENTITY: Dict[str, str] + + @property + def CHARACTER_TO_HTML_ENTITY_RE(self) -> Pattern[str]: + """A regular expression matching (almost) any Unicode string + that corresponds to an HTML5 named entity. + """ + + if not self._CLASS_VARIABLES_POPULATED: + self._populate_class_variables() + return self._CHARACTER_TO_HTML_ENTITY_RE + _CHARACTER_TO_HTML_ENTITY_RE: Pattern[str] + + @property + def CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE(self) -> Pattern[str]: + """A very similar regular expression to + CHARACTER_TO_HTML_ENTITY_RE, but which also matches unescaped + ampersands. This is used by the 'html' formatter to provide + backwards-compatibility, even though the HTML5 spec allows + most ampersands to go unescaped. + """ + if not self._CLASS_VARIABLES_POPULATED: + self._populate_class_variables() + return self._CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE + _CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE: Pattern[str] + + def _populate_class_variables(self) -> None: + """Initialize variables used by EntitySubstitution to manage the plethora of + HTML and HTML5 named entities. + + This method populates the class variables necessary to make + the properties defined in the metaclass work. + """ + if self._CLASS_VARIABLES_POPULATED: + return unicode_to_name = {} name_to_unicode = {} @@ -250,12 +258,16 @@ class EntitySubstitution(object): character = chr(codepoint) unicode_to_name[character] = name - cls.CHARACTER_TO_HTML_ENTITY = unicode_to_name - cls.HTML_ENTITY_TO_CHARACTER = name_to_unicode - cls.CHARACTER_TO_HTML_ENTITY_RE = re.compile(re_definition) - cls.CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE = re.compile( + self._CHARACTER_TO_HTML_ENTITY = unicode_to_name + self._HTML_ENTITY_TO_CHARACTER = name_to_unicode + self._CHARACTER_TO_HTML_ENTITY_RE = re.compile(re_definition) + self._CHARACTER_TO_HTML_ENTITY_WITH_AMPERSAND_RE = re.compile( re_definition_with_ampersand ) + self._CLASS_VARIABLES_POPULATED = True + +class EntitySubstitution(metaclass=EntitySubstitutionMeta): + """The ability to substitute XML or HTML entities for certain characters.""" #: A map of Unicode strings to the corresponding named XML entities. #: @@ -479,9 +491,6 @@ class EntitySubstitution(object): return s -EntitySubstitution._populate_class_variables() - - class EncodingDetector: """This class is capable of guessing a number of possible encodings for a bytestring. diff --git a/libs/bs4/element.py b/libs/bs4/element.py index dd07d8ebc..11c2b7e08 100644 --- a/libs/bs4/element.py +++ b/libs/bs4/element.py @@ -3,6 +3,7 @@ from __future__ import annotations # Use of this source code is governed by the MIT license. __license__ = "MIT" +import inspect import re import warnings @@ -55,28 +56,31 @@ if TYPE_CHECKING: ) from bs4._typing import ( _AtMostOneElement, - _AtMostOneTag, _AtMostOneNavigableString, + _AtMostOneTag, _AttributeValue, _AttributeValues, _Encoding, _InsertableElement, _OneElement, _QueryResults, + _RawAttributeValue, + _RawAttributeValues, _RawOrProcessedAttributeValues, - _StrainableElement, - _StrainableAttribute, - _StrainableAttributes, - _StrainableString, _SomeNavigableStrings, _SomeTags, + _StrainableAttribute, + _StrainableAttributes, + _StrainableElement, + _StrainableString, ) _OneOrMoreStringTypes: TypeAlias = Union[ Type["NavigableString"], Iterable[Type["NavigableString"]] ] -_FindMethodName: TypeAlias = Optional[Union["_StrainableElement", "ElementFilter"]] +_FindMethodName: TypeAlias = Union["_StrainableElement", "ElementFilter"] +_OptionalFindMethodName: TypeAlias = Optional[_FindMethodName] # Deprecated module-level attributes. # See https://peps.python.org/pep-0562/ @@ -553,7 +557,10 @@ class PageElement(object): return separator.join([s for s in self._all_strings(strip, types=types)]) getText = get_text - text = property(get_text) + + @property + def text(self) -> str: + return self.get_text() def replace_with(self, *args: _InsertableElement) -> Self: """Replace this `PageElement` with one or more other elements, @@ -751,31 +758,72 @@ class PageElement(object): return results - # For the suppression of this pyright warning, see discussion here: - # https://github.com/microsoft/pyright/issues/10929 + def new_tag( + self, + name: str, + namespace: Optional[str] = None, + nsprefix: Optional[str] = None, + attrs: Optional[_RawAttributeValues] = None, + sourceline: Optional[int] = None, + sourcepos: Optional[int] = None, + string: Optional[str] = None, + **kwattrs: _RawAttributeValue, + ) -> Tag: + """Create a new Tag associated with the same BeautifulSoup object as this PageElement is.""" + root = self._root_object + if root is None: + raise ValueError("Cannot call new_tag on a PageElement not contained in a BeautifulSoup object") + return root.new_tag(name, namespace, nsprefix, attrs, sourceline, sourcepos, string, **kwattrs) + + def new_string(self, s: str, subclass: Optional[Type[NavigableString]] = None + ) -> NavigableString: + """Create a new NavigableString associated with the same BeautifulSoup object as this PageElement is.""" + root = self._root_object + if root is None: + raise ValueError("Cannot call new_string on a PageElement not contained in a BeautifulSoup object") + return root.new_string(s, subclass) + + @property + def _root_object(self) -> Optional[BeautifulSoup]: + """Find the BeautifulSoup object used to create this PageElement, assuming it's still attached.""" + parent:Optional[Tag] = self.parent + while parent is not None and not parent._is_root: + parent = parent.parent + if parent is None: + return parent + return cast('BeautifulSoup', parent) + + @property + def _is_root(self) -> bool: + """No, this object is not the root of its parse tree; only a BeautifulSoup object can be that.""" + return False + + # No name or attrs + string -> string @overload - def find_next( # pyright: ignore [reportOverlappingOverload] + def find_next( self, - name: _FindMethodName = None, + name: None = None, + attrs: None = None, + *, + string: _StrainableString, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneNavigableString: + ... + + # No string -> tag + @overload + def find_next( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: None=None, **kwargs: _StrainableAttribute, ) -> _AtMostOneTag: ... - @overload - def find_next( - self, - name: None=None, - attrs: None=None, - string: _StrainableString="", - **kwargs: _StrainableAttribute, - ) -> _AtMostOneNavigableString: - ... - def find_next( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, **kwargs: _StrainableAttribute, @@ -795,37 +843,37 @@ class PageElement(object): findNext = _deprecated_function_alias("findNext", "find_next", "4.0.0") - @overload - def find_all_next( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - string: None = None, - limit: Optional[int] = None, - _stacklevel: int = 2, - **kwargs: _StrainableAttribute, - ) -> _SomeTags: - ... - + # No name or attrs + string -> strings @overload def find_all_next( self, name: None = None, attrs: None = None, - string: _StrainableString = "", + *, + string: _StrainableString, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> _SomeNavigableStrings: ... + # No string -> tags + @overload def find_all_next( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + string: None = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + def find_all_next( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> Union[_SomeTags,_SomeNavigableStrings,_QueryResults]: """Find all `PageElement` objects that match the given criteria and @@ -838,7 +886,6 @@ class PageElement(object): :param attrs: Additional filters on attribute values. :param string: A filter for a NavigableString with specific text. :param limit: Stop looking after finding this many results. - :param _stacklevel: Used internally to improve warning messages. :kwargs: Additional filters on attribute values. """ return self._find_all( @@ -847,35 +894,37 @@ class PageElement(object): string, limit, self.next_elements, - _stacklevel=_stacklevel + 1, **kwargs, ) findAllNext = _deprecated_function_alias("findAllNext", "find_all_next", "4.0.0") - @overload - def find_next_sibling( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - string: None=None, - **kwargs: _StrainableAttribute, - ) -> _AtMostOneTag: - ... - + # No name or attrs + string -> strings @overload def find_next_sibling( - self, - name: None=None, - attrs: None=None, - string: _StrainableString="", - **kwargs: _StrainableAttribute, + self, + name: None = None, + attrs: None = None, + *, + string: _StrainableString, + **kwargs: _StrainableAttribute, ) -> _AtMostOneNavigableString: ... + # No string -> tags + @overload + def find_next_sibling( + self, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + string: None = None, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneTag: + ... + def find_next_sibling( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, **kwargs: _StrainableAttribute, @@ -897,37 +946,37 @@ class PageElement(object): "findNextSibling", "find_next_sibling", "4.0.0" ) - @overload - def find_next_siblings( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - string: None = None, - limit: Optional[int] = None, - _stacklevel: int = 2, - **kwargs: _StrainableAttribute, - ) -> _SomeTags: - ... - + # No name or attrs + string -> strings @overload def find_next_siblings( self, name: None = None, attrs: None = None, - string: _StrainableString = "", + *, + string: _StrainableString, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> _SomeNavigableStrings: ... + # No string -> tags + @overload def find_next_siblings( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + string: None = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + def find_next_siblings( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> Union[_SomeTags,_SomeNavigableStrings,_QueryResults]: """Find all siblings of this `PageElement` that match the given criteria @@ -940,7 +989,6 @@ class PageElement(object): :param attrs: Additional filters on attribute values. :param string: A filter for a `NavigableString` with specific text. :param limit: Stop looking after finding this many results. - :param _stacklevel: Used internally to improve warning messages. :kwargs: Additional filters on attribute values. """ return self._find_all( @@ -949,7 +997,6 @@ class PageElement(object): string, limit, self.next_siblings, - _stacklevel=_stacklevel + 1, **kwargs, ) @@ -960,29 +1007,32 @@ class PageElement(object): "fetchNextSiblings", "find_next_siblings", "3.0.0" ) + # No name or attrs + string -> string @overload - def find_previous( # pyright: ignore [reportOverlappingOverload] + def find_previous( self, - name: _FindMethodName = None, + name: None = None, + attrs: None = None, + *, + string: _StrainableString, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneNavigableString: + ... + + # No string -> tag + @overload + def find_previous( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: None=None, **kwargs: _StrainableAttribute, ) -> _AtMostOneTag: ... - @overload - def find_previous( - self, - name: None=None, - attrs: None=None, - string: _StrainableString="", - **kwargs: _StrainableAttribute, - ) -> _AtMostOneNavigableString: - ... - def find_previous( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, **kwargs: _StrainableAttribute, @@ -1002,37 +1052,37 @@ class PageElement(object): findPrevious = _deprecated_function_alias("findPrevious", "find_previous", "3.0.0") - @overload - def find_all_previous( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - string: None = None, - limit: Optional[int] = None, - _stacklevel: int = 2, - **kwargs: _StrainableAttribute, - ) -> _SomeTags: - ... - + # No name or attrs + string -> strings @overload def find_all_previous( self, name: None = None, attrs: None = None, - string: _StrainableString = "", + *, + string: _StrainableString, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> _SomeNavigableStrings: ... + # No string -> tags + @overload def find_all_previous( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + string: None = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + def find_all_previous( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> Union[_SomeTags,_SomeNavigableStrings,_QueryResults]: """Look backwards in the document from this `PageElement` and find all @@ -1045,7 +1095,6 @@ class PageElement(object): :param attrs: Additional filters on attribute values. :param string: A filter for a `NavigableString` with specific text. :param limit: Stop looking after finding this many results. - :param _stacklevel: Used internally to improve warning messages. :kwargs: Additional filters on attribute values. """ return self._find_all( @@ -1054,7 +1103,6 @@ class PageElement(object): string, limit, self.previous_elements, - _stacklevel=_stacklevel + 1, **kwargs, ) @@ -1065,29 +1113,32 @@ class PageElement(object): "fetchAllPrevious", "find_all_previous", "3.0.0" ) - @overload - def find_previous_sibling( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - string: None=None, - **kwargs: _StrainableAttribute, - ) -> _AtMostOneTag: - ... - + # No name or attrs + string -> string @overload def find_previous_sibling( - self, - name: None=None, - attrs: None=None, - string: _StrainableString="", - **kwargs: _StrainableAttribute, + self, + name: None = None, + attrs: None = None, + *, + string: _StrainableString, + **kwargs: _StrainableAttribute, ) -> _AtMostOneNavigableString: ... + # No string -> tag + @overload + def find_previous_sibling( + self, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + string: None = None, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneTag: + ... + def find_previous_sibling( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, **kwargs: _StrainableAttribute, @@ -1111,37 +1162,37 @@ class PageElement(object): "findPreviousSibling", "find_previous_sibling", "4.0.0" ) - @overload - def find_previous_siblings( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - string: None = None, - limit: Optional[int] = None, - _stacklevel: int = 2, - **kwargs: _StrainableAttribute, - ) -> _SomeTags: - ... - + # No name or attrs + string -> strings @overload def find_previous_siblings( self, name: None = None, attrs: None = None, - string: _StrainableString = "", + *, + string: _StrainableString, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> _SomeNavigableStrings: ... + # No string -> tags + @overload def find_previous_siblings( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + string: None = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + def find_previous_siblings( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, string: Optional[_StrainableString] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> Union[_SomeTags,_SomeNavigableStrings,_QueryResults]: """Returns all siblings to this PageElement that match the @@ -1154,7 +1205,6 @@ class PageElement(object): :param attrs: Additional filters on attribute values. :param string: A filter for a NavigableString with specific text. :param limit: Stop looking after finding this many results. - :param _stacklevel: Used internally to improve warning messages. :kwargs: Additional filters on attribute values. """ return self._find_all( @@ -1163,7 +1213,6 @@ class PageElement(object): string, limit, self.previous_siblings, - _stacklevel=_stacklevel + 1, **kwargs, ) @@ -1176,7 +1225,7 @@ class PageElement(object): def find_parent( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, **kwargs: _StrainableAttribute, ) -> _AtMostOneTag: @@ -1196,7 +1245,7 @@ class PageElement(object): # set of arguments. r = None results = self.find_parents( - name, attrs, 1, _stacklevel=3, **kwargs + name, attrs, 1, **kwargs ) if results: r = results[0] @@ -1206,10 +1255,9 @@ class PageElement(object): def find_parents( self, - name: _FindMethodName = None, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> _SomeTags: """Find all parents of this `PageElement` that match the given criteria. @@ -1220,14 +1268,13 @@ class PageElement(object): :param name: A filter on tag name. :param attrs: Additional filters on attribute values. :param limit: Stop looking after finding this many results. - :param _stacklevel: Used internally to improve warning messages. :kwargs: Additional filters on attribute values. """ iterator = self.parents # Only Tags can have children, so this ResultSet will contain # nothing but Tags. return cast(ResultSet[Tag], self._find_all( - name, attrs, None, limit, iterator, _stacklevel=_stacklevel + 1, **kwargs + name, attrs, None, limit, iterator, **kwargs )) findParents = _deprecated_function_alias("findParents", "find_parents", "4.0.0") @@ -1252,25 +1299,49 @@ class PageElement(object): # as callback types." - So, not sure how to get more # specific here. method: Callable, - name: _FindMethodName, + name: _OptionalFindMethodName, attrs: Optional[_StrainableAttributes], string: Optional[_StrainableString], **kwargs: _StrainableAttribute, ) -> _AtMostOneElement: r: _AtMostOneElement = None - results: _QueryResults = method(name, attrs, string, 1, _stacklevel=4, **kwargs) + results: _QueryResults = method(name, attrs, string, 1, **kwargs) if results: r = results[0] return r + @property + def _warning_stack_level(self) -> int: + """Find the appropriate stack level to use when issuing a warning relating to one of the find* methods.""" + # The find* methods call each other, which makes it + # difficult to track how deep we are in the stack + # vis-a-vis the caller's entry point into the bs4.element + # module. However, we know that all of the find* methods + # are in bs4.element, and there's no code in this module + # that triggers the warnings we need to issue. + # + # (There is _test_ code that triggers the warnings, but that's + # in bs4.tests.) + # + # Therefore we can go up the stack until we leave the + # bs4.element module, and use the distance between here and + # there as the stacklevel. + stacklevel = 0 + for frameinfo in inspect.stack(context=0): + if (frameinfo.frame is not None + and frameinfo.frame.f_globals is not None + and frameinfo.frame.f_globals.get('__name__', '') != "bs4.element"): + break + stacklevel += 1 + return stacklevel + def _find_all( self, - name: _FindMethodName, + name: _OptionalFindMethodName, attrs: Optional[_StrainableAttributes], string: Optional[_StrainableString], limit: Optional[int], generator: Iterator[PageElement], - _stacklevel: int = 3, **kwargs: _StrainableAttribute, ) -> _QueryResults: """Iterates over a generator looking for things that match.""" @@ -1280,7 +1351,7 @@ class PageElement(object): warnings.warn( "The 'text' argument to find()-type methods is deprecated. Use 'string' instead.", DeprecationWarning, - stacklevel=_stacklevel, + stacklevel=self._warning_stack_level, ) if "_class" in kwargs: @@ -1291,7 +1362,7 @@ class PageElement(object): autocorrect="class_", ), AttributeResemblesVariableWarning, - stacklevel=_stacklevel, + stacklevel=self._warning_stack_level, ) from bs4.filter import ElementFilter @@ -2238,15 +2309,22 @@ class Tag(PageElement): ": :meta private:" return self.unwrap() - def append(self, tag: _InsertableElement) -> PageElement: - """ - Appends the given `PageElement` to the contents of this `Tag`. + def append(self, tag: _InsertableElement) -> PageElement|List[PageElement]: + """Appends the given `PageElement` to the contents of this `Tag`. - :param tag: A PageElement. + :param tag: A PageElement. If this is another BeautifulSoup + object, all of its contents will be inserted into this + `Tag`, since one BeautifulSoup object can't contain another + one. - :return The newly appended PageElement. + :return: The object that was just appended, or (if `tag` was a BeautifulSoup + object) all such objects. """ - return self.insert(len(self.contents), tag)[0] + inserted = self.insert(len(self.contents), tag) + if isinstance(tag, Tag) and tag.name == "[document]": # TODO: can't reference BeautifulSoup class in this module + return inserted + else: + return inserted[0] def extend(self, tags: Union[Iterable[_InsertableElement], Tag]) -> List[PageElement]: """Appends one or more objects to the contents of this @@ -2281,7 +2359,12 @@ class Tag(PageElement): results: List[PageElement] = [] for tag in tag_list: - results.append(self.append(tag)) + appended = self.append(tag) + if isinstance(appended, list): + # This can happen if you pass in a mixture of Tag and BeautifulSoup objects. + results.extend(appended) + else: + results.append(appended) return results @@ -2424,17 +2507,20 @@ class Tag(PageElement): "Deleting tag[key] deletes all 'key' attributes for the tag." self.attrs.pop(key, None) + # Since Tag.__call__ is effectively the same as PageElement.find_all, see find_all for notes + # on these overloads. + @overload - def __call__( # pyright: ignore [reportOverlappingOverload] + def __call__( self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, + name: None = None, + attrs: None = None, recursive: bool = True, - string: None = None, + *, + string: _StrainableString, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, - ) -> _SomeTags: + ) -> _SomeNavigableStrings: ... @overload @@ -2443,44 +2529,51 @@ class Tag(PageElement): name: None = None, attrs: None = None, recursive: bool = True, - string: _StrainableString = "", + string: None = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, - ) -> _SomeNavigableStrings: + ) -> _SomeTags: ... + @overload def __call__( self, - name: _FindMethodName = None, + name: None, + attrs: _StrainableAttributes, + recursive: bool = True, + string: None = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + @overload + def __call__( + self, + name: _FindMethodName, + attrs: Optional[_StrainableAttributes] = None, + recursive: bool = True, + string: Optional[_StrainableString] = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + def __call__( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, recursive: bool = True, string: Optional[_StrainableString] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> Union[_SomeTags,_SomeNavigableStrings,_QueryResults]: """Calling a Tag like a function is the same as calling its - find_all() method. Eg. tag('a') returns a list of all the A tags - found within this tag.""" - if string is not None and (name is not None or attrs is not None or kwargs): - # TODO: Using the @overload decorator to express the three ways you - # could get into this path is way too much code for a rarely(?) used - # feature. - return cast(ResultSet[Tag], self.find_all(name, attrs, recursive, string, limit, _stacklevel, **kwargs)) #type: ignore + find_all() method. - if string is None: - # If string is None, we're searching for tags. - tags:ResultSet[Tag] = self.find_all( - name, attrs, recursive, None, limit, _stacklevel, **kwargs - ) - return tags - - # Otherwise, we're searching for strings. - strings:ResultSet[NavigableString] = self.find_all( - None, None, recursive, string, limit, _stacklevel, **kwargs - ) - return strings + Eg. tag('a') returns a list of all the A tags found within this tag. + """ + return self._find_all(name, attrs, string, limit, self._generator_for_recursive(recursive), **kwargs) def __getattr__(self, subtag: str) -> Optional[Tag]: """Calling tag.subtag is the same as calling tag.find(name="subtag")""" @@ -2932,31 +3025,84 @@ class Tag(PageElement): return self.encode_contents(indent_level=indentLevel, encoding=encoding) # Soup methods + # + # People who call these methods in a type-safe environment + # basically want to know whether the call is going to return + # NavigableStrings or Tags. It's always one or the other, never + # both, but spelling it out requires a number of overloads for + # each method. + # + # If I had it to do over again I'd design this API differently (it + # would look more like ElementFilter), but that's life. + # + # The overloads all look for a clue in the input which restricts + # the method to returning either only strings or only tags. Only + # the most common cases are covered. + + # e.g. find(string="foo") + # -> string information but no tag information + # -> string + @overload + def find( + self, + name: None = None, + attrs: None = None, + recursive: bool = True, + *, + string: _StrainableString, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneNavigableString: + ... + + # e.g. find() -> default behavior -> tag + # find(attr="value") -> only tags have attrs -> tag @overload def find( self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, + name: None = None, + attrs: None = None, recursive: bool = True, - string: None=None, + string: None = None, **kwargs: _StrainableAttribute, ) -> _AtMostOneTag: ... + # e.g. find(attrs=dict(attr="value")) + # -> only tags have attrs + # -> tag @overload def find( - self, - name: None=None, - attrs: None=None, - recursive: bool = True, - string: _StrainableString="", - ) -> _AtMostOneNavigableString: + self, + name: None, + attrs: _StrainableAttributes, + recursive: bool = True, + string: Optional[_StrainableString] = None, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneTag: ... + # e.g. find(name="a")) -> only tags have names -> tag + # + # The confusing and controversial case of find(name="a", string="foo") + # also hits this overload. + @overload def find( self, - name: _FindMethodName = None, + name: _FindMethodName, + attrs: Optional[_StrainableAttributes] = None, + recursive: bool = True, + string: Optional[_StrainableString] = None, + **kwargs: _StrainableAttribute, + ) -> _AtMostOneTag: + ... + + # Some lesser-used cases are not covered by the overrides. Those + # cases will hit this method directly and return a very general + # type which will need to be cast after the call. + def find( + self, + name: _OptionalFindMethodName = None, attrs: Optional[_StrainableAttributes] = None, recursive: bool = True, string: Optional[_StrainableString] = None, @@ -2976,61 +3122,94 @@ class Tag(PageElement): :param string: A filter on the `Tag.string` attribute. :kwargs: Additional filters on attribute values. """ - if string is not None and (name is not None or attrs is not None or kwargs): - # TODO: Using the @overload decorator to express the three ways you - # could get into this path is way too much code for a rarely(?) used - # feature. - elements = self.find_all(name, attrs, recursive, string, 1, _stacklevel=3, **kwargs) # type:ignore - if elements: - return cast(Tag, elements[0]) - elif string is None: - tags = self.find_all(name, attrs, recursive, None, 1, _stacklevel=3, **kwargs) - if tags: - return cast(Tag, tags[0]) - else: - strings = self.find_all(None, None, recursive, string, 1, _stacklevel=3, **kwargs) - if strings: - return cast(NavigableString, strings[0]) + tags = self._find_all(name, attrs, string, 1, self._generator_for_recursive(recursive), **kwargs) + if tags: + return tags[0] return None findChild = _deprecated_function_alias("findChild", "find", "3.0.0") - @overload - def find_all( # pyright: ignore [reportOverlappingOverload] - self, - name: _FindMethodName = None, - attrs: Optional[_StrainableAttributes] = None, - recursive: bool = True, - string: None = None, - limit: Optional[int] = None, - _stacklevel: int = 2, - **kwargs: _StrainableAttribute, - ) -> _SomeTags: - ... - + # e.g. find_all(string="foo") + # -> string information but no tag information + # -> strings + # + # Also covers unlikely cases like find_all(name=None, string="foo") + # + # "To mark parameters as keyword-only, indicating the parameters + # must be passed by keyword argument, place an * in the arguments + # list just before the first keyword-only parameter." + # + # --https://peps.python.org/pep-0570/#keyword-only-arguments @overload def find_all( self, name: None = None, attrs: None = None, recursive: bool = True, - string: _StrainableString = "", + *, + string: _StrainableString, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, ) -> _SomeNavigableStrings: ... + # e.g. find_all() -> default behavior -> tags + # find_all(attr="value") -> only tags have attrs -> tags + @overload def find_all( self, - name: _FindMethodName = None, + name: None = None, + attrs: None = None, + recursive: bool = True, + string: None = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + # e.g. find_all(attrs=dict(attr="value")) + # -> only tags have attrs + # -> tags + @overload + def find_all( + self, + name: None, + attrs: _StrainableAttributes, + recursive: bool = True, + string: Optional[_StrainableString] = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> _SomeTags: + ... + + # e.g. find_all(name="a")) -> only tags have names -> tags + # + # The confusing and controversial case of find_all(name="a", string="foo") + # also hits this overload. + @overload + def find_all( + self, + name: _FindMethodName, attrs: Optional[_StrainableAttributes] = None, recursive: bool = True, string: Optional[_StrainableString] = None, limit: Optional[int] = None, - _stacklevel: int = 2, **kwargs: _StrainableAttribute, - ) -> Union[_SomeTags,_SomeNavigableStrings,_QueryResults]: + ) -> _SomeTags: + ... + + # Without the clues above, we don't know whether the method will + # return strings or tags. However every common case will trigger one + # of the overloads and give us the clue we need. + def find_all( + self, + name: _OptionalFindMethodName = None, + attrs: Optional[_StrainableAttributes] = None, + recursive: bool = True, + string: Optional[_StrainableString] = None, + limit: Optional[int] = None, + **kwargs: _StrainableAttribute, + ) -> Union[_SomeTags,_SomeNavigableStrings]: """Look in the children of this `PageElement` and find all `PageElement` objects that match the given criteria. @@ -3043,13 +3222,9 @@ class Tag(PageElement): recursive search of this PageElement's children. Otherwise, only the direct children will be considered. :param limit: Stop looking after finding this many results. - :param _stacklevel: Used internally to improve warning messages. :kwargs: Additional filters on attribute values. """ - generator = self.descendants - if not recursive: - generator = self.children - _stacklevel += 1 + generator = self._generator_for_recursive(recursive) if string is not None and (name is not None or attrs is not None or kwargs): # TODO: Using the @overload decorator to express the three ways you @@ -3057,18 +3232,17 @@ class Tag(PageElement): # feature. return cast(ResultSet[Tag], self._find_all(name, attrs, string, limit, generator, - _stacklevel=_stacklevel, **kwargs) - ) + **kwargs)) if string is None: # If string is None, we're searching for tags. return cast(ResultSet[Tag], self._find_all( - name, attrs, None, limit, generator, _stacklevel=_stacklevel, **kwargs + name, attrs, None, limit, generator, **kwargs )) # Otherwise, we're searching for strings. return cast(ResultSet[NavigableString], self._find_all( - None, None, string, limit, generator, _stacklevel=_stacklevel, **kwargs + None, None, string, limit, generator, **kwargs )) findAll = _deprecated_function_alias("findAll", "find_all", "4.0.0") @@ -3105,6 +3279,16 @@ class Tag(PageElement): yield current current = successor + def _generator_for_recursive(self, recursive:bool) -> Iterator[PageElement]: + """Helper method to process the boolean `recursive` argument + for find* methods. + + :return: the appropriate generator + """ + if recursive: + return self.descendants + return self.children + # CSS selector code def select_one( self, selector: str, namespaces: Optional[Dict[str, str]] = None, **kwargs: Any diff --git a/libs/bs4/filter.py b/libs/bs4/filter.py index 8dbfa0034..0eea0b15b 100644 --- a/libs/bs4/filter.py +++ b/libs/bs4/filter.py @@ -144,7 +144,7 @@ class ElementFilter(object): break if i: if self.match(i, _known_rules=True): - yield cast("_OneElement", i) + yield i def find(self, generator: Iterator[PageElement]) -> _AtMostOneElement: """A lower-level equivalent of :py:meth:`Tag.find`. diff --git a/libs/certifi-2026.2.25.dist-info/RECORD b/libs/certifi-2026.2.25.dist-info/RECORD deleted file mode 100644 index 7f1c4aa3d..000000000 --- a/libs/certifi-2026.2.25.dist-info/RECORD +++ /dev/null @@ -1,12 +0,0 @@ -certifi-2026.2.25.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -certifi-2026.2.25.dist-info/METADATA,sha256=4NMuGXdg_hBiRA3paKVXYcDmE3VXEBWxTvCL2xlDyPU,2474 -certifi-2026.2.25.dist-info/RECORD,, -certifi-2026.2.25.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -certifi-2026.2.25.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -certifi-2026.2.25.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 -certifi-2026.2.25.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 -certifi/__init__.py,sha256=c9eaYufv1pSLl0Q8QNcMiMLLH4WquDcxdPyKjmI4opY,94 -certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 -certifi/cacert.pem,sha256=_JFloSQDJj5-v72te-ej6sD6XTJdPHBGXyjTaQByyig,272441 -certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 -certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/libs/certifi-2026.2.25.dist-info/INSTALLER b/libs/certifi-2026.6.17.dist-info/INSTALLER similarity index 100% rename from libs/certifi-2026.2.25.dist-info/INSTALLER rename to libs/certifi-2026.6.17.dist-info/INSTALLER diff --git a/libs/certifi-2026.2.25.dist-info/METADATA b/libs/certifi-2026.6.17.dist-info/METADATA similarity index 96% rename from libs/certifi-2026.2.25.dist-info/METADATA rename to libs/certifi-2026.6.17.dist-info/METADATA index 73df8c84d..0d4f10149 100644 --- a/libs/certifi-2026.2.25.dist-info/METADATA +++ b/libs/certifi-2026.6.17.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: certifi -Version: 2026.2.25 +Version: 2026.6.17 Summary: Python package for providing Mozilla's CA Bundle. Home-page: https://github.com/certifi/python-certifi Author: Kenneth Reitz @@ -67,7 +67,7 @@ Or from the command line:: Enjoy! -.. _`Requests`: https://requests.readthedocs.io/en/master/ +.. _`Requests`: https://requests.readthedocs.io/en/latest/ Addition/Removal of Certificates -------------------------------- diff --git a/libs/certifi-2026.6.17.dist-info/RECORD b/libs/certifi-2026.6.17.dist-info/RECORD new file mode 100644 index 000000000..6d5bfb857 --- /dev/null +++ b/libs/certifi-2026.6.17.dist-info/RECORD @@ -0,0 +1,12 @@ +certifi-2026.6.17.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 +certifi-2026.6.17.dist-info/RECORD,, +certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi-2026.6.17.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/libs/certifi-2026.2.25.dist-info/REQUESTED b/libs/certifi-2026.6.17.dist-info/REQUESTED similarity index 100% rename from libs/certifi-2026.2.25.dist-info/REQUESTED rename to libs/certifi-2026.6.17.dist-info/REQUESTED diff --git a/libs/certifi-2026.2.25.dist-info/WHEEL b/libs/certifi-2026.6.17.dist-info/WHEEL similarity index 100% rename from libs/certifi-2026.2.25.dist-info/WHEEL rename to libs/certifi-2026.6.17.dist-info/WHEEL diff --git a/libs/certifi-2026.2.25.dist-info/licenses/LICENSE b/libs/certifi-2026.6.17.dist-info/licenses/LICENSE similarity index 100% rename from libs/certifi-2026.2.25.dist-info/licenses/LICENSE rename to libs/certifi-2026.6.17.dist-info/licenses/LICENSE diff --git a/libs/certifi-2026.2.25.dist-info/top_level.txt b/libs/certifi-2026.6.17.dist-info/top_level.txt similarity index 100% rename from libs/certifi-2026.2.25.dist-info/top_level.txt rename to libs/certifi-2026.6.17.dist-info/top_level.txt diff --git a/libs/certifi/__init__.py b/libs/certifi/__init__.py index 16c0c7c26..ed9a74b7a 100644 --- a/libs/certifi/__init__.py +++ b/libs/certifi/__init__.py @@ -1,4 +1,4 @@ from .core import contents, where __all__ = ["contents", "where"] -__version__ = "2026.02.25" +__version__ = "2026.06.17" diff --git a/libs/certifi/cacert.pem b/libs/certifi/cacert.pem index 5ec1afe02..1c2dbfeb6 100644 --- a/libs/certifi/cacert.pem +++ b/libs/certifi/cacert.pem @@ -1,316 +1,4 @@ -# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited -# Label: "QuoVadis Root CA 2" -# Serial: 1289 -# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b -# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7 -# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86 ------BEGIN CERTIFICATE----- -MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x -GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv -b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV -BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W -YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa -GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg -Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J -WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB -rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp -+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 -ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i -Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz -PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og -/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH -oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI -yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud -EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 -A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL -MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT -ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f -BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn -g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl -fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K -WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha -B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc -hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR -TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD -mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z -ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y -4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza -8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 3" -# Serial: 1478 -# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf -# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85 -# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35 ------BEGIN CERTIFICATE----- -MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x -GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv -b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV -BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W -YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM -V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB -4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr -H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd -8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv -vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT -mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe -btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc -T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt -WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ -c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A -4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD -VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG -CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 -aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 -aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu -dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw -czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G -A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC -TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg -Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 -7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem -d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd -+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B -4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN -t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x -DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 -k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s -zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j -Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT -mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK -4SVhM7JZG+Ju1zdXtg2pEto= ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root CA" -# Serial: 17154717934120587862167794914071425081 -# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 -# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 -# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c -JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP -mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ -wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 -VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ -AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB -AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun -pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC -dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf -fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm -NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx -H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root CA" -# Serial: 10944719598952040374951832963794454346 -# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e -# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 -# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB -CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 -nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt -43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P -T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 -gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR -TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw -DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr -hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg -06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF -PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls -YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- - -# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert High Assurance EV Root CA" -# Serial: 3553400076410547919724730734378100087 -# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a -# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 -# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j -ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 -LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug -RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm -+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW -PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM -xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB -Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 -hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg -EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA -FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec -nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z -eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF -hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 -Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep -+OkuE6N36B9K ------END CERTIFICATE----- - -# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG -# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG -# Label: "SwissSign Gold CA - G2" -# Serial: 13492815561806991280 -# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93 -# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61 -# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95 ------BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV -BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln -biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF -MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT -d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 -76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ -bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c -6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE -emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd -MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt -MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y -MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y -FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi -aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM -gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB -qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 -lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn -8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov -L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 -45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO -UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 -O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC -bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv -GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a -77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC -hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 -92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp -Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w -ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt -Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ ------END CERTIFICATE----- - -# Issuer: CN=SecureTrust CA O=SecureTrust Corporation -# Subject: CN=SecureTrust CA O=SecureTrust Corporation -# Label: "SecureTrust CA" -# Serial: 17199774589125277788362757014266862032 -# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1 -# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11 -# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73 ------BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz -MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv -cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz -Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO -0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao -wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj -7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS -8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT -BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg -JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 -6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ -3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm -D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS -CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE----- - -# Issuer: CN=Secure Global CA O=SecureTrust Corporation -# Subject: CN=Secure Global CA O=SecureTrust Corporation -# Label: "Secure Global CA" -# Serial: 9751836167731051554232119481456978597 -# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de -# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b -# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69 ------BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx -MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg -Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ -iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa -/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ -jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI -HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 -sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w -gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw -KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG -AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L -URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO -H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm -I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY -iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE----- - -# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO Certification Authority O=COMODO CA Limited -# Label: "COMODO Certification Authority" -# Serial: 104350513648249232941998508985834464573 -# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 -# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b -# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 ------BEGIN CERTIFICATE----- -MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB -gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV -BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw -MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl -YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P -RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 -UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI -2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 -Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp -+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ -DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O -nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW -/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g -PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u -QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY -SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv -IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ -RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 -zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd -BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB -ZQ== ------END CERTIFICATE----- - # Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited # Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited # Label: "COMODO ECC Certification Authority" @@ -335,105 +23,6 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -# Issuer: CN=Certigna O=Dhimyotis -# Subject: CN=Certigna O=Dhimyotis -# Label: "Certigna" -# Serial: 18364802974209362175 -# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff -# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97 -# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d ------BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV -BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X -DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ -BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 -QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny -gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw -zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q -130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 -JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw -ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT -AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj -AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG -9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h -bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc -fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu -HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w -t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw -WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE----- - -# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority -# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority -# Label: "ePKI Root Certification Authority" -# Serial: 28956088682735189655030529057352760477 -# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3 -# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0 -# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 ------BEGIN CERTIFICATE----- -MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe -MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 -ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe -Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw -IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL -SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH -SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh -ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X -DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 -TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ -fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA -sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU -WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS -nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH -dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip -NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC -AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF -MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH -ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB -uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl -PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP -JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ -gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 -j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 -5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB -o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS -/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z -Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE -W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D -hNQ+IIX3Sj0rnP0qCglN6oH4EZw= ------END CERTIFICATE----- - -# Issuer: O=certSIGN OU=certSIGN ROOT CA -# Subject: O=certSIGN OU=certSIGN ROOT CA -# Label: "certSIGN ROOT CA" -# Serial: 35210227249154 -# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17 -# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b -# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb ------BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT -AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD -QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP -MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do -0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ -UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d -RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ -OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv -JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C -AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O -BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ -LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY -MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ -44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I -Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw -i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN -9u6wWk5JRFRYX0KD ------END CERTIFICATE----- - # Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) # Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) # Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" @@ -1097,44 +686,6 @@ aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy KwbQBM0= -----END CERTIFICATE----- -# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera -# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera -# Label: "TeliaSonera Root CA v1" -# Serial: 199041966741090107964904287217786801558 -# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c -# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37 -# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89 ------BEGIN CERTIFICATE----- -MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw -NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv -b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD -VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F -VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 -7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X -Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ -/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs -81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm -dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe -Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu -sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 -pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs -slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ -arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD -VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG -9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl -dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx -0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj -TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed -Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 -Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI -OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 -vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW -t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn -HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx -SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= ------END CERTIFICATE----- - # Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center # Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center # Label: "T-TeleSec GlobalRoot Class 2" @@ -2725,96 +2276,6 @@ PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE QRBdJ3NghVdJIgc= -----END CERTIFICATE----- -# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. -# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. -# Label: "Trustwave Global Certification Authority" -# Serial: 1846098327275375458322922162 -# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e -# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5 -# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8 ------BEGIN CERTIFICATE----- -MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw -CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x -ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 -c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx -OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI -SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn -swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu -7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 -1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW -80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP -JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l -RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw -hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 -coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc -BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n -twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud -DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W -0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe -uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q -lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB -aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE -sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT -MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe -qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh -VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 -h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 -EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK -yeC2nOnOcXHebD8WpHk= ------END CERTIFICATE----- - -# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. -# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. -# Label: "Trustwave Global ECC P256 Certification Authority" -# Serial: 4151900041497450638097112925 -# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54 -# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf -# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4 ------BEGIN CERTIFICATE----- -MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf -BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 -YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x -NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G -A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 -d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF -Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG -SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN -FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w -DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw -CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh -DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 ------END CERTIFICATE----- - -# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. -# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. -# Label: "Trustwave Global ECC P384 Certification Authority" -# Serial: 2704997926503831671788816187 -# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6 -# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2 -# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97 ------BEGIN CERTIFICATE----- -MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf -BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 -YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x -NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G -A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 -d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF -Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB -BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ -j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF -1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G -A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 -AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC -MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu -Sw== ------END CERTIFICATE----- - # Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. # Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. # Label: "NAVER Global Root Certification Authority" @@ -3375,45 +2836,6 @@ Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c -----END CERTIFICATE----- -# Issuer: CN=GTS Root R2 O=Google Trust Services LLC -# Subject: CN=GTS Root R2 O=Google Trust Services LLC -# Label: "GTS Root R2" -# Serial: 159662449406622349769042896298 -# MD5 Fingerprint: 1e:39:c0:53:e6:1e:29:82:0b:ca:52:55:36:5d:57:dc -# SHA1 Fingerprint: 9a:44:49:76:32:db:de:fa:d0:bc:fb:5a:7b:17:bd:9e:56:09:24:94 -# SHA256 Fingerprint: 8d:25:cd:97:22:9d:bf:70:35:6b:da:4e:b3:cc:73:40:31:e2:4c:f0:0f:af:cf:d3:2d:c7:6e:b5:84:1c:7e:a8 ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw -MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp -Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt -nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY -6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu -MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k -RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg -f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV -+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo -dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW -Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa -G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq -gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID -AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H -vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 -0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC -B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u -NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg -yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev -HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 -xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR -TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg -JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV -7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl -6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL ------END CERTIFICATE----- - # Issuer: CN=GTS Root R3 O=Google Trust Services LLC # Subject: CN=GTS Root R3 O=Google Trust Services LLC # Label: "GTS Root R3" @@ -4066,30 +3488,6 @@ ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= -----END CERTIFICATE----- -# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA -# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA -# Label: "FIRMAPROFESIONAL CA ROOT-A WEB" -# Serial: 65916896770016886708751106294915943533 -# MD5 Fingerprint: 82:b2:ad:45:00:82:b0:66:63:f8:5f:c3:67:4e:ce:a3 -# SHA1 Fingerprint: a8:31:11:74:a6:14:15:0d:ca:77:dd:0e:e4:0c:5d:58:fc:a0:72:a5 -# SHA256 Fingerprint: be:f2:56:da:f2:6e:9c:69:bd:ec:16:02:35:97:98:f3:ca:f7:18:21:a0:3e:01:82:57:c5:3c:65:61:7f:3d:4a ------BEGIN CERTIFICATE----- -MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw -CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE -YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB -IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw -CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE -YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB -IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf -e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C -cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB -/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O -BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO -PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw -hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG -XSaQpYXFuXqUPoeovQA= ------END CERTIFICATE----- - # Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA # Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA # Label: "TWCA CYBER Root CA" @@ -4130,35 +3528,6 @@ xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X -----END CERTIFICATE----- -# Issuer: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA12" -# Serial: 587887345431707215246142177076162061960426065942 -# MD5 Fingerprint: c6:89:ca:64:42:9b:62:08:49:0b:1e:7f:e9:07:3d:e8 -# SHA1 Fingerprint: 7a:22:1e:3d:de:1b:06:ac:9e:c8:47:70:16:8e:3c:e5:f7:6b:06:f4 -# SHA256 Fingerprint: 3f:03:4b:b5:70:4d:44:b2:d0:85:45:a0:20:57:de:93:eb:f3:90:5f:ce:72:1a:cb:c7:30:c0:6d:da:ee:90:4e ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u -LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw -NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD -eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS -b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF -KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt -p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd -J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur -FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J -hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K -h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF -AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld -mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ -mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA -8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV -55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ -yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== ------END CERTIFICATE----- - # Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. # Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. # Label: "SecureSign Root CA14" diff --git a/libs/chardet-5.2.0.dist-info/METADATA b/libs/chardet-5.2.0.dist-info/METADATA deleted file mode 100644 index 7eab58f56..000000000 --- a/libs/chardet-5.2.0.dist-info/METADATA +++ /dev/null @@ -1,98 +0,0 @@ -Metadata-Version: 2.4 -Name: chardet -Version: 5.2.0 -Summary: Universal encoding detector for Python 3 -Home-page: https://github.com/chardet/chardet -Author: Mark Pilgrim -Author-email: mark@diveintomark.org -Maintainer: Daniel Blanchard -Maintainer-email: dan.blanchard@gmail.com -License: LGPL -Project-URL: Documentation, https://chardet.readthedocs.io/ -Project-URL: GitHub Project, https://github.com/chardet/chardet -Project-URL: Issue Tracker, https://github.com/chardet/chardet/issues -Keywords: encoding,i18n,xml -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Programming Language :: Python :: Implementation :: PyPy -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: Text Processing :: Linguistic -Requires-Python: >=3.7 -License-File: LICENSE -Dynamic: license-file - -Chardet: The Universal Character Encoding Detector --------------------------------------------------- - -.. image:: https://img.shields.io/travis/chardet/chardet/stable.svg - :alt: Build status - :target: https://travis-ci.org/chardet/chardet - -.. image:: https://img.shields.io/coveralls/chardet/chardet/stable.svg - :target: https://coveralls.io/r/chardet/chardet - -.. image:: https://img.shields.io/pypi/v/chardet.svg - :target: https://warehouse.python.org/project/chardet/ - :alt: Latest version on PyPI - -.. image:: https://img.shields.io/pypi/l/chardet.svg - :alt: License - - -Detects - - ASCII, UTF-8, UTF-16 (2 variants), UTF-32 (4 variants) - - Big5, GB2312, EUC-TW, HZ-GB-2312, ISO-2022-CN (Traditional and Simplified Chinese) - - EUC-JP, SHIFT_JIS, CP932, ISO-2022-JP (Japanese) - - EUC-KR, ISO-2022-KR, Johab (Korean) - - KOI8-R, MacCyrillic, IBM855, IBM866, ISO-8859-5, windows-1251 (Cyrillic) - - ISO-8859-5, windows-1251 (Bulgarian) - - ISO-8859-1, windows-1252, MacRoman (Western European languages) - - ISO-8859-7, windows-1253 (Greek) - - ISO-8859-8, windows-1255 (Visual and Logical Hebrew) - - TIS-620 (Thai) - -.. note:: - Our ISO-8859-2 and windows-1250 (Hungarian) probers have been temporarily - disabled until we can retrain the models. - -Requires Python 3.7+. - -Installation ------------- - -Install from `PyPI `_:: - - pip install chardet - -Documentation -------------- - -For users, docs are now available at https://chardet.readthedocs.io/. - -Command-line Tool ------------------ - -chardet comes with a command-line script which reports on the encodings of one -or more files:: - - % chardetect somefile someotherfile - somefile: windows-1252 with confidence 0.5 - someotherfile: ascii with confidence 1.0 - -About ------ - -This is a continuation of Mark Pilgrim's excellent original chardet port from C, and `Ian Cordasco `_'s -`charade `_ Python 3-compatible fork. - -:maintainer: Dan Blanchard diff --git a/libs/chardet-5.2.0.dist-info/RECORD b/libs/chardet-5.2.0.dist-info/RECORD deleted file mode 100644 index cbb62c9d7..000000000 --- a/libs/chardet-5.2.0.dist-info/RECORD +++ /dev/null @@ -1,59 +0,0 @@ -../../bin/chardetect,sha256=7tQYuvRzQLHg-vSFo9s17ZYVJqV7G4MWmowGBsnMebc,197 -chardet-5.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -chardet-5.2.0.dist-info/METADATA,sha256=rkaOE3KgHvl1lL9e4oH3auGS4m1oS0FqM-KHugXfNtw,3440 -chardet-5.2.0.dist-info/RECORD,, -chardet-5.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -chardet-5.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -chardet-5.2.0.dist-info/entry_points.txt,sha256=_cdvYc4jyY68GYfsQAAthNMxO-yodcGkvNC1xOEsLmI,59 -chardet-5.2.0.dist-info/licenses/LICENSE,sha256=3GJlINzVOiL3J68-5Cx3DlbJemT-OtsGN5nYqwMv5VE,26530 -chardet-5.2.0.dist-info/top_level.txt,sha256=AowzBbZy4x8EirABDdJSLJZMkJ_53iIag8xfKR6D7kI,8 -chardet/__init__.py,sha256=57R-HSxj0PWmILMN0GFmUNqEMfrEVSamXyjD-W6_fbs,4797 -chardet/__main__.py,sha256=puNj2o_QfBRKElEkiVp1zEIL1gGYD2o-JuXLFlqHDC4,123 -chardet/big5freq.py,sha256=ltcfP-3PjlNHCoo5e4a7C4z-2DhBTXRfY6jbMbB7P30,31274 -chardet/big5prober.py,sha256=lPMfwCX6v2AaPgvFh_cSWZcgLDbWiFCHLZ_p9RQ9uxE,1763 -chardet/chardistribution.py,sha256=13B8XUG4oXDuLdXvfbIWwLFeR-ZU21AqTS1zcdON8bU,10032 -chardet/charsetgroupprober.py,sha256=UKK3SaIZB2PCdKSIS0gnvMtLR9JJX62M-fZJu3OlWyg,3915 -chardet/charsetprober.py,sha256=L3t8_wIOov8em-vZWOcbkdsrwe43N6_gqNh5pH7WPd4,5420 -chardet/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -chardet/cli/chardetect.py,sha256=zibMVg5RpKb-ME9_7EYG4ZM2Sf07NHcQzZ12U-rYJho,3242 -chardet/codingstatemachine.py,sha256=K7k69sw3jY5DmTXoSJQVsUtFIQKYPQVOSJJhBuGv_yE,3732 -chardet/codingstatemachinedict.py,sha256=0GY3Hi2qIZvDrOOJ3AtqppM1RsYxr_66ER4EHjuMiMc,542 -chardet/cp949prober.py,sha256=0jKRV7fECuWI16rNnks0ZECKA1iZYCIEaP8A1ZvjUSI,1860 -chardet/enums.py,sha256=TzECiZoCKNMqgwU76cPCeKWFBqaWvAdLMev5_bCkhY8,1683 -chardet/escprober.py,sha256=Kho48X65xE0scFylIdeJjM2bcbvRvv0h0WUbMWrJD3A,4006 -chardet/escsm.py,sha256=AqyXpA2FQFD7k-buBty_7itGEYkhmVa8X09NLRul3QM,12176 -chardet/eucjpprober.py,sha256=5KYaM9fsxkRYzw1b5k0fL-j_-ezIw-ij9r97a9MHxLY,3934 -chardet/euckrfreq.py,sha256=3mHuRvXfsq_QcQysDQFb8qSudvTiol71C6Ic2w57tKM,13566 -chardet/euckrprober.py,sha256=hiFT6wM174GIwRvqDsIcuOc-dDsq2uPKMKbyV8-1Xnc,1753 -chardet/euctwfreq.py,sha256=2alILE1Lh5eqiFJZjzRkMQXolNJRHY5oBQd-vmZYFFM,36913 -chardet/euctwprober.py,sha256=NxbpNdBtU0VFI0bKfGfDkpP7S2_8_6FlO87dVH0ogws,1753 -chardet/gb2312freq.py,sha256=49OrdXzD-HXqwavkqjo8Z7gvs58hONNzDhAyMENNkvY,20735 -chardet/gb2312prober.py,sha256=KPEBueaSLSvBpFeINMu0D6TgHcR90e5PaQawifzF4o0,1759 -chardet/hebrewprober.py,sha256=96T_Lj_OmW-fK7JrSHojYjyG3fsGgbzkoTNleZ3kfYE,14537 -chardet/jisfreq.py,sha256=mm8tfrwqhpOd3wzZKS4NJqkYBQVcDfTM2JiQ5aW932E,25796 -chardet/johabfreq.py,sha256=dBpOYG34GRX6SL8k_LbS9rxZPMjLjoMlgZ03Pz5Hmqc,42498 -chardet/johabprober.py,sha256=O1Qw9nVzRnun7vZp4UZM7wvJSv9W941mEU9uDMnY3DU,1752 -chardet/jpcntx.py,sha256=uhHrYWkLxE_rF5OkHKInm0HUsrjgKHHVQvtt3UcvotA,27055 -chardet/langbulgarianmodel.py,sha256=bGoRpxBYtrbSHa6mX6PkEA26v30pWmhDjemhdxmkew8,104550 -chardet/langgreekmodel.py,sha256=3wMlEzQ8oU2MbrL2xN8lkuOB0dCMLBhW6heekxusoc0,98472 -chardet/langhebrewmodel.py,sha256=ZUTqusxMvR_earWPs5w-rH10xoe5sPjd9FLMu1DUIvE,98184 -chardet/langhungarianmodel.py,sha256=N-YtC2EiswyS7XsUicCPRycrIzRNj47Y048odp9qOoo,101351 -chardet/langrussianmodel.py,sha256=6v7RcZKGj0VH0864BHzizKNceAYbHvGts2p00ifC7w4,128023 -chardet/langthaimodel.py,sha256=Mr673U9U8rkQFfUDtLP01pp-0TOsl2o6sb75YEjvpcs,102762 -chardet/langturkishmodel.py,sha256=LkXCjWhGUEzqKXvfasHN0SFBigwKJ3xeWNVZ0EyI0kA,95360 -chardet/latin1prober.py,sha256=p15EEmFbmQUwbKLC7lOJVGHEZwcG45ubEZYTGu01J5g,5380 -chardet/macromanprober.py,sha256=9anfzmY6TBfUPDyBDOdY07kqmTHpZ1tK0jL-p1JWcOY,6077 -chardet/mbcharsetprober.py,sha256=Wr04WNI4F3X_VxEverNG-H25g7u-MDDKlNt-JGj-_uU,3715 -chardet/mbcsgroupprober.py,sha256=iRpaNBjV0DNwYPu_z6TiHgRpwYahiM7ztI_4kZ4Uz9A,2131 -chardet/mbcssm.py,sha256=hUtPvDYgWDaA2dWdgLsshbwRfm3Q5YRlRogdmeRUNQw,30391 -chardet/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -chardet/metadata/languages.py,sha256=FhvBIdZFxRQ-dTwkb_0madRKgVBCaUMQz9I5xqjE5iQ,13560 -chardet/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -chardet/resultdict.py,sha256=ez4FRvN5KaSosJeJ2WzUyKdDdg35HDy_SSLPXKCdt5M,402 -chardet/sbcharsetprober.py,sha256=-nd3F90i7GpXLjehLVHqVBE0KlWzGvQUPETLBNn4o6U,6400 -chardet/sbcsgroupprober.py,sha256=gcgI0fOfgw_3YTClpbra_MNxwyEyJ3eUXraoLHYb59E,4137 -chardet/sjisprober.py,sha256=aqQufMzRw46ZpFlzmYaYeT2-nzmKb-hmcrApppJ862k,4007 -chardet/universaldetector.py,sha256=xYBrg4x0dd9WnT8qclfADVD9ondrUNkqPmvte1pa520,14848 -chardet/utf1632prober.py,sha256=pw1epGdMj1hDGiCu1AHqqzOEfjX8MVdiW7O1BlT8-eQ,8505 -chardet/utf8prober.py,sha256=8m08Ub5490H4jQ6LYXvFysGtgKoKsHUd2zH_i8_TnVw,2812 -chardet/version.py,sha256=jp8ePp1zC63YxruGcHSuKxtf3-fF1LYAMUZD2eDWYok,244 diff --git a/libs/chardet-5.2.0.dist-info/entry_points.txt b/libs/chardet-5.2.0.dist-info/entry_points.txt deleted file mode 100644 index c36a5e3ee..000000000 --- a/libs/chardet-5.2.0.dist-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -chardetect = chardet.cli.chardetect:main diff --git a/libs/chardet-5.2.0.dist-info/licenses/LICENSE b/libs/chardet-5.2.0.dist-info/licenses/LICENSE deleted file mode 100644 index 4362b4915..000000000 --- a/libs/chardet-5.2.0.dist-info/licenses/LICENSE +++ /dev/null @@ -1,502 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 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. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -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 and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, 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 library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete 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 distribute a copy of this License along with the -Library. - - 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 Library or any portion -of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -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 Library, 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 Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you 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. - - If distribution of 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 satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be 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. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library 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. - - 9. 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 Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -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 with -this License. - - 11. 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 Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library 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 Library. - -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. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library 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. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser 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 Library -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 Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -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 - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "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 -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. 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 LIBRARY 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 -LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. 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. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; 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. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! diff --git a/libs/chardet-5.2.0.dist-info/top_level.txt b/libs/chardet-5.2.0.dist-info/top_level.txt deleted file mode 100644 index 79236f25c..000000000 --- a/libs/chardet-5.2.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -chardet diff --git a/libs/chardet-5.2.0.dist-info/INSTALLER b/libs/chardet-7.4.3.dist-info/INSTALLER similarity index 100% rename from libs/chardet-5.2.0.dist-info/INSTALLER rename to libs/chardet-7.4.3.dist-info/INSTALLER diff --git a/libs/chardet-7.4.3.dist-info/METADATA b/libs/chardet-7.4.3.dist-info/METADATA new file mode 100644 index 000000000..b259e377e --- /dev/null +++ b/libs/chardet-7.4.3.dist-info/METADATA @@ -0,0 +1,221 @@ +Metadata-Version: 2.4 +Name: chardet +Version: 7.4.3 +Summary: Universal character encoding detector +Project-URL: Homepage, https://github.com/chardet/chardet +Project-URL: Repository, https://github.com/chardet/chardet +Project-URL: Bug Tracker, https://github.com/chardet/chardet/issues +Project-URL: Documentation, https://chardet.readthedocs.io +Project-URL: Changelog, https://chardet.readthedocs.io/en/latest/changelog.html +Author-email: Dan Blanchard +Maintainer: Ian Cordasco +Maintainer-email: Dan Blanchard +License-Expression: 0BSD +License-File: LICENSE +Keywords: chardet,charset,detection,encoding,unicode +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Linguistic +Requires-Python: >=3.10 +Description-Content-Type: text/markdown + +# chardet + +Universal character encoding detector. + +[![License: 0BSD](https://img.shields.io/badge/License-0BSD-blue.svg)](LICENSE) +[![Documentation](https://readthedocs.org/projects/chardet/badge/?version=latest)](https://chardet.readthedocs.io) +[![codecov](https://codecov.io/github/chardet/chardet/branch/main/graph/badge.svg?token=m5ZQrMd3vk)](https://codecov.io/github/chardet/chardet) + +chardet 7 is a ground-up, 0BSD-licensed rewrite of [chardet](https://github.com/chardet/chardet). +Same package name, same public API — drop-in replacement for chardet 5.x/6.x, just much faster and more accurate. +Python 3.10+, zero runtime dependencies, works on PyPy. + +[Read more details about the rewrite process.](https://dan-blanchard.github.io/blog/chardet-rewrite-controversy/) + +## Why chardet 7? + +**99.3% accuracy** on 2,517 test files. **47x faster** than chardet 6.0.0 +and **1.5x faster** than charset-normalizer 3.4.6. **Language detection** for +every result. **MIME type detection** for binary files. **0BSD licensed.** + +| | chardet 7.4.0 (mypyc) | chardet 6.0.0 | [charset-normalizer] 3.4.6 | +| ---------------------- | :--------------------: | :-----------: | :-------------------------: | +| Accuracy (2,517 files) | **99.3%** | 88.2% | 85.4% | +| Speed | **551 files/s** | 12 files/s | 376 files/s | +| Language detection | **95.7%** | 40.0% | 59.2% | +| Peak memory | **52.9 MiB** | 29.5 MiB | 78.8 MiB | +| Streaming detection | **yes** | yes | no | +| Encoding era filtering | **yes** | no | no | +| Encoding filters | **yes** | no | yes | +| MIME type detection | **yes** | no | no | +| Supported encodings | 99 | 84 | 99 | +| License | 0BSD | LGPL | MIT | + +[charset-normalizer]: https://github.com/jawah/charset_normalizer + +## Installation + +```bash +pip install chardet +``` + +## Quick Start + +```python +import chardet + +chardet.detect(b"Python is a great programming language for beginners and experts alike.") +# {'encoding': 'ascii', 'confidence': 1.0, 'language': 'en', 'mime_type': 'text/plain'} + +# UTF-8 English with accented characters +chardet.detect("The naïve approach doesn't always work in complex systems.".encode("utf-8")) +# {'encoding': 'utf-8', 'confidence': 0.84, 'language': 'en', 'mime_type': 'text/plain'} + +# Japanese EUC-JP +chardet.detect("日本語の文字コード検出テストです。このテキストはEUC-JPでエンコードされています。正しく検出できるか確認します。".encode("euc-jp")) +# {'encoding': 'EUC-JP', 'confidence': 1.0, 'language': 'ja', 'mime_type': 'text/plain'} + +# Get all candidate encodings ranked by confidence +text = "Le café est une boisson très populaire en France et dans le monde entier." +results = chardet.detect_all(text.encode("windows-1252")) +for r in results[:4]: + print(r["encoding"], round(r["confidence"], 2)) +# Windows-1252 0.32 +# iso8859-15 0.32 +# ISO-8859-1 0.32 +# MacRoman 0.31 +``` + +### Streaming Detection + +For large files or network streams, use `UniversalDetector` to feed data incrementally: + +```python +from chardet import UniversalDetector + +detector = UniversalDetector() +with open("unknown.txt", "rb") as f: + for line in f: + detector.feed(line) + if detector.done: + break +result = detector.close() +print(result) +``` + +### Encoding Era Filtering + +Restrict detection to specific encoding eras to reduce false positives: + +```python +from chardet import detect_all +from chardet.enums import EncodingEra + +data = "Москва является столицей Российской Федерации и крупнейшим городом страны.".encode("windows-1251") + +# All encoding eras are considered by default — 4 candidates across eras +for r in detect_all(data): + print(r["encoding"], round(r["confidence"], 2)) +# Windows-1251 0.46 +# MacCyrillic 0.42 +# KZ1048 0.2 +# ptcp154 0.2 + +# Restrict to modern web encodings — 1 confident result +for r in detect_all(data, encoding_era=EncodingEra.MODERN_WEB): + print(r["encoding"], round(r["confidence"], 2)) +# Windows-1251 0.46 +``` + +### Encoding Filters + +Restrict detection to specific encodings, or exclude encodings you don't want: + +```python +# Only consider UTF-8 and Windows-1252 +chardet.detect(data, include_encodings=["utf-8", "windows-1252"]) + +# Consider everything except EBCDIC +chardet.detect(data, exclude_encodings=["cp037", "cp500"]) +``` + +## CLI + +```bash +chardetect somefile.txt +# somefile.txt: utf-8 with confidence 0.99 + +chardetect --minimal somefile.txt +# utf-8 + +# Include detected language +chardetect -l somefile.txt +# somefile.txt: utf-8 en (English) with confidence 0.99 + +# Only consider specific encodings +chardetect -i utf-8,windows-1252 somefile.txt +# somefile.txt: utf-8 with confidence 0.99 + +# Pipe from stdin +cat somefile.txt | chardetect +# stdin: utf-8 with confidence 0.99 +``` + +## What's New in chardet 7? + +- **0BSD license** (previous versions were LGPL) +- **Ground-up rewrite:** 13-stage detection pipeline using BOM detection, magic number identification, structural probing, byte validity filtering, and bigram statistical models +- **47x faster** than chardet 6.0.0 with mypyc, **1.5x faster** than charset-normalizer 3.4.6 +- **99.3% accuracy:** +11.1pp vs chardet 6.0.0, +13.9pp vs charset-normalizer 3.4.6 +- **Language detection:** 95.7% accuracy across 49 languages, returned with every result +- **MIME type detection:** identifies 40+ binary file formats (images, audio/video, archives, documents, executables, fonts) via magic number signatures, plus `text/html`, `text/xml`, and `text/x-python` for markup +- **Encoding filters:** `include_encodings` and `exclude_encodings` parameters to restrict or exclude specific encodings from the candidate set +- **99 encodings:** full coverage including EBCDIC, Mac, DOS, and Baltic/Central European families +- **Optional mypyc compilation:** 1.67x additional speedup on CPython +- **Thread-safe:** `detect()` and `detect_all()` are safe to call concurrently; scales on free-threaded Python +- **Same API:** `detect()`, `detect_all()`, `UniversalDetector`, and the `chardetect` CLI all work as before + +## Documentation + +Full documentation is available at [chardet.readthedocs.io](https://chardet.readthedocs.io). + +## Project History + +chardet was originally created by [Mark Pilgrim](https://en.wikipedia.org/wiki/Mark_Pilgrim) +in 2006 as a Python port of [Mozilla's universal charset detection library](https://www-archive.mozilla.org/projects/intl/chardet.html). +He released versions 1.0 (2006) and 1.0.1 (2008) on PyPI, then developed +an unreleased Python 3 port (2.0.1) on Google Code. After Mark +[deleted his online accounts](https://en.wikipedia.org/wiki/Mark_Pilgrim#%22Infocide%22) +in 2011, the project was continued by David Cramer, Erik Rose, Toshio Kuratomi, +Ian Cordasco, and Dan Blanchard. + +In 2026, Dan Blanchard rewrote chardet using Claude, releasing chardet 7.0 +under a new license. All releases after 7 are not derivative of the original +chardet code, but are released under the same name to allow an easier +transition for users who can immediately benefit from the speed and accuracy +improvements. For historical preservation and to allow easier comparison with +the other releases, Dan has restored Mark's lost commits to this repository +in the `history/pilgrim` branch. + +To see the full history from 2006 to present in `git log`, fetch the graft +refs: + +``` +git fetch origin 'refs/replace/*:refs/replace/*' +``` + +## License + +[0BSD](LICENSE) diff --git a/libs/chardet-7.4.3.dist-info/RECORD b/libs/chardet-7.4.3.dist-info/RECORD new file mode 100644 index 000000000..4a83f385a --- /dev/null +++ b/libs/chardet-7.4.3.dist-info/RECORD @@ -0,0 +1,38 @@ +../../bin/chardetect,sha256=mEtIZGC7LhH3DsAmWmXKc1pSWymfDVi3E78pjULk_nw,186 +chardet-7.4.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +chardet-7.4.3.dist-info/METADATA,sha256=7fdWo3j-LIeBSq0Cupyg3AFR8CsGw3pDgswtbZh0_ao,9437 +chardet-7.4.3.dist-info/RECORD,, +chardet-7.4.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +chardet-7.4.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +chardet-7.4.3.dist-info/entry_points.txt,sha256=BmSUh6JfFA5WZSpQfj_76XcugwChMxXMbp5OlC9SXaQ,48 +chardet-7.4.3.dist-info/licenses/LICENSE,sha256=DUz2fnom6ZV7iUjn38DjgOYbQnqUsGHmGIiauom7lhY,641 +chardet/__init__.py,sha256=lQryw3Z3D-ijiP9maHUaQjBu5hUXZ3dboQ-13gd_q2k,7109 +chardet/__main__.py,sha256=rRIox2_hyIYSRB_hRsGgXjYezDASR_SkVtPXMApx7UA,143 +chardet/_utils.py,sha256=A9NKL4pcXnd-yt4Glyofyt9hQylNYiRlZJzEtHV7SHM,2775 +chardet/_version.py,sha256=cC7NOxOqrWet_WTrxjvdoBB6-hEo3F47HLSkghPAe3g,520 +chardet/cli.py,sha256=BsWM-VR4HBrUjiTJupx_rS89bD_mAbmbfzrNRQoOZNU,4786 +chardet/detector.py,sha256=vyMmFj18XPml4FTDF6mdVA031dQb_0zRQNKjXC5tx1U,7116 +chardet/enums.py,sha256=6Zd0KfGMobsLEwLJ7cpVMyEDwZEWessfaZf3GysyIDY,972 +chardet/equivalences.py,sha256=854Q7ub6HrbUvWIFmSXXk2JD5eEu3CvEaGBR03VvTz8,14577 +chardet/models/__init__.py,sha256=TXwUgA0w30bYJgxge0-rVyw98iLhu4RHNg7QtcAxRvw,12003 +chardet/models/confusion.bin,sha256=HIU80EtAC9LTdy_K3cRGazLTLbcDp6pBXfWiCBC_nss,8214 +chardet/models/idf.bin,sha256=gwbiiq1IysVNt4NNCfScu7ktOSjt5Txx8rmyMrWGu4o,65536 +chardet/models/models.bin,sha256=B-sdq89PjnFPn4ZuqjVRIb97NWPc3g137Xw2aN5fdfU,585891 +chardet/models/training_metadata.yaml,sha256=VlvH44BYYtaSUF3LoZuq16GKyJPb5IGcB84XMkQHttg,72401 +chardet/pipeline/__init__.py,sha256=tSNouprAuv8NzDFM0eq_de-R4x-fOH8kn5hCd3zbj9A,2766 +chardet/pipeline/ascii.py,sha256=srQ_HmTa2QAEU6EXBsS4gQtW7D5EoY-HoO7AkiOKJec,1617 +chardet/pipeline/binary.py,sha256=6hJEniKPzSkw3-vRKI1rFbUfRfh65gXXzxPt2dCSh98,1065 +chardet/pipeline/bom.py,sha256=uu1oFH2VHV0X9CIhTpjaVqJzrkdriDHhH30j7Jrr9GU,1452 +chardet/pipeline/confusion.py,sha256=sFMLYHesebgJSm-0b3p3D8DuzIuCK0ezDPPLe0TpMQA,10667 +chardet/pipeline/escape.py,sha256=CTO8DZ0mWwOo08Ez4e-XJZiQd6-FJpNfTyLFWHYcKXg,10849 +chardet/pipeline/magic.py,sha256=LIaacnyUHyhg1VauNXqqDegbmVqBtZZtDGO0qHnBzBk,11388 +chardet/pipeline/markup.py,sha256=6QQ6pglf74NKqCQ7Ay52siBT6oS1JMxZRWQRRmMZtRg,3693 +chardet/pipeline/orchestrator.py,sha256=3F0pqyqu185JmB_6dk7D5_dBaATe26SPV2Q1JQ7PvmM,30228 +chardet/pipeline/statistical.py,sha256=v97eBx5y3wLPu4vl1y_2fgzqV1Xc5dTShtLnWZ-G5mk,1208 +chardet/pipeline/structural.py,sha256=Jkm0OrvgapZHaB1NS9nPhkBL-lwXBM4OUammCBciNqg,16119 +chardet/pipeline/utf1632.py,sha256=9C972twVJ8PKtetNvbe8M5_8XxDcsAmMHbu07KExnU8,11317 +chardet/pipeline/utf8.py,sha256=dniIcIG2zDefty9GdgTbDn29MpqfpyYZKtqIkS7ESjA,3334 +chardet/pipeline/validity.py,sha256=df97ee4EQPIkv8RkYWQBxEXYnWnE0jfX9tlcIT89lVc,896 +chardet/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +chardet/registry.py,sha256=pF5MVMiBzhh2AQj-bNsbwJp_bazhSMZFHL8BUw6YPnc,26948 +chardet/universaldetector.py,sha256=q7TvwdChVVrtIJRr1u9oojkMEdOLjR9qhxRs9O5x6mM,718 diff --git a/libs/chardet-5.2.0.dist-info/REQUESTED b/libs/chardet-7.4.3.dist-info/REQUESTED similarity index 100% rename from libs/chardet-5.2.0.dist-info/REQUESTED rename to libs/chardet-7.4.3.dist-info/REQUESTED diff --git a/libs/pygments-2.19.2.dist-info/WHEEL b/libs/chardet-7.4.3.dist-info/WHEEL similarity index 67% rename from libs/pygments-2.19.2.dist-info/WHEEL rename to libs/chardet-7.4.3.dist-info/WHEEL index ae8ec1bda..bd8422674 100644 --- a/libs/pygments-2.19.2.dist-info/WHEEL +++ b/libs/chardet-7.4.3.dist-info/WHEEL @@ -1,4 +1,4 @@ Wheel-Version: 1.0 -Generator: hatchling 1.28.0 +Generator: hatchling 1.30.1 Root-Is-Purelib: true Tag: py3-none-any diff --git a/libs/chardet-7.4.3.dist-info/entry_points.txt b/libs/chardet-7.4.3.dist-info/entry_points.txt new file mode 100644 index 000000000..977dff1bb --- /dev/null +++ b/libs/chardet-7.4.3.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +chardetect = chardet.cli:main diff --git a/libs/chardet-7.4.3.dist-info/licenses/LICENSE b/libs/chardet-7.4.3.dist-info/licenses/LICENSE new file mode 100644 index 000000000..b9dc77f90 --- /dev/null +++ b/libs/chardet-7.4.3.dist-info/licenses/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2026 Dan Blanchard + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/libs/chardet/__init__.py b/libs/chardet/__init__.py index fe581623d..dad7fa82b 100644 --- a/libs/chardet/__init__.py +++ b/libs/chardet/__init__.py @@ -1,115 +1,172 @@ -######################## BEGIN LICENSE BLOCK ######################## -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +"""Universal character encoding detector — 0BSD-licensed rewrite.""" -from typing import List, Union +from __future__ import annotations -from .charsetgroupprober import CharSetGroupProber -from .charsetprober import CharSetProber -from .enums import InputState -from .resultdict import ResultDict -from .universaldetector import UniversalDetector -from .version import VERSION, __version__ +from collections.abc import Iterable -__all__ = ["UniversalDetector", "detect", "detect_all", "__version__", "VERSION"] +from chardet._utils import ( + _DEFAULT_CHUNK_SIZE, + DEFAULT_MAX_BYTES, + MINIMUM_THRESHOLD, + _resolve_prefer_superset, + _validate_max_bytes, + _warn_deprecated_chunk_size, +) +from chardet._version import __version__ +from chardet.detector import UniversalDetector +from chardet.enums import EncodingEra, LanguageFilter +from chardet.equivalences import apply_compat_names, apply_preferred_superset +from chardet.pipeline import DetectionDict, DetectionResult +from chardet.pipeline.orchestrator import run_pipeline +from chardet.registry import _validate_encoding, normalize_encodings + +__all__ = [ + "DEFAULT_MAX_BYTES", + "MINIMUM_THRESHOLD", + "DetectionDict", + "DetectionResult", + "EncodingEra", + "LanguageFilter", + "UniversalDetector", + "__version__", + "detect", + "detect_all", +] -def detect( - byte_str: Union[bytes, bytearray], should_rename_legacy: bool = False -) -> ResultDict: +def detect( # noqa: PLR0913 + byte_str: bytes | bytearray, + should_rename_legacy: bool = False, + encoding_era: EncodingEra = EncodingEra.ALL, + chunk_size: int = _DEFAULT_CHUNK_SIZE, + max_bytes: int = DEFAULT_MAX_BYTES, + *, + prefer_superset: bool = False, + compat_names: bool = True, + include_encodings: Iterable[str] | None = None, + exclude_encodings: Iterable[str] | None = None, + no_match_encoding: str = "cp1252", + empty_input_encoding: str = "utf-8", +) -> DetectionDict: + """Detect the encoding of the given byte string. + + :param byte_str: The byte sequence to detect encoding for. + :param should_rename_legacy: Deprecated alias for *prefer_superset*. + :param encoding_era: Restrict candidate encodings to the given era. + :param chunk_size: Deprecated -- accepted for backward compatibility but + has no effect. + :param max_bytes: Maximum number of bytes to examine from *byte_str*. + :param prefer_superset: If ``True``, remap ISO subset encodings to their + Windows/CP superset equivalents (e.g., ISO-8859-1 -> Windows-1252). + :param compat_names: If ``True`` (default), return encoding names + compatible with chardet 5.x/6.x. If ``False``, return raw Python + codec names. + :param include_encodings: If given, restrict detection to only these + encodings (names or aliases). + :param exclude_encodings: If given, remove these encodings from the + candidate set. + :param no_match_encoding: Encoding to return when no candidate survives + the pipeline. Defaults to ``"cp1252"``. + :param empty_input_encoding: Encoding to return for empty input. Defaults + to ``"utf-8"``. + :returns: A dictionary with keys ``"encoding"``, ``"confidence"``, and + ``"language"``. """ - Detect the encoding of the given byte string. - - :param byte_str: The byte sequence to examine. - :type byte_str: ``bytes`` or ``bytearray`` - :param should_rename_legacy: Should we rename legacy encodings - to their more modern equivalents? - :type should_rename_legacy: ``bool`` - """ - if not isinstance(byte_str, bytearray): - if not isinstance(byte_str, bytes): - raise TypeError( - f"Expected object of type bytes or bytearray, got: {type(byte_str)}" - ) - byte_str = bytearray(byte_str) - detector = UniversalDetector(should_rename_legacy=should_rename_legacy) - detector.feed(byte_str) - return detector.close() + _warn_deprecated_chunk_size(chunk_size) + _validate_max_bytes(max_bytes) + prefer_superset = _resolve_prefer_superset(should_rename_legacy, prefer_superset) + include = normalize_encodings(include_encodings, "include_encodings") + exclude = normalize_encodings(exclude_encodings, "exclude_encodings") + no_match = _validate_encoding(no_match_encoding, "no_match_encoding") + empty = _validate_encoding(empty_input_encoding, "empty_input_encoding") + data = byte_str if isinstance(byte_str, bytes) else bytes(byte_str) + results = run_pipeline( + data, + encoding_era, + max_bytes=max_bytes, + include_encodings=include, + exclude_encodings=exclude, + no_match_encoding=no_match, + empty_input_encoding=empty, + ) + result = results[0].to_dict() + if prefer_superset: + apply_preferred_superset(result) + if compat_names: + apply_compat_names(result) + return result -def detect_all( - byte_str: Union[bytes, bytearray], +def detect_all( # noqa: PLR0913 + byte_str: bytes | bytearray, ignore_threshold: bool = False, should_rename_legacy: bool = False, -) -> List[ResultDict]: + encoding_era: EncodingEra = EncodingEra.ALL, + chunk_size: int = _DEFAULT_CHUNK_SIZE, + max_bytes: int = DEFAULT_MAX_BYTES, + *, + prefer_superset: bool = False, + compat_names: bool = True, + include_encodings: Iterable[str] | None = None, + exclude_encodings: Iterable[str] | None = None, + no_match_encoding: str = "cp1252", + empty_input_encoding: str = "utf-8", +) -> list[DetectionDict]: + """Detect all possible encodings of the given byte string. + + When *ignore_threshold* is False (the default), results with confidence + <= MINIMUM_THRESHOLD (0.20) are filtered out. If all results are below + the threshold, the full unfiltered list is returned as a fallback so the + caller always receives at least one result. + + :param byte_str: The byte sequence to detect encoding for. + :param ignore_threshold: If ``True``, return all candidate encodings + regardless of confidence score. + :param should_rename_legacy: Deprecated alias for *prefer_superset*. + :param encoding_era: Restrict candidate encodings to the given era. + :param chunk_size: Deprecated -- accepted for backward compatibility but + has no effect. + :param max_bytes: Maximum number of bytes to examine from *byte_str*. + :param prefer_superset: If ``True``, remap ISO subset encodings to their + Windows/CP superset equivalents. + :param compat_names: If ``True`` (default), return encoding names + compatible with chardet 5.x/6.x. If ``False``, return raw Python + codec names. + :param include_encodings: If given, restrict detection to only these + encodings (names or aliases). + :param exclude_encodings: If given, remove these encodings from the + candidate set. + :param no_match_encoding: Encoding to return when no candidate survives + the pipeline. Defaults to ``"cp1252"``. + :param empty_input_encoding: Encoding to return for empty input. Defaults + to ``"utf-8"``. + :returns: A list of dictionaries, sorted by descending confidence. """ - Detect all the possible encodings of the given byte string. - - :param byte_str: The byte sequence to examine. - :type byte_str: ``bytes`` or ``bytearray`` - :param ignore_threshold: Include encodings that are below - ``UniversalDetector.MINIMUM_THRESHOLD`` - in results. - :type ignore_threshold: ``bool`` - :param should_rename_legacy: Should we rename legacy encodings - to their more modern equivalents? - :type should_rename_legacy: ``bool`` - """ - if not isinstance(byte_str, bytearray): - if not isinstance(byte_str, bytes): - raise TypeError( - f"Expected object of type bytes or bytearray, got: {type(byte_str)}" - ) - byte_str = bytearray(byte_str) - - detector = UniversalDetector(should_rename_legacy=should_rename_legacy) - detector.feed(byte_str) - detector.close() - - if detector.input_state == InputState.HIGH_BYTE: - results: List[ResultDict] = [] - probers: List[CharSetProber] = [] - for prober in detector.charset_probers: - if isinstance(prober, CharSetGroupProber): - probers.extend(p for p in prober.probers) - else: - probers.append(prober) - for prober in probers: - if ignore_threshold or prober.get_confidence() > detector.MINIMUM_THRESHOLD: - charset_name = prober.charset_name or "" - lower_charset_name = charset_name.lower() - # Use Windows encoding name instead of ISO-8859 if we saw any - # extra Windows-specific bytes - if lower_charset_name.startswith("iso-8859") and detector.has_win_bytes: - charset_name = detector.ISO_WIN_MAP.get( - lower_charset_name, charset_name - ) - # Rename legacy encodings with superset encodings if asked - if should_rename_legacy: - charset_name = detector.LEGACY_MAP.get( - charset_name.lower(), charset_name - ) - results.append( - { - "encoding": charset_name, - "confidence": prober.get_confidence(), - "language": prober.language, - } - ) - if len(results) > 0: - return sorted(results, key=lambda result: -result["confidence"]) - - return [detector.result] + _warn_deprecated_chunk_size(chunk_size) + _validate_max_bytes(max_bytes) + prefer_superset = _resolve_prefer_superset(should_rename_legacy, prefer_superset) + include = normalize_encodings(include_encodings, "include_encodings") + exclude = normalize_encodings(exclude_encodings, "exclude_encodings") + no_match = _validate_encoding(no_match_encoding, "no_match_encoding") + empty = _validate_encoding(empty_input_encoding, "empty_input_encoding") + data = byte_str if isinstance(byte_str, bytes) else bytes(byte_str) + results = run_pipeline( + data, + encoding_era, + max_bytes=max_bytes, + include_encodings=include, + exclude_encodings=exclude, + no_match_encoding=no_match, + empty_input_encoding=empty, + ) + dicts = [r.to_dict() for r in results] + if not ignore_threshold: + filtered = [d for d in dicts if d["confidence"] > MINIMUM_THRESHOLD] + if filtered: + dicts = filtered + for d in dicts: + if prefer_superset: + apply_preferred_superset(d) + if compat_names: + apply_compat_names(d) + return sorted(dicts, key=lambda d: d["confidence"], reverse=True) diff --git a/libs/chardet/__main__.py b/libs/chardet/__main__.py index c19b0d2d7..5d666a725 100644 --- a/libs/chardet/__main__.py +++ b/libs/chardet/__main__.py @@ -1,6 +1,6 @@ -"""Wrapper so people can run python -m chardet""" +"""Allow running chardet as ``python -m chardet``.""" -from .cli.chardetect import main +from chardet.cli import main -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() diff --git a/libs/chardet/_utils.py b/libs/chardet/_utils.py new file mode 100644 index 000000000..9db052a71 --- /dev/null +++ b/libs/chardet/_utils.py @@ -0,0 +1,101 @@ +"""Internal shared utilities for chardet.""" + +from __future__ import annotations + +import warnings + +#: Default maximum number of bytes to examine during detection. +DEFAULT_MAX_BYTES: int = 200_000 + +#: Default minimum confidence threshold for filtering results. +MINIMUM_THRESHOLD: float = 0.20 + +#: Default chunk_size value (deprecated, kept for backward-compat signatures). +_DEFAULT_CHUNK_SIZE: int = 65_536 + + +def _warn_deprecated_chunk_size(chunk_size: int, stacklevel: int = 3) -> None: + """Emit a deprecation warning if *chunk_size* differs from the default.""" + if chunk_size != _DEFAULT_CHUNK_SIZE: + warnings.warn( + "chunk_size is not used in this version of chardet and will be ignored", + DeprecationWarning, + stacklevel=stacklevel, + ) + + +def _validate_max_bytes(max_bytes: int) -> None: + """Raise ValueError if *max_bytes* is not a positive integer.""" + if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes < 1: + msg = "max_bytes must be a positive integer" + raise ValueError(msg) + + +def _resolve_prefer_superset( + should_rename_legacy: bool, prefer_superset: bool, stacklevel: int = 3 +) -> bool: + """Resolve the deprecated *should_rename_legacy* into *prefer_superset*.""" + if should_rename_legacy: + warnings.warn( + "should_rename_legacy is deprecated, use prefer_superset instead", + DeprecationWarning, + stacklevel=stacklevel, + ) + return True + return prefer_superset + + +#: Mapping from ISO 639-1 language codes to English names. +#: Includes ``"und"`` (ISO 639-3 "Undetermined") for use when language is unknown. +ISO_TO_LANGUAGE: dict[str, str] = { + "ar": "arabic", + "be": "belarusian", + "bg": "bulgarian", + "br": "breton", + "cs": "czech", + "cy": "welsh", + "da": "danish", + "de": "german", + "el": "greek", + "en": "english", + "eo": "esperanto", + "es": "spanish", + "et": "estonian", + "fa": "farsi", + "fi": "finnish", + "fr": "french", + "ga": "irish", + "gd": "gaelic", + "he": "hebrew", + "hr": "croatian", + "hu": "hungarian", + "id": "indonesian", + "is": "icelandic", + "it": "italian", + "ja": "japanese", + "kk": "kazakh", + "ko": "korean", + "lt": "lithuanian", + "lv": "latvian", + "mk": "macedonian", + "ms": "malay", + "mt": "maltese", + "nl": "dutch", + "no": "norwegian", + "pl": "polish", + "pt": "portuguese", + "ro": "romanian", + "ru": "russian", + "sk": "slovak", + "sl": "slovene", + "sr": "serbian", + "sv": "swedish", + "tg": "tajik", + "th": "thai", + "tr": "turkish", + "uk": "ukrainian", + "und": "undetermined", + "ur": "urdu", + "vi": "vietnamese", + "zh": "chinese", +} diff --git a/libs/chardet/_version.py b/libs/chardet/_version.py new file mode 100644 index 000000000..d1246a95d --- /dev/null +++ b/libs/chardet/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '7.4.3' +__version_tuple__ = version_tuple = (7, 4, 3) + +__commit_id__ = commit_id = None diff --git a/libs/chardet/big5freq.py b/libs/chardet/big5freq.py deleted file mode 100644 index 87d9f972e..000000000 --- a/libs/chardet/big5freq.py +++ /dev/null @@ -1,386 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# Big5 frequency table -# by Taiwan's Mandarin Promotion Council -# -# -# 128 --> 0.42261 -# 256 --> 0.57851 -# 512 --> 0.74851 -# 1024 --> 0.89384 -# 2048 --> 0.97583 -# -# Ideal Distribution Ratio = 0.74851/(1-0.74851) =2.98 -# Random Distribution Ration = 512/(5401-512)=0.105 -# -# Typical Distribution Ratio about 25% of Ideal one, still much higher than RDR - -BIG5_TYPICAL_DISTRIBUTION_RATIO = 0.75 - -# Char to FreqOrder table -BIG5_TABLE_SIZE = 5376 -# fmt: off -BIG5_CHAR_TO_FREQ_ORDER = ( - 1,1801,1506, 255,1431, 198, 9, 82, 6,5008, 177, 202,3681,1256,2821, 110, # 16 -3814, 33,3274, 261, 76, 44,2114, 16,2946,2187,1176, 659,3971, 26,3451,2653, # 32 -1198,3972,3350,4202, 410,2215, 302, 590, 361,1964, 8, 204, 58,4510,5009,1932, # 48 - 63,5010,5011, 317,1614, 75, 222, 159,4203,2417,1480,5012,3555,3091, 224,2822, # 64 -3682, 3, 10,3973,1471, 29,2787,1135,2866,1940, 873, 130,3275,1123, 312,5013, # 80 -4511,2052, 507, 252, 682,5014, 142,1915, 124, 206,2947, 34,3556,3204, 64, 604, # 96 -5015,2501,1977,1978, 155,1991, 645, 641,1606,5016,3452, 337, 72, 406,5017, 80, # 112 - 630, 238,3205,1509, 263, 939,1092,2654, 756,1440,1094,3453, 449, 69,2987, 591, # 128 - 179,2096, 471, 115,2035,1844, 60, 50,2988, 134, 806,1869, 734,2036,3454, 180, # 144 - 995,1607, 156, 537,2907, 688,5018, 319,1305, 779,2145, 514,2379, 298,4512, 359, # 160 -2502, 90,2716,1338, 663, 11, 906,1099,2553, 20,2441, 182, 532,1716,5019, 732, # 176 -1376,4204,1311,1420,3206, 25,2317,1056, 113, 399, 382,1950, 242,3455,2474, 529, # 192 -3276, 475,1447,3683,5020, 117, 21, 656, 810,1297,2300,2334,3557,5021, 126,4205, # 208 - 706, 456, 150, 613,4513, 71,1118,2037,4206, 145,3092, 85, 835, 486,2115,1246, # 224 -1426, 428, 727,1285,1015, 800, 106, 623, 303,1281,5022,2128,2359, 347,3815, 221, # 240 -3558,3135,5023,1956,1153,4207, 83, 296,1199,3093, 192, 624, 93,5024, 822,1898, # 256 -2823,3136, 795,2065, 991,1554,1542,1592, 27, 43,2867, 859, 139,1456, 860,4514, # 272 - 437, 712,3974, 164,2397,3137, 695, 211,3037,2097, 195,3975,1608,3559,3560,3684, # 288 -3976, 234, 811,2989,2098,3977,2233,1441,3561,1615,2380, 668,2077,1638, 305, 228, # 304 -1664,4515, 467, 415,5025, 262,2099,1593, 239, 108, 300, 200,1033, 512,1247,2078, # 320 -5026,5027,2176,3207,3685,2682, 593, 845,1062,3277, 88,1723,2038,3978,1951, 212, # 336 - 266, 152, 149, 468,1899,4208,4516, 77, 187,5028,3038, 37, 5,2990,5029,3979, # 352 -5030,5031, 39,2524,4517,2908,3208,2079, 55, 148, 74,4518, 545, 483,1474,1029, # 368 -1665, 217,1870,1531,3138,1104,2655,4209, 24, 172,3562, 900,3980,3563,3564,4519, # 384 - 32,1408,2824,1312, 329, 487,2360,2251,2717, 784,2683, 4,3039,3351,1427,1789, # 400 - 188, 109, 499,5032,3686,1717,1790, 888,1217,3040,4520,5033,3565,5034,3352,1520, # 416 -3687,3981, 196,1034, 775,5035,5036, 929,1816, 249, 439, 38,5037,1063,5038, 794, # 432 -3982,1435,2301, 46, 178,3278,2066,5039,2381,5040, 214,1709,4521, 804, 35, 707, # 448 - 324,3688,1601,2554, 140, 459,4210,5041,5042,1365, 839, 272, 978,2262,2580,3456, # 464 -2129,1363,3689,1423, 697, 100,3094, 48, 70,1231, 495,3139,2196,5043,1294,5044, # 480 -2080, 462, 586,1042,3279, 853, 256, 988, 185,2382,3457,1698, 434,1084,5045,3458, # 496 - 314,2625,2788,4522,2335,2336, 569,2285, 637,1817,2525, 757,1162,1879,1616,3459, # 512 - 287,1577,2116, 768,4523,1671,2868,3566,2526,1321,3816, 909,2418,5046,4211, 933, # 528 -3817,4212,2053,2361,1222,4524, 765,2419,1322, 786,4525,5047,1920,1462,1677,2909, # 544 -1699,5048,4526,1424,2442,3140,3690,2600,3353,1775,1941,3460,3983,4213, 309,1369, # 560 -1130,2825, 364,2234,1653,1299,3984,3567,3985,3986,2656, 525,1085,3041, 902,2001, # 576 -1475, 964,4527, 421,1845,1415,1057,2286, 940,1364,3141, 376,4528,4529,1381, 7, # 592 -2527, 983,2383, 336,1710,2684,1846, 321,3461, 559,1131,3042,2752,1809,1132,1313, # 608 - 265,1481,1858,5049, 352,1203,2826,3280, 167,1089, 420,2827, 776, 792,1724,3568, # 624 -4214,2443,3281,5050,4215,5051, 446, 229, 333,2753, 901,3818,1200,1557,4530,2657, # 640 -1921, 395,2754,2685,3819,4216,1836, 125, 916,3209,2626,4531,5052,5053,3820,5054, # 656 -5055,5056,4532,3142,3691,1133,2555,1757,3462,1510,2318,1409,3569,5057,2146, 438, # 672 -2601,2910,2384,3354,1068, 958,3043, 461, 311,2869,2686,4217,1916,3210,4218,1979, # 688 - 383, 750,2755,2627,4219, 274, 539, 385,1278,1442,5058,1154,1965, 384, 561, 210, # 704 - 98,1295,2556,3570,5059,1711,2420,1482,3463,3987,2911,1257, 129,5060,3821, 642, # 720 - 523,2789,2790,2658,5061, 141,2235,1333, 68, 176, 441, 876, 907,4220, 603,2602, # 736 - 710, 171,3464, 404, 549, 18,3143,2398,1410,3692,1666,5062,3571,4533,2912,4534, # 752 -5063,2991, 368,5064, 146, 366, 99, 871,3693,1543, 748, 807,1586,1185, 22,2263, # 768 - 379,3822,3211,5065,3212, 505,1942,2628,1992,1382,2319,5066, 380,2362, 218, 702, # 784 -1818,1248,3465,3044,3572,3355,3282,5067,2992,3694, 930,3283,3823,5068, 59,5069, # 800 - 585, 601,4221, 497,3466,1112,1314,4535,1802,5070,1223,1472,2177,5071, 749,1837, # 816 - 690,1900,3824,1773,3988,1476, 429,1043,1791,2236,2117, 917,4222, 447,1086,1629, # 832 -5072, 556,5073,5074,2021,1654, 844,1090, 105, 550, 966,1758,2828,1008,1783, 686, # 848 -1095,5075,2287, 793,1602,5076,3573,2603,4536,4223,2948,2302,4537,3825, 980,2503, # 864 - 544, 353, 527,4538, 908,2687,2913,5077, 381,2629,1943,1348,5078,1341,1252, 560, # 880 -3095,5079,3467,2870,5080,2054, 973, 886,2081, 143,4539,5081,5082, 157,3989, 496, # 896 -4224, 57, 840, 540,2039,4540,4541,3468,2118,1445, 970,2264,1748,1966,2082,4225, # 912 -3144,1234,1776,3284,2829,3695, 773,1206,2130,1066,2040,1326,3990,1738,1725,4226, # 928 - 279,3145, 51,1544,2604, 423,1578,2131,2067, 173,4542,1880,5083,5084,1583, 264, # 944 - 610,3696,4543,2444, 280, 154,5085,5086,5087,1739, 338,1282,3096, 693,2871,1411, # 960 -1074,3826,2445,5088,4544,5089,5090,1240, 952,2399,5091,2914,1538,2688, 685,1483, # 976 -4227,2475,1436, 953,4228,2055,4545, 671,2400, 79,4229,2446,3285, 608, 567,2689, # 992 -3469,4230,4231,1691, 393,1261,1792,2401,5092,4546,5093,5094,5095,5096,1383,1672, # 1008 -3827,3213,1464, 522,1119, 661,1150, 216, 675,4547,3991,1432,3574, 609,4548,2690, # 1024 -2402,5097,5098,5099,4232,3045, 0,5100,2476, 315, 231,2447, 301,3356,4549,2385, # 1040 -5101, 233,4233,3697,1819,4550,4551,5102, 96,1777,1315,2083,5103, 257,5104,1810, # 1056 -3698,2718,1139,1820,4234,2022,1124,2164,2791,1778,2659,5105,3097, 363,1655,3214, # 1072 -5106,2993,5107,5108,5109,3992,1567,3993, 718, 103,3215, 849,1443, 341,3357,2949, # 1088 -1484,5110,1712, 127, 67, 339,4235,2403, 679,1412, 821,5111,5112, 834, 738, 351, # 1104 -2994,2147, 846, 235,1497,1881, 418,1993,3828,2719, 186,1100,2148,2756,3575,1545, # 1120 -1355,2950,2872,1377, 583,3994,4236,2581,2995,5113,1298,3699,1078,2557,3700,2363, # 1136 - 78,3829,3830, 267,1289,2100,2002,1594,4237, 348, 369,1274,2197,2178,1838,4552, # 1152 -1821,2830,3701,2757,2288,2003,4553,2951,2758, 144,3358, 882,4554,3995,2759,3470, # 1168 -4555,2915,5114,4238,1726, 320,5115,3996,3046, 788,2996,5116,2831,1774,1327,2873, # 1184 -3997,2832,5117,1306,4556,2004,1700,3831,3576,2364,2660, 787,2023, 506, 824,3702, # 1200 - 534, 323,4557,1044,3359,2024,1901, 946,3471,5118,1779,1500,1678,5119,1882,4558, # 1216 - 165, 243,4559,3703,2528, 123, 683,4239, 764,4560, 36,3998,1793, 589,2916, 816, # 1232 - 626,1667,3047,2237,1639,1555,1622,3832,3999,5120,4000,2874,1370,1228,1933, 891, # 1248 -2084,2917, 304,4240,5121, 292,2997,2720,3577, 691,2101,4241,1115,4561, 118, 662, # 1264 -5122, 611,1156, 854,2386,1316,2875, 2, 386, 515,2918,5123,5124,3286, 868,2238, # 1280 -1486, 855,2661, 785,2216,3048,5125,1040,3216,3578,5126,3146, 448,5127,1525,5128, # 1296 -2165,4562,5129,3833,5130,4242,2833,3579,3147, 503, 818,4001,3148,1568, 814, 676, # 1312 -1444, 306,1749,5131,3834,1416,1030, 197,1428, 805,2834,1501,4563,5132,5133,5134, # 1328 -1994,5135,4564,5136,5137,2198, 13,2792,3704,2998,3149,1229,1917,5138,3835,2132, # 1344 -5139,4243,4565,2404,3580,5140,2217,1511,1727,1120,5141,5142, 646,3836,2448, 307, # 1360 -5143,5144,1595,3217,5145,5146,5147,3705,1113,1356,4002,1465,2529,2530,5148, 519, # 1376 -5149, 128,2133, 92,2289,1980,5150,4003,1512, 342,3150,2199,5151,2793,2218,1981, # 1392 -3360,4244, 290,1656,1317, 789, 827,2365,5152,3837,4566, 562, 581,4004,5153, 401, # 1408 -4567,2252, 94,4568,5154,1399,2794,5155,1463,2025,4569,3218,1944,5156, 828,1105, # 1424 -4245,1262,1394,5157,4246, 605,4570,5158,1784,2876,5159,2835, 819,2102, 578,2200, # 1440 -2952,5160,1502, 436,3287,4247,3288,2836,4005,2919,3472,3473,5161,2721,2320,5162, # 1456 -5163,2337,2068, 23,4571, 193, 826,3838,2103, 699,1630,4248,3098, 390,1794,1064, # 1472 -3581,5164,1579,3099,3100,1400,5165,4249,1839,1640,2877,5166,4572,4573, 137,4250, # 1488 - 598,3101,1967, 780, 104, 974,2953,5167, 278, 899, 253, 402, 572, 504, 493,1339, # 1504 -5168,4006,1275,4574,2582,2558,5169,3706,3049,3102,2253, 565,1334,2722, 863, 41, # 1520 -5170,5171,4575,5172,1657,2338, 19, 463,2760,4251, 606,5173,2999,3289,1087,2085, # 1536 -1323,2662,3000,5174,1631,1623,1750,4252,2691,5175,2878, 791,2723,2663,2339, 232, # 1552 -2421,5176,3001,1498,5177,2664,2630, 755,1366,3707,3290,3151,2026,1609, 119,1918, # 1568 -3474, 862,1026,4253,5178,4007,3839,4576,4008,4577,2265,1952,2477,5179,1125, 817, # 1584 -4254,4255,4009,1513,1766,2041,1487,4256,3050,3291,2837,3840,3152,5180,5181,1507, # 1600 -5182,2692, 733, 40,1632,1106,2879, 345,4257, 841,2531, 230,4578,3002,1847,3292, # 1616 -3475,5183,1263, 986,3476,5184, 735, 879, 254,1137, 857, 622,1300,1180,1388,1562, # 1632 -4010,4011,2954, 967,2761,2665,1349, 592,2134,1692,3361,3003,1995,4258,1679,4012, # 1648 -1902,2188,5185, 739,3708,2724,1296,1290,5186,4259,2201,2202,1922,1563,2605,2559, # 1664 -1871,2762,3004,5187, 435,5188, 343,1108, 596, 17,1751,4579,2239,3477,3709,5189, # 1680 -4580, 294,3582,2955,1693, 477, 979, 281,2042,3583, 643,2043,3710,2631,2795,2266, # 1696 -1031,2340,2135,2303,3584,4581, 367,1249,2560,5190,3585,5191,4582,1283,3362,2005, # 1712 - 240,1762,3363,4583,4584, 836,1069,3153, 474,5192,2149,2532, 268,3586,5193,3219, # 1728 -1521,1284,5194,1658,1546,4260,5195,3587,3588,5196,4261,3364,2693,1685,4262, 961, # 1744 -1673,2632, 190,2006,2203,3841,4585,4586,5197, 570,2504,3711,1490,5198,4587,2633, # 1760 -3293,1957,4588, 584,1514, 396,1045,1945,5199,4589,1968,2449,5200,5201,4590,4013, # 1776 - 619,5202,3154,3294, 215,2007,2796,2561,3220,4591,3221,4592, 763,4263,3842,4593, # 1792 -5203,5204,1958,1767,2956,3365,3712,1174, 452,1477,4594,3366,3155,5205,2838,1253, # 1808 -2387,2189,1091,2290,4264, 492,5206, 638,1169,1825,2136,1752,4014, 648, 926,1021, # 1824 -1324,4595, 520,4596, 997, 847,1007, 892,4597,3843,2267,1872,3713,2405,1785,4598, # 1840 -1953,2957,3103,3222,1728,4265,2044,3714,4599,2008,1701,3156,1551, 30,2268,4266, # 1856 -5207,2027,4600,3589,5208, 501,5209,4267, 594,3478,2166,1822,3590,3479,3591,3223, # 1872 - 829,2839,4268,5210,1680,3157,1225,4269,5211,3295,4601,4270,3158,2341,5212,4602, # 1888 -4271,5213,4015,4016,5214,1848,2388,2606,3367,5215,4603, 374,4017, 652,4272,4273, # 1904 - 375,1140, 798,5216,5217,5218,2366,4604,2269, 546,1659, 138,3051,2450,4605,5219, # 1920 -2254, 612,1849, 910, 796,3844,1740,1371, 825,3845,3846,5220,2920,2562,5221, 692, # 1936 - 444,3052,2634, 801,4606,4274,5222,1491, 244,1053,3053,4275,4276, 340,5223,4018, # 1952 -1041,3005, 293,1168, 87,1357,5224,1539, 959,5225,2240, 721, 694,4277,3847, 219, # 1968 -1478, 644,1417,3368,2666,1413,1401,1335,1389,4019,5226,5227,3006,2367,3159,1826, # 1984 - 730,1515, 184,2840, 66,4607,5228,1660,2958, 246,3369, 378,1457, 226,3480, 975, # 2000 -4020,2959,1264,3592, 674, 696,5229, 163,5230,1141,2422,2167, 713,3593,3370,4608, # 2016 -4021,5231,5232,1186, 15,5233,1079,1070,5234,1522,3224,3594, 276,1050,2725, 758, # 2032 -1126, 653,2960,3296,5235,2342, 889,3595,4022,3104,3007, 903,1250,4609,4023,3481, # 2048 -3596,1342,1681,1718, 766,3297, 286, 89,2961,3715,5236,1713,5237,2607,3371,3008, # 2064 -5238,2962,2219,3225,2880,5239,4610,2505,2533, 181, 387,1075,4024, 731,2190,3372, # 2080 -5240,3298, 310, 313,3482,2304, 770,4278, 54,3054, 189,4611,3105,3848,4025,5241, # 2096 -1230,1617,1850, 355,3597,4279,4612,3373, 111,4280,3716,1350,3160,3483,3055,4281, # 2112 -2150,3299,3598,5242,2797,4026,4027,3009, 722,2009,5243,1071, 247,1207,2343,2478, # 2128 -1378,4613,2010, 864,1437,1214,4614, 373,3849,1142,2220, 667,4615, 442,2763,2563, # 2144 -3850,4028,1969,4282,3300,1840, 837, 170,1107, 934,1336,1883,5244,5245,2119,4283, # 2160 -2841, 743,1569,5246,4616,4284, 582,2389,1418,3484,5247,1803,5248, 357,1395,1729, # 2176 -3717,3301,2423,1564,2241,5249,3106,3851,1633,4617,1114,2086,4285,1532,5250, 482, # 2192 -2451,4618,5251,5252,1492, 833,1466,5253,2726,3599,1641,2842,5254,1526,1272,3718, # 2208 -4286,1686,1795, 416,2564,1903,1954,1804,5255,3852,2798,3853,1159,2321,5256,2881, # 2224 -4619,1610,1584,3056,2424,2764, 443,3302,1163,3161,5257,5258,4029,5259,4287,2506, # 2240 -3057,4620,4030,3162,2104,1647,3600,2011,1873,4288,5260,4289, 431,3485,5261, 250, # 2256 - 97, 81,4290,5262,1648,1851,1558, 160, 848,5263, 866, 740,1694,5264,2204,2843, # 2272 -3226,4291,4621,3719,1687, 950,2479, 426, 469,3227,3720,3721,4031,5265,5266,1188, # 2288 - 424,1996, 861,3601,4292,3854,2205,2694, 168,1235,3602,4293,5267,2087,1674,4622, # 2304 -3374,3303, 220,2565,1009,5268,3855, 670,3010, 332,1208, 717,5269,5270,3603,2452, # 2320 -4032,3375,5271, 513,5272,1209,2882,3376,3163,4623,1080,5273,5274,5275,5276,2534, # 2336 -3722,3604, 815,1587,4033,4034,5277,3605,3486,3856,1254,4624,1328,3058,1390,4035, # 2352 -1741,4036,3857,4037,5278, 236,3858,2453,3304,5279,5280,3723,3859,1273,3860,4625, # 2368 -5281, 308,5282,4626, 245,4627,1852,2480,1307,2583, 430, 715,2137,2454,5283, 270, # 2384 - 199,2883,4038,5284,3606,2727,1753, 761,1754, 725,1661,1841,4628,3487,3724,5285, # 2400 -5286, 587, 14,3305, 227,2608, 326, 480,2270, 943,2765,3607, 291, 650,1884,5287, # 2416 -1702,1226, 102,1547, 62,3488, 904,4629,3489,1164,4294,5288,5289,1224,1548,2766, # 2432 - 391, 498,1493,5290,1386,1419,5291,2056,1177,4630, 813, 880,1081,2368, 566,1145, # 2448 -4631,2291,1001,1035,2566,2609,2242, 394,1286,5292,5293,2069,5294, 86,1494,1730, # 2464 -4039, 491,1588, 745, 897,2963, 843,3377,4040,2767,2884,3306,1768, 998,2221,2070, # 2480 - 397,1827,1195,1970,3725,3011,3378, 284,5295,3861,2507,2138,2120,1904,5296,4041, # 2496 -2151,4042,4295,1036,3490,1905, 114,2567,4296, 209,1527,5297,5298,2964,2844,2635, # 2512 -2390,2728,3164, 812,2568,5299,3307,5300,1559, 737,1885,3726,1210, 885, 28,2695, # 2528 -3608,3862,5301,4297,1004,1780,4632,5302, 346,1982,2222,2696,4633,3863,1742, 797, # 2544 -1642,4043,1934,1072,1384,2152, 896,4044,3308,3727,3228,2885,3609,5303,2569,1959, # 2560 -4634,2455,1786,5304,5305,5306,4045,4298,1005,1308,3728,4299,2729,4635,4636,1528, # 2576 -2610, 161,1178,4300,1983, 987,4637,1101,4301, 631,4046,1157,3229,2425,1343,1241, # 2592 -1016,2243,2570, 372, 877,2344,2508,1160, 555,1935, 911,4047,5307, 466,1170, 169, # 2608 -1051,2921,2697,3729,2481,3012,1182,2012,2571,1251,2636,5308, 992,2345,3491,1540, # 2624 -2730,1201,2071,2406,1997,2482,5309,4638, 528,1923,2191,1503,1874,1570,2369,3379, # 2640 -3309,5310, 557,1073,5311,1828,3492,2088,2271,3165,3059,3107, 767,3108,2799,4639, # 2656 -1006,4302,4640,2346,1267,2179,3730,3230, 778,4048,3231,2731,1597,2667,5312,4641, # 2672 -5313,3493,5314,5315,5316,3310,2698,1433,3311, 131, 95,1504,4049, 723,4303,3166, # 2688 -1842,3610,2768,2192,4050,2028,2105,3731,5317,3013,4051,1218,5318,3380,3232,4052, # 2704 -4304,2584, 248,1634,3864, 912,5319,2845,3732,3060,3865, 654, 53,5320,3014,5321, # 2720 -1688,4642, 777,3494,1032,4053,1425,5322, 191, 820,2121,2846, 971,4643, 931,3233, # 2736 - 135, 664, 783,3866,1998, 772,2922,1936,4054,3867,4644,2923,3234, 282,2732, 640, # 2752 -1372,3495,1127, 922, 325,3381,5323,5324, 711,2045,5325,5326,4055,2223,2800,1937, # 2768 -4056,3382,2224,2255,3868,2305,5327,4645,3869,1258,3312,4057,3235,2139,2965,4058, # 2784 -4059,5328,2225, 258,3236,4646, 101,1227,5329,3313,1755,5330,1391,3314,5331,2924, # 2800 -2057, 893,5332,5333,5334,1402,4305,2347,5335,5336,3237,3611,5337,5338, 878,1325, # 2816 -1781,2801,4647, 259,1385,2585, 744,1183,2272,4648,5339,4060,2509,5340, 684,1024, # 2832 -4306,5341, 472,3612,3496,1165,3315,4061,4062, 322,2153, 881, 455,1695,1152,1340, # 2848 - 660, 554,2154,4649,1058,4650,4307, 830,1065,3383,4063,4651,1924,5342,1703,1919, # 2864 -5343, 932,2273, 122,5344,4652, 947, 677,5345,3870,2637, 297,1906,1925,2274,4653, # 2880 -2322,3316,5346,5347,4308,5348,4309, 84,4310, 112, 989,5349, 547,1059,4064, 701, # 2896 -3613,1019,5350,4311,5351,3497, 942, 639, 457,2306,2456, 993,2966, 407, 851, 494, # 2912 -4654,3384, 927,5352,1237,5353,2426,3385, 573,4312, 680, 921,2925,1279,1875, 285, # 2928 - 790,1448,1984, 719,2168,5354,5355,4655,4065,4066,1649,5356,1541, 563,5357,1077, # 2944 -5358,3386,3061,3498, 511,3015,4067,4068,3733,4069,1268,2572,3387,3238,4656,4657, # 2960 -5359, 535,1048,1276,1189,2926,2029,3167,1438,1373,2847,2967,1134,2013,5360,4313, # 2976 -1238,2586,3109,1259,5361, 700,5362,2968,3168,3734,4314,5363,4315,1146,1876,1907, # 2992 -4658,2611,4070, 781,2427, 132,1589, 203, 147, 273,2802,2407, 898,1787,2155,4071, # 3008 -4072,5364,3871,2803,5365,5366,4659,4660,5367,3239,5368,1635,3872, 965,5369,1805, # 3024 -2699,1516,3614,1121,1082,1329,3317,4073,1449,3873, 65,1128,2848,2927,2769,1590, # 3040 -3874,5370,5371, 12,2668, 45, 976,2587,3169,4661, 517,2535,1013,1037,3240,5372, # 3056 -3875,2849,5373,3876,5374,3499,5375,2612, 614,1999,2323,3877,3110,2733,2638,5376, # 3072 -2588,4316, 599,1269,5377,1811,3735,5378,2700,3111, 759,1060, 489,1806,3388,3318, # 3088 -1358,5379,5380,2391,1387,1215,2639,2256, 490,5381,5382,4317,1759,2392,2348,5383, # 3104 -4662,3878,1908,4074,2640,1807,3241,4663,3500,3319,2770,2349, 874,5384,5385,3501, # 3120 -3736,1859, 91,2928,3737,3062,3879,4664,5386,3170,4075,2669,5387,3502,1202,1403, # 3136 -3880,2969,2536,1517,2510,4665,3503,2511,5388,4666,5389,2701,1886,1495,1731,4076, # 3152 -2370,4667,5390,2030,5391,5392,4077,2702,1216, 237,2589,4318,2324,4078,3881,4668, # 3168 -4669,2703,3615,3504, 445,4670,5393,5394,5395,5396,2771, 61,4079,3738,1823,4080, # 3184 -5397, 687,2046, 935, 925, 405,2670, 703,1096,1860,2734,4671,4081,1877,1367,2704, # 3200 -3389, 918,2106,1782,2483, 334,3320,1611,1093,4672, 564,3171,3505,3739,3390, 945, # 3216 -2641,2058,4673,5398,1926, 872,4319,5399,3506,2705,3112, 349,4320,3740,4082,4674, # 3232 -3882,4321,3741,2156,4083,4675,4676,4322,4677,2408,2047, 782,4084, 400, 251,4323, # 3248 -1624,5400,5401, 277,3742, 299,1265, 476,1191,3883,2122,4324,4325,1109, 205,5402, # 3264 -2590,1000,2157,3616,1861,5403,5404,5405,4678,5406,4679,2573, 107,2484,2158,4085, # 3280 -3507,3172,5407,1533, 541,1301, 158, 753,4326,2886,3617,5408,1696, 370,1088,4327, # 3296 -4680,3618, 579, 327, 440, 162,2244, 269,1938,1374,3508, 968,3063, 56,1396,3113, # 3312 -2107,3321,3391,5409,1927,2159,4681,3016,5410,3619,5411,5412,3743,4682,2485,5413, # 3328 -2804,5414,1650,4683,5415,2613,5416,5417,4086,2671,3392,1149,3393,4087,3884,4088, # 3344 -5418,1076, 49,5419, 951,3242,3322,3323, 450,2850, 920,5420,1812,2805,2371,4328, # 3360 -1909,1138,2372,3885,3509,5421,3243,4684,1910,1147,1518,2428,4685,3886,5422,4686, # 3376 -2393,2614, 260,1796,3244,5423,5424,3887,3324, 708,5425,3620,1704,5426,3621,1351, # 3392 -1618,3394,3017,1887, 944,4329,3395,4330,3064,3396,4331,5427,3744, 422, 413,1714, # 3408 -3325, 500,2059,2350,4332,2486,5428,1344,1911, 954,5429,1668,5430,5431,4089,2409, # 3424 -4333,3622,3888,4334,5432,2307,1318,2512,3114, 133,3115,2887,4687, 629, 31,2851, # 3440 -2706,3889,4688, 850, 949,4689,4090,2970,1732,2089,4335,1496,1853,5433,4091, 620, # 3456 -3245, 981,1242,3745,3397,1619,3746,1643,3326,2140,2457,1971,1719,3510,2169,5434, # 3472 -3246,5435,5436,3398,1829,5437,1277,4690,1565,2048,5438,1636,3623,3116,5439, 869, # 3488 -2852, 655,3890,3891,3117,4092,3018,3892,1310,3624,4691,5440,5441,5442,1733, 558, # 3504 -4692,3747, 335,1549,3065,1756,4336,3748,1946,3511,1830,1291,1192, 470,2735,2108, # 3520 -2806, 913,1054,4093,5443,1027,5444,3066,4094,4693, 982,2672,3399,3173,3512,3247, # 3536 -3248,1947,2807,5445, 571,4694,5446,1831,5447,3625,2591,1523,2429,5448,2090, 984, # 3552 -4695,3749,1960,5449,3750, 852, 923,2808,3513,3751, 969,1519, 999,2049,2325,1705, # 3568 -5450,3118, 615,1662, 151, 597,4095,2410,2326,1049, 275,4696,3752,4337, 568,3753, # 3584 -3626,2487,4338,3754,5451,2430,2275, 409,3249,5452,1566,2888,3514,1002, 769,2853, # 3600 - 194,2091,3174,3755,2226,3327,4339, 628,1505,5453,5454,1763,2180,3019,4096, 521, # 3616 -1161,2592,1788,2206,2411,4697,4097,1625,4340,4341, 412, 42,3119, 464,5455,2642, # 3632 -4698,3400,1760,1571,2889,3515,2537,1219,2207,3893,2643,2141,2373,4699,4700,3328, # 3648 -1651,3401,3627,5456,5457,3628,2488,3516,5458,3756,5459,5460,2276,2092, 460,5461, # 3664 -4701,5462,3020, 962, 588,3629, 289,3250,2644,1116, 52,5463,3067,1797,5464,5465, # 3680 -5466,1467,5467,1598,1143,3757,4342,1985,1734,1067,4702,1280,3402, 465,4703,1572, # 3696 - 510,5468,1928,2245,1813,1644,3630,5469,4704,3758,5470,5471,2673,1573,1534,5472, # 3712 -5473, 536,1808,1761,3517,3894,3175,2645,5474,5475,5476,4705,3518,2929,1912,2809, # 3728 -5477,3329,1122, 377,3251,5478, 360,5479,5480,4343,1529, 551,5481,2060,3759,1769, # 3744 -2431,5482,2930,4344,3330,3120,2327,2109,2031,4706,1404, 136,1468,1479, 672,1171, # 3760 -3252,2308, 271,3176,5483,2772,5484,2050, 678,2736, 865,1948,4707,5485,2014,4098, # 3776 -2971,5486,2737,2227,1397,3068,3760,4708,4709,1735,2931,3403,3631,5487,3895, 509, # 3792 -2854,2458,2890,3896,5488,5489,3177,3178,4710,4345,2538,4711,2309,1166,1010, 552, # 3808 - 681,1888,5490,5491,2972,2973,4099,1287,1596,1862,3179, 358, 453, 736, 175, 478, # 3824 -1117, 905,1167,1097,5492,1854,1530,5493,1706,5494,2181,3519,2292,3761,3520,3632, # 3840 -4346,2093,4347,5495,3404,1193,2489,4348,1458,2193,2208,1863,1889,1421,3331,2932, # 3856 -3069,2182,3521, 595,2123,5496,4100,5497,5498,4349,1707,2646, 223,3762,1359, 751, # 3872 -3121, 183,3522,5499,2810,3021, 419,2374, 633, 704,3897,2394, 241,5500,5501,5502, # 3888 - 838,3022,3763,2277,2773,2459,3898,1939,2051,4101,1309,3122,2246,1181,5503,1136, # 3904 -2209,3899,2375,1446,4350,2310,4712,5504,5505,4351,1055,2615, 484,3764,5506,4102, # 3920 - 625,4352,2278,3405,1499,4353,4103,5507,4104,4354,3253,2279,2280,3523,5508,5509, # 3936 -2774, 808,2616,3765,3406,4105,4355,3123,2539, 526,3407,3900,4356, 955,5510,1620, # 3952 -4357,2647,2432,5511,1429,3766,1669,1832, 994, 928,5512,3633,1260,5513,5514,5515, # 3968 -1949,2293, 741,2933,1626,4358,2738,2460, 867,1184, 362,3408,1392,5516,5517,4106, # 3984 -4359,1770,1736,3254,2934,4713,4714,1929,2707,1459,1158,5518,3070,3409,2891,1292, # 4000 -1930,2513,2855,3767,1986,1187,2072,2015,2617,4360,5519,2574,2514,2170,3768,2490, # 4016 -3332,5520,3769,4715,5521,5522, 666,1003,3023,1022,3634,4361,5523,4716,1814,2257, # 4032 - 574,3901,1603, 295,1535, 705,3902,4362, 283, 858, 417,5524,5525,3255,4717,4718, # 4048 -3071,1220,1890,1046,2281,2461,4107,1393,1599, 689,2575, 388,4363,5526,2491, 802, # 4064 -5527,2811,3903,2061,1405,2258,5528,4719,3904,2110,1052,1345,3256,1585,5529, 809, # 4080 -5530,5531,5532, 575,2739,3524, 956,1552,1469,1144,2328,5533,2329,1560,2462,3635, # 4096 -3257,4108, 616,2210,4364,3180,2183,2294,5534,1833,5535,3525,4720,5536,1319,3770, # 4112 -3771,1211,3636,1023,3258,1293,2812,5537,5538,5539,3905, 607,2311,3906, 762,2892, # 4128 -1439,4365,1360,4721,1485,3072,5540,4722,1038,4366,1450,2062,2648,4367,1379,4723, # 4144 -2593,5541,5542,4368,1352,1414,2330,2935,1172,5543,5544,3907,3908,4724,1798,1451, # 4160 -5545,5546,5547,5548,2936,4109,4110,2492,2351, 411,4111,4112,3637,3333,3124,4725, # 4176 -1561,2674,1452,4113,1375,5549,5550, 47,2974, 316,5551,1406,1591,2937,3181,5552, # 4192 -1025,2142,3125,3182, 354,2740, 884,2228,4369,2412, 508,3772, 726,3638, 996,2433, # 4208 -3639, 729,5553, 392,2194,1453,4114,4726,3773,5554,5555,2463,3640,2618,1675,2813, # 4224 - 919,2352,2975,2353,1270,4727,4115, 73,5556,5557, 647,5558,3259,2856,2259,1550, # 4240 -1346,3024,5559,1332, 883,3526,5560,5561,5562,5563,3334,2775,5564,1212, 831,1347, # 4256 -4370,4728,2331,3909,1864,3073, 720,3910,4729,4730,3911,5565,4371,5566,5567,4731, # 4272 -5568,5569,1799,4732,3774,2619,4733,3641,1645,2376,4734,5570,2938, 669,2211,2675, # 4288 -2434,5571,2893,5572,5573,1028,3260,5574,4372,2413,5575,2260,1353,5576,5577,4735, # 4304 -3183, 518,5578,4116,5579,4373,1961,5580,2143,4374,5581,5582,3025,2354,2355,3912, # 4320 - 516,1834,1454,4117,2708,4375,4736,2229,2620,1972,1129,3642,5583,2776,5584,2976, # 4336 -1422, 577,1470,3026,1524,3410,5585,5586, 432,4376,3074,3527,5587,2594,1455,2515, # 4352 -2230,1973,1175,5588,1020,2741,4118,3528,4737,5589,2742,5590,1743,1361,3075,3529, # 4368 -2649,4119,4377,4738,2295, 895, 924,4378,2171, 331,2247,3076, 166,1627,3077,1098, # 4384 -5591,1232,2894,2231,3411,4739, 657, 403,1196,2377, 542,3775,3412,1600,4379,3530, # 4400 -5592,4740,2777,3261, 576, 530,1362,4741,4742,2540,2676,3776,4120,5593, 842,3913, # 4416 -5594,2814,2032,1014,4121, 213,2709,3413, 665, 621,4380,5595,3777,2939,2435,5596, # 4432 -2436,3335,3643,3414,4743,4381,2541,4382,4744,3644,1682,4383,3531,1380,5597, 724, # 4448 -2282, 600,1670,5598,1337,1233,4745,3126,2248,5599,1621,4746,5600, 651,4384,5601, # 4464 -1612,4385,2621,5602,2857,5603,2743,2312,3078,5604, 716,2464,3079, 174,1255,2710, # 4480 -4122,3645, 548,1320,1398, 728,4123,1574,5605,1891,1197,3080,4124,5606,3081,3082, # 4496 -3778,3646,3779, 747,5607, 635,4386,4747,5608,5609,5610,4387,5611,5612,4748,5613, # 4512 -3415,4749,2437, 451,5614,3780,2542,2073,4388,2744,4389,4125,5615,1764,4750,5616, # 4528 -4390, 350,4751,2283,2395,2493,5617,4391,4126,2249,1434,4127, 488,4752, 458,4392, # 4544 -4128,3781, 771,1330,2396,3914,2576,3184,2160,2414,1553,2677,3185,4393,5618,2494, # 4560 -2895,2622,1720,2711,4394,3416,4753,5619,2543,4395,5620,3262,4396,2778,5621,2016, # 4576 -2745,5622,1155,1017,3782,3915,5623,3336,2313, 201,1865,4397,1430,5624,4129,5625, # 4592 -5626,5627,5628,5629,4398,1604,5630, 414,1866, 371,2595,4754,4755,3532,2017,3127, # 4608 -4756,1708, 960,4399, 887, 389,2172,1536,1663,1721,5631,2232,4130,2356,2940,1580, # 4624 -5632,5633,1744,4757,2544,4758,4759,5634,4760,5635,2074,5636,4761,3647,3417,2896, # 4640 -4400,5637,4401,2650,3418,2815, 673,2712,2465, 709,3533,4131,3648,4402,5638,1148, # 4656 - 502, 634,5639,5640,1204,4762,3649,1575,4763,2623,3783,5641,3784,3128, 948,3263, # 4672 - 121,1745,3916,1110,5642,4403,3083,2516,3027,4132,3785,1151,1771,3917,1488,4133, # 4688 -1987,5643,2438,3534,5644,5645,2094,5646,4404,3918,1213,1407,2816, 531,2746,2545, # 4704 -3264,1011,1537,4764,2779,4405,3129,1061,5647,3786,3787,1867,2897,5648,2018, 120, # 4720 -4406,4407,2063,3650,3265,2314,3919,2678,3419,1955,4765,4134,5649,3535,1047,2713, # 4736 -1266,5650,1368,4766,2858, 649,3420,3920,2546,2747,1102,2859,2679,5651,5652,2000, # 4752 -5653,1111,3651,2977,5654,2495,3921,3652,2817,1855,3421,3788,5655,5656,3422,2415, # 4768 -2898,3337,3266,3653,5657,2577,5658,3654,2818,4135,1460, 856,5659,3655,5660,2899, # 4784 -2978,5661,2900,3922,5662,4408, 632,2517, 875,3923,1697,3924,2296,5663,5664,4767, # 4800 -3028,1239, 580,4768,4409,5665, 914, 936,2075,1190,4136,1039,2124,5666,5667,5668, # 4816 -5669,3423,1473,5670,1354,4410,3925,4769,2173,3084,4137, 915,3338,4411,4412,3339, # 4832 -1605,1835,5671,2748, 398,3656,4413,3926,4138, 328,1913,2860,4139,3927,1331,4414, # 4848 -3029, 937,4415,5672,3657,4140,4141,3424,2161,4770,3425, 524, 742, 538,3085,1012, # 4864 -5673,5674,3928,2466,5675, 658,1103, 225,3929,5676,5677,4771,5678,4772,5679,3267, # 4880 -1243,5680,4142, 963,2250,4773,5681,2714,3658,3186,5682,5683,2596,2332,5684,4774, # 4896 -5685,5686,5687,3536, 957,3426,2547,2033,1931,2941,2467, 870,2019,3659,1746,2780, # 4912 -2781,2439,2468,5688,3930,5689,3789,3130,3790,3537,3427,3791,5690,1179,3086,5691, # 4928 -3187,2378,4416,3792,2548,3188,3131,2749,4143,5692,3428,1556,2549,2297, 977,2901, # 4944 -2034,4144,1205,3429,5693,1765,3430,3189,2125,1271, 714,1689,4775,3538,5694,2333, # 4960 -3931, 533,4417,3660,2184, 617,5695,2469,3340,3539,2315,5696,5697,3190,5698,5699, # 4976 -3932,1988, 618, 427,2651,3540,3431,5700,5701,1244,1690,5702,2819,4418,4776,5703, # 4992 -3541,4777,5704,2284,1576, 473,3661,4419,3432, 972,5705,3662,5706,3087,5707,5708, # 5008 -4778,4779,5709,3793,4145,4146,5710, 153,4780, 356,5711,1892,2902,4420,2144, 408, # 5024 - 803,2357,5712,3933,5713,4421,1646,2578,2518,4781,4782,3934,5714,3935,4422,5715, # 5040 -2416,3433, 752,5716,5717,1962,3341,2979,5718, 746,3030,2470,4783,4423,3794, 698, # 5056 -4784,1893,4424,3663,2550,4785,3664,3936,5719,3191,3434,5720,1824,1302,4147,2715, # 5072 -3937,1974,4425,5721,4426,3192, 823,1303,1288,1236,2861,3542,4148,3435, 774,3938, # 5088 -5722,1581,4786,1304,2862,3939,4787,5723,2440,2162,1083,3268,4427,4149,4428, 344, # 5104 -1173, 288,2316, 454,1683,5724,5725,1461,4788,4150,2597,5726,5727,4789, 985, 894, # 5120 -5728,3436,3193,5729,1914,2942,3795,1989,5730,2111,1975,5731,4151,5732,2579,1194, # 5136 - 425,5733,4790,3194,1245,3796,4429,5734,5735,2863,5736, 636,4791,1856,3940, 760, # 5152 -1800,5737,4430,2212,1508,4792,4152,1894,1684,2298,5738,5739,4793,4431,4432,2213, # 5168 - 479,5740,5741, 832,5742,4153,2496,5743,2980,2497,3797, 990,3132, 627,1815,2652, # 5184 -4433,1582,4434,2126,2112,3543,4794,5744, 799,4435,3195,5745,4795,2113,1737,3031, # 5200 -1018, 543, 754,4436,3342,1676,4796,4797,4154,4798,1489,5746,3544,5747,2624,2903, # 5216 -4155,5748,5749,2981,5750,5751,5752,5753,3196,4799,4800,2185,1722,5754,3269,3270, # 5232 -1843,3665,1715, 481, 365,1976,1857,5755,5756,1963,2498,4801,5757,2127,3666,3271, # 5248 - 433,1895,2064,2076,5758, 602,2750,5759,5760,5761,5762,5763,3032,1628,3437,5764, # 5264 -3197,4802,4156,2904,4803,2519,5765,2551,2782,5766,5767,5768,3343,4804,2905,5769, # 5280 -4805,5770,2864,4806,4807,1221,2982,4157,2520,5771,5772,5773,1868,1990,5774,5775, # 5296 -5776,1896,5777,5778,4808,1897,4158, 318,5779,2095,4159,4437,5780,5781, 485,5782, # 5312 - 938,3941, 553,2680, 116,5783,3942,3667,5784,3545,2681,2783,3438,3344,2820,5785, # 5328 -3668,2943,4160,1747,2944,2983,5786,5787, 207,5788,4809,5789,4810,2521,5790,3033, # 5344 - 890,3669,3943,5791,1878,3798,3439,5792,2186,2358,3440,1652,5793,5794,5795, 941, # 5360 -2299, 208,3546,4161,2020, 330,4438,3944,2906,2499,3799,4439,4811,5796,5797,5798, # 5376 -) -# fmt: on diff --git a/libs/chardet/big5prober.py b/libs/chardet/big5prober.py deleted file mode 100644 index ef09c60e3..000000000 --- a/libs/chardet/big5prober.py +++ /dev/null @@ -1,47 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .chardistribution import Big5DistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import BIG5_SM_MODEL - - -class Big5Prober(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(BIG5_SM_MODEL) - self.distribution_analyzer = Big5DistributionAnalysis() - self.reset() - - @property - def charset_name(self) -> str: - return "Big5" - - @property - def language(self) -> str: - return "Chinese" diff --git a/libs/chardet/chardistribution.py b/libs/chardet/chardistribution.py deleted file mode 100644 index 176cb9964..000000000 --- a/libs/chardet/chardistribution.py +++ /dev/null @@ -1,261 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Tuple, Union - -from .big5freq import ( - BIG5_CHAR_TO_FREQ_ORDER, - BIG5_TABLE_SIZE, - BIG5_TYPICAL_DISTRIBUTION_RATIO, -) -from .euckrfreq import ( - EUCKR_CHAR_TO_FREQ_ORDER, - EUCKR_TABLE_SIZE, - EUCKR_TYPICAL_DISTRIBUTION_RATIO, -) -from .euctwfreq import ( - EUCTW_CHAR_TO_FREQ_ORDER, - EUCTW_TABLE_SIZE, - EUCTW_TYPICAL_DISTRIBUTION_RATIO, -) -from .gb2312freq import ( - GB2312_CHAR_TO_FREQ_ORDER, - GB2312_TABLE_SIZE, - GB2312_TYPICAL_DISTRIBUTION_RATIO, -) -from .jisfreq import ( - JIS_CHAR_TO_FREQ_ORDER, - JIS_TABLE_SIZE, - JIS_TYPICAL_DISTRIBUTION_RATIO, -) -from .johabfreq import JOHAB_TO_EUCKR_ORDER_TABLE - - -class CharDistributionAnalysis: - ENOUGH_DATA_THRESHOLD = 1024 - SURE_YES = 0.99 - SURE_NO = 0.01 - MINIMUM_DATA_THRESHOLD = 3 - - def __init__(self) -> None: - # Mapping table to get frequency order from char order (get from - # GetOrder()) - self._char_to_freq_order: Tuple[int, ...] = tuple() - self._table_size = 0 # Size of above table - # This is a constant value which varies from language to language, - # used in calculating confidence. See - # http://www.mozilla.org/projects/intl/UniversalCharsetDetection.html - # for further detail. - self.typical_distribution_ratio = 0.0 - self._done = False - self._total_chars = 0 - self._freq_chars = 0 - self.reset() - - def reset(self) -> None: - """reset analyser, clear any state""" - # If this flag is set to True, detection is done and conclusion has - # been made - self._done = False - self._total_chars = 0 # Total characters encountered - # The number of characters whose frequency order is less than 512 - self._freq_chars = 0 - - def feed(self, char: Union[bytes, bytearray], char_len: int) -> None: - """feed a character with known length""" - if char_len == 2: - # we only care about 2-bytes character in our distribution analysis - order = self.get_order(char) - else: - order = -1 - if order >= 0: - self._total_chars += 1 - # order is valid - if order < self._table_size: - if 512 > self._char_to_freq_order[order]: - self._freq_chars += 1 - - def get_confidence(self) -> float: - """return confidence based on existing data""" - # if we didn't receive any character in our consideration range, - # return negative answer - if self._total_chars <= 0 or self._freq_chars <= self.MINIMUM_DATA_THRESHOLD: - return self.SURE_NO - - if self._total_chars != self._freq_chars: - r = self._freq_chars / ( - (self._total_chars - self._freq_chars) * self.typical_distribution_ratio - ) - if r < self.SURE_YES: - return r - - # normalize confidence (we don't want to be 100% sure) - return self.SURE_YES - - def got_enough_data(self) -> bool: - # It is not necessary to receive all data to draw conclusion. - # For charset detection, certain amount of data is enough - return self._total_chars > self.ENOUGH_DATA_THRESHOLD - - def get_order(self, _: Union[bytes, bytearray]) -> int: - # We do not handle characters based on the original encoding string, - # but convert this encoding string to a number, here called order. - # This allows multiple encodings of a language to share one frequency - # table. - return -1 - - -class EUCTWDistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = EUCTW_CHAR_TO_FREQ_ORDER - self._table_size = EUCTW_TABLE_SIZE - self.typical_distribution_ratio = EUCTW_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - # for euc-TW encoding, we are interested - # first byte range: 0xc4 -- 0xfe - # second byte range: 0xa1 -- 0xfe - # no validation needed here. State machine has done that - first_char = byte_str[0] - if first_char >= 0xC4: - return 94 * (first_char - 0xC4) + byte_str[1] - 0xA1 - return -1 - - -class EUCKRDistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = EUCKR_CHAR_TO_FREQ_ORDER - self._table_size = EUCKR_TABLE_SIZE - self.typical_distribution_ratio = EUCKR_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - # for euc-KR encoding, we are interested - # first byte range: 0xb0 -- 0xfe - # second byte range: 0xa1 -- 0xfe - # no validation needed here. State machine has done that - first_char = byte_str[0] - if first_char >= 0xB0: - return 94 * (first_char - 0xB0) + byte_str[1] - 0xA1 - return -1 - - -class JOHABDistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = EUCKR_CHAR_TO_FREQ_ORDER - self._table_size = EUCKR_TABLE_SIZE - self.typical_distribution_ratio = EUCKR_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - first_char = byte_str[0] - if 0x88 <= first_char < 0xD4: - code = first_char * 256 + byte_str[1] - return JOHAB_TO_EUCKR_ORDER_TABLE.get(code, -1) - return -1 - - -class GB2312DistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = GB2312_CHAR_TO_FREQ_ORDER - self._table_size = GB2312_TABLE_SIZE - self.typical_distribution_ratio = GB2312_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - # for GB2312 encoding, we are interested - # first byte range: 0xb0 -- 0xfe - # second byte range: 0xa1 -- 0xfe - # no validation needed here. State machine has done that - first_char, second_char = byte_str[0], byte_str[1] - if (first_char >= 0xB0) and (second_char >= 0xA1): - return 94 * (first_char - 0xB0) + second_char - 0xA1 - return -1 - - -class Big5DistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = BIG5_CHAR_TO_FREQ_ORDER - self._table_size = BIG5_TABLE_SIZE - self.typical_distribution_ratio = BIG5_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - # for big5 encoding, we are interested - # first byte range: 0xa4 -- 0xfe - # second byte range: 0x40 -- 0x7e , 0xa1 -- 0xfe - # no validation needed here. State machine has done that - first_char, second_char = byte_str[0], byte_str[1] - if first_char >= 0xA4: - if second_char >= 0xA1: - return 157 * (first_char - 0xA4) + second_char - 0xA1 + 63 - return 157 * (first_char - 0xA4) + second_char - 0x40 - return -1 - - -class SJISDistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = JIS_CHAR_TO_FREQ_ORDER - self._table_size = JIS_TABLE_SIZE - self.typical_distribution_ratio = JIS_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - # for sjis encoding, we are interested - # first byte range: 0x81 -- 0x9f , 0xe0 -- 0xfe - # second byte range: 0x40 -- 0x7e, 0x81 -- oxfe - # no validation needed here. State machine has done that - first_char, second_char = byte_str[0], byte_str[1] - if 0x81 <= first_char <= 0x9F: - order = 188 * (first_char - 0x81) - elif 0xE0 <= first_char <= 0xEF: - order = 188 * (first_char - 0xE0 + 31) - else: - return -1 - order = order + second_char - 0x40 - if second_char > 0x7F: - order = -1 - return order - - -class EUCJPDistributionAnalysis(CharDistributionAnalysis): - def __init__(self) -> None: - super().__init__() - self._char_to_freq_order = JIS_CHAR_TO_FREQ_ORDER - self._table_size = JIS_TABLE_SIZE - self.typical_distribution_ratio = JIS_TYPICAL_DISTRIBUTION_RATIO - - def get_order(self, byte_str: Union[bytes, bytearray]) -> int: - # for euc-JP encoding, we are interested - # first byte range: 0xa0 -- 0xfe - # second byte range: 0xa1 -- 0xfe - # no validation needed here. State machine has done that - char = byte_str[0] - if char >= 0xA0: - return 94 * (char - 0xA1) + byte_str[1] - 0xA1 - return -1 diff --git a/libs/chardet/charsetgroupprober.py b/libs/chardet/charsetgroupprober.py deleted file mode 100644 index 6def56b4a..000000000 --- a/libs/chardet/charsetgroupprober.py +++ /dev/null @@ -1,106 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import List, Optional, Union - -from .charsetprober import CharSetProber -from .enums import LanguageFilter, ProbingState - - -class CharSetGroupProber(CharSetProber): - def __init__(self, lang_filter: LanguageFilter = LanguageFilter.NONE) -> None: - super().__init__(lang_filter=lang_filter) - self._active_num = 0 - self.probers: List[CharSetProber] = [] - self._best_guess_prober: Optional[CharSetProber] = None - - def reset(self) -> None: - super().reset() - self._active_num = 0 - for prober in self.probers: - prober.reset() - prober.active = True - self._active_num += 1 - self._best_guess_prober = None - - @property - def charset_name(self) -> Optional[str]: - if not self._best_guess_prober: - self.get_confidence() - if not self._best_guess_prober: - return None - return self._best_guess_prober.charset_name - - @property - def language(self) -> Optional[str]: - if not self._best_guess_prober: - self.get_confidence() - if not self._best_guess_prober: - return None - return self._best_guess_prober.language - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - for prober in self.probers: - if not prober.active: - continue - state = prober.feed(byte_str) - if not state: - continue - if state == ProbingState.FOUND_IT: - self._best_guess_prober = prober - self._state = ProbingState.FOUND_IT - return self.state - if state == ProbingState.NOT_ME: - prober.active = False - self._active_num -= 1 - if self._active_num <= 0: - self._state = ProbingState.NOT_ME - return self.state - return self.state - - def get_confidence(self) -> float: - state = self.state - if state == ProbingState.FOUND_IT: - return 0.99 - if state == ProbingState.NOT_ME: - return 0.01 - best_conf = 0.0 - self._best_guess_prober = None - for prober in self.probers: - if not prober.active: - self.logger.debug("%s not active", prober.charset_name) - continue - conf = prober.get_confidence() - self.logger.debug( - "%s %s confidence = %s", prober.charset_name, prober.language, conf - ) - if best_conf < conf: - best_conf = conf - self._best_guess_prober = prober - if not self._best_guess_prober: - return 0.0 - return best_conf diff --git a/libs/chardet/charsetprober.py b/libs/chardet/charsetprober.py deleted file mode 100644 index a103ca113..000000000 --- a/libs/chardet/charsetprober.py +++ /dev/null @@ -1,147 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -import logging -import re -from typing import Optional, Union - -from .enums import LanguageFilter, ProbingState - -INTERNATIONAL_WORDS_PATTERN = re.compile( - b"[a-zA-Z]*[\x80-\xFF]+[a-zA-Z]*[^a-zA-Z\x80-\xFF]?" -) - - -class CharSetProber: - - SHORTCUT_THRESHOLD = 0.95 - - def __init__(self, lang_filter: LanguageFilter = LanguageFilter.NONE) -> None: - self._state = ProbingState.DETECTING - self.active = True - self.lang_filter = lang_filter - self.logger = logging.getLogger(__name__) - - def reset(self) -> None: - self._state = ProbingState.DETECTING - - @property - def charset_name(self) -> Optional[str]: - return None - - @property - def language(self) -> Optional[str]: - raise NotImplementedError - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - raise NotImplementedError - - @property - def state(self) -> ProbingState: - return self._state - - def get_confidence(self) -> float: - return 0.0 - - @staticmethod - def filter_high_byte_only(buf: Union[bytes, bytearray]) -> bytes: - buf = re.sub(b"([\x00-\x7F])+", b" ", buf) - return buf - - @staticmethod - def filter_international_words(buf: Union[bytes, bytearray]) -> bytearray: - """ - We define three types of bytes: - alphabet: english alphabets [a-zA-Z] - international: international characters [\x80-\xFF] - marker: everything else [^a-zA-Z\x80-\xFF] - The input buffer can be thought to contain a series of words delimited - by markers. This function works to filter all words that contain at - least one international character. All contiguous sequences of markers - are replaced by a single space ascii character. - This filter applies to all scripts which do not use English characters. - """ - filtered = bytearray() - - # This regex expression filters out only words that have at-least one - # international character. The word may include one marker character at - # the end. - words = INTERNATIONAL_WORDS_PATTERN.findall(buf) - - for word in words: - filtered.extend(word[:-1]) - - # If the last character in the word is a marker, replace it with a - # space as markers shouldn't affect our analysis (they are used - # similarly across all languages and may thus have similar - # frequencies). - last_char = word[-1:] - if not last_char.isalpha() and last_char < b"\x80": - last_char = b" " - filtered.extend(last_char) - - return filtered - - @staticmethod - def remove_xml_tags(buf: Union[bytes, bytearray]) -> bytes: - """ - Returns a copy of ``buf`` that retains only the sequences of English - alphabet and high byte characters that are not between <> characters. - This filter can be applied to all scripts which contain both English - characters and extended ASCII characters, but is currently only used by - ``Latin1Prober``. - """ - filtered = bytearray() - in_tag = False - prev = 0 - buf = memoryview(buf).cast("c") - - for curr, buf_char in enumerate(buf): - # Check if we're coming out of or entering an XML tag - - # https://github.com/python/typeshed/issues/8182 - if buf_char == b">": # type: ignore[comparison-overlap] - prev = curr + 1 - in_tag = False - # https://github.com/python/typeshed/issues/8182 - elif buf_char == b"<": # type: ignore[comparison-overlap] - if curr > prev and not in_tag: - # Keep everything after last non-extended-ASCII, - # non-alphabetic character - filtered.extend(buf[prev:curr]) - # Output a space to delimit stretch we kept - filtered.extend(b" ") - in_tag = True - - # If we're not in a tag... - if not in_tag: - # Keep everything after last non-extended-ASCII, non-alphabetic - # character - filtered.extend(buf[prev:]) - - return filtered diff --git a/libs/chardet/cli.py b/libs/chardet/cli.py new file mode 100644 index 000000000..1ff461954 --- /dev/null +++ b/libs/chardet/cli.py @@ -0,0 +1,150 @@ +"""Command-line interface for chardet.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import chardet +from chardet._utils import DEFAULT_MAX_BYTES, ISO_TO_LANGUAGE +from chardet.enums import EncodingEra +from chardet.pipeline import DetectionDict + +_ERA_NAMES = [e.name.lower() for e in EncodingEra if e.bit_count() == 1] + ["all"] + + +def _print_result( + result: DetectionDict, label: str, *, minimal: bool, language: bool +) -> None: + """Print a detection result to stdout.""" + if minimal: + if language: + iso = result["language"] or "und" + print(f"{result['encoding']} {iso}") + else: + print(result["encoding"]) + elif language: + iso = result["language"] or "und" + name = ISO_TO_LANGUAGE.get(iso, iso).title() + print( + f"{label}: {result['encoding']} {iso} ({name}) " + f"with confidence {result['confidence']}" + ) + else: + print(f"{label}: {result['encoding']} with confidence {result['confidence']}") + + +def main(argv: list[str] | None = None) -> None: + """Run the ``chardetect`` command-line tool. + + :param argv: Command-line arguments. Defaults to ``sys.argv[1:]``. + """ + parser = argparse.ArgumentParser(description="Detect character encoding of files.") + parser.add_argument("files", nargs="*", help="Files to detect encoding of") + parser.add_argument( + "--minimal", action="store_true", help="Output only the encoding name" + ) + parser.add_argument( + "-l", + "--language", + action="store_true", + help="Include detected language in output", + ) + parser.add_argument( + "-e", + "--encoding-era", + default=None, + choices=_ERA_NAMES, + help="Encoding era filter", + ) + parser.add_argument( + "-i", + "--include-encodings", + default=None, + help="Comma-separated list of encodings to consider", + ) + parser.add_argument( + "-x", + "--exclude-encodings", + default=None, + help="Comma-separated list of encodings to exclude", + ) + parser.add_argument( + "--no-match-encoding", + default="cp1252", + help="Encoding to return when detection is inconclusive (default: cp1252)", + ) + parser.add_argument( + "--empty-input-encoding", + default="utf-8", + help="Encoding to return for empty input (default: utf-8)", + ) + parser.add_argument( + "--version", action="version", version=f"chardet {chardet.__version__}" + ) + + args = parser.parse_args(argv) + + era = ( + EncodingEra[args.encoding_era.upper()] if args.encoding_era else EncodingEra.ALL + ) + + include = ( + [s.strip() for s in args.include_encodings.split(",")] + if args.include_encodings + else None + ) + exclude = ( + [s.strip() for s in args.exclude_encodings.split(",")] + if args.exclude_encodings + else None + ) + + if args.files: + errors = 0 + for filepath in args.files: + try: + with Path(filepath).open("rb") as f: + data = f.read(DEFAULT_MAX_BYTES) + except OSError as e: + print(f"chardetect: {filepath}: {e}", file=sys.stderr) + errors += 1 + continue + try: + result = chardet.detect( + data, + encoding_era=era, + include_encodings=include, + exclude_encodings=exclude, + no_match_encoding=args.no_match_encoding, + empty_input_encoding=args.empty_input_encoding, + ) + except Exception as e: # noqa: BLE001 + print(f"chardetect: {filepath}: detection failed: {e}", file=sys.stderr) + errors += 1 + continue + _print_result( + result, filepath, minimal=args.minimal, language=args.language + ) + if errors == len(args.files): + sys.exit(1) + else: + data = sys.stdin.buffer.read(DEFAULT_MAX_BYTES) + try: + result = chardet.detect( + data, + encoding_era=era, + include_encodings=include, + exclude_encodings=exclude, + no_match_encoding=args.no_match_encoding, + empty_input_encoding=args.empty_input_encoding, + ) + except Exception as e: # noqa: BLE001 + print(f"chardetect: stdin: detection failed: {e}", file=sys.stderr) + sys.exit(1) + _print_result(result, "stdin", minimal=args.minimal, language=args.language) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/libs/chardet/cli/chardetect.py b/libs/chardet/cli/chardetect.py deleted file mode 100644 index 43f6e144f..000000000 --- a/libs/chardet/cli/chardetect.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Script which takes one or more file paths and reports on their detected -encodings - -Example:: - - % chardetect somefile someotherfile - somefile: windows-1252 with confidence 0.5 - someotherfile: ascii with confidence 1.0 - -If no paths are provided, it takes its input from stdin. - -""" - - -import argparse -import sys -from typing import Iterable, List, Optional - -from .. import __version__ -from ..universaldetector import UniversalDetector - - -def description_of( - lines: Iterable[bytes], - name: str = "stdin", - minimal: bool = False, - should_rename_legacy: bool = False, -) -> Optional[str]: - """ - Return a string describing the probable encoding of a file or - list of strings. - - :param lines: The lines to get the encoding of. - :type lines: Iterable of bytes - :param name: Name of file or collection of lines - :type name: str - :param should_rename_legacy: Should we rename legacy encodings to - their more modern equivalents? - :type should_rename_legacy: ``bool`` - """ - u = UniversalDetector(should_rename_legacy=should_rename_legacy) - for line in lines: - line = bytearray(line) - u.feed(line) - # shortcut out of the loop to save reading further - particularly useful if we read a BOM. - if u.done: - break - u.close() - result = u.result - if minimal: - return result["encoding"] - if result["encoding"]: - return f'{name}: {result["encoding"]} with confidence {result["confidence"]}' - return f"{name}: no result" - - -def main(argv: Optional[List[str]] = None) -> None: - """ - Handles command line arguments and gets things started. - - :param argv: List of arguments, as if specified on the command-line. - If None, ``sys.argv[1:]`` is used instead. - :type argv: list of str - """ - # Get command line arguments - parser = argparse.ArgumentParser( - description=( - "Takes one or more file paths and reports their detected encodings" - ) - ) - parser.add_argument( - "input", - help="File whose encoding we would like to determine. (default: stdin)", - type=argparse.FileType("rb"), - nargs="*", - default=[sys.stdin.buffer], - ) - parser.add_argument( - "--minimal", - help="Print only the encoding to standard output", - action="store_true", - ) - parser.add_argument( - "-l", - "--legacy", - help="Rename legacy encodings to more modern ones.", - action="store_true", - ) - parser.add_argument( - "--version", action="version", version=f"%(prog)s {__version__}" - ) - args = parser.parse_args(argv) - - for f in args.input: - if f.isatty(): - print( - "You are running chardetect interactively. Press " - "CTRL-D twice at the start of a blank line to signal the " - "end of your input. If you want help, run chardetect " - "--help\n", - file=sys.stderr, - ) - print( - description_of( - f, f.name, minimal=args.minimal, should_rename_legacy=args.legacy - ) - ) - - -if __name__ == "__main__": - main() diff --git a/libs/chardet/codingstatemachine.py b/libs/chardet/codingstatemachine.py deleted file mode 100644 index 8ed4a8773..000000000 --- a/libs/chardet/codingstatemachine.py +++ /dev/null @@ -1,90 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -import logging - -from .codingstatemachinedict import CodingStateMachineDict -from .enums import MachineState - - -class CodingStateMachine: - """ - A state machine to verify a byte sequence for a particular encoding. For - each byte the detector receives, it will feed that byte to every active - state machine available, one byte at a time. The state machine changes its - state based on its previous state and the byte it receives. There are 3 - states in a state machine that are of interest to an auto-detector: - - START state: This is the state to start with, or a legal byte sequence - (i.e. a valid code point) for character has been identified. - - ME state: This indicates that the state machine identified a byte sequence - that is specific to the charset it is designed for and that - there is no other possible encoding which can contain this byte - sequence. This will to lead to an immediate positive answer for - the detector. - - ERROR state: This indicates the state machine identified an illegal byte - sequence for that encoding. This will lead to an immediate - negative answer for this encoding. Detector will exclude this - encoding from consideration from here on. - """ - - def __init__(self, sm: CodingStateMachineDict) -> None: - self._model = sm - self._curr_byte_pos = 0 - self._curr_char_len = 0 - self._curr_state = MachineState.START - self.active = True - self.logger = logging.getLogger(__name__) - self.reset() - - def reset(self) -> None: - self._curr_state = MachineState.START - - def next_state(self, c: int) -> int: - # for each byte we get its class - # if it is first byte, we also get byte length - byte_class = self._model["class_table"][c] - if self._curr_state == MachineState.START: - self._curr_byte_pos = 0 - self._curr_char_len = self._model["char_len_table"][byte_class] - # from byte's class and state_table, we get its next state - curr_state = self._curr_state * self._model["class_factor"] + byte_class - self._curr_state = self._model["state_table"][curr_state] - self._curr_byte_pos += 1 - return self._curr_state - - def get_current_charlen(self) -> int: - return self._curr_char_len - - def get_coding_state_machine(self) -> str: - return self._model["name"] - - @property - def language(self) -> str: - return self._model["language"] diff --git a/libs/chardet/codingstatemachinedict.py b/libs/chardet/codingstatemachinedict.py deleted file mode 100644 index 7a3c4c7e3..000000000 --- a/libs/chardet/codingstatemachinedict.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import TYPE_CHECKING, Tuple - -if TYPE_CHECKING: - # TypedDict was introduced in Python 3.8. - # - # TODO: Remove the else block and TYPE_CHECKING check when dropping support - # for Python 3.7. - from typing import TypedDict - - class CodingStateMachineDict(TypedDict, total=False): - class_table: Tuple[int, ...] - class_factor: int - state_table: Tuple[int, ...] - char_len_table: Tuple[int, ...] - name: str - language: str # Optional key - -else: - CodingStateMachineDict = dict diff --git a/libs/chardet/cp949prober.py b/libs/chardet/cp949prober.py deleted file mode 100644 index fa7307ed8..000000000 --- a/libs/chardet/cp949prober.py +++ /dev/null @@ -1,49 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .chardistribution import EUCKRDistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import CP949_SM_MODEL - - -class CP949Prober(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(CP949_SM_MODEL) - # NOTE: CP949 is a superset of EUC-KR, so the distribution should be - # not different. - self.distribution_analyzer = EUCKRDistributionAnalysis() - self.reset() - - @property - def charset_name(self) -> str: - return "CP949" - - @property - def language(self) -> str: - return "Korean" diff --git a/libs/chardet/detector.py b/libs/chardet/detector.py new file mode 100644 index 000000000..c1ad6a6c2 --- /dev/null +++ b/libs/chardet/detector.py @@ -0,0 +1,188 @@ +"""UniversalDetector — streaming encoding detection.""" + +from __future__ import annotations + +import warnings +from collections.abc import Iterable +from types import MappingProxyType +from typing import ClassVar + +from chardet import _utils +from chardet._utils import ( + DEFAULT_MAX_BYTES, + _resolve_prefer_superset, + _validate_max_bytes, +) +from chardet.enums import EncodingEra, LanguageFilter +from chardet.equivalences import ( + PREFERRED_SUPERSET, + apply_compat_names, + apply_preferred_superset, +) +from chardet.pipeline import _NONE_RESULT, DetectionDict, DetectionResult +from chardet.pipeline.orchestrator import run_pipeline +from chardet.registry import _validate_encoding, normalize_encodings + + +class UniversalDetector: + """Streaming character encoding detector. + + Implements a feed/close pattern for incremental detection of character + encoding from byte streams. Compatible with the chardet 6.x API. + + All detection is performed by the same pipeline used by + :func:`chardet.detect` and :func:`chardet.detect_all`, ensuring + consistent results regardless of which API is used. + + .. note:: + + This class is **not** thread-safe. Each thread should create its own + :class:`UniversalDetector` instance. + """ + + MINIMUM_THRESHOLD = _utils.MINIMUM_THRESHOLD + # Exposed for backward compatibility with chardet 6.x callers that + # reference UniversalDetector.LEGACY_MAP directly. + LEGACY_MAP: ClassVar[MappingProxyType[str, str]] = MappingProxyType( + PREFERRED_SUPERSET + ) + + def __init__( # noqa: PLR0913 + self, + lang_filter: LanguageFilter = LanguageFilter.ALL, + should_rename_legacy: bool = False, + encoding_era: EncodingEra = EncodingEra.ALL, + max_bytes: int = DEFAULT_MAX_BYTES, + *, + prefer_superset: bool = False, + compat_names: bool = True, + include_encodings: Iterable[str] | None = None, + exclude_encodings: Iterable[str] | None = None, + no_match_encoding: str = "cp1252", + empty_input_encoding: str = "utf-8", + ) -> None: + """Initialize the detector. + + :param lang_filter: Deprecated -- accepted for backward compatibility + but has no effect. A warning is emitted when set to anything + other than :attr:`LanguageFilter.ALL`. + :param should_rename_legacy: Deprecated alias for *prefer_superset*. + :param encoding_era: Restrict candidate encodings to the given era. + :param max_bytes: Maximum number of bytes to buffer from + :meth:`feed` calls before stopping accumulation. + :param prefer_superset: If ``True``, remap ISO subset encodings to + their Windows/CP superset equivalents (e.g., ISO-8859-1 -> + Windows-1252). + :param compat_names: If ``True`` (default), return encoding names + compatible with chardet 5.x/6.x. If ``False``, return raw Python + codec names. + :param include_encodings: If given, restrict detection to only these + encodings (names or aliases). + :param exclude_encodings: If given, remove these encodings from the + candidate set. + :param no_match_encoding: Encoding to return when no candidate + survives the pipeline. Defaults to ``"cp1252"``. + :param empty_input_encoding: Encoding to return for empty input. + Defaults to ``"utf-8"``. + """ + if lang_filter != LanguageFilter.ALL: + warnings.warn( + "lang_filter is not implemented in this version of chardet " + "and will be ignored", + DeprecationWarning, + stacklevel=2, + ) + prefer_superset = _resolve_prefer_superset( + should_rename_legacy, prefer_superset + ) + self._prefer_superset = prefer_superset + self._compat_names = compat_names + _validate_max_bytes(max_bytes) + self._encoding_era = encoding_era + self._max_bytes = max_bytes + self._include_encodings = normalize_encodings( + include_encodings, "include_encodings" + ) + self._exclude_encodings = normalize_encodings( + exclude_encodings, "exclude_encodings" + ) + self._no_match_encoding = _validate_encoding( + no_match_encoding, "no_match_encoding" + ) + self._empty_input_encoding = _validate_encoding( + empty_input_encoding, "empty_input_encoding" + ) + self._buffer = bytearray() + self._done = False + self._closed = False + self._result: DetectionResult | None = None + + def feed(self, byte_str: bytes | bytearray) -> None: + """Feed a chunk of bytes to the detector. + + Data is accumulated in an internal buffer. Once *max_bytes* have + been buffered, :attr:`done` is set to ``True`` and further data is + ignored until :meth:`reset` is called. + + :param byte_str: The next chunk of bytes to examine. + :raises ValueError: If called after :meth:`close` without a + :meth:`reset`. + """ + if self._closed: + msg = "feed() called after close() without reset()" + raise ValueError(msg) + if self._done: + return + remaining = self._max_bytes - len(self._buffer) + if remaining > 0: + self._buffer.extend(byte_str[:remaining]) + if len(self._buffer) >= self._max_bytes: + self._done = True + + def close(self) -> DetectionDict: + """Finalize detection and return the best result. + + Runs the full detection pipeline on the buffered data. + + :returns: A dictionary with keys ``"encoding"``, ``"confidence"``, + and ``"language"``. + """ + if not self._closed: + self._closed = True + data = bytes(self._buffer) + results = run_pipeline( + data, + self._encoding_era, + max_bytes=self._max_bytes, + include_encodings=self._include_encodings, + exclude_encodings=self._exclude_encodings, + no_match_encoding=self._no_match_encoding, + empty_input_encoding=self._empty_input_encoding, + ) + self._result = results[0] + self._done = True + return self.result + + def reset(self) -> None: + """Reset the detector to its initial state for reuse.""" + self._buffer = bytearray() + self._done = False + self._closed = False + self._result = None + + @property + def done(self) -> bool: + """Whether detection is complete and no more data is needed.""" + return self._done + + @property + def result(self) -> DetectionDict: + """The current best detection result.""" + if self._result is not None: + d = self._result.to_dict() + if self._prefer_superset: + apply_preferred_superset(d) + if self._compat_names: + apply_compat_names(d) + return d + return _NONE_RESULT.to_dict() diff --git a/libs/chardet/enums.py b/libs/chardet/enums.py index 5e3e19823..a85b231b7 100644 --- a/libs/chardet/enums.py +++ b/libs/chardet/enums.py @@ -1,29 +1,32 @@ -""" -All of the Enums that are used throughout the chardet package. +"""Enumerations for chardet.""" -:author: Dan Blanchard (dan.blanchard@gmail.com) -""" +from __future__ import annotations -from enum import Enum, Flag +import enum -class InputState: - """ - This enum represents the different states a universal detector can be in. +class EncodingEra(enum.IntFlag): + """Bit flags representing encoding eras for filtering detection candidates.""" + + MODERN_WEB = 1 + LEGACY_ISO = 2 + LEGACY_MAC = 4 + LEGACY_REGIONAL = 8 + DOS = 16 + MAINFRAME = 32 + ALL = MODERN_WEB | LEGACY_ISO | LEGACY_MAC | LEGACY_REGIONAL | DOS | MAINFRAME + + +class LanguageFilter(enum.IntFlag): + """Language filter flags for UniversalDetector (chardet 6.x API compat). + + Accepted but not used — our pipeline does not filter by language group. + + .. deprecated:: + Retained only for backward compatibility with chardet 6.x callers. + Will be removed in a future major version. """ - PURE_ASCII = 0 - ESC_ASCII = 1 - HIGH_BYTE = 2 - - -class LanguageFilter(Flag): - """ - This enum represents the different language filters we can apply to a - ``UniversalDetector``. - """ - - NONE = 0x00 CHINESE_SIMPLIFIED = 0x01 CHINESE_TRADITIONAL = 0x02 JAPANESE = 0x04 @@ -32,54 +35,3 @@ class LanguageFilter(Flag): ALL = 0x1F CHINESE = CHINESE_SIMPLIFIED | CHINESE_TRADITIONAL CJK = CHINESE | JAPANESE | KOREAN - - -class ProbingState(Enum): - """ - This enum represents the different states a prober can be in. - """ - - DETECTING = 0 - FOUND_IT = 1 - NOT_ME = 2 - - -class MachineState: - """ - This enum represents the different states a state machine can be in. - """ - - START = 0 - ERROR = 1 - ITS_ME = 2 - - -class SequenceLikelihood: - """ - This enum represents the likelihood of a character following the previous one. - """ - - NEGATIVE = 0 - UNLIKELY = 1 - LIKELY = 2 - POSITIVE = 3 - - @classmethod - def get_num_categories(cls) -> int: - """:returns: The number of likelihood categories in the enum.""" - return 4 - - -class CharacterCategory: - """ - This enum represents the different categories language models for - ``SingleByteCharsetProber`` put characters into. - - Anything less than CONTROL is considered a letter. - """ - - UNDEFINED = 255 - LINE_BREAK = 254 - SYMBOL = 253 - DIGIT = 252 - CONTROL = 251 diff --git a/libs/chardet/equivalences.py b/libs/chardet/equivalences.py new file mode 100644 index 000000000..f23c7425d --- /dev/null +++ b/libs/chardet/equivalences.py @@ -0,0 +1,387 @@ +"""Encoding equivalences and name remapping. + +This module defines: + +1. **Directional supersets** for accuracy evaluation: detecting a superset + encoding when the expected encoding is a subset is correct (e.g., detecting + UTF-8 when expected is ASCII), but not the reverse. + +2. **Bidirectional equivalents**: groups of encodings where detecting any + member when another member was expected is considered correct. This + includes UTF-16/UTF-32 endian variants (which encode the same text with + different byte order) and ISO-2022-JP branch variants (which are + compatible extensions of the same base encoding). + +3. **Preferred superset mapping** for the ``prefer_superset`` API option: + replaces detected ISO/subset encoding names with their Windows/CP superset + equivalents that modern software actually uses. + +4. **Compatibility names** for the default ``compat_names=True`` mode: maps + internal Python codec names to the names chardet 5.x/6.x returned, + preserving backward compatibility for callers that compare encoding + strings directly. +""" + +from __future__ import annotations + +import unicodedata +from collections.abc import Callable + +from chardet.pipeline import DetectionDict +from chardet.registry import lookup_encoding + +# Directional superset relationships: detecting any of the supersets +# when the expected encoding is the subset counts as correct. +# E.g., expected=ascii, detected=utf-8 -> correct (utf-8 ⊃ ascii). +# But expected=utf-8, detected=ascii -> wrong (ascii ⊄ utf-8). +# +# Note: some subset keys (iso-8859-11) are not in the detection +# registry — the detector never returns them. They appear here because +# chardet test-suite expected values use these names, so the superset +# mapping is needed for accuracy evaluation only. +SUPERSETS: dict[str, frozenset[str]] = { + "ASCII": frozenset({"utf-8", "cp1252"}), + "TIS-620": frozenset({"iso8859-11", "cp874"}), + "ISO-8859-11": frozenset({"cp874"}), + "GB2312": frozenset({"gb18030"}), + "GBK": frozenset({"gb18030"}), + "Big5": frozenset({"big5hkscs", "cp950"}), + "Shift_JIS": frozenset({"cp932", "shift_jis_2004"}), + "Shift-JISX0213": frozenset({"shift_jis_2004"}), + "EUC-JP": frozenset({"euc_jis_2004"}), + "EUC-JISX0213": frozenset({"euc_jis_2004"}), + "EUC-KR": frozenset({"cp949"}), + "CP037": frozenset({"cp1140"}), + # ISO-2022-JP subsets: any branch variant is acceptable. + # In our registry, base ISO-2022-JP is an alias of iso2022_jp_2, so all + # three extended variants are supersets of the same base. While the + # extended variants use different escape sequences for non-basic characters, + # real-world files rarely use those extensions — the base JIS X 0208 + # character set is shared by all variants and cross-decodes identically. + # ISO2022-JP-1 and ISO2022-JP-3 use Python codec names (no hyphen between + # "ISO" and "2022") because they appear as expected values in the test suite, + # not as canonical chardet output. They are consumed through + # _NORMALIZED_SUPERSETS which normalizes via codecs.lookup(). + "ISO-2022-JP": frozenset({"iso2022_jp_2", "iso2022_jp_2004", "iso2022_jp_ext"}), + "ISO2022-JP-1": frozenset({"iso2022_jp_2", "iso2022_jp_ext"}), + "ISO2022-JP-3": frozenset({"iso2022_jp_2004"}), + # ISO/Windows superset pairs + "ISO-8859-1": frozenset({"cp1252"}), + "ISO-8859-2": frozenset({"cp1250"}), + "ISO-8859-5": frozenset({"cp1251"}), + "ISO-8859-6": frozenset({"cp1256"}), + "ISO-8859-7": frozenset({"cp1253"}), + "ISO-8859-8": frozenset({"cp1255"}), + "ISO-8859-9": frozenset({"cp1254"}), + "ISO-8859-13": frozenset({"cp1257"}), + # UTF-16/32: bare form (BOM-aware) is interchangeable with either endianness, + # but LE and BE are NOT interchangeable with each other. + "UTF-16": frozenset({"utf-16-le", "utf-16-be"}), + "UTF-16-LE": frozenset({"utf-16"}), + "UTF-16-BE": frozenset({"utf-16"}), + "UTF-32": frozenset({"utf-32-le", "utf-32-be"}), + "UTF-32-LE": frozenset({"utf-32"}), + "UTF-32-BE": frozenset({"utf-32"}), +} + +# Preferred superset name for each encoding, used by the ``should_rename_legacy`` +# API option. When enabled, detected encoding names are replaced with the +# Windows/CP superset that modern software actually uses (browsers, editors, +# etc. treat these ISO subsets as their Windows counterparts). +# Values use display-cased names (e.g. "Windows-1252") to match chardet 6.x output. +PREFERRED_SUPERSET: dict[str, str] = { + "ascii": "cp1252", + "euc_kr": "cp949", + "iso8859-1": "cp1252", + "iso8859-2": "cp1250", + "iso8859-5": "cp1251", + "iso8859-6": "cp1256", + "iso8859-7": "cp1253", + "iso8859-8": "cp1255", + "iso8859-9": "cp1254", + "iso8859-11": "cp874", + "iso8859-13": "cp1257", + "tis-620": "cp874", +} + + +def _remap_encoding(result: DetectionDict, mapping: dict[str, str]) -> DetectionDict: + """Replace the encoding name using *mapping*, modifying *result* in-place.""" + enc = result.get("encoding") + if isinstance(enc, str): + result["encoding"] = mapping.get(enc, enc) + return result + + +def apply_preferred_superset( + result: DetectionDict, +) -> DetectionDict: + """Replace the encoding name with its preferred Windows/CP superset. + + Modifies the ``"encoding"`` value in *result* in-place and returns *result* + for fluent chaining. + + :param result: A detection result dict containing an ``"encoding"`` key. + :returns: The same *result* dict, modified in-place. + """ + return _remap_encoding(result, PREFERRED_SUPERSET) + + +# Deprecated alias — kept for external consumers. +apply_legacy_rename = apply_preferred_superset + + +# Mapping from Python codec names to chardet 5.x/6.x compatible display names. +# Only entries where codec name differs from the compat output are listed. +# Encodings where codec name == compat name (e.g., "ascii", "utf-8") and +# encodings new to v7 have no entry — the codec name passes through unchanged. +_COMPAT_NAMES: dict[str, str] = { + # 5.x compat — these encodings existed in chardet 5.x with different names + "big5hkscs": "Big5", + "cp855": "IBM855", + "cp866": "IBM866", + "cp949": "CP949", + "euc_jis_2004": "EUC-JP", + "euc_kr": "EUC-KR", + "gb18030": "GB18030", + "hz": "HZ-GB-2312", + "iso2022_jp_2": "ISO-2022-JP", + "iso2022_kr": "ISO-2022-KR", + "iso8859-1": "ISO-8859-1", + "iso8859-5": "ISO-8859-5", + "iso8859-7": "ISO-8859-7", + "iso8859-8": "ISO-8859-8", + "iso8859-9": "ISO-8859-9", + "johab": "Johab", + "koi8-r": "KOI8-R", + "mac-cyrillic": "MacCyrillic", + "mac-roman": "MacRoman", + "shift_jis_2004": "SHIFT_JIS", + "tis-620": "TIS-620", + "utf-16": "UTF-16", + "utf-32": "UTF-32", + "utf-8-sig": "UTF-8-SIG", + "cp1251": "Windows-1251", + "cp1252": "Windows-1252", + "cp1253": "Windows-1253", + "cp1254": "Windows-1254", + "cp1255": "Windows-1255", + # 6.x compat — new in chardet 6.x with different names + "kz1048": "KZ1048", + "mac-greek": "MacGreek", + "mac-iceland": "MacIceland", + "mac-latin2": "MacLatin2", + "mac-turkish": "MacTurkish", +} + + +def apply_compat_names( + result: DetectionDict, +) -> DetectionDict: + """Convert internal codec names to chardet 5.x/6.x compatible names. + + Modifies the ``"encoding"`` value in *result* in-place and returns *result* + for fluent chaining. + + :param result: A detection result dict containing an ``"encoding"`` key. + :returns: The same *result* dict, modified in-place. + """ + return _remap_encoding(result, _COMPAT_NAMES) + + +# Bidirectional equivalents -- groups where any member is acceptable for any other. +# Bidirectional equivalents -- groups where any member is acceptable for any other. +# +# NOTE: UTF-16/32 endianness is handled via directional SUPERSETS instead, +# because wrong endianness garbles text. ISO-2022-JP variants remain here +# because base ISO-2022-JP is an alias of iso2022_jp_2 in our registry, so +# the SUPERSETS entries already make all variants interchangeable via the +# shared base. +BIDIRECTIONAL_GROUPS: tuple[tuple[str, ...], ...] = ( + ("iso2022_jp_2", "iso2022_jp_2004", "iso2022_jp_ext"), +) + +# Bidirectional language equivalences — groups of ISO 639-1 codes for +# languages that are nearly indistinguishable by statistical detection. +# Detecting any member when another member of the same group was expected +# is considered acceptable. +LANGUAGE_EQUIVALENCES: tuple[tuple[str, ...], ...] = ( + ("sk", "cs"), # Slovak / Czech — ~85% mutual intelligibility + ( + "uk", + "ru", + "bg", + "be", + ), # East Slavic + Bulgarian — shared Cyrillic, high written overlap + ("ms", "id"), # Malay / Indonesian — standardized variants of one language + ( + "no", + "da", + "sv", + ), # Scandinavian — mutual intelligibility across the dialect continuum +) + + +def _build_group_index( + groups: tuple[tuple[str, ...], ...], + normalize: Callable[[str], str] = lambda x: x, +) -> dict[str, frozenset[str]]: + """Build a lookup: key -> frozenset of all equivalent keys in the same group.""" + result: dict[str, frozenset[str]] = {} + for group in groups: + normed = frozenset(normalize(n) for n in group) + for name in group: + result[normalize(name)] = normed + return result + + +_LANGUAGE_EQUIV: dict[str, frozenset[str]] = _build_group_index(LANGUAGE_EQUIVALENCES) + + +def is_language_equivalent(expected: str, detected: str) -> bool: + """Check whether *detected* is an acceptable language for *expected*. + + Returns ``True`` when *expected* and *detected* are the same ISO 639-1 + code, or belong to the same equivalence group in + :data:`LANGUAGE_EQUIVALENCES`. + + :param expected: Expected ISO 639-1 language code. + :param detected: Detected ISO 639-1 language code. + :returns: ``True`` if the languages are equivalent. + """ + if expected == detected: + return True + group = _LANGUAGE_EQUIV.get(expected) + return group is not None and detected in group + + +# Pre-built normalized lookups for fast comparison. +# Built iteratively because multiple SUPERSETS keys can normalize to the same +# canonical name (e.g., Shift_JIS and Shift-JISX0213 both → shift_jis_2004). +# Values are merged (unioned) when keys collide. +_NORMALIZED_SUPERSETS: dict[str, frozenset[str]] = {} +for _subset, _supersets in SUPERSETS.items(): + _key = lookup_encoding(_subset) or _subset + _normed = frozenset(lookup_encoding(s) or s for s in _supersets) + _NORMALIZED_SUPERSETS[_key] = _NORMALIZED_SUPERSETS.get(_key, frozenset()) | _normed + + +_NORMALIZED_BIDIR: dict[str, frozenset[str]] = _build_group_index( + BIDIRECTIONAL_GROUPS, normalize=lambda n: lookup_encoding(n) or n +) + + +def is_correct(expected: str | None, detected: str | None) -> bool: + """Check whether *detected* is an acceptable answer for *expected*. + + Acceptable means: + + 1. Exact match (after normalization), OR + 2. Both belong to the same bidirectional byte-order group, OR + 3. *detected* is a known superset of *expected*. + + :param expected: The expected encoding name, or ``None`` for binary files. + :param detected: The detected encoding name, or ``None``. + :returns: ``True`` if the detection is acceptable. + """ + if expected is None: + return detected is None + if detected is None: + return False + norm_exp = lookup_encoding(expected) or expected.lower() + norm_det = lookup_encoding(detected) or detected.lower() + + # 1. Exact match + if norm_exp == norm_det: + return True + + # 2. Bidirectional (same byte-order group) + if norm_exp in _NORMALIZED_BIDIR and norm_det in _NORMALIZED_BIDIR[norm_exp]: + return True + + # 3. Superset is acceptable (detected is a known superset of expected) + return ( + norm_exp in _NORMALIZED_SUPERSETS + and norm_det in _NORMALIZED_SUPERSETS[norm_exp] + ) + + +def _strip_combining(text: str) -> str: + """NFKD-normalize *text* and strip all combining marks.""" + nfkd = unicodedata.normalize("NFKD", text) + return "".join(c for c in nfkd if not unicodedata.combining(c)) + + +# Pre-computed symbol pair lookups for O(1) equivalence checks. +# Both orderings are stored to avoid constructing temporaries per call. +_EQUIVALENT_SYMBOL_PAIRS: frozenset[tuple[str, str]] = frozenset( + { + ("¤", "€"), + ("€", "¤"), + } +) + + +def _chars_equivalent(a: str, b: str) -> bool: + """Return True if characters *a* and *b* are functionally equivalent. + + Equivalent means: + - Same character, OR + - Same base letter after stripping combining marks, OR + - An explicitly listed symbol equivalence (e.g. ¤ ↔ €) + """ + if a == b: + return True + if (a, b) in _EQUIVALENT_SYMBOL_PAIRS: + return True + # Compare base letters after stripping combining marks. + return _strip_combining(a) == _strip_combining(b) + + +def is_equivalent_detection( + data: bytes, expected: str | None, detected: str | None +) -> bool: + """Check whether *detected* produces functionally identical text to *expected*. + + Returns ``True`` when: + + 1. *detected* is not ``None`` and both encoding names normalize to the same + codec, OR + 2. Decoding *data* with both encodings yields identical strings, OR + 3. Every differing character pair is functionally equivalent: same base + letter after stripping combining marks, or an explicitly listed symbol + equivalence (e.g. ¤ ↔ €). + + Returns ``False`` if *detected* is ``None``, either encoding is unknown, + or either encoding cannot decode *data*. + + :param data: The raw byte data that was detected. + :param expected: The expected encoding name, or ``None`` for binary files. + :param detected: The detected encoding name, or ``None``. + :returns: ``True`` if decoding with *detected* yields functionally identical + text to decoding with *expected*. + """ + if expected is None: + return detected is None + if detected is None: + return False + + norm_exp = lookup_encoding(expected) or expected.lower() + norm_det = lookup_encoding(detected) or detected.lower() + + if norm_exp == norm_det: + return True + + try: + text_exp = data.decode(norm_exp) + text_det = data.decode(norm_det) + except (UnicodeDecodeError, LookupError): + return False + + if text_exp == text_det: + return True + + if len(text_exp) != len(text_det): + return False + + return all(_chars_equivalent(a, b) for a, b in zip(text_exp, text_det, strict=True)) diff --git a/libs/chardet/escprober.py b/libs/chardet/escprober.py deleted file mode 100644 index fd713830d..000000000 --- a/libs/chardet/escprober.py +++ /dev/null @@ -1,102 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Optional, Union - -from .charsetprober import CharSetProber -from .codingstatemachine import CodingStateMachine -from .enums import LanguageFilter, MachineState, ProbingState -from .escsm import ( - HZ_SM_MODEL, - ISO2022CN_SM_MODEL, - ISO2022JP_SM_MODEL, - ISO2022KR_SM_MODEL, -) - - -class EscCharSetProber(CharSetProber): - """ - This CharSetProber uses a "code scheme" approach for detecting encodings, - whereby easily recognizable escape or shift sequences are relied on to - identify these encodings. - """ - - def __init__(self, lang_filter: LanguageFilter = LanguageFilter.NONE) -> None: - super().__init__(lang_filter=lang_filter) - self.coding_sm = [] - if self.lang_filter & LanguageFilter.CHINESE_SIMPLIFIED: - self.coding_sm.append(CodingStateMachine(HZ_SM_MODEL)) - self.coding_sm.append(CodingStateMachine(ISO2022CN_SM_MODEL)) - if self.lang_filter & LanguageFilter.JAPANESE: - self.coding_sm.append(CodingStateMachine(ISO2022JP_SM_MODEL)) - if self.lang_filter & LanguageFilter.KOREAN: - self.coding_sm.append(CodingStateMachine(ISO2022KR_SM_MODEL)) - self.active_sm_count = 0 - self._detected_charset: Optional[str] = None - self._detected_language: Optional[str] = None - self._state = ProbingState.DETECTING - self.reset() - - def reset(self) -> None: - super().reset() - for coding_sm in self.coding_sm: - coding_sm.active = True - coding_sm.reset() - self.active_sm_count = len(self.coding_sm) - self._detected_charset = None - self._detected_language = None - - @property - def charset_name(self) -> Optional[str]: - return self._detected_charset - - @property - def language(self) -> Optional[str]: - return self._detected_language - - def get_confidence(self) -> float: - return 0.99 if self._detected_charset else 0.00 - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - for c in byte_str: - for coding_sm in self.coding_sm: - if not coding_sm.active: - continue - coding_state = coding_sm.next_state(c) - if coding_state == MachineState.ERROR: - coding_sm.active = False - self.active_sm_count -= 1 - if self.active_sm_count <= 0: - self._state = ProbingState.NOT_ME - return self.state - elif coding_state == MachineState.ITS_ME: - self._state = ProbingState.FOUND_IT - self._detected_charset = coding_sm.get_coding_state_machine() - self._detected_language = coding_sm.language - return self.state - - return self.state diff --git a/libs/chardet/escsm.py b/libs/chardet/escsm.py deleted file mode 100644 index 11d4adf77..000000000 --- a/libs/chardet/escsm.py +++ /dev/null @@ -1,261 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .codingstatemachinedict import CodingStateMachineDict -from .enums import MachineState - -# fmt: off -HZ_CLS = ( - 1, 0, 0, 0, 0, 0, 0, 0, # 00 - 07 - 0, 0, 0, 0, 0, 0, 0, 0, # 08 - 0f - 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17 - 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f - 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27 - 0, 0, 0, 0, 0, 0, 0, 0, # 28 - 2f - 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37 - 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f - 0, 0, 0, 0, 0, 0, 0, 0, # 40 - 47 - 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f - 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57 - 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f - 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67 - 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f - 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77 - 0, 0, 0, 4, 0, 5, 2, 0, # 78 - 7f - 1, 1, 1, 1, 1, 1, 1, 1, # 80 - 87 - 1, 1, 1, 1, 1, 1, 1, 1, # 88 - 8f - 1, 1, 1, 1, 1, 1, 1, 1, # 90 - 97 - 1, 1, 1, 1, 1, 1, 1, 1, # 98 - 9f - 1, 1, 1, 1, 1, 1, 1, 1, # a0 - a7 - 1, 1, 1, 1, 1, 1, 1, 1, # a8 - af - 1, 1, 1, 1, 1, 1, 1, 1, # b0 - b7 - 1, 1, 1, 1, 1, 1, 1, 1, # b8 - bf - 1, 1, 1, 1, 1, 1, 1, 1, # c0 - c7 - 1, 1, 1, 1, 1, 1, 1, 1, # c8 - cf - 1, 1, 1, 1, 1, 1, 1, 1, # d0 - d7 - 1, 1, 1, 1, 1, 1, 1, 1, # d8 - df - 1, 1, 1, 1, 1, 1, 1, 1, # e0 - e7 - 1, 1, 1, 1, 1, 1, 1, 1, # e8 - ef - 1, 1, 1, 1, 1, 1, 1, 1, # f0 - f7 - 1, 1, 1, 1, 1, 1, 1, 1, # f8 - ff -) - -HZ_ST = ( -MachineState.START, MachineState.ERROR, 3, MachineState.START, MachineState.START, MachineState.START, MachineState.ERROR, MachineState.ERROR, # 00-07 -MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 08-0f -MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.START, MachineState.START, 4, MachineState.ERROR, # 10-17 - 5, MachineState.ERROR, 6, MachineState.ERROR, 5, 5, 4, MachineState.ERROR, # 18-1f - 4, MachineState.ERROR, 4, 4, 4, MachineState.ERROR, 4, MachineState.ERROR, # 20-27 - 4, MachineState.ITS_ME, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 28-2f -) -# fmt: on - -HZ_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0) - -HZ_SM_MODEL: CodingStateMachineDict = { - "class_table": HZ_CLS, - "class_factor": 6, - "state_table": HZ_ST, - "char_len_table": HZ_CHAR_LEN_TABLE, - "name": "HZ-GB-2312", - "language": "Chinese", -} - -# fmt: off -ISO2022CN_CLS = ( - 2, 0, 0, 0, 0, 0, 0, 0, # 00 - 07 - 0, 0, 0, 0, 0, 0, 0, 0, # 08 - 0f - 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17 - 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f - 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27 - 0, 3, 0, 0, 0, 0, 0, 0, # 28 - 2f - 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37 - 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f - 0, 0, 0, 4, 0, 0, 0, 0, # 40 - 47 - 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f - 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57 - 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f - 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67 - 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f - 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77 - 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f - 2, 2, 2, 2, 2, 2, 2, 2, # 80 - 87 - 2, 2, 2, 2, 2, 2, 2, 2, # 88 - 8f - 2, 2, 2, 2, 2, 2, 2, 2, # 90 - 97 - 2, 2, 2, 2, 2, 2, 2, 2, # 98 - 9f - 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7 - 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af - 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7 - 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf - 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7 - 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf - 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7 - 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df - 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7 - 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef - 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7 - 2, 2, 2, 2, 2, 2, 2, 2, # f8 - ff -) - -ISO2022CN_ST = ( - MachineState.START, 3, MachineState.ERROR, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 00-07 - MachineState.START, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 08-0f - MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 10-17 - MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 4, MachineState.ERROR, # 18-1f - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 20-27 - 5, 6, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 28-2f - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 30-37 - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.START, # 38-3f -) -# fmt: on - -ISO2022CN_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0, 0, 0, 0) - -ISO2022CN_SM_MODEL: CodingStateMachineDict = { - "class_table": ISO2022CN_CLS, - "class_factor": 9, - "state_table": ISO2022CN_ST, - "char_len_table": ISO2022CN_CHAR_LEN_TABLE, - "name": "ISO-2022-CN", - "language": "Chinese", -} - -# fmt: off -ISO2022JP_CLS = ( - 2, 0, 0, 0, 0, 0, 0, 0, # 00 - 07 - 0, 0, 0, 0, 0, 0, 2, 2, # 08 - 0f - 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17 - 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f - 0, 0, 0, 0, 7, 0, 0, 0, # 20 - 27 - 3, 0, 0, 0, 0, 0, 0, 0, # 28 - 2f - 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37 - 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f - 6, 0, 4, 0, 8, 0, 0, 0, # 40 - 47 - 0, 9, 5, 0, 0, 0, 0, 0, # 48 - 4f - 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57 - 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f - 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67 - 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f - 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77 - 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f - 2, 2, 2, 2, 2, 2, 2, 2, # 80 - 87 - 2, 2, 2, 2, 2, 2, 2, 2, # 88 - 8f - 2, 2, 2, 2, 2, 2, 2, 2, # 90 - 97 - 2, 2, 2, 2, 2, 2, 2, 2, # 98 - 9f - 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7 - 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af - 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7 - 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf - 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7 - 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf - 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7 - 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df - 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7 - 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef - 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7 - 2, 2, 2, 2, 2, 2, 2, 2, # f8 - ff -) - -ISO2022JP_ST = ( - MachineState.START, 3, MachineState.ERROR, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 00-07 - MachineState.START, MachineState.START, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 08-0f - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 10-17 - MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, # 18-1f - MachineState.ERROR, 5, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 4, MachineState.ERROR, MachineState.ERROR, # 20-27 - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 6, MachineState.ITS_ME, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, # 28-2f - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, # 30-37 - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 38-3f - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.START, MachineState.START, # 40-47 -) -# fmt: on - -ISO2022JP_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - -ISO2022JP_SM_MODEL: CodingStateMachineDict = { - "class_table": ISO2022JP_CLS, - "class_factor": 10, - "state_table": ISO2022JP_ST, - "char_len_table": ISO2022JP_CHAR_LEN_TABLE, - "name": "ISO-2022-JP", - "language": "Japanese", -} - -# fmt: off -ISO2022KR_CLS = ( - 2, 0, 0, 0, 0, 0, 0, 0, # 00 - 07 - 0, 0, 0, 0, 0, 0, 0, 0, # 08 - 0f - 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17 - 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f - 0, 0, 0, 0, 3, 0, 0, 0, # 20 - 27 - 0, 4, 0, 0, 0, 0, 0, 0, # 28 - 2f - 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37 - 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f - 0, 0, 0, 5, 0, 0, 0, 0, # 40 - 47 - 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f - 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57 - 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f - 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67 - 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f - 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77 - 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f - 2, 2, 2, 2, 2, 2, 2, 2, # 80 - 87 - 2, 2, 2, 2, 2, 2, 2, 2, # 88 - 8f - 2, 2, 2, 2, 2, 2, 2, 2, # 90 - 97 - 2, 2, 2, 2, 2, 2, 2, 2, # 98 - 9f - 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7 - 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af - 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7 - 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf - 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7 - 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf - 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7 - 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df - 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7 - 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef - 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7 - 2, 2, 2, 2, 2, 2, 2, 2, # f8 - ff -) - -ISO2022KR_ST = ( - MachineState.START, 3, MachineState.ERROR, MachineState.START, MachineState.START, MachineState.START, MachineState.ERROR, MachineState.ERROR, # 00-07 - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 08-0f - MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 4, MachineState.ERROR, MachineState.ERROR, # 10-17 - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 5, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 18-1f - MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 20-27 -) -# fmt: on - -ISO2022KR_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0) - -ISO2022KR_SM_MODEL: CodingStateMachineDict = { - "class_table": ISO2022KR_CLS, - "class_factor": 6, - "state_table": ISO2022KR_ST, - "char_len_table": ISO2022KR_CHAR_LEN_TABLE, - "name": "ISO-2022-KR", - "language": "Korean", -} diff --git a/libs/chardet/eucjpprober.py b/libs/chardet/eucjpprober.py deleted file mode 100644 index 39487f409..000000000 --- a/libs/chardet/eucjpprober.py +++ /dev/null @@ -1,102 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Union - -from .chardistribution import EUCJPDistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .enums import MachineState, ProbingState -from .jpcntx import EUCJPContextAnalysis -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import EUCJP_SM_MODEL - - -class EUCJPProber(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(EUCJP_SM_MODEL) - self.distribution_analyzer = EUCJPDistributionAnalysis() - self.context_analyzer = EUCJPContextAnalysis() - self.reset() - - def reset(self) -> None: - super().reset() - self.context_analyzer.reset() - - @property - def charset_name(self) -> str: - return "EUC-JP" - - @property - def language(self) -> str: - return "Japanese" - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - assert self.coding_sm is not None - assert self.distribution_analyzer is not None - - for i, byte in enumerate(byte_str): - # PY3K: byte_str is a byte array, so byte is an int, not a byte - coding_state = self.coding_sm.next_state(byte) - if coding_state == MachineState.ERROR: - self.logger.debug( - "%s %s prober hit error at byte %s", - self.charset_name, - self.language, - i, - ) - self._state = ProbingState.NOT_ME - break - if coding_state == MachineState.ITS_ME: - self._state = ProbingState.FOUND_IT - break - if coding_state == MachineState.START: - char_len = self.coding_sm.get_current_charlen() - if i == 0: - self._last_char[1] = byte - self.context_analyzer.feed(self._last_char, char_len) - self.distribution_analyzer.feed(self._last_char, char_len) - else: - self.context_analyzer.feed(byte_str[i - 1 : i + 1], char_len) - self.distribution_analyzer.feed(byte_str[i - 1 : i + 1], char_len) - - self._last_char[0] = byte_str[-1] - - if self.state == ProbingState.DETECTING: - if self.context_analyzer.got_enough_data() and ( - self.get_confidence() > self.SHORTCUT_THRESHOLD - ): - self._state = ProbingState.FOUND_IT - - return self.state - - def get_confidence(self) -> float: - assert self.distribution_analyzer is not None - - context_conf = self.context_analyzer.get_confidence() - distrib_conf = self.distribution_analyzer.get_confidence() - return max(context_conf, distrib_conf) diff --git a/libs/chardet/euckrfreq.py b/libs/chardet/euckrfreq.py deleted file mode 100644 index 7dc3b1038..000000000 --- a/libs/chardet/euckrfreq.py +++ /dev/null @@ -1,196 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# Sampling from about 20M text materials include literature and computer technology - -# 128 --> 0.79 -# 256 --> 0.92 -# 512 --> 0.986 -# 1024 --> 0.99944 -# 2048 --> 0.99999 -# -# Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24 -# Random Distribution Ration = 512 / (2350-512) = 0.279. -# -# Typical Distribution Ratio - -EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0 - -EUCKR_TABLE_SIZE = 2352 - -# Char to FreqOrder table , -# fmt: off -EUCKR_CHAR_TO_FREQ_ORDER = ( - 13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722, 87, -1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398, -1399,1729,1730,1731, 141, 621, 326,1057, 368,1732, 267, 488, 20,1733,1269,1734, - 945,1400,1735, 47, 904,1270,1736,1737, 773, 248,1738, 409, 313, 786, 429,1739, - 116, 987, 813,1401, 683, 75,1204, 145,1740,1741,1742,1743, 16, 847, 667, 622, - 708,1744,1745,1746, 966, 787, 304, 129,1747, 60, 820, 123, 676,1748,1749,1750, -1751, 617,1752, 626,1753,1754,1755,1756, 653,1757,1758,1759,1760,1761,1762, 856, - 344,1763,1764,1765,1766, 89, 401, 418, 806, 905, 848,1767,1768,1769, 946,1205, - 709,1770,1118,1771, 241,1772,1773,1774,1271,1775, 569,1776, 999,1777,1778,1779, -1780, 337, 751,1058, 28, 628, 254,1781, 177, 906, 270, 349, 891,1079,1782, 19, -1783, 379,1784, 315,1785, 629, 754,1402, 559,1786, 636, 203,1206,1787, 710, 567, -1788, 935, 814,1789,1790,1207, 766, 528,1791,1792,1208,1793,1794,1795,1796,1797, -1403,1798,1799, 533,1059,1404,1405,1156,1406, 936, 884,1080,1800, 351,1801,1802, -1803,1804,1805, 801,1806,1807,1808,1119,1809,1157, 714, 474,1407,1810, 298, 899, - 885,1811,1120, 802,1158,1812, 892,1813,1814,1408, 659,1815,1816,1121,1817,1818, -1819,1820,1821,1822, 319,1823, 594, 545,1824, 815, 937,1209,1825,1826, 573,1409, -1022,1827,1210,1828,1829,1830,1831,1832,1833, 556, 722, 807,1122,1060,1834, 697, -1835, 900, 557, 715,1836,1410, 540,1411, 752,1159, 294, 597,1211, 976, 803, 770, -1412,1837,1838, 39, 794,1413, 358,1839, 371, 925,1840, 453, 661, 788, 531, 723, - 544,1023,1081, 869, 91,1841, 392, 430, 790, 602,1414, 677,1082, 457,1415,1416, -1842,1843, 475, 327,1024,1417, 795, 121,1844, 733, 403,1418,1845,1846,1847, 300, - 119, 711,1212, 627,1848,1272, 207,1849,1850, 796,1213, 382,1851, 519,1852,1083, - 893,1853,1854,1855, 367, 809, 487, 671,1856, 663,1857,1858, 956, 471, 306, 857, -1859,1860,1160,1084,1861,1862,1863,1864,1865,1061,1866,1867,1868,1869,1870,1871, - 282, 96, 574,1872, 502,1085,1873,1214,1874, 907,1875,1876, 827, 977,1419,1420, -1421, 268,1877,1422,1878,1879,1880, 308,1881, 2, 537,1882,1883,1215,1884,1885, - 127, 791,1886,1273,1423,1887, 34, 336, 404, 643,1888, 571, 654, 894, 840,1889, - 0, 886,1274, 122, 575, 260, 908, 938,1890,1275, 410, 316,1891,1892, 100,1893, -1894,1123, 48,1161,1124,1025,1895, 633, 901,1276,1896,1897, 115, 816,1898, 317, -1899, 694,1900, 909, 734,1424, 572, 866,1425, 691, 85, 524,1010, 543, 394, 841, -1901,1902,1903,1026,1904,1905,1906,1907,1908,1909, 30, 451, 651, 988, 310,1910, -1911,1426, 810,1216, 93,1912,1913,1277,1217,1914, 858, 759, 45, 58, 181, 610, - 269,1915,1916, 131,1062, 551, 443,1000, 821,1427, 957, 895,1086,1917,1918, 375, -1919, 359,1920, 687,1921, 822,1922, 293,1923,1924, 40, 662, 118, 692, 29, 939, - 887, 640, 482, 174,1925, 69,1162, 728,1428, 910,1926,1278,1218,1279, 386, 870, - 217, 854,1163, 823,1927,1928,1929,1930, 834,1931, 78,1932, 859,1933,1063,1934, -1935,1936,1937, 438,1164, 208, 595,1938,1939,1940,1941,1219,1125,1942, 280, 888, -1429,1430,1220,1431,1943,1944,1945,1946,1947,1280, 150, 510,1432,1948,1949,1950, -1951,1952,1953,1954,1011,1087,1955,1433,1043,1956, 881,1957, 614, 958,1064,1065, -1221,1958, 638,1001, 860, 967, 896,1434, 989, 492, 553,1281,1165,1959,1282,1002, -1283,1222,1960,1961,1962,1963, 36, 383, 228, 753, 247, 454,1964, 876, 678,1965, -1966,1284, 126, 464, 490, 835, 136, 672, 529, 940,1088,1435, 473,1967,1968, 467, - 50, 390, 227, 587, 279, 378, 598, 792, 968, 240, 151, 160, 849, 882,1126,1285, - 639,1044, 133, 140, 288, 360, 811, 563,1027, 561, 142, 523,1969,1970,1971, 7, - 103, 296, 439, 407, 506, 634, 990,1972,1973,1974,1975, 645,1976,1977,1978,1979, -1980,1981, 236,1982,1436,1983,1984,1089, 192, 828, 618, 518,1166, 333,1127,1985, - 818,1223,1986,1987,1988,1989,1990,1991,1992,1993, 342,1128,1286, 746, 842,1994, -1995, 560, 223,1287, 98, 8, 189, 650, 978,1288,1996,1437,1997, 17, 345, 250, - 423, 277, 234, 512, 226, 97, 289, 42, 167,1998, 201,1999,2000, 843, 836, 824, - 532, 338, 783,1090, 182, 576, 436,1438,1439, 527, 500,2001, 947, 889,2002,2003, -2004,2005, 262, 600, 314, 447,2006, 547,2007, 693, 738,1129,2008, 71,1440, 745, - 619, 688,2009, 829,2010,2011, 147,2012, 33, 948,2013,2014, 74, 224,2015, 61, - 191, 918, 399, 637,2016,1028,1130, 257, 902,2017,2018,2019,2020,2021,2022,2023, -2024,2025,2026, 837,2027,2028,2029,2030, 179, 874, 591, 52, 724, 246,2031,2032, -2033,2034,1167, 969,2035,1289, 630, 605, 911,1091,1168,2036,2037,2038,1441, 912, -2039, 623,2040,2041, 253,1169,1290,2042,1442, 146, 620, 611, 577, 433,2043,1224, - 719,1170, 959, 440, 437, 534, 84, 388, 480,1131, 159, 220, 198, 679,2044,1012, - 819,1066,1443, 113,1225, 194, 318,1003,1029,2045,2046,2047,2048,1067,2049,2050, -2051,2052,2053, 59, 913, 112,2054, 632,2055, 455, 144, 739,1291,2056, 273, 681, - 499,2057, 448,2058,2059, 760,2060,2061, 970, 384, 169, 245,1132,2062,2063, 414, -1444,2064,2065, 41, 235,2066, 157, 252, 877, 568, 919, 789, 580,2067, 725,2068, -2069,1292,2070,2071,1445,2072,1446,2073,2074, 55, 588, 66,1447, 271,1092,2075, -1226,2076, 960,1013, 372,2077,2078,2079,2080,2081,1293,2082,2083,2084,2085, 850, -2086,2087,2088,2089,2090, 186,2091,1068, 180,2092,2093,2094, 109,1227, 522, 606, -2095, 867,1448,1093, 991,1171, 926, 353,1133,2096, 581,2097,2098,2099,1294,1449, -1450,2100, 596,1172,1014,1228,2101,1451,1295,1173,1229,2102,2103,1296,1134,1452, - 949,1135,2104,2105,1094,1453,1454,1455,2106,1095,2107,2108,2109,2110,2111,2112, -2113,2114,2115,2116,2117, 804,2118,2119,1230,1231, 805,1456, 405,1136,2120,2121, -2122,2123,2124, 720, 701,1297, 992,1457, 927,1004,2125,2126,2127,2128,2129,2130, - 22, 417,2131, 303,2132, 385,2133, 971, 520, 513,2134,1174, 73,1096, 231, 274, - 962,1458, 673,2135,1459,2136, 152,1137,2137,2138,2139,2140,1005,1138,1460,1139, -2141,2142,2143,2144, 11, 374, 844,2145, 154,1232, 46,1461,2146, 838, 830, 721, -1233, 106,2147, 90, 428, 462, 578, 566,1175, 352,2148,2149, 538,1234, 124,1298, -2150,1462, 761, 565,2151, 686,2152, 649,2153, 72, 173,2154, 460, 415,2155,1463, -2156,1235, 305,2157,2158,2159,2160,2161,2162, 579,2163,2164,2165,2166,2167, 747, -2168,2169,2170,2171,1464, 669,2172,2173,2174,2175,2176,1465,2177, 23, 530, 285, -2178, 335, 729,2179, 397,2180,2181,2182,1030,2183,2184, 698,2185,2186, 325,2187, -2188, 369,2189, 799,1097,1015, 348,2190,1069, 680,2191, 851,1466,2192,2193, 10, -2194, 613, 424,2195, 979, 108, 449, 589, 27, 172, 81,1031, 80, 774, 281, 350, -1032, 525, 301, 582,1176,2196, 674,1045,2197,2198,1467, 730, 762,2199,2200,2201, -2202,1468,2203, 993,2204,2205, 266,1070, 963,1140,2206,2207,2208, 664,1098, 972, -2209,2210,2211,1177,1469,1470, 871,2212,2213,2214,2215,2216,1471,2217,2218,2219, -2220,2221,2222,2223,2224,2225,2226,2227,1472,1236,2228,2229,2230,2231,2232,2233, -2234,2235,1299,2236,2237, 200,2238, 477, 373,2239,2240, 731, 825, 777,2241,2242, -2243, 521, 486, 548,2244,2245,2246,1473,1300, 53, 549, 137, 875, 76, 158,2247, -1301,1474, 469, 396,1016, 278, 712,2248, 321, 442, 503, 767, 744, 941,1237,1178, -1475,2249, 82, 178,1141,1179, 973,2250,1302,2251, 297,2252,2253, 570,2254,2255, -2256, 18, 450, 206,2257, 290, 292,1142,2258, 511, 162, 99, 346, 164, 735,2259, -1476,1477, 4, 554, 343, 798,1099,2260,1100,2261, 43, 171,1303, 139, 215,2262, -2263, 717, 775,2264,1033, 322, 216,2265, 831,2266, 149,2267,1304,2268,2269, 702, -1238, 135, 845, 347, 309,2270, 484,2271, 878, 655, 238,1006,1478,2272, 67,2273, - 295,2274,2275, 461,2276, 478, 942, 412,2277,1034,2278,2279,2280, 265,2281, 541, -2282,2283,2284,2285,2286, 70, 852,1071,2287,2288,2289,2290, 21, 56, 509, 117, - 432,2291,2292, 331, 980, 552,1101, 148, 284, 105, 393,1180,1239, 755,2293, 187, -2294,1046,1479,2295, 340,2296, 63,1047, 230,2297,2298,1305, 763,1306, 101, 800, - 808, 494,2299,2300,2301, 903,2302, 37,1072, 14, 5,2303, 79, 675,2304, 312, -2305,2306,2307,2308,2309,1480, 6,1307,2310,2311,2312, 1, 470, 35, 24, 229, -2313, 695, 210, 86, 778, 15, 784, 592, 779, 32, 77, 855, 964,2314, 259,2315, - 501, 380,2316,2317, 83, 981, 153, 689,1308,1481,1482,1483,2318,2319, 716,1484, -2320,2321,2322,2323,2324,2325,1485,2326,2327, 128, 57, 68, 261,1048, 211, 170, -1240, 31,2328, 51, 435, 742,2329,2330,2331, 635,2332, 264, 456,2333,2334,2335, - 425,2336,1486, 143, 507, 263, 943,2337, 363, 920,1487, 256,1488,1102, 243, 601, -1489,2338,2339,2340,2341,2342,2343,2344, 861,2345,2346,2347,2348,2349,2350, 395, -2351,1490,1491, 62, 535, 166, 225,2352,2353, 668, 419,1241, 138, 604, 928,2354, -1181,2355,1492,1493,2356,2357,2358,1143,2359, 696,2360, 387, 307,1309, 682, 476, -2361,2362, 332, 12, 222, 156,2363, 232,2364, 641, 276, 656, 517,1494,1495,1035, - 416, 736,1496,2365,1017, 586,2366,2367,2368,1497,2369, 242,2370,2371,2372,1498, -2373, 965, 713,2374,2375,2376,2377, 740, 982,1499, 944,1500,1007,2378,2379,1310, -1501,2380,2381,2382, 785, 329,2383,2384,1502,2385,2386,2387, 932,2388,1503,2389, -2390,2391,2392,1242,2393,2394,2395,2396,2397, 994, 950,2398,2399,2400,2401,1504, -1311,2402,2403,2404,2405,1049, 749,2406,2407, 853, 718,1144,1312,2408,1182,1505, -2409,2410, 255, 516, 479, 564, 550, 214,1506,1507,1313, 413, 239, 444, 339,1145, -1036,1508,1509,1314,1037,1510,1315,2411,1511,2412,2413,2414, 176, 703, 497, 624, - 593, 921, 302,2415, 341, 165,1103,1512,2416,1513,2417,2418,2419, 376,2420, 700, -2421,2422,2423, 258, 768,1316,2424,1183,2425, 995, 608,2426,2427,2428,2429, 221, -2430,2431,2432,2433,2434,2435,2436,2437, 195, 323, 726, 188, 897, 983,1317, 377, - 644,1050, 879,2438, 452,2439,2440,2441,2442,2443,2444, 914,2445,2446,2447,2448, - 915, 489,2449,1514,1184,2450,2451, 515, 64, 427, 495,2452, 583,2453, 483, 485, -1038, 562, 213,1515, 748, 666,2454,2455,2456,2457, 334,2458, 780, 996,1008, 705, -1243,2459,2460,2461,2462,2463, 114,2464, 493,1146, 366, 163,1516, 961,1104,2465, - 291,2466,1318,1105,2467,1517, 365,2468, 355, 951,1244,2469,1319,2470, 631,2471, -2472, 218,1320, 364, 320, 756,1518,1519,1321,1520,1322,2473,2474,2475,2476, 997, -2477,2478,2479,2480, 665,1185,2481, 916,1521,2482,2483,2484, 584, 684,2485,2486, - 797,2487,1051,1186,2488,2489,2490,1522,2491,2492, 370,2493,1039,1187, 65,2494, - 434, 205, 463,1188,2495, 125, 812, 391, 402, 826, 699, 286, 398, 155, 781, 771, - 585,2496, 590, 505,1073,2497, 599, 244, 219, 917,1018, 952, 646,1523,2498,1323, -2499,2500, 49, 984, 354, 741,2501, 625,2502,1324,2503,1019, 190, 357, 757, 491, - 95, 782, 868,2504,2505,2506,2507,2508,2509, 134,1524,1074, 422,1525, 898,2510, - 161,2511,2512,2513,2514, 769,2515,1526,2516,2517, 411,1325,2518, 472,1527,2519, -2520,2521,2522,2523,2524, 985,2525,2526,2527,2528,2529,2530, 764,2531,1245,2532, -2533, 25, 204, 311,2534, 496,2535,1052,2536,2537,2538,2539,2540,2541,2542, 199, - 704, 504, 468, 758, 657,1528, 196, 44, 839,1246, 272, 750,2543, 765, 862,2544, -2545,1326,2546, 132, 615, 933,2547, 732,2548,2549,2550,1189,1529,2551, 283,1247, -1053, 607, 929,2552,2553,2554, 930, 183, 872, 616,1040,1147,2555,1148,1020, 441, - 249,1075,2556,2557,2558, 466, 743,2559,2560,2561, 92, 514, 426, 420, 526,2562, -2563,2564,2565,2566,2567,2568, 185,2569,2570,2571,2572, 776,1530, 658,2573, 362, -2574, 361, 922,1076, 793,2575,2576,2577,2578,2579,2580,1531, 251,2581,2582,2583, -2584,1532, 54, 612, 237,1327,2585,2586, 275, 408, 647, 111,2587,1533,1106, 465, - 3, 458, 9, 38,2588, 107, 110, 890, 209, 26, 737, 498,2589,1534,2590, 431, - 202, 88,1535, 356, 287,1107, 660,1149,2591, 381,1536, 986,1150, 445,1248,1151, - 974,2592,2593, 846,2594, 446, 953, 184,1249,1250, 727,2595, 923, 193, 883,2596, -2597,2598, 102, 324, 539, 817,2599, 421,1041,2600, 832,2601, 94, 175, 197, 406, -2602, 459,2603,2604,2605,2606,2607, 330, 555,2608,2609,2610, 706,1108, 389,2611, -2612,2613,2614, 233,2615, 833, 558, 931, 954,1251,2616,2617,1537, 546,2618,2619, -1009,2620,2621,2622,1538, 690,1328,2623, 955,2624,1539,2625,2626, 772,2627,2628, -2629,2630,2631, 924, 648, 863, 603,2632,2633, 934,1540, 864, 865,2634, 642,1042, - 670,1190,2635,2636,2637,2638, 168,2639, 652, 873, 542,1054,1541,2640,2641,2642, # 512, 256 -) -# fmt: on diff --git a/libs/chardet/euckrprober.py b/libs/chardet/euckrprober.py deleted file mode 100644 index 1fc5de046..000000000 --- a/libs/chardet/euckrprober.py +++ /dev/null @@ -1,47 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .chardistribution import EUCKRDistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import EUCKR_SM_MODEL - - -class EUCKRProber(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(EUCKR_SM_MODEL) - self.distribution_analyzer = EUCKRDistributionAnalysis() - self.reset() - - @property - def charset_name(self) -> str: - return "EUC-KR" - - @property - def language(self) -> str: - return "Korean" diff --git a/libs/chardet/euctwfreq.py b/libs/chardet/euctwfreq.py deleted file mode 100644 index 4900ccc16..000000000 --- a/libs/chardet/euctwfreq.py +++ /dev/null @@ -1,388 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# EUCTW frequency table -# Converted from big5 work -# by Taiwan's Mandarin Promotion Council -# - -# 128 --> 0.42261 -# 256 --> 0.57851 -# 512 --> 0.74851 -# 1024 --> 0.89384 -# 2048 --> 0.97583 -# -# Idea Distribution Ratio = 0.74851/(1-0.74851) =2.98 -# Random Distribution Ration = 512/(5401-512)=0.105 -# -# Typical Distribution Ratio about 25% of Ideal one, still much higher than RDR - -EUCTW_TYPICAL_DISTRIBUTION_RATIO = 0.75 - -# Char to FreqOrder table -EUCTW_TABLE_SIZE = 5376 - -# fmt: off -EUCTW_CHAR_TO_FREQ_ORDER = ( - 1, 1800, 1506, 255, 1431, 198, 9, 82, 6, 7310, 177, 202, 3615, 1256, 2808, 110, # 2742 - 3735, 33, 3241, 261, 76, 44, 2113, 16, 2931, 2184, 1176, 659, 3868, 26, 3404, 2643, # 2758 - 1198, 3869, 3313, 4060, 410, 2211, 302, 590, 361, 1963, 8, 204, 58, 4296, 7311, 1931, # 2774 - 63, 7312, 7313, 317, 1614, 75, 222, 159, 4061, 2412, 1480, 7314, 3500, 3068, 224, 2809, # 2790 - 3616, 3, 10, 3870, 1471, 29, 2774, 1135, 2852, 1939, 873, 130, 3242, 1123, 312, 7315, # 2806 - 4297, 2051, 507, 252, 682, 7316, 142, 1914, 124, 206, 2932, 34, 3501, 3173, 64, 604, # 2822 - 7317, 2494, 1976, 1977, 155, 1990, 645, 641, 1606, 7318, 3405, 337, 72, 406, 7319, 80, # 2838 - 630, 238, 3174, 1509, 263, 939, 1092, 2644, 756, 1440, 1094, 3406, 449, 69, 2969, 591, # 2854 - 179, 2095, 471, 115, 2034, 1843, 60, 50, 2970, 134, 806, 1868, 734, 2035, 3407, 180, # 2870 - 995, 1607, 156, 537, 2893, 688, 7320, 319, 1305, 779, 2144, 514, 2374, 298, 4298, 359, # 2886 - 2495, 90, 2707, 1338, 663, 11, 906, 1099, 2545, 20, 2436, 182, 532, 1716, 7321, 732, # 2902 - 1376, 4062, 1311, 1420, 3175, 25, 2312, 1056, 113, 399, 382, 1949, 242, 3408, 2467, 529, # 2918 - 3243, 475, 1447, 3617, 7322, 117, 21, 656, 810, 1297, 2295, 2329, 3502, 7323, 126, 4063, # 2934 - 706, 456, 150, 613, 4299, 71, 1118, 2036, 4064, 145, 3069, 85, 835, 486, 2114, 1246, # 2950 - 1426, 428, 727, 1285, 1015, 800, 106, 623, 303, 1281, 7324, 2127, 2354, 347, 3736, 221, # 2966 - 3503, 3110, 7325, 1955, 1153, 4065, 83, 296, 1199, 3070, 192, 624, 93, 7326, 822, 1897, # 2982 - 2810, 3111, 795, 2064, 991, 1554, 1542, 1592, 27, 43, 2853, 859, 139, 1456, 860, 4300, # 2998 - 437, 712, 3871, 164, 2392, 3112, 695, 211, 3017, 2096, 195, 3872, 1608, 3504, 3505, 3618, # 3014 - 3873, 234, 811, 2971, 2097, 3874, 2229, 1441, 3506, 1615, 2375, 668, 2076, 1638, 305, 228, # 3030 - 1664, 4301, 467, 415, 7327, 262, 2098, 1593, 239, 108, 300, 200, 1033, 512, 1247, 2077, # 3046 - 7328, 7329, 2173, 3176, 3619, 2673, 593, 845, 1062, 3244, 88, 1723, 2037, 3875, 1950, 212, # 3062 - 266, 152, 149, 468, 1898, 4066, 4302, 77, 187, 7330, 3018, 37, 5, 2972, 7331, 3876, # 3078 - 7332, 7333, 39, 2517, 4303, 2894, 3177, 2078, 55, 148, 74, 4304, 545, 483, 1474, 1029, # 3094 - 1665, 217, 1869, 1531, 3113, 1104, 2645, 4067, 24, 172, 3507, 900, 3877, 3508, 3509, 4305, # 3110 - 32, 1408, 2811, 1312, 329, 487, 2355, 2247, 2708, 784, 2674, 4, 3019, 3314, 1427, 1788, # 3126 - 188, 109, 499, 7334, 3620, 1717, 1789, 888, 1217, 3020, 4306, 7335, 3510, 7336, 3315, 1520, # 3142 - 3621, 3878, 196, 1034, 775, 7337, 7338, 929, 1815, 249, 439, 38, 7339, 1063, 7340, 794, # 3158 - 3879, 1435, 2296, 46, 178, 3245, 2065, 7341, 2376, 7342, 214, 1709, 4307, 804, 35, 707, # 3174 - 324, 3622, 1601, 2546, 140, 459, 4068, 7343, 7344, 1365, 839, 272, 978, 2257, 2572, 3409, # 3190 - 2128, 1363, 3623, 1423, 697, 100, 3071, 48, 70, 1231, 495, 3114, 2193, 7345, 1294, 7346, # 3206 - 2079, 462, 586, 1042, 3246, 853, 256, 988, 185, 2377, 3410, 1698, 434, 1084, 7347, 3411, # 3222 - 314, 2615, 2775, 4308, 2330, 2331, 569, 2280, 637, 1816, 2518, 757, 1162, 1878, 1616, 3412, # 3238 - 287, 1577, 2115, 768, 4309, 1671, 2854, 3511, 2519, 1321, 3737, 909, 2413, 7348, 4069, 933, # 3254 - 3738, 7349, 2052, 2356, 1222, 4310, 765, 2414, 1322, 786, 4311, 7350, 1919, 1462, 1677, 2895, # 3270 - 1699, 7351, 4312, 1424, 2437, 3115, 3624, 2590, 3316, 1774, 1940, 3413, 3880, 4070, 309, 1369, # 3286 - 1130, 2812, 364, 2230, 1653, 1299, 3881, 3512, 3882, 3883, 2646, 525, 1085, 3021, 902, 2000, # 3302 - 1475, 964, 4313, 421, 1844, 1415, 1057, 2281, 940, 1364, 3116, 376, 4314, 4315, 1381, 7, # 3318 - 2520, 983, 2378, 336, 1710, 2675, 1845, 321, 3414, 559, 1131, 3022, 2742, 1808, 1132, 1313, # 3334 - 265, 1481, 1857, 7352, 352, 1203, 2813, 3247, 167, 1089, 420, 2814, 776, 792, 1724, 3513, # 3350 - 4071, 2438, 3248, 7353, 4072, 7354, 446, 229, 333, 2743, 901, 3739, 1200, 1557, 4316, 2647, # 3366 - 1920, 395, 2744, 2676, 3740, 4073, 1835, 125, 916, 3178, 2616, 4317, 7355, 7356, 3741, 7357, # 3382 - 7358, 7359, 4318, 3117, 3625, 1133, 2547, 1757, 3415, 1510, 2313, 1409, 3514, 7360, 2145, 438, # 3398 - 2591, 2896, 2379, 3317, 1068, 958, 3023, 461, 311, 2855, 2677, 4074, 1915, 3179, 4075, 1978, # 3414 - 383, 750, 2745, 2617, 4076, 274, 539, 385, 1278, 1442, 7361, 1154, 1964, 384, 561, 210, # 3430 - 98, 1295, 2548, 3515, 7362, 1711, 2415, 1482, 3416, 3884, 2897, 1257, 129, 7363, 3742, 642, # 3446 - 523, 2776, 2777, 2648, 7364, 141, 2231, 1333, 68, 176, 441, 876, 907, 4077, 603, 2592, # 3462 - 710, 171, 3417, 404, 549, 18, 3118, 2393, 1410, 3626, 1666, 7365, 3516, 4319, 2898, 4320, # 3478 - 7366, 2973, 368, 7367, 146, 366, 99, 871, 3627, 1543, 748, 807, 1586, 1185, 22, 2258, # 3494 - 379, 3743, 3180, 7368, 3181, 505, 1941, 2618, 1991, 1382, 2314, 7369, 380, 2357, 218, 702, # 3510 - 1817, 1248, 3418, 3024, 3517, 3318, 3249, 7370, 2974, 3628, 930, 3250, 3744, 7371, 59, 7372, # 3526 - 585, 601, 4078, 497, 3419, 1112, 1314, 4321, 1801, 7373, 1223, 1472, 2174, 7374, 749, 1836, # 3542 - 690, 1899, 3745, 1772, 3885, 1476, 429, 1043, 1790, 2232, 2116, 917, 4079, 447, 1086, 1629, # 3558 - 7375, 556, 7376, 7377, 2020, 1654, 844, 1090, 105, 550, 966, 1758, 2815, 1008, 1782, 686, # 3574 - 1095, 7378, 2282, 793, 1602, 7379, 3518, 2593, 4322, 4080, 2933, 2297, 4323, 3746, 980, 2496, # 3590 - 544, 353, 527, 4324, 908, 2678, 2899, 7380, 381, 2619, 1942, 1348, 7381, 1341, 1252, 560, # 3606 - 3072, 7382, 3420, 2856, 7383, 2053, 973, 886, 2080, 143, 4325, 7384, 7385, 157, 3886, 496, # 3622 - 4081, 57, 840, 540, 2038, 4326, 4327, 3421, 2117, 1445, 970, 2259, 1748, 1965, 2081, 4082, # 3638 - 3119, 1234, 1775, 3251, 2816, 3629, 773, 1206, 2129, 1066, 2039, 1326, 3887, 1738, 1725, 4083, # 3654 - 279, 3120, 51, 1544, 2594, 423, 1578, 2130, 2066, 173, 4328, 1879, 7386, 7387, 1583, 264, # 3670 - 610, 3630, 4329, 2439, 280, 154, 7388, 7389, 7390, 1739, 338, 1282, 3073, 693, 2857, 1411, # 3686 - 1074, 3747, 2440, 7391, 4330, 7392, 7393, 1240, 952, 2394, 7394, 2900, 1538, 2679, 685, 1483, # 3702 - 4084, 2468, 1436, 953, 4085, 2054, 4331, 671, 2395, 79, 4086, 2441, 3252, 608, 567, 2680, # 3718 - 3422, 4087, 4088, 1691, 393, 1261, 1791, 2396, 7395, 4332, 7396, 7397, 7398, 7399, 1383, 1672, # 3734 - 3748, 3182, 1464, 522, 1119, 661, 1150, 216, 675, 4333, 3888, 1432, 3519, 609, 4334, 2681, # 3750 - 2397, 7400, 7401, 7402, 4089, 3025, 0, 7403, 2469, 315, 231, 2442, 301, 3319, 4335, 2380, # 3766 - 7404, 233, 4090, 3631, 1818, 4336, 4337, 7405, 96, 1776, 1315, 2082, 7406, 257, 7407, 1809, # 3782 - 3632, 2709, 1139, 1819, 4091, 2021, 1124, 2163, 2778, 1777, 2649, 7408, 3074, 363, 1655, 3183, # 3798 - 7409, 2975, 7410, 7411, 7412, 3889, 1567, 3890, 718, 103, 3184, 849, 1443, 341, 3320, 2934, # 3814 - 1484, 7413, 1712, 127, 67, 339, 4092, 2398, 679, 1412, 821, 7414, 7415, 834, 738, 351, # 3830 - 2976, 2146, 846, 235, 1497, 1880, 418, 1992, 3749, 2710, 186, 1100, 2147, 2746, 3520, 1545, # 3846 - 1355, 2935, 2858, 1377, 583, 3891, 4093, 2573, 2977, 7416, 1298, 3633, 1078, 2549, 3634, 2358, # 3862 - 78, 3750, 3751, 267, 1289, 2099, 2001, 1594, 4094, 348, 369, 1274, 2194, 2175, 1837, 4338, # 3878 - 1820, 2817, 3635, 2747, 2283, 2002, 4339, 2936, 2748, 144, 3321, 882, 4340, 3892, 2749, 3423, # 3894 - 4341, 2901, 7417, 4095, 1726, 320, 7418, 3893, 3026, 788, 2978, 7419, 2818, 1773, 1327, 2859, # 3910 - 3894, 2819, 7420, 1306, 4342, 2003, 1700, 3752, 3521, 2359, 2650, 787, 2022, 506, 824, 3636, # 3926 - 534, 323, 4343, 1044, 3322, 2023, 1900, 946, 3424, 7421, 1778, 1500, 1678, 7422, 1881, 4344, # 3942 - 165, 243, 4345, 3637, 2521, 123, 683, 4096, 764, 4346, 36, 3895, 1792, 589, 2902, 816, # 3958 - 626, 1667, 3027, 2233, 1639, 1555, 1622, 3753, 3896, 7423, 3897, 2860, 1370, 1228, 1932, 891, # 3974 - 2083, 2903, 304, 4097, 7424, 292, 2979, 2711, 3522, 691, 2100, 4098, 1115, 4347, 118, 662, # 3990 - 7425, 611, 1156, 854, 2381, 1316, 2861, 2, 386, 515, 2904, 7426, 7427, 3253, 868, 2234, # 4006 - 1486, 855, 2651, 785, 2212, 3028, 7428, 1040, 3185, 3523, 7429, 3121, 448, 7430, 1525, 7431, # 4022 - 2164, 4348, 7432, 3754, 7433, 4099, 2820, 3524, 3122, 503, 818, 3898, 3123, 1568, 814, 676, # 4038 - 1444, 306, 1749, 7434, 3755, 1416, 1030, 197, 1428, 805, 2821, 1501, 4349, 7435, 7436, 7437, # 4054 - 1993, 7438, 4350, 7439, 7440, 2195, 13, 2779, 3638, 2980, 3124, 1229, 1916, 7441, 3756, 2131, # 4070 - 7442, 4100, 4351, 2399, 3525, 7443, 2213, 1511, 1727, 1120, 7444, 7445, 646, 3757, 2443, 307, # 4086 - 7446, 7447, 1595, 3186, 7448, 7449, 7450, 3639, 1113, 1356, 3899, 1465, 2522, 2523, 7451, 519, # 4102 - 7452, 128, 2132, 92, 2284, 1979, 7453, 3900, 1512, 342, 3125, 2196, 7454, 2780, 2214, 1980, # 4118 - 3323, 7455, 290, 1656, 1317, 789, 827, 2360, 7456, 3758, 4352, 562, 581, 3901, 7457, 401, # 4134 - 4353, 2248, 94, 4354, 1399, 2781, 7458, 1463, 2024, 4355, 3187, 1943, 7459, 828, 1105, 4101, # 4150 - 1262, 1394, 7460, 4102, 605, 4356, 7461, 1783, 2862, 7462, 2822, 819, 2101, 578, 2197, 2937, # 4166 - 7463, 1502, 436, 3254, 4103, 3255, 2823, 3902, 2905, 3425, 3426, 7464, 2712, 2315, 7465, 7466, # 4182 - 2332, 2067, 23, 4357, 193, 826, 3759, 2102, 699, 1630, 4104, 3075, 390, 1793, 1064, 3526, # 4198 - 7467, 1579, 3076, 3077, 1400, 7468, 4105, 1838, 1640, 2863, 7469, 4358, 4359, 137, 4106, 598, # 4214 - 3078, 1966, 780, 104, 974, 2938, 7470, 278, 899, 253, 402, 572, 504, 493, 1339, 7471, # 4230 - 3903, 1275, 4360, 2574, 2550, 7472, 3640, 3029, 3079, 2249, 565, 1334, 2713, 863, 41, 7473, # 4246 - 7474, 4361, 7475, 1657, 2333, 19, 463, 2750, 4107, 606, 7476, 2981, 3256, 1087, 2084, 1323, # 4262 - 2652, 2982, 7477, 1631, 1623, 1750, 4108, 2682, 7478, 2864, 791, 2714, 2653, 2334, 232, 2416, # 4278 - 7479, 2983, 1498, 7480, 2654, 2620, 755, 1366, 3641, 3257, 3126, 2025, 1609, 119, 1917, 3427, # 4294 - 862, 1026, 4109, 7481, 3904, 3760, 4362, 3905, 4363, 2260, 1951, 2470, 7482, 1125, 817, 4110, # 4310 - 4111, 3906, 1513, 1766, 2040, 1487, 4112, 3030, 3258, 2824, 3761, 3127, 7483, 7484, 1507, 7485, # 4326 - 2683, 733, 40, 1632, 1106, 2865, 345, 4113, 841, 2524, 230, 4364, 2984, 1846, 3259, 3428, # 4342 - 7486, 1263, 986, 3429, 7487, 735, 879, 254, 1137, 857, 622, 1300, 1180, 1388, 1562, 3907, # 4358 - 3908, 2939, 967, 2751, 2655, 1349, 592, 2133, 1692, 3324, 2985, 1994, 4114, 1679, 3909, 1901, # 4374 - 2185, 7488, 739, 3642, 2715, 1296, 1290, 7489, 4115, 2198, 2199, 1921, 1563, 2595, 2551, 1870, # 4390 - 2752, 2986, 7490, 435, 7491, 343, 1108, 596, 17, 1751, 4365, 2235, 3430, 3643, 7492, 4366, # 4406 - 294, 3527, 2940, 1693, 477, 979, 281, 2041, 3528, 643, 2042, 3644, 2621, 2782, 2261, 1031, # 4422 - 2335, 2134, 2298, 3529, 4367, 367, 1249, 2552, 7493, 3530, 7494, 4368, 1283, 3325, 2004, 240, # 4438 - 1762, 3326, 4369, 4370, 836, 1069, 3128, 474, 7495, 2148, 2525, 268, 3531, 7496, 3188, 1521, # 4454 - 1284, 7497, 1658, 1546, 4116, 7498, 3532, 3533, 7499, 4117, 3327, 2684, 1685, 4118, 961, 1673, # 4470 - 2622, 190, 2005, 2200, 3762, 4371, 4372, 7500, 570, 2497, 3645, 1490, 7501, 4373, 2623, 3260, # 4486 - 1956, 4374, 584, 1514, 396, 1045, 1944, 7502, 4375, 1967, 2444, 7503, 7504, 4376, 3910, 619, # 4502 - 7505, 3129, 3261, 215, 2006, 2783, 2553, 3189, 4377, 3190, 4378, 763, 4119, 3763, 4379, 7506, # 4518 - 7507, 1957, 1767, 2941, 3328, 3646, 1174, 452, 1477, 4380, 3329, 3130, 7508, 2825, 1253, 2382, # 4534 - 2186, 1091, 2285, 4120, 492, 7509, 638, 1169, 1824, 2135, 1752, 3911, 648, 926, 1021, 1324, # 4550 - 4381, 520, 4382, 997, 847, 1007, 892, 4383, 3764, 2262, 1871, 3647, 7510, 2400, 1784, 4384, # 4566 - 1952, 2942, 3080, 3191, 1728, 4121, 2043, 3648, 4385, 2007, 1701, 3131, 1551, 30, 2263, 4122, # 4582 - 7511, 2026, 4386, 3534, 7512, 501, 7513, 4123, 594, 3431, 2165, 1821, 3535, 3432, 3536, 3192, # 4598 - 829, 2826, 4124, 7514, 1680, 3132, 1225, 4125, 7515, 3262, 4387, 4126, 3133, 2336, 7516, 4388, # 4614 - 4127, 7517, 3912, 3913, 7518, 1847, 2383, 2596, 3330, 7519, 4389, 374, 3914, 652, 4128, 4129, # 4630 - 375, 1140, 798, 7520, 7521, 7522, 2361, 4390, 2264, 546, 1659, 138, 3031, 2445, 4391, 7523, # 4646 - 2250, 612, 1848, 910, 796, 3765, 1740, 1371, 825, 3766, 3767, 7524, 2906, 2554, 7525, 692, # 4662 - 444, 3032, 2624, 801, 4392, 4130, 7526, 1491, 244, 1053, 3033, 4131, 4132, 340, 7527, 3915, # 4678 - 1041, 2987, 293, 1168, 87, 1357, 7528, 1539, 959, 7529, 2236, 721, 694, 4133, 3768, 219, # 4694 - 1478, 644, 1417, 3331, 2656, 1413, 1401, 1335, 1389, 3916, 7530, 7531, 2988, 2362, 3134, 1825, # 4710 - 730, 1515, 184, 2827, 66, 4393, 7532, 1660, 2943, 246, 3332, 378, 1457, 226, 3433, 975, # 4726 - 3917, 2944, 1264, 3537, 674, 696, 7533, 163, 7534, 1141, 2417, 2166, 713, 3538, 3333, 4394, # 4742 - 3918, 7535, 7536, 1186, 15, 7537, 1079, 1070, 7538, 1522, 3193, 3539, 276, 1050, 2716, 758, # 4758 - 1126, 653, 2945, 3263, 7539, 2337, 889, 3540, 3919, 3081, 2989, 903, 1250, 4395, 3920, 3434, # 4774 - 3541, 1342, 1681, 1718, 766, 3264, 286, 89, 2946, 3649, 7540, 1713, 7541, 2597, 3334, 2990, # 4790 - 7542, 2947, 2215, 3194, 2866, 7543, 4396, 2498, 2526, 181, 387, 1075, 3921, 731, 2187, 3335, # 4806 - 7544, 3265, 310, 313, 3435, 2299, 770, 4134, 54, 3034, 189, 4397, 3082, 3769, 3922, 7545, # 4822 - 1230, 1617, 1849, 355, 3542, 4135, 4398, 3336, 111, 4136, 3650, 1350, 3135, 3436, 3035, 4137, # 4838 - 2149, 3266, 3543, 7546, 2784, 3923, 3924, 2991, 722, 2008, 7547, 1071, 247, 1207, 2338, 2471, # 4854 - 1378, 4399, 2009, 864, 1437, 1214, 4400, 373, 3770, 1142, 2216, 667, 4401, 442, 2753, 2555, # 4870 - 3771, 3925, 1968, 4138, 3267, 1839, 837, 170, 1107, 934, 1336, 1882, 7548, 7549, 2118, 4139, # 4886 - 2828, 743, 1569, 7550, 4402, 4140, 582, 2384, 1418, 3437, 7551, 1802, 7552, 357, 1395, 1729, # 4902 - 3651, 3268, 2418, 1564, 2237, 7553, 3083, 3772, 1633, 4403, 1114, 2085, 4141, 1532, 7554, 482, # 4918 - 2446, 4404, 7555, 7556, 1492, 833, 1466, 7557, 2717, 3544, 1641, 2829, 7558, 1526, 1272, 3652, # 4934 - 4142, 1686, 1794, 416, 2556, 1902, 1953, 1803, 7559, 3773, 2785, 3774, 1159, 2316, 7560, 2867, # 4950 - 4405, 1610, 1584, 3036, 2419, 2754, 443, 3269, 1163, 3136, 7561, 7562, 3926, 7563, 4143, 2499, # 4966 - 3037, 4406, 3927, 3137, 2103, 1647, 3545, 2010, 1872, 4144, 7564, 4145, 431, 3438, 7565, 250, # 4982 - 97, 81, 4146, 7566, 1648, 1850, 1558, 160, 848, 7567, 866, 740, 1694, 7568, 2201, 2830, # 4998 - 3195, 4147, 4407, 3653, 1687, 950, 2472, 426, 469, 3196, 3654, 3655, 3928, 7569, 7570, 1188, # 5014 - 424, 1995, 861, 3546, 4148, 3775, 2202, 2685, 168, 1235, 3547, 4149, 7571, 2086, 1674, 4408, # 5030 - 3337, 3270, 220, 2557, 1009, 7572, 3776, 670, 2992, 332, 1208, 717, 7573, 7574, 3548, 2447, # 5046 - 3929, 3338, 7575, 513, 7576, 1209, 2868, 3339, 3138, 4409, 1080, 7577, 7578, 7579, 7580, 2527, # 5062 - 3656, 3549, 815, 1587, 3930, 3931, 7581, 3550, 3439, 3777, 1254, 4410, 1328, 3038, 1390, 3932, # 5078 - 1741, 3933, 3778, 3934, 7582, 236, 3779, 2448, 3271, 7583, 7584, 3657, 3780, 1273, 3781, 4411, # 5094 - 7585, 308, 7586, 4412, 245, 4413, 1851, 2473, 1307, 2575, 430, 715, 2136, 2449, 7587, 270, # 5110 - 199, 2869, 3935, 7588, 3551, 2718, 1753, 761, 1754, 725, 1661, 1840, 4414, 3440, 3658, 7589, # 5126 - 7590, 587, 14, 3272, 227, 2598, 326, 480, 2265, 943, 2755, 3552, 291, 650, 1883, 7591, # 5142 - 1702, 1226, 102, 1547, 62, 3441, 904, 4415, 3442, 1164, 4150, 7592, 7593, 1224, 1548, 2756, # 5158 - 391, 498, 1493, 7594, 1386, 1419, 7595, 2055, 1177, 4416, 813, 880, 1081, 2363, 566, 1145, # 5174 - 4417, 2286, 1001, 1035, 2558, 2599, 2238, 394, 1286, 7596, 7597, 2068, 7598, 86, 1494, 1730, # 5190 - 3936, 491, 1588, 745, 897, 2948, 843, 3340, 3937, 2757, 2870, 3273, 1768, 998, 2217, 2069, # 5206 - 397, 1826, 1195, 1969, 3659, 2993, 3341, 284, 7599, 3782, 2500, 2137, 2119, 1903, 7600, 3938, # 5222 - 2150, 3939, 4151, 1036, 3443, 1904, 114, 2559, 4152, 209, 1527, 7601, 7602, 2949, 2831, 2625, # 5238 - 2385, 2719, 3139, 812, 2560, 7603, 3274, 7604, 1559, 737, 1884, 3660, 1210, 885, 28, 2686, # 5254 - 3553, 3783, 7605, 4153, 1004, 1779, 4418, 7606, 346, 1981, 2218, 2687, 4419, 3784, 1742, 797, # 5270 - 1642, 3940, 1933, 1072, 1384, 2151, 896, 3941, 3275, 3661, 3197, 2871, 3554, 7607, 2561, 1958, # 5286 - 4420, 2450, 1785, 7608, 7609, 7610, 3942, 4154, 1005, 1308, 3662, 4155, 2720, 4421, 4422, 1528, # 5302 - 2600, 161, 1178, 4156, 1982, 987, 4423, 1101, 4157, 631, 3943, 1157, 3198, 2420, 1343, 1241, # 5318 - 1016, 2239, 2562, 372, 877, 2339, 2501, 1160, 555, 1934, 911, 3944, 7611, 466, 1170, 169, # 5334 - 1051, 2907, 2688, 3663, 2474, 2994, 1182, 2011, 2563, 1251, 2626, 7612, 992, 2340, 3444, 1540, # 5350 - 2721, 1201, 2070, 2401, 1996, 2475, 7613, 4424, 528, 1922, 2188, 1503, 1873, 1570, 2364, 3342, # 5366 - 3276, 7614, 557, 1073, 7615, 1827, 3445, 2087, 2266, 3140, 3039, 3084, 767, 3085, 2786, 4425, # 5382 - 1006, 4158, 4426, 2341, 1267, 2176, 3664, 3199, 778, 3945, 3200, 2722, 1597, 2657, 7616, 4427, # 5398 - 7617, 3446, 7618, 7619, 7620, 3277, 2689, 1433, 3278, 131, 95, 1504, 3946, 723, 4159, 3141, # 5414 - 1841, 3555, 2758, 2189, 3947, 2027, 2104, 3665, 7621, 2995, 3948, 1218, 7622, 3343, 3201, 3949, # 5430 - 4160, 2576, 248, 1634, 3785, 912, 7623, 2832, 3666, 3040, 3786, 654, 53, 7624, 2996, 7625, # 5446 - 1688, 4428, 777, 3447, 1032, 3950, 1425, 7626, 191, 820, 2120, 2833, 971, 4429, 931, 3202, # 5462 - 135, 664, 783, 3787, 1997, 772, 2908, 1935, 3951, 3788, 4430, 2909, 3203, 282, 2723, 640, # 5478 - 1372, 3448, 1127, 922, 325, 3344, 7627, 7628, 711, 2044, 7629, 7630, 3952, 2219, 2787, 1936, # 5494 - 3953, 3345, 2220, 2251, 3789, 2300, 7631, 4431, 3790, 1258, 3279, 3954, 3204, 2138, 2950, 3955, # 5510 - 3956, 7632, 2221, 258, 3205, 4432, 101, 1227, 7633, 3280, 1755, 7634, 1391, 3281, 7635, 2910, # 5526 - 2056, 893, 7636, 7637, 7638, 1402, 4161, 2342, 7639, 7640, 3206, 3556, 7641, 7642, 878, 1325, # 5542 - 1780, 2788, 4433, 259, 1385, 2577, 744, 1183, 2267, 4434, 7643, 3957, 2502, 7644, 684, 1024, # 5558 - 4162, 7645, 472, 3557, 3449, 1165, 3282, 3958, 3959, 322, 2152, 881, 455, 1695, 1152, 1340, # 5574 - 660, 554, 2153, 4435, 1058, 4436, 4163, 830, 1065, 3346, 3960, 4437, 1923, 7646, 1703, 1918, # 5590 - 7647, 932, 2268, 122, 7648, 4438, 947, 677, 7649, 3791, 2627, 297, 1905, 1924, 2269, 4439, # 5606 - 2317, 3283, 7650, 7651, 4164, 7652, 4165, 84, 4166, 112, 989, 7653, 547, 1059, 3961, 701, # 5622 - 3558, 1019, 7654, 4167, 7655, 3450, 942, 639, 457, 2301, 2451, 993, 2951, 407, 851, 494, # 5638 - 4440, 3347, 927, 7656, 1237, 7657, 2421, 3348, 573, 4168, 680, 921, 2911, 1279, 1874, 285, # 5654 - 790, 1448, 1983, 719, 2167, 7658, 7659, 4441, 3962, 3963, 1649, 7660, 1541, 563, 7661, 1077, # 5670 - 7662, 3349, 3041, 3451, 511, 2997, 3964, 3965, 3667, 3966, 1268, 2564, 3350, 3207, 4442, 4443, # 5686 - 7663, 535, 1048, 1276, 1189, 2912, 2028, 3142, 1438, 1373, 2834, 2952, 1134, 2012, 7664, 4169, # 5702 - 1238, 2578, 3086, 1259, 7665, 700, 7666, 2953, 3143, 3668, 4170, 7667, 4171, 1146, 1875, 1906, # 5718 - 4444, 2601, 3967, 781, 2422, 132, 1589, 203, 147, 273, 2789, 2402, 898, 1786, 2154, 3968, # 5734 - 3969, 7668, 3792, 2790, 7669, 7670, 4445, 4446, 7671, 3208, 7672, 1635, 3793, 965, 7673, 1804, # 5750 - 2690, 1516, 3559, 1121, 1082, 1329, 3284, 3970, 1449, 3794, 65, 1128, 2835, 2913, 2759, 1590, # 5766 - 3795, 7674, 7675, 12, 2658, 45, 976, 2579, 3144, 4447, 517, 2528, 1013, 1037, 3209, 7676, # 5782 - 3796, 2836, 7677, 3797, 7678, 3452, 7679, 2602, 614, 1998, 2318, 3798, 3087, 2724, 2628, 7680, # 5798 - 2580, 4172, 599, 1269, 7681, 1810, 3669, 7682, 2691, 3088, 759, 1060, 489, 1805, 3351, 3285, # 5814 - 1358, 7683, 7684, 2386, 1387, 1215, 2629, 2252, 490, 7685, 7686, 4173, 1759, 2387, 2343, 7687, # 5830 - 4448, 3799, 1907, 3971, 2630, 1806, 3210, 4449, 3453, 3286, 2760, 2344, 874, 7688, 7689, 3454, # 5846 - 3670, 1858, 91, 2914, 3671, 3042, 3800, 4450, 7690, 3145, 3972, 2659, 7691, 3455, 1202, 1403, # 5862 - 3801, 2954, 2529, 1517, 2503, 4451, 3456, 2504, 7692, 4452, 7693, 2692, 1885, 1495, 1731, 3973, # 5878 - 2365, 4453, 7694, 2029, 7695, 7696, 3974, 2693, 1216, 237, 2581, 4174, 2319, 3975, 3802, 4454, # 5894 - 4455, 2694, 3560, 3457, 445, 4456, 7697, 7698, 7699, 7700, 2761, 61, 3976, 3672, 1822, 3977, # 5910 - 7701, 687, 2045, 935, 925, 405, 2660, 703, 1096, 1859, 2725, 4457, 3978, 1876, 1367, 2695, # 5926 - 3352, 918, 2105, 1781, 2476, 334, 3287, 1611, 1093, 4458, 564, 3146, 3458, 3673, 3353, 945, # 5942 - 2631, 2057, 4459, 7702, 1925, 872, 4175, 7703, 3459, 2696, 3089, 349, 4176, 3674, 3979, 4460, # 5958 - 3803, 4177, 3675, 2155, 3980, 4461, 4462, 4178, 4463, 2403, 2046, 782, 3981, 400, 251, 4179, # 5974 - 1624, 7704, 7705, 277, 3676, 299, 1265, 476, 1191, 3804, 2121, 4180, 4181, 1109, 205, 7706, # 5990 - 2582, 1000, 2156, 3561, 1860, 7707, 7708, 7709, 4464, 7710, 4465, 2565, 107, 2477, 2157, 3982, # 6006 - 3460, 3147, 7711, 1533, 541, 1301, 158, 753, 4182, 2872, 3562, 7712, 1696, 370, 1088, 4183, # 6022 - 4466, 3563, 579, 327, 440, 162, 2240, 269, 1937, 1374, 3461, 968, 3043, 56, 1396, 3090, # 6038 - 2106, 3288, 3354, 7713, 1926, 2158, 4467, 2998, 7714, 3564, 7715, 7716, 3677, 4468, 2478, 7717, # 6054 - 2791, 7718, 1650, 4469, 7719, 2603, 7720, 7721, 3983, 2661, 3355, 1149, 3356, 3984, 3805, 3985, # 6070 - 7722, 1076, 49, 7723, 951, 3211, 3289, 3290, 450, 2837, 920, 7724, 1811, 2792, 2366, 4184, # 6086 - 1908, 1138, 2367, 3806, 3462, 7725, 3212, 4470, 1909, 1147, 1518, 2423, 4471, 3807, 7726, 4472, # 6102 - 2388, 2604, 260, 1795, 3213, 7727, 7728, 3808, 3291, 708, 7729, 3565, 1704, 7730, 3566, 1351, # 6118 - 1618, 3357, 2999, 1886, 944, 4185, 3358, 4186, 3044, 3359, 4187, 7731, 3678, 422, 413, 1714, # 6134 - 3292, 500, 2058, 2345, 4188, 2479, 7732, 1344, 1910, 954, 7733, 1668, 7734, 7735, 3986, 2404, # 6150 - 4189, 3567, 3809, 4190, 7736, 2302, 1318, 2505, 3091, 133, 3092, 2873, 4473, 629, 31, 2838, # 6166 - 2697, 3810, 4474, 850, 949, 4475, 3987, 2955, 1732, 2088, 4191, 1496, 1852, 7737, 3988, 620, # 6182 - 3214, 981, 1242, 3679, 3360, 1619, 3680, 1643, 3293, 2139, 2452, 1970, 1719, 3463, 2168, 7738, # 6198 - 3215, 7739, 7740, 3361, 1828, 7741, 1277, 4476, 1565, 2047, 7742, 1636, 3568, 3093, 7743, 869, # 6214 - 2839, 655, 3811, 3812, 3094, 3989, 3000, 3813, 1310, 3569, 4477, 7744, 7745, 7746, 1733, 558, # 6230 - 4478, 3681, 335, 1549, 3045, 1756, 4192, 3682, 1945, 3464, 1829, 1291, 1192, 470, 2726, 2107, # 6246 - 2793, 913, 1054, 3990, 7747, 1027, 7748, 3046, 3991, 4479, 982, 2662, 3362, 3148, 3465, 3216, # 6262 - 3217, 1946, 2794, 7749, 571, 4480, 7750, 1830, 7751, 3570, 2583, 1523, 2424, 7752, 2089, 984, # 6278 - 4481, 3683, 1959, 7753, 3684, 852, 923, 2795, 3466, 3685, 969, 1519, 999, 2048, 2320, 1705, # 6294 - 7754, 3095, 615, 1662, 151, 597, 3992, 2405, 2321, 1049, 275, 4482, 3686, 4193, 568, 3687, # 6310 - 3571, 2480, 4194, 3688, 7755, 2425, 2270, 409, 3218, 7756, 1566, 2874, 3467, 1002, 769, 2840, # 6326 - 194, 2090, 3149, 3689, 2222, 3294, 4195, 628, 1505, 7757, 7758, 1763, 2177, 3001, 3993, 521, # 6342 - 1161, 2584, 1787, 2203, 2406, 4483, 3994, 1625, 4196, 4197, 412, 42, 3096, 464, 7759, 2632, # 6358 - 4484, 3363, 1760, 1571, 2875, 3468, 2530, 1219, 2204, 3814, 2633, 2140, 2368, 4485, 4486, 3295, # 6374 - 1651, 3364, 3572, 7760, 7761, 3573, 2481, 3469, 7762, 3690, 7763, 7764, 2271, 2091, 460, 7765, # 6390 - 4487, 7766, 3002, 962, 588, 3574, 289, 3219, 2634, 1116, 52, 7767, 3047, 1796, 7768, 7769, # 6406 - 7770, 1467, 7771, 1598, 1143, 3691, 4198, 1984, 1734, 1067, 4488, 1280, 3365, 465, 4489, 1572, # 6422 - 510, 7772, 1927, 2241, 1812, 1644, 3575, 7773, 4490, 3692, 7774, 7775, 2663, 1573, 1534, 7776, # 6438 - 7777, 4199, 536, 1807, 1761, 3470, 3815, 3150, 2635, 7778, 7779, 7780, 4491, 3471, 2915, 1911, # 6454 - 2796, 7781, 3296, 1122, 377, 3220, 7782, 360, 7783, 7784, 4200, 1529, 551, 7785, 2059, 3693, # 6470 - 1769, 2426, 7786, 2916, 4201, 3297, 3097, 2322, 2108, 2030, 4492, 1404, 136, 1468, 1479, 672, # 6486 - 1171, 3221, 2303, 271, 3151, 7787, 2762, 7788, 2049, 678, 2727, 865, 1947, 4493, 7789, 2013, # 6502 - 3995, 2956, 7790, 2728, 2223, 1397, 3048, 3694, 4494, 4495, 1735, 2917, 3366, 3576, 7791, 3816, # 6518 - 509, 2841, 2453, 2876, 3817, 7792, 7793, 3152, 3153, 4496, 4202, 2531, 4497, 2304, 1166, 1010, # 6534 - 552, 681, 1887, 7794, 7795, 2957, 2958, 3996, 1287, 1596, 1861, 3154, 358, 453, 736, 175, # 6550 - 478, 1117, 905, 1167, 1097, 7796, 1853, 1530, 7797, 1706, 7798, 2178, 3472, 2287, 3695, 3473, # 6566 - 3577, 4203, 2092, 4204, 7799, 3367, 1193, 2482, 4205, 1458, 2190, 2205, 1862, 1888, 1421, 3298, # 6582 - 2918, 3049, 2179, 3474, 595, 2122, 7800, 3997, 7801, 7802, 4206, 1707, 2636, 223, 3696, 1359, # 6598 - 751, 3098, 183, 3475, 7803, 2797, 3003, 419, 2369, 633, 704, 3818, 2389, 241, 7804, 7805, # 6614 - 7806, 838, 3004, 3697, 2272, 2763, 2454, 3819, 1938, 2050, 3998, 1309, 3099, 2242, 1181, 7807, # 6630 - 1136, 2206, 3820, 2370, 1446, 4207, 2305, 4498, 7808, 7809, 4208, 1055, 2605, 484, 3698, 7810, # 6646 - 3999, 625, 4209, 2273, 3368, 1499, 4210, 4000, 7811, 4001, 4211, 3222, 2274, 2275, 3476, 7812, # 6662 - 7813, 2764, 808, 2606, 3699, 3369, 4002, 4212, 3100, 2532, 526, 3370, 3821, 4213, 955, 7814, # 6678 - 1620, 4214, 2637, 2427, 7815, 1429, 3700, 1669, 1831, 994, 928, 7816, 3578, 1260, 7817, 7818, # 6694 - 7819, 1948, 2288, 741, 2919, 1626, 4215, 2729, 2455, 867, 1184, 362, 3371, 1392, 7820, 7821, # 6710 - 4003, 4216, 1770, 1736, 3223, 2920, 4499, 4500, 1928, 2698, 1459, 1158, 7822, 3050, 3372, 2877, # 6726 - 1292, 1929, 2506, 2842, 3701, 1985, 1187, 2071, 2014, 2607, 4217, 7823, 2566, 2507, 2169, 3702, # 6742 - 2483, 3299, 7824, 3703, 4501, 7825, 7826, 666, 1003, 3005, 1022, 3579, 4218, 7827, 4502, 1813, # 6758 - 2253, 574, 3822, 1603, 295, 1535, 705, 3823, 4219, 283, 858, 417, 7828, 7829, 3224, 4503, # 6774 - 4504, 3051, 1220, 1889, 1046, 2276, 2456, 4004, 1393, 1599, 689, 2567, 388, 4220, 7830, 2484, # 6790 - 802, 7831, 2798, 3824, 2060, 1405, 2254, 7832, 4505, 3825, 2109, 1052, 1345, 3225, 1585, 7833, # 6806 - 809, 7834, 7835, 7836, 575, 2730, 3477, 956, 1552, 1469, 1144, 2323, 7837, 2324, 1560, 2457, # 6822 - 3580, 3226, 4005, 616, 2207, 3155, 2180, 2289, 7838, 1832, 7839, 3478, 4506, 7840, 1319, 3704, # 6838 - 3705, 1211, 3581, 1023, 3227, 1293, 2799, 7841, 7842, 7843, 3826, 607, 2306, 3827, 762, 2878, # 6854 - 1439, 4221, 1360, 7844, 1485, 3052, 7845, 4507, 1038, 4222, 1450, 2061, 2638, 4223, 1379, 4508, # 6870 - 2585, 7846, 7847, 4224, 1352, 1414, 2325, 2921, 1172, 7848, 7849, 3828, 3829, 7850, 1797, 1451, # 6886 - 7851, 7852, 7853, 7854, 2922, 4006, 4007, 2485, 2346, 411, 4008, 4009, 3582, 3300, 3101, 4509, # 6902 - 1561, 2664, 1452, 4010, 1375, 7855, 7856, 47, 2959, 316, 7857, 1406, 1591, 2923, 3156, 7858, # 6918 - 1025, 2141, 3102, 3157, 354, 2731, 884, 2224, 4225, 2407, 508, 3706, 726, 3583, 996, 2428, # 6934 - 3584, 729, 7859, 392, 2191, 1453, 4011, 4510, 3707, 7860, 7861, 2458, 3585, 2608, 1675, 2800, # 6950 - 919, 2347, 2960, 2348, 1270, 4511, 4012, 73, 7862, 7863, 647, 7864, 3228, 2843, 2255, 1550, # 6966 - 1346, 3006, 7865, 1332, 883, 3479, 7866, 7867, 7868, 7869, 3301, 2765, 7870, 1212, 831, 1347, # 6982 - 4226, 4512, 2326, 3830, 1863, 3053, 720, 3831, 4513, 4514, 3832, 7871, 4227, 7872, 7873, 4515, # 6998 - 7874, 7875, 1798, 4516, 3708, 2609, 4517, 3586, 1645, 2371, 7876, 7877, 2924, 669, 2208, 2665, # 7014 - 2429, 7878, 2879, 7879, 7880, 1028, 3229, 7881, 4228, 2408, 7882, 2256, 1353, 7883, 7884, 4518, # 7030 - 3158, 518, 7885, 4013, 7886, 4229, 1960, 7887, 2142, 4230, 7888, 7889, 3007, 2349, 2350, 3833, # 7046 - 516, 1833, 1454, 4014, 2699, 4231, 4519, 2225, 2610, 1971, 1129, 3587, 7890, 2766, 7891, 2961, # 7062 - 1422, 577, 1470, 3008, 1524, 3373, 7892, 7893, 432, 4232, 3054, 3480, 7894, 2586, 1455, 2508, # 7078 - 2226, 1972, 1175, 7895, 1020, 2732, 4015, 3481, 4520, 7896, 2733, 7897, 1743, 1361, 3055, 3482, # 7094 - 2639, 4016, 4233, 4521, 2290, 895, 924, 4234, 2170, 331, 2243, 3056, 166, 1627, 3057, 1098, # 7110 - 7898, 1232, 2880, 2227, 3374, 4522, 657, 403, 1196, 2372, 542, 3709, 3375, 1600, 4235, 3483, # 7126 - 7899, 4523, 2767, 3230, 576, 530, 1362, 7900, 4524, 2533, 2666, 3710, 4017, 7901, 842, 3834, # 7142 - 7902, 2801, 2031, 1014, 4018, 213, 2700, 3376, 665, 621, 4236, 7903, 3711, 2925, 2430, 7904, # 7158 - 2431, 3302, 3588, 3377, 7905, 4237, 2534, 4238, 4525, 3589, 1682, 4239, 3484, 1380, 7906, 724, # 7174 - 2277, 600, 1670, 7907, 1337, 1233, 4526, 3103, 2244, 7908, 1621, 4527, 7909, 651, 4240, 7910, # 7190 - 1612, 4241, 2611, 7911, 2844, 7912, 2734, 2307, 3058, 7913, 716, 2459, 3059, 174, 1255, 2701, # 7206 - 4019, 3590, 548, 1320, 1398, 728, 4020, 1574, 7914, 1890, 1197, 3060, 4021, 7915, 3061, 3062, # 7222 - 3712, 3591, 3713, 747, 7916, 635, 4242, 4528, 7917, 7918, 7919, 4243, 7920, 7921, 4529, 7922, # 7238 - 3378, 4530, 2432, 451, 7923, 3714, 2535, 2072, 4244, 2735, 4245, 4022, 7924, 1764, 4531, 7925, # 7254 - 4246, 350, 7926, 2278, 2390, 2486, 7927, 4247, 4023, 2245, 1434, 4024, 488, 4532, 458, 4248, # 7270 - 4025, 3715, 771, 1330, 2391, 3835, 2568, 3159, 2159, 2409, 1553, 2667, 3160, 4249, 7928, 2487, # 7286 - 2881, 2612, 1720, 2702, 4250, 3379, 4533, 7929, 2536, 4251, 7930, 3231, 4252, 2768, 7931, 2015, # 7302 - 2736, 7932, 1155, 1017, 3716, 3836, 7933, 3303, 2308, 201, 1864, 4253, 1430, 7934, 4026, 7935, # 7318 - 7936, 7937, 7938, 7939, 4254, 1604, 7940, 414, 1865, 371, 2587, 4534, 4535, 3485, 2016, 3104, # 7334 - 4536, 1708, 960, 4255, 887, 389, 2171, 1536, 1663, 1721, 7941, 2228, 4027, 2351, 2926, 1580, # 7350 - 7942, 7943, 7944, 1744, 7945, 2537, 4537, 4538, 7946, 4539, 7947, 2073, 7948, 7949, 3592, 3380, # 7366 - 2882, 4256, 7950, 4257, 2640, 3381, 2802, 673, 2703, 2460, 709, 3486, 4028, 3593, 4258, 7951, # 7382 - 1148, 502, 634, 7952, 7953, 1204, 4540, 3594, 1575, 4541, 2613, 3717, 7954, 3718, 3105, 948, # 7398 - 3232, 121, 1745, 3837, 1110, 7955, 4259, 3063, 2509, 3009, 4029, 3719, 1151, 1771, 3838, 1488, # 7414 - 4030, 1986, 7956, 2433, 3487, 7957, 7958, 2093, 7959, 4260, 3839, 1213, 1407, 2803, 531, 2737, # 7430 - 2538, 3233, 1011, 1537, 7960, 2769, 4261, 3106, 1061, 7961, 3720, 3721, 1866, 2883, 7962, 2017, # 7446 - 120, 4262, 4263, 2062, 3595, 3234, 2309, 3840, 2668, 3382, 1954, 4542, 7963, 7964, 3488, 1047, # 7462 - 2704, 1266, 7965, 1368, 4543, 2845, 649, 3383, 3841, 2539, 2738, 1102, 2846, 2669, 7966, 7967, # 7478 - 1999, 7968, 1111, 3596, 2962, 7969, 2488, 3842, 3597, 2804, 1854, 3384, 3722, 7970, 7971, 3385, # 7494 - 2410, 2884, 3304, 3235, 3598, 7972, 2569, 7973, 3599, 2805, 4031, 1460, 856, 7974, 3600, 7975, # 7510 - 2885, 2963, 7976, 2886, 3843, 7977, 4264, 632, 2510, 875, 3844, 1697, 3845, 2291, 7978, 7979, # 7526 - 4544, 3010, 1239, 580, 4545, 4265, 7980, 914, 936, 2074, 1190, 4032, 1039, 2123, 7981, 7982, # 7542 - 7983, 3386, 1473, 7984, 1354, 4266, 3846, 7985, 2172, 3064, 4033, 915, 3305, 4267, 4268, 3306, # 7558 - 1605, 1834, 7986, 2739, 398, 3601, 4269, 3847, 4034, 328, 1912, 2847, 4035, 3848, 1331, 4270, # 7574 - 3011, 937, 4271, 7987, 3602, 4036, 4037, 3387, 2160, 4546, 3388, 524, 742, 538, 3065, 1012, # 7590 - 7988, 7989, 3849, 2461, 7990, 658, 1103, 225, 3850, 7991, 7992, 4547, 7993, 4548, 7994, 3236, # 7606 - 1243, 7995, 4038, 963, 2246, 4549, 7996, 2705, 3603, 3161, 7997, 7998, 2588, 2327, 7999, 4550, # 7622 - 8000, 8001, 8002, 3489, 3307, 957, 3389, 2540, 2032, 1930, 2927, 2462, 870, 2018, 3604, 1746, # 7638 - 2770, 2771, 2434, 2463, 8003, 3851, 8004, 3723, 3107, 3724, 3490, 3390, 3725, 8005, 1179, 3066, # 7654 - 8006, 3162, 2373, 4272, 3726, 2541, 3163, 3108, 2740, 4039, 8007, 3391, 1556, 2542, 2292, 977, # 7670 - 2887, 2033, 4040, 1205, 3392, 8008, 1765, 3393, 3164, 2124, 1271, 1689, 714, 4551, 3491, 8009, # 7686 - 2328, 3852, 533, 4273, 3605, 2181, 617, 8010, 2464, 3308, 3492, 2310, 8011, 8012, 3165, 8013, # 7702 - 8014, 3853, 1987, 618, 427, 2641, 3493, 3394, 8015, 8016, 1244, 1690, 8017, 2806, 4274, 4552, # 7718 - 8018, 3494, 8019, 8020, 2279, 1576, 473, 3606, 4275, 3395, 972, 8021, 3607, 8022, 3067, 8023, # 7734 - 8024, 4553, 4554, 8025, 3727, 4041, 4042, 8026, 153, 4555, 356, 8027, 1891, 2888, 4276, 2143, # 7750 - 408, 803, 2352, 8028, 3854, 8029, 4277, 1646, 2570, 2511, 4556, 4557, 3855, 8030, 3856, 4278, # 7766 - 8031, 2411, 3396, 752, 8032, 8033, 1961, 2964, 8034, 746, 3012, 2465, 8035, 4279, 3728, 698, # 7782 - 4558, 1892, 4280, 3608, 2543, 4559, 3609, 3857, 8036, 3166, 3397, 8037, 1823, 1302, 4043, 2706, # 7798 - 3858, 1973, 4281, 8038, 4282, 3167, 823, 1303, 1288, 1236, 2848, 3495, 4044, 3398, 774, 3859, # 7814 - 8039, 1581, 4560, 1304, 2849, 3860, 4561, 8040, 2435, 2161, 1083, 3237, 4283, 4045, 4284, 344, # 7830 - 1173, 288, 2311, 454, 1683, 8041, 8042, 1461, 4562, 4046, 2589, 8043, 8044, 4563, 985, 894, # 7846 - 8045, 3399, 3168, 8046, 1913, 2928, 3729, 1988, 8047, 2110, 1974, 8048, 4047, 8049, 2571, 1194, # 7862 - 425, 8050, 4564, 3169, 1245, 3730, 4285, 8051, 8052, 2850, 8053, 636, 4565, 1855, 3861, 760, # 7878 - 1799, 8054, 4286, 2209, 1508, 4566, 4048, 1893, 1684, 2293, 8055, 8056, 8057, 4287, 4288, 2210, # 7894 - 479, 8058, 8059, 832, 8060, 4049, 2489, 8061, 2965, 2490, 3731, 990, 3109, 627, 1814, 2642, # 7910 - 4289, 1582, 4290, 2125, 2111, 3496, 4567, 8062, 799, 4291, 3170, 8063, 4568, 2112, 1737, 3013, # 7926 - 1018, 543, 754, 4292, 3309, 1676, 4569, 4570, 4050, 8064, 1489, 8065, 3497, 8066, 2614, 2889, # 7942 - 4051, 8067, 8068, 2966, 8069, 8070, 8071, 8072, 3171, 4571, 4572, 2182, 1722, 8073, 3238, 3239, # 7958 - 1842, 3610, 1715, 481, 365, 1975, 1856, 8074, 8075, 1962, 2491, 4573, 8076, 2126, 3611, 3240, # 7974 - 433, 1894, 2063, 2075, 8077, 602, 2741, 8078, 8079, 8080, 8081, 8082, 3014, 1628, 3400, 8083, # 7990 - 3172, 4574, 4052, 2890, 4575, 2512, 8084, 2544, 2772, 8085, 8086, 8087, 3310, 4576, 2891, 8088, # 8006 - 4577, 8089, 2851, 4578, 4579, 1221, 2967, 4053, 2513, 8090, 8091, 8092, 1867, 1989, 8093, 8094, # 8022 - 8095, 1895, 8096, 8097, 4580, 1896, 4054, 318, 8098, 2094, 4055, 4293, 8099, 8100, 485, 8101, # 8038 - 938, 3862, 553, 2670, 116, 8102, 3863, 3612, 8103, 3498, 2671, 2773, 3401, 3311, 2807, 8104, # 8054 - 3613, 2929, 4056, 1747, 2930, 2968, 8105, 8106, 207, 8107, 8108, 2672, 4581, 2514, 8109, 3015, # 8070 - 890, 3614, 3864, 8110, 1877, 3732, 3402, 8111, 2183, 2353, 3403, 1652, 8112, 8113, 8114, 941, # 8086 - 2294, 208, 3499, 4057, 2019, 330, 4294, 3865, 2892, 2492, 3733, 4295, 8115, 8116, 8117, 8118, # 8102 -) -# fmt: on diff --git a/libs/chardet/euctwprober.py b/libs/chardet/euctwprober.py deleted file mode 100644 index a37ab1899..000000000 --- a/libs/chardet/euctwprober.py +++ /dev/null @@ -1,47 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .chardistribution import EUCTWDistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import EUCTW_SM_MODEL - - -class EUCTWProber(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(EUCTW_SM_MODEL) - self.distribution_analyzer = EUCTWDistributionAnalysis() - self.reset() - - @property - def charset_name(self) -> str: - return "EUC-TW" - - @property - def language(self) -> str: - return "Taiwan" diff --git a/libs/chardet/gb2312freq.py b/libs/chardet/gb2312freq.py deleted file mode 100644 index b32bfc742..000000000 --- a/libs/chardet/gb2312freq.py +++ /dev/null @@ -1,284 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# GB2312 most frequently used character table -# -# Char to FreqOrder table , from hz6763 - -# 512 --> 0.79 -- 0.79 -# 1024 --> 0.92 -- 0.13 -# 2048 --> 0.98 -- 0.06 -# 6768 --> 1.00 -- 0.02 -# -# Ideal Distribution Ratio = 0.79135/(1-0.79135) = 3.79 -# Random Distribution Ration = 512 / (3755 - 512) = 0.157 -# -# Typical Distribution Ratio about 25% of Ideal one, still much higher that RDR - -GB2312_TYPICAL_DISTRIBUTION_RATIO = 0.9 - -GB2312_TABLE_SIZE = 3760 - -# fmt: off -GB2312_CHAR_TO_FREQ_ORDER = ( -1671, 749,1443,2364,3924,3807,2330,3921,1704,3463,2691,1511,1515, 572,3191,2205, -2361, 224,2558, 479,1711, 963,3162, 440,4060,1905,2966,2947,3580,2647,3961,3842, -2204, 869,4207, 970,2678,5626,2944,2956,1479,4048, 514,3595, 588,1346,2820,3409, - 249,4088,1746,1873,2047,1774, 581,1813, 358,1174,3590,1014,1561,4844,2245, 670, -1636,3112, 889,1286, 953, 556,2327,3060,1290,3141, 613, 185,3477,1367, 850,3820, -1715,2428,2642,2303,2732,3041,2562,2648,3566,3946,1349, 388,3098,2091,1360,3585, - 152,1687,1539, 738,1559, 59,1232,2925,2267,1388,1249,1741,1679,2960, 151,1566, -1125,1352,4271, 924,4296, 385,3166,4459, 310,1245,2850, 70,3285,2729,3534,3575, -2398,3298,3466,1960,2265, 217,3647, 864,1909,2084,4401,2773,1010,3269,5152, 853, -3051,3121,1244,4251,1895, 364,1499,1540,2313,1180,3655,2268, 562, 715,2417,3061, - 544, 336,3768,2380,1752,4075, 950, 280,2425,4382, 183,2759,3272, 333,4297,2155, -1688,2356,1444,1039,4540, 736,1177,3349,2443,2368,2144,2225, 565, 196,1482,3406, - 927,1335,4147, 692, 878,1311,1653,3911,3622,1378,4200,1840,2969,3149,2126,1816, -2534,1546,2393,2760, 737,2494, 13, 447, 245,2747, 38,2765,2129,2589,1079, 606, - 360, 471,3755,2890, 404, 848, 699,1785,1236, 370,2221,1023,3746,2074,2026,2023, -2388,1581,2119, 812,1141,3091,2536,1519, 804,2053, 406,1596,1090, 784, 548,4414, -1806,2264,2936,1100, 343,4114,5096, 622,3358, 743,3668,1510,1626,5020,3567,2513, -3195,4115,5627,2489,2991, 24,2065,2697,1087,2719, 48,1634, 315, 68, 985,2052, - 198,2239,1347,1107,1439, 597,2366,2172, 871,3307, 919,2487,2790,1867, 236,2570, -1413,3794, 906,3365,3381,1701,1982,1818,1524,2924,1205, 616,2586,2072,2004, 575, - 253,3099, 32,1365,1182, 197,1714,2454,1201, 554,3388,3224,2748, 756,2587, 250, -2567,1507,1517,3529,1922,2761,2337,3416,1961,1677,2452,2238,3153, 615, 911,1506, -1474,2495,1265,1906,2749,3756,3280,2161, 898,2714,1759,3450,2243,2444, 563, 26, -3286,2266,3769,3344,2707,3677, 611,1402, 531,1028,2871,4548,1375, 261,2948, 835, -1190,4134, 353, 840,2684,1900,3082,1435,2109,1207,1674, 329,1872,2781,4055,2686, -2104, 608,3318,2423,2957,2768,1108,3739,3512,3271,3985,2203,1771,3520,1418,2054, -1681,1153, 225,1627,2929, 162,2050,2511,3687,1954, 124,1859,2431,1684,3032,2894, - 585,4805,3969,2869,2704,2088,2032,2095,3656,2635,4362,2209, 256, 518,2042,2105, -3777,3657, 643,2298,1148,1779, 190, 989,3544, 414, 11,2135,2063,2979,1471, 403, -3678, 126, 770,1563, 671,2499,3216,2877, 600,1179, 307,2805,4937,1268,1297,2694, - 252,4032,1448,1494,1331,1394, 127,2256, 222,1647,1035,1481,3056,1915,1048, 873, -3651, 210, 33,1608,2516, 200,1520, 415, 102, 0,3389,1287, 817, 91,3299,2940, - 836,1814, 549,2197,1396,1669,2987,3582,2297,2848,4528,1070, 687, 20,1819, 121, -1552,1364,1461,1968,2617,3540,2824,2083, 177, 948,4938,2291, 110,4549,2066, 648, -3359,1755,2110,2114,4642,4845,1693,3937,3308,1257,1869,2123, 208,1804,3159,2992, -2531,2549,3361,2418,1350,2347,2800,2568,1291,2036,2680, 72, 842,1990, 212,1233, -1154,1586, 75,2027,3410,4900,1823,1337,2710,2676, 728,2810,1522,3026,4995, 157, - 755,1050,4022, 710, 785,1936,2194,2085,1406,2777,2400, 150,1250,4049,1206, 807, -1910, 534, 529,3309,1721,1660, 274, 39,2827, 661,2670,1578, 925,3248,3815,1094, -4278,4901,4252, 41,1150,3747,2572,2227,4501,3658,4902,3813,3357,3617,2884,2258, - 887, 538,4187,3199,1294,2439,3042,2329,2343,2497,1255, 107, 543,1527, 521,3478, -3568, 194,5062, 15, 961,3870,1241,1192,2664, 66,5215,3260,2111,1295,1127,2152, -3805,4135, 901,1164,1976, 398,1278, 530,1460, 748, 904,1054,1966,1426, 53,2909, - 509, 523,2279,1534, 536,1019, 239,1685, 460,2353, 673,1065,2401,3600,4298,2272, -1272,2363, 284,1753,3679,4064,1695, 81, 815,2677,2757,2731,1386, 859, 500,4221, -2190,2566, 757,1006,2519,2068,1166,1455, 337,2654,3203,1863,1682,1914,3025,1252, -1409,1366, 847, 714,2834,2038,3209, 964,2970,1901, 885,2553,1078,1756,3049, 301, -1572,3326, 688,2130,1996,2429,1805,1648,2930,3421,2750,3652,3088, 262,1158,1254, - 389,1641,1812, 526,1719, 923,2073,1073,1902, 468, 489,4625,1140, 857,2375,3070, -3319,2863, 380, 116,1328,2693,1161,2244, 273,1212,1884,2769,3011,1775,1142, 461, -3066,1200,2147,2212, 790, 702,2695,4222,1601,1058, 434,2338,5153,3640, 67,2360, -4099,2502, 618,3472,1329, 416,1132, 830,2782,1807,2653,3211,3510,1662, 192,2124, - 296,3979,1739,1611,3684, 23, 118, 324, 446,1239,1225, 293,2520,3814,3795,2535, -3116, 17,1074, 467,2692,2201, 387,2922, 45,1326,3055,1645,3659,2817, 958, 243, -1903,2320,1339,2825,1784,3289, 356, 576, 865,2315,2381,3377,3916,1088,3122,1713, -1655, 935, 628,4689,1034,1327, 441, 800, 720, 894,1979,2183,1528,5289,2702,1071, -4046,3572,2399,1571,3281, 79, 761,1103, 327, 134, 758,1899,1371,1615, 879, 442, - 215,2605,2579, 173,2048,2485,1057,2975,3317,1097,2253,3801,4263,1403,1650,2946, - 814,4968,3487,1548,2644,1567,1285, 2, 295,2636, 97, 946,3576, 832, 141,4257, -3273, 760,3821,3521,3156,2607, 949,1024,1733,1516,1803,1920,2125,2283,2665,3180, -1501,2064,3560,2171,1592, 803,3518,1416, 732,3897,4258,1363,1362,2458, 119,1427, - 602,1525,2608,1605,1639,3175, 694,3064, 10, 465, 76,2000,4846,4208, 444,3781, -1619,3353,2206,1273,3796, 740,2483, 320,1723,2377,3660,2619,1359,1137,1762,1724, -2345,2842,1850,1862, 912, 821,1866, 612,2625,1735,2573,3369,1093, 844, 89, 937, - 930,1424,3564,2413,2972,1004,3046,3019,2011, 711,3171,1452,4178, 428, 801,1943, - 432, 445,2811, 206,4136,1472, 730, 349, 73, 397,2802,2547, 998,1637,1167, 789, - 396,3217, 154,1218, 716,1120,1780,2819,4826,1931,3334,3762,2139,1215,2627, 552, -3664,3628,3232,1405,2383,3111,1356,2652,3577,3320,3101,1703, 640,1045,1370,1246, -4996, 371,1575,2436,1621,2210, 984,4033,1734,2638, 16,4529, 663,2755,3255,1451, -3917,2257,1253,1955,2234,1263,2951, 214,1229, 617, 485, 359,1831,1969, 473,2310, - 750,2058, 165, 80,2864,2419, 361,4344,2416,2479,1134, 796,3726,1266,2943, 860, -2715, 938, 390,2734,1313,1384, 248, 202, 877,1064,2854, 522,3907, 279,1602, 297, -2357, 395,3740, 137,2075, 944,4089,2584,1267,3802, 62,1533,2285, 178, 176, 780, -2440, 201,3707, 590, 478,1560,4354,2117,1075, 30, 74,4643,4004,1635,1441,2745, - 776,2596, 238,1077,1692,1912,2844, 605, 499,1742,3947, 241,3053, 980,1749, 936, -2640,4511,2582, 515,1543,2162,5322,2892,2993, 890,2148,1924, 665,1827,3581,1032, - 968,3163, 339,1044,1896, 270, 583,1791,1720,4367,1194,3488,3669, 43,2523,1657, - 163,2167, 290,1209,1622,3378, 550, 634,2508,2510, 695,2634,2384,2512,1476,1414, - 220,1469,2341,2138,2852,3183,2900,4939,2865,3502,1211,3680, 854,3227,1299,2976, -3172, 186,2998,1459, 443,1067,3251,1495, 321,1932,3054, 909, 753,1410,1828, 436, -2441,1119,1587,3164,2186,1258, 227, 231,1425,1890,3200,3942, 247, 959, 725,5254, -2741, 577,2158,2079, 929, 120, 174, 838,2813, 591,1115, 417,2024, 40,3240,1536, -1037, 291,4151,2354, 632,1298,2406,2500,3535,1825,1846,3451, 205,1171, 345,4238, - 18,1163, 811, 685,2208,1217, 425,1312,1508,1175,4308,2552,1033, 587,1381,3059, -2984,3482, 340,1316,4023,3972, 792,3176, 519, 777,4690, 918, 933,4130,2981,3741, - 90,3360,2911,2200,5184,4550, 609,3079,2030, 272,3379,2736, 363,3881,1130,1447, - 286, 779, 357,1169,3350,3137,1630,1220,2687,2391, 747,1277,3688,2618,2682,2601, -1156,3196,5290,4034,3102,1689,3596,3128, 874, 219,2783, 798, 508,1843,2461, 269, -1658,1776,1392,1913,2983,3287,2866,2159,2372, 829,4076, 46,4253,2873,1889,1894, - 915,1834,1631,2181,2318, 298, 664,2818,3555,2735, 954,3228,3117, 527,3511,2173, - 681,2712,3033,2247,2346,3467,1652, 155,2164,3382, 113,1994, 450, 899, 494, 994, -1237,2958,1875,2336,1926,3727, 545,1577,1550, 633,3473, 204,1305,3072,2410,1956, -2471, 707,2134, 841,2195,2196,2663,3843,1026,4940, 990,3252,4997, 368,1092, 437, -3212,3258,1933,1829, 675,2977,2893, 412, 943,3723,4644,3294,3283,2230,2373,5154, -2389,2241,2661,2323,1404,2524, 593, 787, 677,3008,1275,2059, 438,2709,2609,2240, -2269,2246,1446, 36,1568,1373,3892,1574,2301,1456,3962, 693,2276,5216,2035,1143, -2720,1919,1797,1811,2763,4137,2597,1830,1699,1488,1198,2090, 424,1694, 312,3634, -3390,4179,3335,2252,1214, 561,1059,3243,2295,2561, 975,5155,2321,2751,3772, 472, -1537,3282,3398,1047,2077,2348,2878,1323,3340,3076, 690,2906, 51, 369, 170,3541, -1060,2187,2688,3670,2541,1083,1683, 928,3918, 459, 109,4427, 599,3744,4286, 143, -2101,2730,2490, 82,1588,3036,2121, 281,1860, 477,4035,1238,2812,3020,2716,3312, -1530,2188,2055,1317, 843, 636,1808,1173,3495, 649, 181,1002, 147,3641,1159,2414, -3750,2289,2795, 813,3123,2610,1136,4368, 5,3391,4541,2174, 420, 429,1728, 754, -1228,2115,2219, 347,2223,2733, 735,1518,3003,2355,3134,1764,3948,3329,1888,2424, -1001,1234,1972,3321,3363,1672,1021,1450,1584, 226, 765, 655,2526,3404,3244,2302, -3665, 731, 594,2184, 319,1576, 621, 658,2656,4299,2099,3864,1279,2071,2598,2739, - 795,3086,3699,3908,1707,2352,2402,1382,3136,2475,1465,4847,3496,3865,1085,3004, -2591,1084, 213,2287,1963,3565,2250, 822, 793,4574,3187,1772,1789,3050, 595,1484, -1959,2770,1080,2650, 456, 422,2996, 940,3322,4328,4345,3092,2742, 965,2784, 739, -4124, 952,1358,2498,2949,2565, 332,2698,2378, 660,2260,2473,4194,3856,2919, 535, -1260,2651,1208,1428,1300,1949,1303,2942, 433,2455,2450,1251,1946, 614,1269, 641, -1306,1810,2737,3078,2912, 564,2365,1419,1415,1497,4460,2367,2185,1379,3005,1307, -3218,2175,1897,3063, 682,1157,4040,4005,1712,1160,1941,1399, 394, 402,2952,1573, -1151,2986,2404, 862, 299,2033,1489,3006, 346, 171,2886,3401,1726,2932, 168,2533, - 47,2507,1030,3735,1145,3370,1395,1318,1579,3609,4560,2857,4116,1457,2529,1965, - 504,1036,2690,2988,2405, 745,5871, 849,2397,2056,3081, 863,2359,3857,2096, 99, -1397,1769,2300,4428,1643,3455,1978,1757,3718,1440, 35,4879,3742,1296,4228,2280, - 160,5063,1599,2013, 166, 520,3479,1646,3345,3012, 490,1937,1545,1264,2182,2505, -1096,1188,1369,1436,2421,1667,2792,2460,1270,2122, 727,3167,2143, 806,1706,1012, -1800,3037, 960,2218,1882, 805, 139,2456,1139,1521, 851,1052,3093,3089, 342,2039, - 744,5097,1468,1502,1585,2087, 223, 939, 326,2140,2577, 892,2481,1623,4077, 982, -3708, 135,2131, 87,2503,3114,2326,1106, 876,1616, 547,2997,2831,2093,3441,4530, -4314, 9,3256,4229,4148, 659,1462,1986,1710,2046,2913,2231,4090,4880,5255,3392, -3274,1368,3689,4645,1477, 705,3384,3635,1068,1529,2941,1458,3782,1509, 100,1656, -2548, 718,2339, 408,1590,2780,3548,1838,4117,3719,1345,3530, 717,3442,2778,3220, -2898,1892,4590,3614,3371,2043,1998,1224,3483, 891, 635, 584,2559,3355, 733,1766, -1729,1172,3789,1891,2307, 781,2982,2271,1957,1580,5773,2633,2005,4195,3097,1535, -3213,1189,1934,5693,3262, 586,3118,1324,1598, 517,1564,2217,1868,1893,4445,3728, -2703,3139,1526,1787,1992,3882,2875,1549,1199,1056,2224,1904,2711,5098,4287, 338, -1993,3129,3489,2689,1809,2815,1997, 957,1855,3898,2550,3275,3057,1105,1319, 627, -1505,1911,1883,3526, 698,3629,3456,1833,1431, 746, 77,1261,2017,2296,1977,1885, - 125,1334,1600, 525,1798,1109,2222,1470,1945, 559,2236,1186,3443,2476,1929,1411, -2411,3135,1777,3372,2621,1841,1613,3229, 668,1430,1839,2643,2916, 195,1989,2671, -2358,1387, 629,3205,2293,5256,4439, 123,1310, 888,1879,4300,3021,3605,1003,1162, -3192,2910,2010, 140,2395,2859, 55,1082,2012,2901, 662, 419,2081,1438, 680,2774, -4654,3912,1620,1731,1625,5035,4065,2328, 512,1344, 802,5443,2163,2311,2537, 524, -3399, 98,1155,2103,1918,2606,3925,2816,1393,2465,1504,3773,2177,3963,1478,4346, - 180,1113,4655,3461,2028,1698, 833,2696,1235,1322,1594,4408,3623,3013,3225,2040, -3022, 541,2881, 607,3632,2029,1665,1219, 639,1385,1686,1099,2803,3231,1938,3188, -2858, 427, 676,2772,1168,2025, 454,3253,2486,3556, 230,1950, 580, 791,1991,1280, -1086,1974,2034, 630, 257,3338,2788,4903,1017, 86,4790, 966,2789,1995,1696,1131, - 259,3095,4188,1308, 179,1463,5257, 289,4107,1248, 42,3413,1725,2288, 896,1947, - 774,4474,4254, 604,3430,4264, 392,2514,2588, 452, 237,1408,3018, 988,4531,1970, -3034,3310, 540,2370,1562,1288,2990, 502,4765,1147, 4,1853,2708, 207, 294,2814, -4078,2902,2509, 684, 34,3105,3532,2551, 644, 709,2801,2344, 573,1727,3573,3557, -2021,1081,3100,4315,2100,3681, 199,2263,1837,2385, 146,3484,1195,2776,3949, 997, -1939,3973,1008,1091,1202,1962,1847,1149,4209,5444,1076, 493, 117,5400,2521, 972, -1490,2934,1796,4542,2374,1512,2933,2657, 413,2888,1135,2762,2314,2156,1355,2369, - 766,2007,2527,2170,3124,2491,2593,2632,4757,2437, 234,3125,3591,1898,1750,1376, -1942,3468,3138, 570,2127,2145,3276,4131, 962, 132,1445,4196, 19, 941,3624,3480, -3366,1973,1374,4461,3431,2629, 283,2415,2275, 808,2887,3620,2112,2563,1353,3610, - 955,1089,3103,1053, 96, 88,4097, 823,3808,1583, 399, 292,4091,3313, 421,1128, - 642,4006, 903,2539,1877,2082, 596, 29,4066,1790, 722,2157, 130, 995,1569, 769, -1485, 464, 513,2213, 288,1923,1101,2453,4316, 133, 486,2445, 50, 625, 487,2207, - 57, 423, 481,2962, 159,3729,1558, 491, 303, 482, 501, 240,2837, 112,3648,2392, -1783, 362, 8,3433,3422, 610,2793,3277,1390,1284,1654, 21,3823, 734, 367, 623, - 193, 287, 374,1009,1483, 816, 476, 313,2255,2340,1262,2150,2899,1146,2581, 782, -2116,1659,2018,1880, 255,3586,3314,1110,2867,2137,2564, 986,2767,5185,2006, 650, - 158, 926, 762, 881,3157,2717,2362,3587, 306,3690,3245,1542,3077,2427,1691,2478, -2118,2985,3490,2438, 539,2305, 983, 129,1754, 355,4201,2386, 827,2923, 104,1773, -2838,2771, 411,2905,3919, 376, 767, 122,1114, 828,2422,1817,3506, 266,3460,1007, -1609,4998, 945,2612,4429,2274, 726,1247,1964,2914,2199,2070,4002,4108, 657,3323, -1422, 579, 455,2764,4737,1222,2895,1670, 824,1223,1487,2525, 558, 861,3080, 598, -2659,2515,1967, 752,2583,2376,2214,4180, 977, 704,2464,4999,2622,4109,1210,2961, - 819,1541, 142,2284, 44, 418, 457,1126,3730,4347,4626,1644,1876,3671,1864, 302, -1063,5694, 624, 723,1984,3745,1314,1676,2488,1610,1449,3558,3569,2166,2098, 409, -1011,2325,3704,2306, 818,1732,1383,1824,1844,3757, 999,2705,3497,1216,1423,2683, -2426,2954,2501,2726,2229,1475,2554,5064,1971,1794,1666,2014,1343, 783, 724, 191, -2434,1354,2220,5065,1763,2752,2472,4152, 131, 175,2885,3434, 92,1466,4920,2616, -3871,3872,3866, 128,1551,1632, 669,1854,3682,4691,4125,1230, 188,2973,3290,1302, -1213, 560,3266, 917, 763,3909,3249,1760, 868,1958, 764,1782,2097, 145,2277,3774, -4462, 64,1491,3062, 971,2132,3606,2442, 221,1226,1617, 218, 323,1185,3207,3147, - 571, 619,1473,1005,1744,2281, 449,1887,2396,3685, 275, 375,3816,1743,3844,3731, - 845,1983,2350,4210,1377, 773, 967,3499,3052,3743,2725,4007,1697,1022,3943,1464, -3264,2855,2722,1952,1029,2839,2467, 84,4383,2215, 820,1391,2015,2448,3672, 377, -1948,2168, 797,2545,3536,2578,2645, 94,2874,1678, 405,1259,3071, 771, 546,1315, - 470,1243,3083, 895,2468, 981, 969,2037, 846,4181, 653,1276,2928, 14,2594, 557, -3007,2474, 156, 902,1338,1740,2574, 537,2518, 973,2282,2216,2433,1928, 138,2903, -1293,2631,1612, 646,3457, 839,2935, 111, 496,2191,2847, 589,3186, 149,3994,2060, -4031,2641,4067,3145,1870, 37,3597,2136,1025,2051,3009,3383,3549,1121,1016,3261, -1301, 251,2446,2599,2153, 872,3246, 637, 334,3705, 831, 884, 921,3065,3140,4092, -2198,1944, 246,2964, 108,2045,1152,1921,2308,1031, 203,3173,4170,1907,3890, 810, -1401,2003,1690, 506, 647,1242,2828,1761,1649,3208,2249,1589,3709,2931,5156,1708, - 498, 666,2613, 834,3817,1231, 184,2851,1124, 883,3197,2261,3710,1765,1553,2658, -1178,2639,2351, 93,1193, 942,2538,2141,4402, 235,1821, 870,1591,2192,1709,1871, -3341,1618,4126,2595,2334, 603, 651, 69, 701, 268,2662,3411,2555,1380,1606, 503, - 448, 254,2371,2646, 574,1187,2309,1770, 322,2235,1292,1801, 305, 566,1133, 229, -2067,2057, 706, 167, 483,2002,2672,3295,1820,3561,3067, 316, 378,2746,3452,1112, - 136,1981, 507,1651,2917,1117, 285,4591, 182,2580,3522,1304, 335,3303,1835,2504, -1795,1792,2248, 674,1018,2106,2449,1857,2292,2845, 976,3047,1781,2600,2727,1389, -1281, 52,3152, 153, 265,3950, 672,3485,3951,4463, 430,1183, 365, 278,2169, 27, -1407,1336,2304, 209,1340,1730,2202,1852,2403,2883, 979,1737,1062, 631,2829,2542, -3876,2592, 825,2086,2226,3048,3625, 352,1417,3724, 542, 991, 431,1351,3938,1861, -2294, 826,1361,2927,3142,3503,1738, 463,2462,2723, 582,1916,1595,2808, 400,3845, -3891,2868,3621,2254, 58,2492,1123, 910,2160,2614,1372,1603,1196,1072,3385,1700, -3267,1980, 696, 480,2430, 920, 799,1570,2920,1951,2041,4047,2540,1321,4223,2469, -3562,2228,1271,2602, 401,2833,3351,2575,5157, 907,2312,1256, 410, 263,3507,1582, - 996, 678,1849,2316,1480, 908,3545,2237, 703,2322, 667,1826,2849,1531,2604,2999, -2407,3146,2151,2630,1786,3711, 469,3542, 497,3899,2409, 858, 837,4446,3393,1274, - 786, 620,1845,2001,3311, 484, 308,3367,1204,1815,3691,2332,1532,2557,1842,2020, -2724,1927,2333,4440, 567, 22,1673,2728,4475,1987,1858,1144,1597, 101,1832,3601, - 12, 974,3783,4391, 951,1412, 1,3720, 453,4608,4041, 528,1041,1027,3230,2628, -1129, 875,1051,3291,1203,2262,1069,2860,2799,2149,2615,3278, 144,1758,3040, 31, - 475,1680, 366,2685,3184, 311,1642,4008,2466,5036,1593,1493,2809, 216,1420,1668, - 233, 304,2128,3284, 232,1429,1768,1040,2008,3407,2740,2967,2543, 242,2133, 778, -1565,2022,2620, 505,2189,2756,1098,2273, 372,1614, 708, 553,2846,2094,2278, 169, -3626,2835,4161, 228,2674,3165, 809,1454,1309, 466,1705,1095, 900,3423, 880,2667, -3751,5258,2317,3109,2571,4317,2766,1503,1342, 866,4447,1118, 63,2076, 314,1881, -1348,1061, 172, 978,3515,1747, 532, 511,3970, 6, 601, 905,2699,3300,1751, 276, -1467,3725,2668, 65,4239,2544,2779,2556,1604, 578,2451,1802, 992,2331,2624,1320, -3446, 713,1513,1013, 103,2786,2447,1661, 886,1702, 916, 654,3574,2031,1556, 751, -2178,2821,2179,1498,1538,2176, 271, 914,2251,2080,1325, 638,1953,2937,3877,2432, -2754, 95,3265,1716, 260,1227,4083, 775, 106,1357,3254, 426,1607, 555,2480, 772, -1985, 244,2546, 474, 495,1046,2611,1851,2061, 71,2089,1675,2590, 742,3758,2843, -3222,1433, 267,2180,2576,2826,2233,2092,3913,2435, 956,1745,3075, 856,2113,1116, - 451, 3,1988,2896,1398, 993,2463,1878,2049,1341,2718,2721,2870,2108, 712,2904, -4363,2753,2324, 277,2872,2349,2649, 384, 987, 435, 691,3000, 922, 164,3939, 652, -1500,1184,4153,2482,3373,2165,4848,2335,3775,3508,3154,2806,2830,1554,2102,1664, -2530,1434,2408, 893,1547,2623,3447,2832,2242,2532,3169,2856,3223,2078, 49,3770, -3469, 462, 318, 656,2259,3250,3069, 679,1629,2758, 344,1138,1104,3120,1836,1283, -3115,2154,1437,4448, 934, 759,1999, 794,2862,1038, 533,2560,1722,2342, 855,2626, -1197,1663,4476,3127, 85,4240,2528, 25,1111,1181,3673, 407,3470,4561,2679,2713, - 768,1925,2841,3986,1544,1165, 932, 373,1240,2146,1930,2673, 721,4766, 354,4333, - 391,2963, 187, 61,3364,1442,1102, 330,1940,1767, 341,3809,4118, 393,2496,2062, -2211, 105, 331, 300, 439, 913,1332, 626, 379,3304,1557, 328, 689,3952, 309,1555, - 931, 317,2517,3027, 325, 569, 686,2107,3084, 60,1042,1333,2794, 264,3177,4014, -1628, 258,3712, 7,4464,1176,1043,1778, 683, 114,1975, 78,1492, 383,1886, 510, - 386, 645,5291,2891,2069,3305,4138,3867,2939,2603,2493,1935,1066,1848,3588,1015, -1282,1289,4609, 697,1453,3044,2666,3611,1856,2412, 54, 719,1330, 568,3778,2459, -1748, 788, 492, 551,1191,1000, 488,3394,3763, 282,1799, 348,2016,1523,3155,2390, -1049, 382,2019,1788,1170, 729,2968,3523, 897,3926,2785,2938,3292, 350,2319,3238, -1718,1717,2655,3453,3143,4465, 161,2889,2980,2009,1421, 56,1908,1640,2387,2232, -1917,1874,2477,4921, 148, 83,3438, 592,4245,2882,1822,1055, 741, 115,1496,1624, - 381,1638,4592,1020, 516,3214, 458, 947,4575,1432, 211,1514,2926,1865,2142, 189, - 852,1221,1400,1486, 882,2299,4036, 351, 28,1122, 700,6479,6480,6481,6482,6483, #last 512 -) -# fmt: on diff --git a/libs/chardet/gb2312prober.py b/libs/chardet/gb2312prober.py deleted file mode 100644 index d423e7311..000000000 --- a/libs/chardet/gb2312prober.py +++ /dev/null @@ -1,47 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .chardistribution import GB2312DistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import GB2312_SM_MODEL - - -class GB2312Prober(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(GB2312_SM_MODEL) - self.distribution_analyzer = GB2312DistributionAnalysis() - self.reset() - - @property - def charset_name(self) -> str: - return "GB2312" - - @property - def language(self) -> str: - return "Chinese" diff --git a/libs/chardet/hebrewprober.py b/libs/chardet/hebrewprober.py deleted file mode 100644 index 785d0057b..000000000 --- a/libs/chardet/hebrewprober.py +++ /dev/null @@ -1,316 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Shy Shalom -# Portions created by the Initial Developer are Copyright (C) 2005 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Optional, Union - -from .charsetprober import CharSetProber -from .enums import ProbingState -from .sbcharsetprober import SingleByteCharSetProber - -# This prober doesn't actually recognize a language or a charset. -# It is a helper prober for the use of the Hebrew model probers - -### General ideas of the Hebrew charset recognition ### -# -# Four main charsets exist in Hebrew: -# "ISO-8859-8" - Visual Hebrew -# "windows-1255" - Logical Hebrew -# "ISO-8859-8-I" - Logical Hebrew -# "x-mac-hebrew" - ?? Logical Hebrew ?? -# -# Both "ISO" charsets use a completely identical set of code points, whereas -# "windows-1255" and "x-mac-hebrew" are two different proper supersets of -# these code points. windows-1255 defines additional characters in the range -# 0x80-0x9F as some misc punctuation marks as well as some Hebrew-specific -# diacritics and additional 'Yiddish' ligature letters in the range 0xc0-0xd6. -# x-mac-hebrew defines similar additional code points but with a different -# mapping. -# -# As far as an average Hebrew text with no diacritics is concerned, all four -# charsets are identical with respect to code points. Meaning that for the -# main Hebrew alphabet, all four map the same values to all 27 Hebrew letters -# (including final letters). -# -# The dominant difference between these charsets is their directionality. -# "Visual" directionality means that the text is ordered as if the renderer is -# not aware of a BIDI rendering algorithm. The renderer sees the text and -# draws it from left to right. The text itself when ordered naturally is read -# backwards. A buffer of Visual Hebrew generally looks like so: -# "[last word of first line spelled backwards] [whole line ordered backwards -# and spelled backwards] [first word of first line spelled backwards] -# [end of line] [last word of second line] ... etc' " -# adding punctuation marks, numbers and English text to visual text is -# naturally also "visual" and from left to right. -# -# "Logical" directionality means the text is ordered "naturally" according to -# the order it is read. It is the responsibility of the renderer to display -# the text from right to left. A BIDI algorithm is used to place general -# punctuation marks, numbers and English text in the text. -# -# Texts in x-mac-hebrew are almost impossible to find on the Internet. From -# what little evidence I could find, it seems that its general directionality -# is Logical. -# -# To sum up all of the above, the Hebrew probing mechanism knows about two -# charsets: -# Visual Hebrew - "ISO-8859-8" - backwards text - Words and sentences are -# backwards while line order is natural. For charset recognition purposes -# the line order is unimportant (In fact, for this implementation, even -# word order is unimportant). -# Logical Hebrew - "windows-1255" - normal, naturally ordered text. -# -# "ISO-8859-8-I" is a subset of windows-1255 and doesn't need to be -# specifically identified. -# "x-mac-hebrew" is also identified as windows-1255. A text in x-mac-hebrew -# that contain special punctuation marks or diacritics is displayed with -# some unconverted characters showing as question marks. This problem might -# be corrected using another model prober for x-mac-hebrew. Due to the fact -# that x-mac-hebrew texts are so rare, writing another model prober isn't -# worth the effort and performance hit. -# -#### The Prober #### -# -# The prober is divided between two SBCharSetProbers and a HebrewProber, -# all of which are managed, created, fed data, inquired and deleted by the -# SBCSGroupProber. The two SBCharSetProbers identify that the text is in -# fact some kind of Hebrew, Logical or Visual. The final decision about which -# one is it is made by the HebrewProber by combining final-letter scores -# with the scores of the two SBCharSetProbers to produce a final answer. -# -# The SBCSGroupProber is responsible for stripping the original text of HTML -# tags, English characters, numbers, low-ASCII punctuation characters, spaces -# and new lines. It reduces any sequence of such characters to a single space. -# The buffer fed to each prober in the SBCS group prober is pure text in -# high-ASCII. -# The two SBCharSetProbers (model probers) share the same language model: -# Win1255Model. -# The first SBCharSetProber uses the model normally as any other -# SBCharSetProber does, to recognize windows-1255, upon which this model was -# built. The second SBCharSetProber is told to make the pair-of-letter -# lookup in the language model backwards. This in practice exactly simulates -# a visual Hebrew model using the windows-1255 logical Hebrew model. -# -# The HebrewProber is not using any language model. All it does is look for -# final-letter evidence suggesting the text is either logical Hebrew or visual -# Hebrew. Disjointed from the model probers, the results of the HebrewProber -# alone are meaningless. HebrewProber always returns 0.00 as confidence -# since it never identifies a charset by itself. Instead, the pointer to the -# HebrewProber is passed to the model probers as a helper "Name Prober". -# When the Group prober receives a positive identification from any prober, -# it asks for the name of the charset identified. If the prober queried is a -# Hebrew model prober, the model prober forwards the call to the -# HebrewProber to make the final decision. In the HebrewProber, the -# decision is made according to the final-letters scores maintained and Both -# model probers scores. The answer is returned in the form of the name of the -# charset identified, either "windows-1255" or "ISO-8859-8". - - -class HebrewProber(CharSetProber): - SPACE = 0x20 - # windows-1255 / ISO-8859-8 code points of interest - FINAL_KAF = 0xEA - NORMAL_KAF = 0xEB - FINAL_MEM = 0xED - NORMAL_MEM = 0xEE - FINAL_NUN = 0xEF - NORMAL_NUN = 0xF0 - FINAL_PE = 0xF3 - NORMAL_PE = 0xF4 - FINAL_TSADI = 0xF5 - NORMAL_TSADI = 0xF6 - - # Minimum Visual vs Logical final letter score difference. - # If the difference is below this, don't rely solely on the final letter score - # distance. - MIN_FINAL_CHAR_DISTANCE = 5 - - # Minimum Visual vs Logical model score difference. - # If the difference is below this, don't rely at all on the model score - # distance. - MIN_MODEL_DISTANCE = 0.01 - - VISUAL_HEBREW_NAME = "ISO-8859-8" - LOGICAL_HEBREW_NAME = "windows-1255" - - def __init__(self) -> None: - super().__init__() - self._final_char_logical_score = 0 - self._final_char_visual_score = 0 - self._prev = self.SPACE - self._before_prev = self.SPACE - self._logical_prober: Optional[SingleByteCharSetProber] = None - self._visual_prober: Optional[SingleByteCharSetProber] = None - self.reset() - - def reset(self) -> None: - self._final_char_logical_score = 0 - self._final_char_visual_score = 0 - # The two last characters seen in the previous buffer, - # mPrev and mBeforePrev are initialized to space in order to simulate - # a word delimiter at the beginning of the data - self._prev = self.SPACE - self._before_prev = self.SPACE - # These probers are owned by the group prober. - - def set_model_probers( - self, - logical_prober: SingleByteCharSetProber, - visual_prober: SingleByteCharSetProber, - ) -> None: - self._logical_prober = logical_prober - self._visual_prober = visual_prober - - def is_final(self, c: int) -> bool: - return c in [ - self.FINAL_KAF, - self.FINAL_MEM, - self.FINAL_NUN, - self.FINAL_PE, - self.FINAL_TSADI, - ] - - def is_non_final(self, c: int) -> bool: - # The normal Tsadi is not a good Non-Final letter due to words like - # 'lechotet' (to chat) containing an apostrophe after the tsadi. This - # apostrophe is converted to a space in FilterWithoutEnglishLetters - # causing the Non-Final tsadi to appear at an end of a word even - # though this is not the case in the original text. - # The letters Pe and Kaf rarely display a related behavior of not being - # a good Non-Final letter. Words like 'Pop', 'Winamp' and 'Mubarak' - # for example legally end with a Non-Final Pe or Kaf. However, the - # benefit of these letters as Non-Final letters outweighs the damage - # since these words are quite rare. - return c in [self.NORMAL_KAF, self.NORMAL_MEM, self.NORMAL_NUN, self.NORMAL_PE] - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - # Final letter analysis for logical-visual decision. - # Look for evidence that the received buffer is either logical Hebrew - # or visual Hebrew. - # The following cases are checked: - # 1) A word longer than 1 letter, ending with a final letter. This is - # an indication that the text is laid out "naturally" since the - # final letter really appears at the end. +1 for logical score. - # 2) A word longer than 1 letter, ending with a Non-Final letter. In - # normal Hebrew, words ending with Kaf, Mem, Nun, Pe or Tsadi, - # should not end with the Non-Final form of that letter. Exceptions - # to this rule are mentioned above in isNonFinal(). This is an - # indication that the text is laid out backwards. +1 for visual - # score - # 3) A word longer than 1 letter, starting with a final letter. Final - # letters should not appear at the beginning of a word. This is an - # indication that the text is laid out backwards. +1 for visual - # score. - # - # The visual score and logical score are accumulated throughout the - # text and are finally checked against each other in GetCharSetName(). - # No checking for final letters in the middle of words is done since - # that case is not an indication for either Logical or Visual text. - # - # We automatically filter out all 7-bit characters (replace them with - # spaces) so the word boundary detection works properly. [MAP] - - if self.state == ProbingState.NOT_ME: - # Both model probers say it's not them. No reason to continue. - return ProbingState.NOT_ME - - byte_str = self.filter_high_byte_only(byte_str) - - for cur in byte_str: - if cur == self.SPACE: - # We stand on a space - a word just ended - if self._before_prev != self.SPACE: - # next-to-last char was not a space so self._prev is not a - # 1 letter word - if self.is_final(self._prev): - # case (1) [-2:not space][-1:final letter][cur:space] - self._final_char_logical_score += 1 - elif self.is_non_final(self._prev): - # case (2) [-2:not space][-1:Non-Final letter][ - # cur:space] - self._final_char_visual_score += 1 - else: - # Not standing on a space - if ( - (self._before_prev == self.SPACE) - and (self.is_final(self._prev)) - and (cur != self.SPACE) - ): - # case (3) [-2:space][-1:final letter][cur:not space] - self._final_char_visual_score += 1 - self._before_prev = self._prev - self._prev = cur - - # Forever detecting, till the end or until both model probers return - # ProbingState.NOT_ME (handled above) - return ProbingState.DETECTING - - @property - def charset_name(self) -> str: - assert self._logical_prober is not None - assert self._visual_prober is not None - - # Make the decision: is it Logical or Visual? - # If the final letter score distance is dominant enough, rely on it. - finalsub = self._final_char_logical_score - self._final_char_visual_score - if finalsub >= self.MIN_FINAL_CHAR_DISTANCE: - return self.LOGICAL_HEBREW_NAME - if finalsub <= -self.MIN_FINAL_CHAR_DISTANCE: - return self.VISUAL_HEBREW_NAME - - # It's not dominant enough, try to rely on the model scores instead. - modelsub = ( - self._logical_prober.get_confidence() - self._visual_prober.get_confidence() - ) - if modelsub > self.MIN_MODEL_DISTANCE: - return self.LOGICAL_HEBREW_NAME - if modelsub < -self.MIN_MODEL_DISTANCE: - return self.VISUAL_HEBREW_NAME - - # Still no good, back to final letter distance, maybe it'll save the - # day. - if finalsub < 0.0: - return self.VISUAL_HEBREW_NAME - - # (finalsub > 0 - Logical) or (don't know what to do) default to - # Logical. - return self.LOGICAL_HEBREW_NAME - - @property - def language(self) -> str: - return "Hebrew" - - @property - def state(self) -> ProbingState: - assert self._logical_prober is not None - assert self._visual_prober is not None - - # Remain active as long as any of the model probers are active. - if (self._logical_prober.state == ProbingState.NOT_ME) and ( - self._visual_prober.state == ProbingState.NOT_ME - ): - return ProbingState.NOT_ME - return ProbingState.DETECTING diff --git a/libs/chardet/jisfreq.py b/libs/chardet/jisfreq.py deleted file mode 100644 index 3293576e0..000000000 --- a/libs/chardet/jisfreq.py +++ /dev/null @@ -1,325 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# Sampling from about 20M text materials include literature and computer technology -# -# Japanese frequency table, applied to both S-JIS and EUC-JP -# They are sorted in order. - -# 128 --> 0.77094 -# 256 --> 0.85710 -# 512 --> 0.92635 -# 1024 --> 0.97130 -# 2048 --> 0.99431 -# -# Ideal Distribution Ratio = 0.92635 / (1-0.92635) = 12.58 -# Random Distribution Ration = 512 / (2965+62+83+86-512) = 0.191 -# -# Typical Distribution Ratio, 25% of IDR - -JIS_TYPICAL_DISTRIBUTION_RATIO = 3.0 - -# Char to FreqOrder table , -JIS_TABLE_SIZE = 4368 - -# fmt: off -JIS_CHAR_TO_FREQ_ORDER = ( - 40, 1, 6, 182, 152, 180, 295,2127, 285, 381,3295,4304,3068,4606,3165,3510, # 16 -3511,1822,2785,4607,1193,2226,5070,4608, 171,2996,1247, 18, 179,5071, 856,1661, # 32 -1262,5072, 619, 127,3431,3512,3230,1899,1700, 232, 228,1294,1298, 284, 283,2041, # 48 -2042,1061,1062, 48, 49, 44, 45, 433, 434,1040,1041, 996, 787,2997,1255,4305, # 64 -2108,4609,1684,1648,5073,5074,5075,5076,5077,5078,3687,5079,4610,5080,3927,3928, # 80 -5081,3296,3432, 290,2285,1471,2187,5082,2580,2825,1303,2140,1739,1445,2691,3375, # 96 -1691,3297,4306,4307,4611, 452,3376,1182,2713,3688,3069,4308,5083,5084,5085,5086, # 112 -5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102, # 128 -5103,5104,5105,5106,5107,5108,5109,5110,5111,5112,4097,5113,5114,5115,5116,5117, # 144 -5118,5119,5120,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133, # 160 -5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149, # 176 -5150,5151,5152,4612,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164, # 192 -5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,1472, 598, 618, 820,1205, # 208 -1309,1412,1858,1307,1692,5176,5177,5178,5179,5180,5181,5182,1142,1452,1234,1172, # 224 -1875,2043,2149,1793,1382,2973, 925,2404,1067,1241, 960,1377,2935,1491, 919,1217, # 240 -1865,2030,1406,1499,2749,4098,5183,5184,5185,5186,5187,5188,2561,4099,3117,1804, # 256 -2049,3689,4309,3513,1663,5189,3166,3118,3298,1587,1561,3433,5190,3119,1625,2998, # 272 -3299,4613,1766,3690,2786,4614,5191,5192,5193,5194,2161, 26,3377, 2,3929, 20, # 288 -3691, 47,4100, 50, 17, 16, 35, 268, 27, 243, 42, 155, 24, 154, 29, 184, # 304 - 4, 91, 14, 92, 53, 396, 33, 289, 9, 37, 64, 620, 21, 39, 321, 5, # 320 - 12, 11, 52, 13, 3, 208, 138, 0, 7, 60, 526, 141, 151,1069, 181, 275, # 336 -1591, 83, 132,1475, 126, 331, 829, 15, 69, 160, 59, 22, 157, 55,1079, 312, # 352 - 109, 38, 23, 25, 10, 19, 79,5195, 61, 382,1124, 8, 30,5196,5197,5198, # 368 -5199,5200,5201,5202,5203,5204,5205,5206, 89, 62, 74, 34,2416, 112, 139, 196, # 384 - 271, 149, 84, 607, 131, 765, 46, 88, 153, 683, 76, 874, 101, 258, 57, 80, # 400 - 32, 364, 121,1508, 169,1547, 68, 235, 145,2999, 41, 360,3027, 70, 63, 31, # 416 - 43, 259, 262,1383, 99, 533, 194, 66, 93, 846, 217, 192, 56, 106, 58, 565, # 432 - 280, 272, 311, 256, 146, 82, 308, 71, 100, 128, 214, 655, 110, 261, 104,1140, # 448 - 54, 51, 36, 87, 67,3070, 185,2618,2936,2020, 28,1066,2390,2059,5207,5208, # 464 -5209,5210,5211,5212,5213,5214,5215,5216,4615,5217,5218,5219,5220,5221,5222,5223, # 480 -5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,3514,5237,5238, # 496 -5239,5240,5241,5242,5243,5244,2297,2031,4616,4310,3692,5245,3071,5246,3598,5247, # 512 -4617,3231,3515,5248,4101,4311,4618,3808,4312,4102,5249,4103,4104,3599,5250,5251, # 528 -5252,5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5263,5264,5265,5266,5267, # 544 -5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278,5279,5280,5281,5282,5283, # 560 -5284,5285,5286,5287,5288,5289,5290,5291,5292,5293,5294,5295,5296,5297,5298,5299, # 576 -5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310,5311,5312,5313,5314,5315, # 592 -5316,5317,5318,5319,5320,5321,5322,5323,5324,5325,5326,5327,5328,5329,5330,5331, # 608 -5332,5333,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347, # 624 -5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363, # 640 -5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379, # 656 -5380,5381, 363, 642,2787,2878,2788,2789,2316,3232,2317,3434,2011, 165,1942,3930, # 672 -3931,3932,3933,5382,4619,5383,4620,5384,5385,5386,5387,5388,5389,5390,5391,5392, # 688 -5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408, # 704 -5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424, # 720 -5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440, # 736 -5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456, # 752 -5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472, # 768 -5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488, # 784 -5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504, # 800 -5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520, # 816 -5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536, # 832 -5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552, # 848 -5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568, # 864 -5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584, # 880 -5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600, # 896 -5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616, # 912 -5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632, # 928 -5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648, # 944 -5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664, # 960 -5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680, # 976 -5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696, # 992 -5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712, # 1008 -5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728, # 1024 -5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5741,5742,5743,5744, # 1040 -5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5760, # 1056 -5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776, # 1072 -5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5787,5788,5789,5790,5791,5792, # 1088 -5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808, # 1104 -5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824, # 1120 -5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840, # 1136 -5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856, # 1152 -5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5867,5868,5869,5870,5871,5872, # 1168 -5873,5874,5875,5876,5877,5878,5879,5880,5881,5882,5883,5884,5885,5886,5887,5888, # 1184 -5889,5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5901,5902,5903,5904, # 1200 -5905,5906,5907,5908,5909,5910,5911,5912,5913,5914,5915,5916,5917,5918,5919,5920, # 1216 -5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936, # 1232 -5937,5938,5939,5940,5941,5942,5943,5944,5945,5946,5947,5948,5949,5950,5951,5952, # 1248 -5953,5954,5955,5956,5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968, # 1264 -5969,5970,5971,5972,5973,5974,5975,5976,5977,5978,5979,5980,5981,5982,5983,5984, # 1280 -5985,5986,5987,5988,5989,5990,5991,5992,5993,5994,5995,5996,5997,5998,5999,6000, # 1296 -6001,6002,6003,6004,6005,6006,6007,6008,6009,6010,6011,6012,6013,6014,6015,6016, # 1312 -6017,6018,6019,6020,6021,6022,6023,6024,6025,6026,6027,6028,6029,6030,6031,6032, # 1328 -6033,6034,6035,6036,6037,6038,6039,6040,6041,6042,6043,6044,6045,6046,6047,6048, # 1344 -6049,6050,6051,6052,6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064, # 1360 -6065,6066,6067,6068,6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080, # 1376 -6081,6082,6083,6084,6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096, # 1392 -6097,6098,6099,6100,6101,6102,6103,6104,6105,6106,6107,6108,6109,6110,6111,6112, # 1408 -6113,6114,2044,2060,4621, 997,1235, 473,1186,4622, 920,3378,6115,6116, 379,1108, # 1424 -4313,2657,2735,3934,6117,3809, 636,3233, 573,1026,3693,3435,2974,3300,2298,4105, # 1440 - 854,2937,2463, 393,2581,2417, 539, 752,1280,2750,2480, 140,1161, 440, 708,1569, # 1456 - 665,2497,1746,1291,1523,3000, 164,1603, 847,1331, 537,1997, 486, 508,1693,2418, # 1472 -1970,2227, 878,1220, 299,1030, 969, 652,2751, 624,1137,3301,2619, 65,3302,2045, # 1488 -1761,1859,3120,1930,3694,3516, 663,1767, 852, 835,3695, 269, 767,2826,2339,1305, # 1504 - 896,1150, 770,1616,6118, 506,1502,2075,1012,2519, 775,2520,2975,2340,2938,4314, # 1520 -3028,2086,1224,1943,2286,6119,3072,4315,2240,1273,1987,3935,1557, 175, 597, 985, # 1536 -3517,2419,2521,1416,3029, 585, 938,1931,1007,1052,1932,1685,6120,3379,4316,4623, # 1552 - 804, 599,3121,1333,2128,2539,1159,1554,2032,3810, 687,2033,2904, 952, 675,1467, # 1568 -3436,6121,2241,1096,1786,2440,1543,1924, 980,1813,2228, 781,2692,1879, 728,1918, # 1584 -3696,4624, 548,1950,4625,1809,1088,1356,3303,2522,1944, 502, 972, 373, 513,2827, # 1600 - 586,2377,2391,1003,1976,1631,6122,2464,1084, 648,1776,4626,2141, 324, 962,2012, # 1616 -2177,2076,1384, 742,2178,1448,1173,1810, 222, 102, 301, 445, 125,2420, 662,2498, # 1632 - 277, 200,1476,1165,1068, 224,2562,1378,1446, 450,1880, 659, 791, 582,4627,2939, # 1648 -3936,1516,1274, 555,2099,3697,1020,1389,1526,3380,1762,1723,1787,2229, 412,2114, # 1664 -1900,2392,3518, 512,2597, 427,1925,2341,3122,1653,1686,2465,2499, 697, 330, 273, # 1680 - 380,2162, 951, 832, 780, 991,1301,3073, 965,2270,3519, 668,2523,2636,1286, 535, # 1696 -1407, 518, 671, 957,2658,2378, 267, 611,2197,3030,6123, 248,2299, 967,1799,2356, # 1712 - 850,1418,3437,1876,1256,1480,2828,1718,6124,6125,1755,1664,2405,6126,4628,2879, # 1728 -2829, 499,2179, 676,4629, 557,2329,2214,2090, 325,3234, 464, 811,3001, 992,2342, # 1744 -2481,1232,1469, 303,2242, 466,1070,2163, 603,1777,2091,4630,2752,4631,2714, 322, # 1760 -2659,1964,1768, 481,2188,1463,2330,2857,3600,2092,3031,2421,4632,2318,2070,1849, # 1776 -2598,4633,1302,2254,1668,1701,2422,3811,2905,3032,3123,2046,4106,1763,1694,4634, # 1792 -1604, 943,1724,1454, 917, 868,2215,1169,2940, 552,1145,1800,1228,1823,1955, 316, # 1808 -1080,2510, 361,1807,2830,4107,2660,3381,1346,1423,1134,4108,6127, 541,1263,1229, # 1824 -1148,2540, 545, 465,1833,2880,3438,1901,3074,2482, 816,3937, 713,1788,2500, 122, # 1840 -1575, 195,1451,2501,1111,6128, 859, 374,1225,2243,2483,4317, 390,1033,3439,3075, # 1856 -2524,1687, 266, 793,1440,2599, 946, 779, 802, 507, 897,1081, 528,2189,1292, 711, # 1872 -1866,1725,1167,1640, 753, 398,2661,1053, 246, 348,4318, 137,1024,3440,1600,2077, # 1888 -2129, 825,4319, 698, 238, 521, 187,2300,1157,2423,1641,1605,1464,1610,1097,2541, # 1904 -1260,1436, 759,2255,1814,2150, 705,3235, 409,2563,3304, 561,3033,2005,2564, 726, # 1920 -1956,2343,3698,4109, 949,3812,3813,3520,1669, 653,1379,2525, 881,2198, 632,2256, # 1936 -1027, 778,1074, 733,1957, 514,1481,2466, 554,2180, 702,3938,1606,1017,1398,6129, # 1952 -1380,3521, 921, 993,1313, 594, 449,1489,1617,1166, 768,1426,1360, 495,1794,3601, # 1968 -1177,3602,1170,4320,2344, 476, 425,3167,4635,3168,1424, 401,2662,1171,3382,1998, # 1984 -1089,4110, 477,3169, 474,6130,1909, 596,2831,1842, 494, 693,1051,1028,1207,3076, # 2000 - 606,2115, 727,2790,1473,1115, 743,3522, 630, 805,1532,4321,2021, 366,1057, 838, # 2016 - 684,1114,2142,4322,2050,1492,1892,1808,2271,3814,2424,1971,1447,1373,3305,1090, # 2032 -1536,3939,3523,3306,1455,2199, 336, 369,2331,1035, 584,2393, 902, 718,2600,6131, # 2048 -2753, 463,2151,1149,1611,2467, 715,1308,3124,1268, 343,1413,3236,1517,1347,2663, # 2064 -2093,3940,2022,1131,1553,2100,2941,1427,3441,2942,1323,2484,6132,1980, 872,2368, # 2080 -2441,2943, 320,2369,2116,1082, 679,1933,3941,2791,3815, 625,1143,2023, 422,2200, # 2096 -3816,6133, 730,1695, 356,2257,1626,2301,2858,2637,1627,1778, 937, 883,2906,2693, # 2112 -3002,1769,1086, 400,1063,1325,3307,2792,4111,3077, 456,2345,1046, 747,6134,1524, # 2128 - 884,1094,3383,1474,2164,1059, 974,1688,2181,2258,1047, 345,1665,1187, 358, 875, # 2144 -3170, 305, 660,3524,2190,1334,1135,3171,1540,1649,2542,1527, 927, 968,2793, 885, # 2160 -1972,1850, 482, 500,2638,1218,1109,1085,2543,1654,2034, 876, 78,2287,1482,1277, # 2176 - 861,1675,1083,1779, 724,2754, 454, 397,1132,1612,2332, 893, 672,1237, 257,2259, # 2192 -2370, 135,3384, 337,2244, 547, 352, 340, 709,2485,1400, 788,1138,2511, 540, 772, # 2208 -1682,2260,2272,2544,2013,1843,1902,4636,1999,1562,2288,4637,2201,1403,1533, 407, # 2224 - 576,3308,1254,2071, 978,3385, 170, 136,1201,3125,2664,3172,2394, 213, 912, 873, # 2240 -3603,1713,2202, 699,3604,3699, 813,3442, 493, 531,1054, 468,2907,1483, 304, 281, # 2256 -4112,1726,1252,2094, 339,2319,2130,2639, 756,1563,2944, 748, 571,2976,1588,2425, # 2272 -2715,1851,1460,2426,1528,1392,1973,3237, 288,3309, 685,3386, 296, 892,2716,2216, # 2288 -1570,2245, 722,1747,2217, 905,3238,1103,6135,1893,1441,1965, 251,1805,2371,3700, # 2304 -2601,1919,1078, 75,2182,1509,1592,1270,2640,4638,2152,6136,3310,3817, 524, 706, # 2320 -1075, 292,3818,1756,2602, 317, 98,3173,3605,3525,1844,2218,3819,2502, 814, 567, # 2336 - 385,2908,1534,6137, 534,1642,3239, 797,6138,1670,1529, 953,4323, 188,1071, 538, # 2352 - 178, 729,3240,2109,1226,1374,2000,2357,2977, 731,2468,1116,2014,2051,6139,1261, # 2368 -1593, 803,2859,2736,3443, 556, 682, 823,1541,6140,1369,2289,1706,2794, 845, 462, # 2384 -2603,2665,1361, 387, 162,2358,1740, 739,1770,1720,1304,1401,3241,1049, 627,1571, # 2400 -2427,3526,1877,3942,1852,1500, 431,1910,1503, 677, 297,2795, 286,1433,1038,1198, # 2416 -2290,1133,1596,4113,4639,2469,1510,1484,3943,6141,2442, 108, 712,4640,2372, 866, # 2432 -3701,2755,3242,1348, 834,1945,1408,3527,2395,3243,1811, 824, 994,1179,2110,1548, # 2448 -1453, 790,3003, 690,4324,4325,2832,2909,3820,1860,3821, 225,1748, 310, 346,1780, # 2464 -2470, 821,1993,2717,2796, 828, 877,3528,2860,2471,1702,2165,2910,2486,1789, 453, # 2480 - 359,2291,1676, 73,1164,1461,1127,3311, 421, 604, 314,1037, 589, 116,2487, 737, # 2496 - 837,1180, 111, 244, 735,6142,2261,1861,1362, 986, 523, 418, 581,2666,3822, 103, # 2512 - 855, 503,1414,1867,2488,1091, 657,1597, 979, 605,1316,4641,1021,2443,2078,2001, # 2528 -1209, 96, 587,2166,1032, 260,1072,2153, 173, 94, 226,3244, 819,2006,4642,4114, # 2544 -2203, 231,1744, 782, 97,2667, 786,3387, 887, 391, 442,2219,4326,1425,6143,2694, # 2560 - 633,1544,1202, 483,2015, 592,2052,1958,2472,1655, 419, 129,4327,3444,3312,1714, # 2576 -1257,3078,4328,1518,1098, 865,1310,1019,1885,1512,1734, 469,2444, 148, 773, 436, # 2592 -1815,1868,1128,1055,4329,1245,2756,3445,2154,1934,1039,4643, 579,1238, 932,2320, # 2608 - 353, 205, 801, 115,2428, 944,2321,1881, 399,2565,1211, 678, 766,3944, 335,2101, # 2624 -1459,1781,1402,3945,2737,2131,1010, 844, 981,1326,1013, 550,1816,1545,2620,1335, # 2640 -1008, 371,2881, 936,1419,1613,3529,1456,1395,2273,1834,2604,1317,2738,2503, 416, # 2656 -1643,4330, 806,1126, 229, 591,3946,1314,1981,1576,1837,1666, 347,1790, 977,3313, # 2672 - 764,2861,1853, 688,2429,1920,1462, 77, 595, 415,2002,3034, 798,1192,4115,6144, # 2688 -2978,4331,3035,2695,2582,2072,2566, 430,2430,1727, 842,1396,3947,3702, 613, 377, # 2704 - 278, 236,1417,3388,3314,3174, 757,1869, 107,3530,6145,1194, 623,2262, 207,1253, # 2720 -2167,3446,3948, 492,1117,1935, 536,1838,2757,1246,4332, 696,2095,2406,1393,1572, # 2736 -3175,1782, 583, 190, 253,1390,2230, 830,3126,3389, 934,3245,1703,1749,2979,1870, # 2752 -2545,1656,2204, 869,2346,4116,3176,1817, 496,1764,4644, 942,1504, 404,1903,1122, # 2768 -1580,3606,2945,1022, 515, 372,1735, 955,2431,3036,6146,2797,1110,2302,2798, 617, # 2784 -6147, 441, 762,1771,3447,3607,3608,1904, 840,3037, 86, 939,1385, 572,1370,2445, # 2800 -1336, 114,3703, 898, 294, 203,3315, 703,1583,2274, 429, 961,4333,1854,1951,3390, # 2816 -2373,3704,4334,1318,1381, 966,1911,2322,1006,1155, 309, 989, 458,2718,1795,1372, # 2832 -1203, 252,1689,1363,3177, 517,1936, 168,1490, 562, 193,3823,1042,4117,1835, 551, # 2848 - 470,4645, 395, 489,3448,1871,1465,2583,2641, 417,1493, 279,1295, 511,1236,1119, # 2864 - 72,1231,1982,1812,3004, 871,1564, 984,3449,1667,2696,2096,4646,2347,2833,1673, # 2880 -3609, 695,3246,2668, 807,1183,4647, 890, 388,2333,1801,1457,2911,1765,1477,1031, # 2896 -3316,3317,1278,3391,2799,2292,2526, 163,3450,4335,2669,1404,1802,6148,2323,2407, # 2912 -1584,1728,1494,1824,1269, 298, 909,3318,1034,1632, 375, 776,1683,2061, 291, 210, # 2928 -1123, 809,1249,1002,2642,3038, 206,1011,2132, 144, 975, 882,1565, 342, 667, 754, # 2944 -1442,2143,1299,2303,2062, 447, 626,2205,1221,2739,2912,1144,1214,2206,2584, 760, # 2960 -1715, 614, 950,1281,2670,2621, 810, 577,1287,2546,4648, 242,2168, 250,2643, 691, # 2976 - 123,2644, 647, 313,1029, 689,1357,2946,1650, 216, 771,1339,1306, 808,2063, 549, # 2992 - 913,1371,2913,2914,6149,1466,1092,1174,1196,1311,2605,2396,1783,1796,3079, 406, # 3008 -2671,2117,3949,4649, 487,1825,2220,6150,2915, 448,2348,1073,6151,2397,1707, 130, # 3024 - 900,1598, 329, 176,1959,2527,1620,6152,2275,4336,3319,1983,2191,3705,3610,2155, # 3040 -3706,1912,1513,1614,6153,1988, 646, 392,2304,1589,3320,3039,1826,1239,1352,1340, # 3056 -2916, 505,2567,1709,1437,2408,2547, 906,6154,2672, 384,1458,1594,1100,1329, 710, # 3072 - 423,3531,2064,2231,2622,1989,2673,1087,1882, 333, 841,3005,1296,2882,2379, 580, # 3088 -1937,1827,1293,2585, 601, 574, 249,1772,4118,2079,1120, 645, 901,1176,1690, 795, # 3104 -2207, 478,1434, 516,1190,1530, 761,2080, 930,1264, 355, 435,1552, 644,1791, 987, # 3120 - 220,1364,1163,1121,1538, 306,2169,1327,1222, 546,2645, 218, 241, 610,1704,3321, # 3136 -1984,1839,1966,2528, 451,6155,2586,3707,2568, 907,3178, 254,2947, 186,1845,4650, # 3152 - 745, 432,1757, 428,1633, 888,2246,2221,2489,3611,2118,1258,1265, 956,3127,1784, # 3168 -4337,2490, 319, 510, 119, 457,3612, 274,2035,2007,4651,1409,3128, 970,2758, 590, # 3184 -2800, 661,2247,4652,2008,3950,1420,1549,3080,3322,3951,1651,1375,2111, 485,2491, # 3200 -1429,1156,6156,2548,2183,1495, 831,1840,2529,2446, 501,1657, 307,1894,3247,1341, # 3216 - 666, 899,2156,1539,2549,1559, 886, 349,2208,3081,2305,1736,3824,2170,2759,1014, # 3232 -1913,1386, 542,1397,2948, 490, 368, 716, 362, 159, 282,2569,1129,1658,1288,1750, # 3248 -2674, 276, 649,2016, 751,1496, 658,1818,1284,1862,2209,2087,2512,3451, 622,2834, # 3264 - 376, 117,1060,2053,1208,1721,1101,1443, 247,1250,3179,1792,3952,2760,2398,3953, # 3280 -6157,2144,3708, 446,2432,1151,2570,3452,2447,2761,2835,1210,2448,3082, 424,2222, # 3296 -1251,2449,2119,2836, 504,1581,4338, 602, 817, 857,3825,2349,2306, 357,3826,1470, # 3312 -1883,2883, 255, 958, 929,2917,3248, 302,4653,1050,1271,1751,2307,1952,1430,2697, # 3328 -2719,2359, 354,3180, 777, 158,2036,4339,1659,4340,4654,2308,2949,2248,1146,2232, # 3344 -3532,2720,1696,2623,3827,6158,3129,1550,2698,1485,1297,1428, 637, 931,2721,2145, # 3360 - 914,2550,2587, 81,2450, 612, 827,2646,1242,4655,1118,2884, 472,1855,3181,3533, # 3376 -3534, 569,1353,2699,1244,1758,2588,4119,2009,2762,2171,3709,1312,1531,6159,1152, # 3392 -1938, 134,1830, 471,3710,2276,1112,1535,3323,3453,3535, 982,1337,2950, 488, 826, # 3408 - 674,1058,1628,4120,2017, 522,2399, 211, 568,1367,3454, 350, 293,1872,1139,3249, # 3424 -1399,1946,3006,1300,2360,3324, 588, 736,6160,2606, 744, 669,3536,3828,6161,1358, # 3440 - 199, 723, 848, 933, 851,1939,1505,1514,1338,1618,1831,4656,1634,3613, 443,2740, # 3456 -3829, 717,1947, 491,1914,6162,2551,1542,4121,1025,6163,1099,1223, 198,3040,2722, # 3472 - 370, 410,1905,2589, 998,1248,3182,2380, 519,1449,4122,1710, 947, 928,1153,4341, # 3488 -2277, 344,2624,1511, 615, 105, 161,1212,1076,1960,3130,2054,1926,1175,1906,2473, # 3504 - 414,1873,2801,6164,2309, 315,1319,3325, 318,2018,2146,2157, 963, 631, 223,4342, # 3520 -4343,2675, 479,3711,1197,2625,3712,2676,2361,6165,4344,4123,6166,2451,3183,1886, # 3536 -2184,1674,1330,1711,1635,1506, 799, 219,3250,3083,3954,1677,3713,3326,2081,3614, # 3552 -1652,2073,4657,1147,3041,1752, 643,1961, 147,1974,3955,6167,1716,2037, 918,3007, # 3568 -1994, 120,1537, 118, 609,3184,4345, 740,3455,1219, 332,1615,3830,6168,1621,2980, # 3584 -1582, 783, 212, 553,2350,3714,1349,2433,2082,4124, 889,6169,2310,1275,1410, 973, # 3600 - 166,1320,3456,1797,1215,3185,2885,1846,2590,2763,4658, 629, 822,3008, 763, 940, # 3616 -1990,2862, 439,2409,1566,1240,1622, 926,1282,1907,2764, 654,2210,1607, 327,1130, # 3632 -3956,1678,1623,6170,2434,2192, 686, 608,3831,3715, 903,3957,3042,6171,2741,1522, # 3648 -1915,1105,1555,2552,1359, 323,3251,4346,3457, 738,1354,2553,2311,2334,1828,2003, # 3664 -3832,1753,2351,1227,6172,1887,4125,1478,6173,2410,1874,1712,1847, 520,1204,2607, # 3680 - 264,4659, 836,2677,2102, 600,4660,3833,2278,3084,6174,4347,3615,1342, 640, 532, # 3696 - 543,2608,1888,2400,2591,1009,4348,1497, 341,1737,3616,2723,1394, 529,3252,1321, # 3712 - 983,4661,1515,2120, 971,2592, 924, 287,1662,3186,4349,2700,4350,1519, 908,1948, # 3728 -2452, 156, 796,1629,1486,2223,2055, 694,4126,1259,1036,3392,1213,2249,2742,1889, # 3744 -1230,3958,1015, 910, 408, 559,3617,4662, 746, 725, 935,4663,3959,3009,1289, 563, # 3760 - 867,4664,3960,1567,2981,2038,2626, 988,2263,2381,4351, 143,2374, 704,1895,6175, # 3776 -1188,3716,2088, 673,3085,2362,4352, 484,1608,1921,2765,2918, 215, 904,3618,3537, # 3792 - 894, 509, 976,3043,2701,3961,4353,2837,2982, 498,6176,6177,1102,3538,1332,3393, # 3808 -1487,1636,1637, 233, 245,3962, 383, 650, 995,3044, 460,1520,1206,2352, 749,3327, # 3824 - 530, 700, 389,1438,1560,1773,3963,2264, 719,2951,2724,3834, 870,1832,1644,1000, # 3840 - 839,2474,3717, 197,1630,3394, 365,2886,3964,1285,2133, 734, 922, 818,1106, 732, # 3856 - 480,2083,1774,3458, 923,2279,1350, 221,3086, 85,2233,2234,3835,1585,3010,2147, # 3872 -1387,1705,2382,1619,2475, 133, 239,2802,1991,1016,2084,2383, 411,2838,1113, 651, # 3888 -1985,1160,3328, 990,1863,3087,1048,1276,2647, 265,2627,1599,3253,2056, 150, 638, # 3904 -2019, 656, 853, 326,1479, 680,1439,4354,1001,1759, 413,3459,3395,2492,1431, 459, # 3920 -4355,1125,3329,2265,1953,1450,2065,2863, 849, 351,2678,3131,3254,3255,1104,1577, # 3936 - 227,1351,1645,2453,2193,1421,2887, 812,2121, 634, 95,2435, 201,2312,4665,1646, # 3952 -1671,2743,1601,2554,2702,2648,2280,1315,1366,2089,3132,1573,3718,3965,1729,1189, # 3968 - 328,2679,1077,1940,1136, 558,1283, 964,1195, 621,2074,1199,1743,3460,3619,1896, # 3984 -1916,1890,3836,2952,1154,2112,1064, 862, 378,3011,2066,2113,2803,1568,2839,6178, # 4000 -3088,2919,1941,1660,2004,1992,2194, 142, 707,1590,1708,1624,1922,1023,1836,1233, # 4016 -1004,2313, 789, 741,3620,6179,1609,2411,1200,4127,3719,3720,4666,2057,3721, 593, # 4032 -2840, 367,2920,1878,6180,3461,1521, 628,1168, 692,2211,2649, 300, 720,2067,2571, # 4048 -2953,3396, 959,2504,3966,3539,3462,1977, 701,6181, 954,1043, 800, 681, 183,3722, # 4064 -1803,1730,3540,4128,2103, 815,2314, 174, 467, 230,2454,1093,2134, 755,3541,3397, # 4080 -1141,1162,6182,1738,2039, 270,3256,2513,1005,1647,2185,3837, 858,1679,1897,1719, # 4096 -2954,2324,1806, 402, 670, 167,4129,1498,2158,2104, 750,6183, 915, 189,1680,1551, # 4112 - 455,4356,1501,2455, 405,1095,2955, 338,1586,1266,1819, 570, 641,1324, 237,1556, # 4128 -2650,1388,3723,6184,1368,2384,1343,1978,3089,2436, 879,3724, 792,1191, 758,3012, # 4144 -1411,2135,1322,4357, 240,4667,1848,3725,1574,6185, 420,3045,1546,1391, 714,4358, # 4160 -1967, 941,1864, 863, 664, 426, 560,1731,2680,1785,2864,1949,2363, 403,3330,1415, # 4176 -1279,2136,1697,2335, 204, 721,2097,3838, 90,6186,2085,2505, 191,3967, 124,2148, # 4192 -1376,1798,1178,1107,1898,1405, 860,4359,1243,1272,2375,2983,1558,2456,1638, 113, # 4208 -3621, 578,1923,2609, 880, 386,4130, 784,2186,2266,1422,2956,2172,1722, 497, 263, # 4224 -2514,1267,2412,2610, 177,2703,3542, 774,1927,1344, 616,1432,1595,1018, 172,4360, # 4240 -2325, 911,4361, 438,1468,3622, 794,3968,2024,2173,1681,1829,2957, 945, 895,3090, # 4256 - 575,2212,2476, 475,2401,2681, 785,2744,1745,2293,2555,1975,3133,2865, 394,4668, # 4272 -3839, 635,4131, 639, 202,1507,2195,2766,1345,1435,2572,3726,1908,1184,1181,2457, # 4288 -3727,3134,4362, 843,2611, 437, 916,4669, 234, 769,1884,3046,3047,3623, 833,6187, # 4304 -1639,2250,2402,1355,1185,2010,2047, 999, 525,1732,1290,1488,2612, 948,1578,3728, # 4320 -2413,2477,1216,2725,2159, 334,3840,1328,3624,2921,1525,4132, 564,1056, 891,4363, # 4336 -1444,1698,2385,2251,3729,1365,2281,2235,1717,6188, 864,3841,2515, 444, 527,2767, # 4352 -2922,3625, 544, 461,6189, 566, 209,2437,3398,2098,1065,2068,3331,3626,3257,2137, # 4368 #last 512 -) -# fmt: on diff --git a/libs/chardet/johabfreq.py b/libs/chardet/johabfreq.py deleted file mode 100644 index c12969990..000000000 --- a/libs/chardet/johabfreq.py +++ /dev/null @@ -1,2382 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# The frequency data itself is the same as euc-kr. -# This is just a mapping table to euc-kr. - -JOHAB_TO_EUCKR_ORDER_TABLE = { - 0x8861: 0, - 0x8862: 1, - 0x8865: 2, - 0x8868: 3, - 0x8869: 4, - 0x886A: 5, - 0x886B: 6, - 0x8871: 7, - 0x8873: 8, - 0x8874: 9, - 0x8875: 10, - 0x8876: 11, - 0x8877: 12, - 0x8878: 13, - 0x8879: 14, - 0x887B: 15, - 0x887C: 16, - 0x887D: 17, - 0x8881: 18, - 0x8882: 19, - 0x8885: 20, - 0x8889: 21, - 0x8891: 22, - 0x8893: 23, - 0x8895: 24, - 0x8896: 25, - 0x8897: 26, - 0x88A1: 27, - 0x88A2: 28, - 0x88A5: 29, - 0x88A9: 30, - 0x88B5: 31, - 0x88B7: 32, - 0x88C1: 33, - 0x88C5: 34, - 0x88C9: 35, - 0x88E1: 36, - 0x88E2: 37, - 0x88E5: 38, - 0x88E8: 39, - 0x88E9: 40, - 0x88EB: 41, - 0x88F1: 42, - 0x88F3: 43, - 0x88F5: 44, - 0x88F6: 45, - 0x88F7: 46, - 0x88F8: 47, - 0x88FB: 48, - 0x88FC: 49, - 0x88FD: 50, - 0x8941: 51, - 0x8945: 52, - 0x8949: 53, - 0x8951: 54, - 0x8953: 55, - 0x8955: 56, - 0x8956: 57, - 0x8957: 58, - 0x8961: 59, - 0x8962: 60, - 0x8963: 61, - 0x8965: 62, - 0x8968: 63, - 0x8969: 64, - 0x8971: 65, - 0x8973: 66, - 0x8975: 67, - 0x8976: 68, - 0x8977: 69, - 0x897B: 70, - 0x8981: 71, - 0x8985: 72, - 0x8989: 73, - 0x8993: 74, - 0x8995: 75, - 0x89A1: 76, - 0x89A2: 77, - 0x89A5: 78, - 0x89A8: 79, - 0x89A9: 80, - 0x89AB: 81, - 0x89AD: 82, - 0x89B0: 83, - 0x89B1: 84, - 0x89B3: 85, - 0x89B5: 86, - 0x89B7: 87, - 0x89B8: 88, - 0x89C1: 89, - 0x89C2: 90, - 0x89C5: 91, - 0x89C9: 92, - 0x89CB: 93, - 0x89D1: 94, - 0x89D3: 95, - 0x89D5: 96, - 0x89D7: 97, - 0x89E1: 98, - 0x89E5: 99, - 0x89E9: 100, - 0x89F3: 101, - 0x89F6: 102, - 0x89F7: 103, - 0x8A41: 104, - 0x8A42: 105, - 0x8A45: 106, - 0x8A49: 107, - 0x8A51: 108, - 0x8A53: 109, - 0x8A55: 110, - 0x8A57: 111, - 0x8A61: 112, - 0x8A65: 113, - 0x8A69: 114, - 0x8A73: 115, - 0x8A75: 116, - 0x8A81: 117, - 0x8A82: 118, - 0x8A85: 119, - 0x8A88: 120, - 0x8A89: 121, - 0x8A8A: 122, - 0x8A8B: 123, - 0x8A90: 124, - 0x8A91: 125, - 0x8A93: 126, - 0x8A95: 127, - 0x8A97: 128, - 0x8A98: 129, - 0x8AA1: 130, - 0x8AA2: 131, - 0x8AA5: 132, - 0x8AA9: 133, - 0x8AB6: 134, - 0x8AB7: 135, - 0x8AC1: 136, - 0x8AD5: 137, - 0x8AE1: 138, - 0x8AE2: 139, - 0x8AE5: 140, - 0x8AE9: 141, - 0x8AF1: 142, - 0x8AF3: 143, - 0x8AF5: 144, - 0x8B41: 145, - 0x8B45: 146, - 0x8B49: 147, - 0x8B61: 148, - 0x8B62: 149, - 0x8B65: 150, - 0x8B68: 151, - 0x8B69: 152, - 0x8B6A: 153, - 0x8B71: 154, - 0x8B73: 155, - 0x8B75: 156, - 0x8B77: 157, - 0x8B81: 158, - 0x8BA1: 159, - 0x8BA2: 160, - 0x8BA5: 161, - 0x8BA8: 162, - 0x8BA9: 163, - 0x8BAB: 164, - 0x8BB1: 165, - 0x8BB3: 166, - 0x8BB5: 167, - 0x8BB7: 168, - 0x8BB8: 169, - 0x8BBC: 170, - 0x8C61: 171, - 0x8C62: 172, - 0x8C63: 173, - 0x8C65: 174, - 0x8C69: 175, - 0x8C6B: 176, - 0x8C71: 177, - 0x8C73: 178, - 0x8C75: 179, - 0x8C76: 180, - 0x8C77: 181, - 0x8C7B: 182, - 0x8C81: 183, - 0x8C82: 184, - 0x8C85: 185, - 0x8C89: 186, - 0x8C91: 187, - 0x8C93: 188, - 0x8C95: 189, - 0x8C96: 190, - 0x8C97: 191, - 0x8CA1: 192, - 0x8CA2: 193, - 0x8CA9: 194, - 0x8CE1: 195, - 0x8CE2: 196, - 0x8CE3: 197, - 0x8CE5: 198, - 0x8CE9: 199, - 0x8CF1: 200, - 0x8CF3: 201, - 0x8CF5: 202, - 0x8CF6: 203, - 0x8CF7: 204, - 0x8D41: 205, - 0x8D42: 206, - 0x8D45: 207, - 0x8D51: 208, - 0x8D55: 209, - 0x8D57: 210, - 0x8D61: 211, - 0x8D65: 212, - 0x8D69: 213, - 0x8D75: 214, - 0x8D76: 215, - 0x8D7B: 216, - 0x8D81: 217, - 0x8DA1: 218, - 0x8DA2: 219, - 0x8DA5: 220, - 0x8DA7: 221, - 0x8DA9: 222, - 0x8DB1: 223, - 0x8DB3: 224, - 0x8DB5: 225, - 0x8DB7: 226, - 0x8DB8: 227, - 0x8DB9: 228, - 0x8DC1: 229, - 0x8DC2: 230, - 0x8DC9: 231, - 0x8DD6: 232, - 0x8DD7: 233, - 0x8DE1: 234, - 0x8DE2: 235, - 0x8DF7: 236, - 0x8E41: 237, - 0x8E45: 238, - 0x8E49: 239, - 0x8E51: 240, - 0x8E53: 241, - 0x8E57: 242, - 0x8E61: 243, - 0x8E81: 244, - 0x8E82: 245, - 0x8E85: 246, - 0x8E89: 247, - 0x8E90: 248, - 0x8E91: 249, - 0x8E93: 250, - 0x8E95: 251, - 0x8E97: 252, - 0x8E98: 253, - 0x8EA1: 254, - 0x8EA9: 255, - 0x8EB6: 256, - 0x8EB7: 257, - 0x8EC1: 258, - 0x8EC2: 259, - 0x8EC5: 260, - 0x8EC9: 261, - 0x8ED1: 262, - 0x8ED3: 263, - 0x8ED6: 264, - 0x8EE1: 265, - 0x8EE5: 266, - 0x8EE9: 267, - 0x8EF1: 268, - 0x8EF3: 269, - 0x8F41: 270, - 0x8F61: 271, - 0x8F62: 272, - 0x8F65: 273, - 0x8F67: 274, - 0x8F69: 275, - 0x8F6B: 276, - 0x8F70: 277, - 0x8F71: 278, - 0x8F73: 279, - 0x8F75: 280, - 0x8F77: 281, - 0x8F7B: 282, - 0x8FA1: 283, - 0x8FA2: 284, - 0x8FA5: 285, - 0x8FA9: 286, - 0x8FB1: 287, - 0x8FB3: 288, - 0x8FB5: 289, - 0x8FB7: 290, - 0x9061: 291, - 0x9062: 292, - 0x9063: 293, - 0x9065: 294, - 0x9068: 295, - 0x9069: 296, - 0x906A: 297, - 0x906B: 298, - 0x9071: 299, - 0x9073: 300, - 0x9075: 301, - 0x9076: 302, - 0x9077: 303, - 0x9078: 304, - 0x9079: 305, - 0x907B: 306, - 0x907D: 307, - 0x9081: 308, - 0x9082: 309, - 0x9085: 310, - 0x9089: 311, - 0x9091: 312, - 0x9093: 313, - 0x9095: 314, - 0x9096: 315, - 0x9097: 316, - 0x90A1: 317, - 0x90A2: 318, - 0x90A5: 319, - 0x90A9: 320, - 0x90B1: 321, - 0x90B7: 322, - 0x90E1: 323, - 0x90E2: 324, - 0x90E4: 325, - 0x90E5: 326, - 0x90E9: 327, - 0x90EB: 328, - 0x90EC: 329, - 0x90F1: 330, - 0x90F3: 331, - 0x90F5: 332, - 0x90F6: 333, - 0x90F7: 334, - 0x90FD: 335, - 0x9141: 336, - 0x9142: 337, - 0x9145: 338, - 0x9149: 339, - 0x9151: 340, - 0x9153: 341, - 0x9155: 342, - 0x9156: 343, - 0x9157: 344, - 0x9161: 345, - 0x9162: 346, - 0x9165: 347, - 0x9169: 348, - 0x9171: 349, - 0x9173: 350, - 0x9176: 351, - 0x9177: 352, - 0x917A: 353, - 0x9181: 354, - 0x9185: 355, - 0x91A1: 356, - 0x91A2: 357, - 0x91A5: 358, - 0x91A9: 359, - 0x91AB: 360, - 0x91B1: 361, - 0x91B3: 362, - 0x91B5: 363, - 0x91B7: 364, - 0x91BC: 365, - 0x91BD: 366, - 0x91C1: 367, - 0x91C5: 368, - 0x91C9: 369, - 0x91D6: 370, - 0x9241: 371, - 0x9245: 372, - 0x9249: 373, - 0x9251: 374, - 0x9253: 375, - 0x9255: 376, - 0x9261: 377, - 0x9262: 378, - 0x9265: 379, - 0x9269: 380, - 0x9273: 381, - 0x9275: 382, - 0x9277: 383, - 0x9281: 384, - 0x9282: 385, - 0x9285: 386, - 0x9288: 387, - 0x9289: 388, - 0x9291: 389, - 0x9293: 390, - 0x9295: 391, - 0x9297: 392, - 0x92A1: 393, - 0x92B6: 394, - 0x92C1: 395, - 0x92E1: 396, - 0x92E5: 397, - 0x92E9: 398, - 0x92F1: 399, - 0x92F3: 400, - 0x9341: 401, - 0x9342: 402, - 0x9349: 403, - 0x9351: 404, - 0x9353: 405, - 0x9357: 406, - 0x9361: 407, - 0x9362: 408, - 0x9365: 409, - 0x9369: 410, - 0x936A: 411, - 0x936B: 412, - 0x9371: 413, - 0x9373: 414, - 0x9375: 415, - 0x9377: 416, - 0x9378: 417, - 0x937C: 418, - 0x9381: 419, - 0x9385: 420, - 0x9389: 421, - 0x93A1: 422, - 0x93A2: 423, - 0x93A5: 424, - 0x93A9: 425, - 0x93AB: 426, - 0x93B1: 427, - 0x93B3: 428, - 0x93B5: 429, - 0x93B7: 430, - 0x93BC: 431, - 0x9461: 432, - 0x9462: 433, - 0x9463: 434, - 0x9465: 435, - 0x9468: 436, - 0x9469: 437, - 0x946A: 438, - 0x946B: 439, - 0x946C: 440, - 0x9470: 441, - 0x9471: 442, - 0x9473: 443, - 0x9475: 444, - 0x9476: 445, - 0x9477: 446, - 0x9478: 447, - 0x9479: 448, - 0x947D: 449, - 0x9481: 450, - 0x9482: 451, - 0x9485: 452, - 0x9489: 453, - 0x9491: 454, - 0x9493: 455, - 0x9495: 456, - 0x9496: 457, - 0x9497: 458, - 0x94A1: 459, - 0x94E1: 460, - 0x94E2: 461, - 0x94E3: 462, - 0x94E5: 463, - 0x94E8: 464, - 0x94E9: 465, - 0x94EB: 466, - 0x94EC: 467, - 0x94F1: 468, - 0x94F3: 469, - 0x94F5: 470, - 0x94F7: 471, - 0x94F9: 472, - 0x94FC: 473, - 0x9541: 474, - 0x9542: 475, - 0x9545: 476, - 0x9549: 477, - 0x9551: 478, - 0x9553: 479, - 0x9555: 480, - 0x9556: 481, - 0x9557: 482, - 0x9561: 483, - 0x9565: 484, - 0x9569: 485, - 0x9576: 486, - 0x9577: 487, - 0x9581: 488, - 0x9585: 489, - 0x95A1: 490, - 0x95A2: 491, - 0x95A5: 492, - 0x95A8: 493, - 0x95A9: 494, - 0x95AB: 495, - 0x95AD: 496, - 0x95B1: 497, - 0x95B3: 498, - 0x95B5: 499, - 0x95B7: 500, - 0x95B9: 501, - 0x95BB: 502, - 0x95C1: 503, - 0x95C5: 504, - 0x95C9: 505, - 0x95E1: 506, - 0x95F6: 507, - 0x9641: 508, - 0x9645: 509, - 0x9649: 510, - 0x9651: 511, - 0x9653: 512, - 0x9655: 513, - 0x9661: 514, - 0x9681: 515, - 0x9682: 516, - 0x9685: 517, - 0x9689: 518, - 0x9691: 519, - 0x9693: 520, - 0x9695: 521, - 0x9697: 522, - 0x96A1: 523, - 0x96B6: 524, - 0x96C1: 525, - 0x96D7: 526, - 0x96E1: 527, - 0x96E5: 528, - 0x96E9: 529, - 0x96F3: 530, - 0x96F5: 531, - 0x96F7: 532, - 0x9741: 533, - 0x9745: 534, - 0x9749: 535, - 0x9751: 536, - 0x9757: 537, - 0x9761: 538, - 0x9762: 539, - 0x9765: 540, - 0x9768: 541, - 0x9769: 542, - 0x976B: 543, - 0x9771: 544, - 0x9773: 545, - 0x9775: 546, - 0x9777: 547, - 0x9781: 548, - 0x97A1: 549, - 0x97A2: 550, - 0x97A5: 551, - 0x97A8: 552, - 0x97A9: 553, - 0x97B1: 554, - 0x97B3: 555, - 0x97B5: 556, - 0x97B6: 557, - 0x97B7: 558, - 0x97B8: 559, - 0x9861: 560, - 0x9862: 561, - 0x9865: 562, - 0x9869: 563, - 0x9871: 564, - 0x9873: 565, - 0x9875: 566, - 0x9876: 567, - 0x9877: 568, - 0x987D: 569, - 0x9881: 570, - 0x9882: 571, - 0x9885: 572, - 0x9889: 573, - 0x9891: 574, - 0x9893: 575, - 0x9895: 576, - 0x9896: 577, - 0x9897: 578, - 0x98E1: 579, - 0x98E2: 580, - 0x98E5: 581, - 0x98E9: 582, - 0x98EB: 583, - 0x98EC: 584, - 0x98F1: 585, - 0x98F3: 586, - 0x98F5: 587, - 0x98F6: 588, - 0x98F7: 589, - 0x98FD: 590, - 0x9941: 591, - 0x9942: 592, - 0x9945: 593, - 0x9949: 594, - 0x9951: 595, - 0x9953: 596, - 0x9955: 597, - 0x9956: 598, - 0x9957: 599, - 0x9961: 600, - 0x9976: 601, - 0x99A1: 602, - 0x99A2: 603, - 0x99A5: 604, - 0x99A9: 605, - 0x99B7: 606, - 0x99C1: 607, - 0x99C9: 608, - 0x99E1: 609, - 0x9A41: 610, - 0x9A45: 611, - 0x9A81: 612, - 0x9A82: 613, - 0x9A85: 614, - 0x9A89: 615, - 0x9A90: 616, - 0x9A91: 617, - 0x9A97: 618, - 0x9AC1: 619, - 0x9AE1: 620, - 0x9AE5: 621, - 0x9AE9: 622, - 0x9AF1: 623, - 0x9AF3: 624, - 0x9AF7: 625, - 0x9B61: 626, - 0x9B62: 627, - 0x9B65: 628, - 0x9B68: 629, - 0x9B69: 630, - 0x9B71: 631, - 0x9B73: 632, - 0x9B75: 633, - 0x9B81: 634, - 0x9B85: 635, - 0x9B89: 636, - 0x9B91: 637, - 0x9B93: 638, - 0x9BA1: 639, - 0x9BA5: 640, - 0x9BA9: 641, - 0x9BB1: 642, - 0x9BB3: 643, - 0x9BB5: 644, - 0x9BB7: 645, - 0x9C61: 646, - 0x9C62: 647, - 0x9C65: 648, - 0x9C69: 649, - 0x9C71: 650, - 0x9C73: 651, - 0x9C75: 652, - 0x9C76: 653, - 0x9C77: 654, - 0x9C78: 655, - 0x9C7C: 656, - 0x9C7D: 657, - 0x9C81: 658, - 0x9C82: 659, - 0x9C85: 660, - 0x9C89: 661, - 0x9C91: 662, - 0x9C93: 663, - 0x9C95: 664, - 0x9C96: 665, - 0x9C97: 666, - 0x9CA1: 667, - 0x9CA2: 668, - 0x9CA5: 669, - 0x9CB5: 670, - 0x9CB7: 671, - 0x9CE1: 672, - 0x9CE2: 673, - 0x9CE5: 674, - 0x9CE9: 675, - 0x9CF1: 676, - 0x9CF3: 677, - 0x9CF5: 678, - 0x9CF6: 679, - 0x9CF7: 680, - 0x9CFD: 681, - 0x9D41: 682, - 0x9D42: 683, - 0x9D45: 684, - 0x9D49: 685, - 0x9D51: 686, - 0x9D53: 687, - 0x9D55: 688, - 0x9D57: 689, - 0x9D61: 690, - 0x9D62: 691, - 0x9D65: 692, - 0x9D69: 693, - 0x9D71: 694, - 0x9D73: 695, - 0x9D75: 696, - 0x9D76: 697, - 0x9D77: 698, - 0x9D81: 699, - 0x9D85: 700, - 0x9D93: 701, - 0x9D95: 702, - 0x9DA1: 703, - 0x9DA2: 704, - 0x9DA5: 705, - 0x9DA9: 706, - 0x9DB1: 707, - 0x9DB3: 708, - 0x9DB5: 709, - 0x9DB7: 710, - 0x9DC1: 711, - 0x9DC5: 712, - 0x9DD7: 713, - 0x9DF6: 714, - 0x9E41: 715, - 0x9E45: 716, - 0x9E49: 717, - 0x9E51: 718, - 0x9E53: 719, - 0x9E55: 720, - 0x9E57: 721, - 0x9E61: 722, - 0x9E65: 723, - 0x9E69: 724, - 0x9E73: 725, - 0x9E75: 726, - 0x9E77: 727, - 0x9E81: 728, - 0x9E82: 729, - 0x9E85: 730, - 0x9E89: 731, - 0x9E91: 732, - 0x9E93: 733, - 0x9E95: 734, - 0x9E97: 735, - 0x9EA1: 736, - 0x9EB6: 737, - 0x9EC1: 738, - 0x9EE1: 739, - 0x9EE2: 740, - 0x9EE5: 741, - 0x9EE9: 742, - 0x9EF1: 743, - 0x9EF5: 744, - 0x9EF7: 745, - 0x9F41: 746, - 0x9F42: 747, - 0x9F45: 748, - 0x9F49: 749, - 0x9F51: 750, - 0x9F53: 751, - 0x9F55: 752, - 0x9F57: 753, - 0x9F61: 754, - 0x9F62: 755, - 0x9F65: 756, - 0x9F69: 757, - 0x9F71: 758, - 0x9F73: 759, - 0x9F75: 760, - 0x9F77: 761, - 0x9F78: 762, - 0x9F7B: 763, - 0x9F7C: 764, - 0x9FA1: 765, - 0x9FA2: 766, - 0x9FA5: 767, - 0x9FA9: 768, - 0x9FB1: 769, - 0x9FB3: 770, - 0x9FB5: 771, - 0x9FB7: 772, - 0xA061: 773, - 0xA062: 774, - 0xA065: 775, - 0xA067: 776, - 0xA068: 777, - 0xA069: 778, - 0xA06A: 779, - 0xA06B: 780, - 0xA071: 781, - 0xA073: 782, - 0xA075: 783, - 0xA077: 784, - 0xA078: 785, - 0xA07B: 786, - 0xA07D: 787, - 0xA081: 788, - 0xA082: 789, - 0xA085: 790, - 0xA089: 791, - 0xA091: 792, - 0xA093: 793, - 0xA095: 794, - 0xA096: 795, - 0xA097: 796, - 0xA098: 797, - 0xA0A1: 798, - 0xA0A2: 799, - 0xA0A9: 800, - 0xA0B7: 801, - 0xA0E1: 802, - 0xA0E2: 803, - 0xA0E5: 804, - 0xA0E9: 805, - 0xA0EB: 806, - 0xA0F1: 807, - 0xA0F3: 808, - 0xA0F5: 809, - 0xA0F7: 810, - 0xA0F8: 811, - 0xA0FD: 812, - 0xA141: 813, - 0xA142: 814, - 0xA145: 815, - 0xA149: 816, - 0xA151: 817, - 0xA153: 818, - 0xA155: 819, - 0xA156: 820, - 0xA157: 821, - 0xA161: 822, - 0xA162: 823, - 0xA165: 824, - 0xA169: 825, - 0xA175: 826, - 0xA176: 827, - 0xA177: 828, - 0xA179: 829, - 0xA181: 830, - 0xA1A1: 831, - 0xA1A2: 832, - 0xA1A4: 833, - 0xA1A5: 834, - 0xA1A9: 835, - 0xA1AB: 836, - 0xA1B1: 837, - 0xA1B3: 838, - 0xA1B5: 839, - 0xA1B7: 840, - 0xA1C1: 841, - 0xA1C5: 842, - 0xA1D6: 843, - 0xA1D7: 844, - 0xA241: 845, - 0xA245: 846, - 0xA249: 847, - 0xA253: 848, - 0xA255: 849, - 0xA257: 850, - 0xA261: 851, - 0xA265: 852, - 0xA269: 853, - 0xA273: 854, - 0xA275: 855, - 0xA281: 856, - 0xA282: 857, - 0xA283: 858, - 0xA285: 859, - 0xA288: 860, - 0xA289: 861, - 0xA28A: 862, - 0xA28B: 863, - 0xA291: 864, - 0xA293: 865, - 0xA295: 866, - 0xA297: 867, - 0xA29B: 868, - 0xA29D: 869, - 0xA2A1: 870, - 0xA2A5: 871, - 0xA2A9: 872, - 0xA2B3: 873, - 0xA2B5: 874, - 0xA2C1: 875, - 0xA2E1: 876, - 0xA2E5: 877, - 0xA2E9: 878, - 0xA341: 879, - 0xA345: 880, - 0xA349: 881, - 0xA351: 882, - 0xA355: 883, - 0xA361: 884, - 0xA365: 885, - 0xA369: 886, - 0xA371: 887, - 0xA375: 888, - 0xA3A1: 889, - 0xA3A2: 890, - 0xA3A5: 891, - 0xA3A8: 892, - 0xA3A9: 893, - 0xA3AB: 894, - 0xA3B1: 895, - 0xA3B3: 896, - 0xA3B5: 897, - 0xA3B6: 898, - 0xA3B7: 899, - 0xA3B9: 900, - 0xA3BB: 901, - 0xA461: 902, - 0xA462: 903, - 0xA463: 904, - 0xA464: 905, - 0xA465: 906, - 0xA468: 907, - 0xA469: 908, - 0xA46A: 909, - 0xA46B: 910, - 0xA46C: 911, - 0xA471: 912, - 0xA473: 913, - 0xA475: 914, - 0xA477: 915, - 0xA47B: 916, - 0xA481: 917, - 0xA482: 918, - 0xA485: 919, - 0xA489: 920, - 0xA491: 921, - 0xA493: 922, - 0xA495: 923, - 0xA496: 924, - 0xA497: 925, - 0xA49B: 926, - 0xA4A1: 927, - 0xA4A2: 928, - 0xA4A5: 929, - 0xA4B3: 930, - 0xA4E1: 931, - 0xA4E2: 932, - 0xA4E5: 933, - 0xA4E8: 934, - 0xA4E9: 935, - 0xA4EB: 936, - 0xA4F1: 937, - 0xA4F3: 938, - 0xA4F5: 939, - 0xA4F7: 940, - 0xA4F8: 941, - 0xA541: 942, - 0xA542: 943, - 0xA545: 944, - 0xA548: 945, - 0xA549: 946, - 0xA551: 947, - 0xA553: 948, - 0xA555: 949, - 0xA556: 950, - 0xA557: 951, - 0xA561: 952, - 0xA562: 953, - 0xA565: 954, - 0xA569: 955, - 0xA573: 956, - 0xA575: 957, - 0xA576: 958, - 0xA577: 959, - 0xA57B: 960, - 0xA581: 961, - 0xA585: 962, - 0xA5A1: 963, - 0xA5A2: 964, - 0xA5A3: 965, - 0xA5A5: 966, - 0xA5A9: 967, - 0xA5B1: 968, - 0xA5B3: 969, - 0xA5B5: 970, - 0xA5B7: 971, - 0xA5C1: 972, - 0xA5C5: 973, - 0xA5D6: 974, - 0xA5E1: 975, - 0xA5F6: 976, - 0xA641: 977, - 0xA642: 978, - 0xA645: 979, - 0xA649: 980, - 0xA651: 981, - 0xA653: 982, - 0xA661: 983, - 0xA665: 984, - 0xA681: 985, - 0xA682: 986, - 0xA685: 987, - 0xA688: 988, - 0xA689: 989, - 0xA68A: 990, - 0xA68B: 991, - 0xA691: 992, - 0xA693: 993, - 0xA695: 994, - 0xA697: 995, - 0xA69B: 996, - 0xA69C: 997, - 0xA6A1: 998, - 0xA6A9: 999, - 0xA6B6: 1000, - 0xA6C1: 1001, - 0xA6E1: 1002, - 0xA6E2: 1003, - 0xA6E5: 1004, - 0xA6E9: 1005, - 0xA6F7: 1006, - 0xA741: 1007, - 0xA745: 1008, - 0xA749: 1009, - 0xA751: 1010, - 0xA755: 1011, - 0xA757: 1012, - 0xA761: 1013, - 0xA762: 1014, - 0xA765: 1015, - 0xA769: 1016, - 0xA771: 1017, - 0xA773: 1018, - 0xA775: 1019, - 0xA7A1: 1020, - 0xA7A2: 1021, - 0xA7A5: 1022, - 0xA7A9: 1023, - 0xA7AB: 1024, - 0xA7B1: 1025, - 0xA7B3: 1026, - 0xA7B5: 1027, - 0xA7B7: 1028, - 0xA7B8: 1029, - 0xA7B9: 1030, - 0xA861: 1031, - 0xA862: 1032, - 0xA865: 1033, - 0xA869: 1034, - 0xA86B: 1035, - 0xA871: 1036, - 0xA873: 1037, - 0xA875: 1038, - 0xA876: 1039, - 0xA877: 1040, - 0xA87D: 1041, - 0xA881: 1042, - 0xA882: 1043, - 0xA885: 1044, - 0xA889: 1045, - 0xA891: 1046, - 0xA893: 1047, - 0xA895: 1048, - 0xA896: 1049, - 0xA897: 1050, - 0xA8A1: 1051, - 0xA8A2: 1052, - 0xA8B1: 1053, - 0xA8E1: 1054, - 0xA8E2: 1055, - 0xA8E5: 1056, - 0xA8E8: 1057, - 0xA8E9: 1058, - 0xA8F1: 1059, - 0xA8F5: 1060, - 0xA8F6: 1061, - 0xA8F7: 1062, - 0xA941: 1063, - 0xA957: 1064, - 0xA961: 1065, - 0xA962: 1066, - 0xA971: 1067, - 0xA973: 1068, - 0xA975: 1069, - 0xA976: 1070, - 0xA977: 1071, - 0xA9A1: 1072, - 0xA9A2: 1073, - 0xA9A5: 1074, - 0xA9A9: 1075, - 0xA9B1: 1076, - 0xA9B3: 1077, - 0xA9B7: 1078, - 0xAA41: 1079, - 0xAA61: 1080, - 0xAA77: 1081, - 0xAA81: 1082, - 0xAA82: 1083, - 0xAA85: 1084, - 0xAA89: 1085, - 0xAA91: 1086, - 0xAA95: 1087, - 0xAA97: 1088, - 0xAB41: 1089, - 0xAB57: 1090, - 0xAB61: 1091, - 0xAB65: 1092, - 0xAB69: 1093, - 0xAB71: 1094, - 0xAB73: 1095, - 0xABA1: 1096, - 0xABA2: 1097, - 0xABA5: 1098, - 0xABA9: 1099, - 0xABB1: 1100, - 0xABB3: 1101, - 0xABB5: 1102, - 0xABB7: 1103, - 0xAC61: 1104, - 0xAC62: 1105, - 0xAC64: 1106, - 0xAC65: 1107, - 0xAC68: 1108, - 0xAC69: 1109, - 0xAC6A: 1110, - 0xAC6B: 1111, - 0xAC71: 1112, - 0xAC73: 1113, - 0xAC75: 1114, - 0xAC76: 1115, - 0xAC77: 1116, - 0xAC7B: 1117, - 0xAC81: 1118, - 0xAC82: 1119, - 0xAC85: 1120, - 0xAC89: 1121, - 0xAC91: 1122, - 0xAC93: 1123, - 0xAC95: 1124, - 0xAC96: 1125, - 0xAC97: 1126, - 0xACA1: 1127, - 0xACA2: 1128, - 0xACA5: 1129, - 0xACA9: 1130, - 0xACB1: 1131, - 0xACB3: 1132, - 0xACB5: 1133, - 0xACB7: 1134, - 0xACC1: 1135, - 0xACC5: 1136, - 0xACC9: 1137, - 0xACD1: 1138, - 0xACD7: 1139, - 0xACE1: 1140, - 0xACE2: 1141, - 0xACE3: 1142, - 0xACE4: 1143, - 0xACE5: 1144, - 0xACE8: 1145, - 0xACE9: 1146, - 0xACEB: 1147, - 0xACEC: 1148, - 0xACF1: 1149, - 0xACF3: 1150, - 0xACF5: 1151, - 0xACF6: 1152, - 0xACF7: 1153, - 0xACFC: 1154, - 0xAD41: 1155, - 0xAD42: 1156, - 0xAD45: 1157, - 0xAD49: 1158, - 0xAD51: 1159, - 0xAD53: 1160, - 0xAD55: 1161, - 0xAD56: 1162, - 0xAD57: 1163, - 0xAD61: 1164, - 0xAD62: 1165, - 0xAD65: 1166, - 0xAD69: 1167, - 0xAD71: 1168, - 0xAD73: 1169, - 0xAD75: 1170, - 0xAD76: 1171, - 0xAD77: 1172, - 0xAD81: 1173, - 0xAD85: 1174, - 0xAD89: 1175, - 0xAD97: 1176, - 0xADA1: 1177, - 0xADA2: 1178, - 0xADA3: 1179, - 0xADA5: 1180, - 0xADA9: 1181, - 0xADAB: 1182, - 0xADB1: 1183, - 0xADB3: 1184, - 0xADB5: 1185, - 0xADB7: 1186, - 0xADBB: 1187, - 0xADC1: 1188, - 0xADC2: 1189, - 0xADC5: 1190, - 0xADC9: 1191, - 0xADD7: 1192, - 0xADE1: 1193, - 0xADE5: 1194, - 0xADE9: 1195, - 0xADF1: 1196, - 0xADF5: 1197, - 0xADF6: 1198, - 0xAE41: 1199, - 0xAE45: 1200, - 0xAE49: 1201, - 0xAE51: 1202, - 0xAE53: 1203, - 0xAE55: 1204, - 0xAE61: 1205, - 0xAE62: 1206, - 0xAE65: 1207, - 0xAE69: 1208, - 0xAE71: 1209, - 0xAE73: 1210, - 0xAE75: 1211, - 0xAE77: 1212, - 0xAE81: 1213, - 0xAE82: 1214, - 0xAE85: 1215, - 0xAE88: 1216, - 0xAE89: 1217, - 0xAE91: 1218, - 0xAE93: 1219, - 0xAE95: 1220, - 0xAE97: 1221, - 0xAE99: 1222, - 0xAE9B: 1223, - 0xAE9C: 1224, - 0xAEA1: 1225, - 0xAEB6: 1226, - 0xAEC1: 1227, - 0xAEC2: 1228, - 0xAEC5: 1229, - 0xAEC9: 1230, - 0xAED1: 1231, - 0xAED7: 1232, - 0xAEE1: 1233, - 0xAEE2: 1234, - 0xAEE5: 1235, - 0xAEE9: 1236, - 0xAEF1: 1237, - 0xAEF3: 1238, - 0xAEF5: 1239, - 0xAEF7: 1240, - 0xAF41: 1241, - 0xAF42: 1242, - 0xAF49: 1243, - 0xAF51: 1244, - 0xAF55: 1245, - 0xAF57: 1246, - 0xAF61: 1247, - 0xAF62: 1248, - 0xAF65: 1249, - 0xAF69: 1250, - 0xAF6A: 1251, - 0xAF71: 1252, - 0xAF73: 1253, - 0xAF75: 1254, - 0xAF77: 1255, - 0xAFA1: 1256, - 0xAFA2: 1257, - 0xAFA5: 1258, - 0xAFA8: 1259, - 0xAFA9: 1260, - 0xAFB0: 1261, - 0xAFB1: 1262, - 0xAFB3: 1263, - 0xAFB5: 1264, - 0xAFB7: 1265, - 0xAFBC: 1266, - 0xB061: 1267, - 0xB062: 1268, - 0xB064: 1269, - 0xB065: 1270, - 0xB069: 1271, - 0xB071: 1272, - 0xB073: 1273, - 0xB076: 1274, - 0xB077: 1275, - 0xB07D: 1276, - 0xB081: 1277, - 0xB082: 1278, - 0xB085: 1279, - 0xB089: 1280, - 0xB091: 1281, - 0xB093: 1282, - 0xB096: 1283, - 0xB097: 1284, - 0xB0B7: 1285, - 0xB0E1: 1286, - 0xB0E2: 1287, - 0xB0E5: 1288, - 0xB0E9: 1289, - 0xB0EB: 1290, - 0xB0F1: 1291, - 0xB0F3: 1292, - 0xB0F6: 1293, - 0xB0F7: 1294, - 0xB141: 1295, - 0xB145: 1296, - 0xB149: 1297, - 0xB185: 1298, - 0xB1A1: 1299, - 0xB1A2: 1300, - 0xB1A5: 1301, - 0xB1A8: 1302, - 0xB1A9: 1303, - 0xB1AB: 1304, - 0xB1B1: 1305, - 0xB1B3: 1306, - 0xB1B7: 1307, - 0xB1C1: 1308, - 0xB1C2: 1309, - 0xB1C5: 1310, - 0xB1D6: 1311, - 0xB1E1: 1312, - 0xB1F6: 1313, - 0xB241: 1314, - 0xB245: 1315, - 0xB249: 1316, - 0xB251: 1317, - 0xB253: 1318, - 0xB261: 1319, - 0xB281: 1320, - 0xB282: 1321, - 0xB285: 1322, - 0xB289: 1323, - 0xB291: 1324, - 0xB293: 1325, - 0xB297: 1326, - 0xB2A1: 1327, - 0xB2B6: 1328, - 0xB2C1: 1329, - 0xB2E1: 1330, - 0xB2E5: 1331, - 0xB357: 1332, - 0xB361: 1333, - 0xB362: 1334, - 0xB365: 1335, - 0xB369: 1336, - 0xB36B: 1337, - 0xB370: 1338, - 0xB371: 1339, - 0xB373: 1340, - 0xB381: 1341, - 0xB385: 1342, - 0xB389: 1343, - 0xB391: 1344, - 0xB3A1: 1345, - 0xB3A2: 1346, - 0xB3A5: 1347, - 0xB3A9: 1348, - 0xB3B1: 1349, - 0xB3B3: 1350, - 0xB3B5: 1351, - 0xB3B7: 1352, - 0xB461: 1353, - 0xB462: 1354, - 0xB465: 1355, - 0xB466: 1356, - 0xB467: 1357, - 0xB469: 1358, - 0xB46A: 1359, - 0xB46B: 1360, - 0xB470: 1361, - 0xB471: 1362, - 0xB473: 1363, - 0xB475: 1364, - 0xB476: 1365, - 0xB477: 1366, - 0xB47B: 1367, - 0xB47C: 1368, - 0xB481: 1369, - 0xB482: 1370, - 0xB485: 1371, - 0xB489: 1372, - 0xB491: 1373, - 0xB493: 1374, - 0xB495: 1375, - 0xB496: 1376, - 0xB497: 1377, - 0xB4A1: 1378, - 0xB4A2: 1379, - 0xB4A5: 1380, - 0xB4A9: 1381, - 0xB4AC: 1382, - 0xB4B1: 1383, - 0xB4B3: 1384, - 0xB4B5: 1385, - 0xB4B7: 1386, - 0xB4BB: 1387, - 0xB4BD: 1388, - 0xB4C1: 1389, - 0xB4C5: 1390, - 0xB4C9: 1391, - 0xB4D3: 1392, - 0xB4E1: 1393, - 0xB4E2: 1394, - 0xB4E5: 1395, - 0xB4E6: 1396, - 0xB4E8: 1397, - 0xB4E9: 1398, - 0xB4EA: 1399, - 0xB4EB: 1400, - 0xB4F1: 1401, - 0xB4F3: 1402, - 0xB4F4: 1403, - 0xB4F5: 1404, - 0xB4F6: 1405, - 0xB4F7: 1406, - 0xB4F8: 1407, - 0xB4FA: 1408, - 0xB4FC: 1409, - 0xB541: 1410, - 0xB542: 1411, - 0xB545: 1412, - 0xB549: 1413, - 0xB551: 1414, - 0xB553: 1415, - 0xB555: 1416, - 0xB557: 1417, - 0xB561: 1418, - 0xB562: 1419, - 0xB563: 1420, - 0xB565: 1421, - 0xB569: 1422, - 0xB56B: 1423, - 0xB56C: 1424, - 0xB571: 1425, - 0xB573: 1426, - 0xB574: 1427, - 0xB575: 1428, - 0xB576: 1429, - 0xB577: 1430, - 0xB57B: 1431, - 0xB57C: 1432, - 0xB57D: 1433, - 0xB581: 1434, - 0xB585: 1435, - 0xB589: 1436, - 0xB591: 1437, - 0xB593: 1438, - 0xB595: 1439, - 0xB596: 1440, - 0xB5A1: 1441, - 0xB5A2: 1442, - 0xB5A5: 1443, - 0xB5A9: 1444, - 0xB5AA: 1445, - 0xB5AB: 1446, - 0xB5AD: 1447, - 0xB5B0: 1448, - 0xB5B1: 1449, - 0xB5B3: 1450, - 0xB5B5: 1451, - 0xB5B7: 1452, - 0xB5B9: 1453, - 0xB5C1: 1454, - 0xB5C2: 1455, - 0xB5C5: 1456, - 0xB5C9: 1457, - 0xB5D1: 1458, - 0xB5D3: 1459, - 0xB5D5: 1460, - 0xB5D6: 1461, - 0xB5D7: 1462, - 0xB5E1: 1463, - 0xB5E2: 1464, - 0xB5E5: 1465, - 0xB5F1: 1466, - 0xB5F5: 1467, - 0xB5F7: 1468, - 0xB641: 1469, - 0xB642: 1470, - 0xB645: 1471, - 0xB649: 1472, - 0xB651: 1473, - 0xB653: 1474, - 0xB655: 1475, - 0xB657: 1476, - 0xB661: 1477, - 0xB662: 1478, - 0xB665: 1479, - 0xB669: 1480, - 0xB671: 1481, - 0xB673: 1482, - 0xB675: 1483, - 0xB677: 1484, - 0xB681: 1485, - 0xB682: 1486, - 0xB685: 1487, - 0xB689: 1488, - 0xB68A: 1489, - 0xB68B: 1490, - 0xB691: 1491, - 0xB693: 1492, - 0xB695: 1493, - 0xB697: 1494, - 0xB6A1: 1495, - 0xB6A2: 1496, - 0xB6A5: 1497, - 0xB6A9: 1498, - 0xB6B1: 1499, - 0xB6B3: 1500, - 0xB6B6: 1501, - 0xB6B7: 1502, - 0xB6C1: 1503, - 0xB6C2: 1504, - 0xB6C5: 1505, - 0xB6C9: 1506, - 0xB6D1: 1507, - 0xB6D3: 1508, - 0xB6D7: 1509, - 0xB6E1: 1510, - 0xB6E2: 1511, - 0xB6E5: 1512, - 0xB6E9: 1513, - 0xB6F1: 1514, - 0xB6F3: 1515, - 0xB6F5: 1516, - 0xB6F7: 1517, - 0xB741: 1518, - 0xB742: 1519, - 0xB745: 1520, - 0xB749: 1521, - 0xB751: 1522, - 0xB753: 1523, - 0xB755: 1524, - 0xB757: 1525, - 0xB759: 1526, - 0xB761: 1527, - 0xB762: 1528, - 0xB765: 1529, - 0xB769: 1530, - 0xB76F: 1531, - 0xB771: 1532, - 0xB773: 1533, - 0xB775: 1534, - 0xB777: 1535, - 0xB778: 1536, - 0xB779: 1537, - 0xB77A: 1538, - 0xB77B: 1539, - 0xB77C: 1540, - 0xB77D: 1541, - 0xB781: 1542, - 0xB785: 1543, - 0xB789: 1544, - 0xB791: 1545, - 0xB795: 1546, - 0xB7A1: 1547, - 0xB7A2: 1548, - 0xB7A5: 1549, - 0xB7A9: 1550, - 0xB7AA: 1551, - 0xB7AB: 1552, - 0xB7B0: 1553, - 0xB7B1: 1554, - 0xB7B3: 1555, - 0xB7B5: 1556, - 0xB7B6: 1557, - 0xB7B7: 1558, - 0xB7B8: 1559, - 0xB7BC: 1560, - 0xB861: 1561, - 0xB862: 1562, - 0xB865: 1563, - 0xB867: 1564, - 0xB868: 1565, - 0xB869: 1566, - 0xB86B: 1567, - 0xB871: 1568, - 0xB873: 1569, - 0xB875: 1570, - 0xB876: 1571, - 0xB877: 1572, - 0xB878: 1573, - 0xB881: 1574, - 0xB882: 1575, - 0xB885: 1576, - 0xB889: 1577, - 0xB891: 1578, - 0xB893: 1579, - 0xB895: 1580, - 0xB896: 1581, - 0xB897: 1582, - 0xB8A1: 1583, - 0xB8A2: 1584, - 0xB8A5: 1585, - 0xB8A7: 1586, - 0xB8A9: 1587, - 0xB8B1: 1588, - 0xB8B7: 1589, - 0xB8C1: 1590, - 0xB8C5: 1591, - 0xB8C9: 1592, - 0xB8E1: 1593, - 0xB8E2: 1594, - 0xB8E5: 1595, - 0xB8E9: 1596, - 0xB8EB: 1597, - 0xB8F1: 1598, - 0xB8F3: 1599, - 0xB8F5: 1600, - 0xB8F7: 1601, - 0xB8F8: 1602, - 0xB941: 1603, - 0xB942: 1604, - 0xB945: 1605, - 0xB949: 1606, - 0xB951: 1607, - 0xB953: 1608, - 0xB955: 1609, - 0xB957: 1610, - 0xB961: 1611, - 0xB965: 1612, - 0xB969: 1613, - 0xB971: 1614, - 0xB973: 1615, - 0xB976: 1616, - 0xB977: 1617, - 0xB981: 1618, - 0xB9A1: 1619, - 0xB9A2: 1620, - 0xB9A5: 1621, - 0xB9A9: 1622, - 0xB9AB: 1623, - 0xB9B1: 1624, - 0xB9B3: 1625, - 0xB9B5: 1626, - 0xB9B7: 1627, - 0xB9B8: 1628, - 0xB9B9: 1629, - 0xB9BD: 1630, - 0xB9C1: 1631, - 0xB9C2: 1632, - 0xB9C9: 1633, - 0xB9D3: 1634, - 0xB9D5: 1635, - 0xB9D7: 1636, - 0xB9E1: 1637, - 0xB9F6: 1638, - 0xB9F7: 1639, - 0xBA41: 1640, - 0xBA45: 1641, - 0xBA49: 1642, - 0xBA51: 1643, - 0xBA53: 1644, - 0xBA55: 1645, - 0xBA57: 1646, - 0xBA61: 1647, - 0xBA62: 1648, - 0xBA65: 1649, - 0xBA77: 1650, - 0xBA81: 1651, - 0xBA82: 1652, - 0xBA85: 1653, - 0xBA89: 1654, - 0xBA8A: 1655, - 0xBA8B: 1656, - 0xBA91: 1657, - 0xBA93: 1658, - 0xBA95: 1659, - 0xBA97: 1660, - 0xBAA1: 1661, - 0xBAB6: 1662, - 0xBAC1: 1663, - 0xBAE1: 1664, - 0xBAE2: 1665, - 0xBAE5: 1666, - 0xBAE9: 1667, - 0xBAF1: 1668, - 0xBAF3: 1669, - 0xBAF5: 1670, - 0xBB41: 1671, - 0xBB45: 1672, - 0xBB49: 1673, - 0xBB51: 1674, - 0xBB61: 1675, - 0xBB62: 1676, - 0xBB65: 1677, - 0xBB69: 1678, - 0xBB71: 1679, - 0xBB73: 1680, - 0xBB75: 1681, - 0xBB77: 1682, - 0xBBA1: 1683, - 0xBBA2: 1684, - 0xBBA5: 1685, - 0xBBA8: 1686, - 0xBBA9: 1687, - 0xBBAB: 1688, - 0xBBB1: 1689, - 0xBBB3: 1690, - 0xBBB5: 1691, - 0xBBB7: 1692, - 0xBBB8: 1693, - 0xBBBB: 1694, - 0xBBBC: 1695, - 0xBC61: 1696, - 0xBC62: 1697, - 0xBC65: 1698, - 0xBC67: 1699, - 0xBC69: 1700, - 0xBC6C: 1701, - 0xBC71: 1702, - 0xBC73: 1703, - 0xBC75: 1704, - 0xBC76: 1705, - 0xBC77: 1706, - 0xBC81: 1707, - 0xBC82: 1708, - 0xBC85: 1709, - 0xBC89: 1710, - 0xBC91: 1711, - 0xBC93: 1712, - 0xBC95: 1713, - 0xBC96: 1714, - 0xBC97: 1715, - 0xBCA1: 1716, - 0xBCA5: 1717, - 0xBCB7: 1718, - 0xBCE1: 1719, - 0xBCE2: 1720, - 0xBCE5: 1721, - 0xBCE9: 1722, - 0xBCF1: 1723, - 0xBCF3: 1724, - 0xBCF5: 1725, - 0xBCF6: 1726, - 0xBCF7: 1727, - 0xBD41: 1728, - 0xBD57: 1729, - 0xBD61: 1730, - 0xBD76: 1731, - 0xBDA1: 1732, - 0xBDA2: 1733, - 0xBDA5: 1734, - 0xBDA9: 1735, - 0xBDB1: 1736, - 0xBDB3: 1737, - 0xBDB5: 1738, - 0xBDB7: 1739, - 0xBDB9: 1740, - 0xBDC1: 1741, - 0xBDC2: 1742, - 0xBDC9: 1743, - 0xBDD6: 1744, - 0xBDE1: 1745, - 0xBDF6: 1746, - 0xBE41: 1747, - 0xBE45: 1748, - 0xBE49: 1749, - 0xBE51: 1750, - 0xBE53: 1751, - 0xBE77: 1752, - 0xBE81: 1753, - 0xBE82: 1754, - 0xBE85: 1755, - 0xBE89: 1756, - 0xBE91: 1757, - 0xBE93: 1758, - 0xBE97: 1759, - 0xBEA1: 1760, - 0xBEB6: 1761, - 0xBEB7: 1762, - 0xBEE1: 1763, - 0xBF41: 1764, - 0xBF61: 1765, - 0xBF71: 1766, - 0xBF75: 1767, - 0xBF77: 1768, - 0xBFA1: 1769, - 0xBFA2: 1770, - 0xBFA5: 1771, - 0xBFA9: 1772, - 0xBFB1: 1773, - 0xBFB3: 1774, - 0xBFB7: 1775, - 0xBFB8: 1776, - 0xBFBD: 1777, - 0xC061: 1778, - 0xC062: 1779, - 0xC065: 1780, - 0xC067: 1781, - 0xC069: 1782, - 0xC071: 1783, - 0xC073: 1784, - 0xC075: 1785, - 0xC076: 1786, - 0xC077: 1787, - 0xC078: 1788, - 0xC081: 1789, - 0xC082: 1790, - 0xC085: 1791, - 0xC089: 1792, - 0xC091: 1793, - 0xC093: 1794, - 0xC095: 1795, - 0xC096: 1796, - 0xC097: 1797, - 0xC0A1: 1798, - 0xC0A5: 1799, - 0xC0A7: 1800, - 0xC0A9: 1801, - 0xC0B1: 1802, - 0xC0B7: 1803, - 0xC0E1: 1804, - 0xC0E2: 1805, - 0xC0E5: 1806, - 0xC0E9: 1807, - 0xC0F1: 1808, - 0xC0F3: 1809, - 0xC0F5: 1810, - 0xC0F6: 1811, - 0xC0F7: 1812, - 0xC141: 1813, - 0xC142: 1814, - 0xC145: 1815, - 0xC149: 1816, - 0xC151: 1817, - 0xC153: 1818, - 0xC155: 1819, - 0xC157: 1820, - 0xC161: 1821, - 0xC165: 1822, - 0xC176: 1823, - 0xC181: 1824, - 0xC185: 1825, - 0xC197: 1826, - 0xC1A1: 1827, - 0xC1A2: 1828, - 0xC1A5: 1829, - 0xC1A9: 1830, - 0xC1B1: 1831, - 0xC1B3: 1832, - 0xC1B5: 1833, - 0xC1B7: 1834, - 0xC1C1: 1835, - 0xC1C5: 1836, - 0xC1C9: 1837, - 0xC1D7: 1838, - 0xC241: 1839, - 0xC245: 1840, - 0xC249: 1841, - 0xC251: 1842, - 0xC253: 1843, - 0xC255: 1844, - 0xC257: 1845, - 0xC261: 1846, - 0xC271: 1847, - 0xC281: 1848, - 0xC282: 1849, - 0xC285: 1850, - 0xC289: 1851, - 0xC291: 1852, - 0xC293: 1853, - 0xC295: 1854, - 0xC297: 1855, - 0xC2A1: 1856, - 0xC2B6: 1857, - 0xC2C1: 1858, - 0xC2C5: 1859, - 0xC2E1: 1860, - 0xC2E5: 1861, - 0xC2E9: 1862, - 0xC2F1: 1863, - 0xC2F3: 1864, - 0xC2F5: 1865, - 0xC2F7: 1866, - 0xC341: 1867, - 0xC345: 1868, - 0xC349: 1869, - 0xC351: 1870, - 0xC357: 1871, - 0xC361: 1872, - 0xC362: 1873, - 0xC365: 1874, - 0xC369: 1875, - 0xC371: 1876, - 0xC373: 1877, - 0xC375: 1878, - 0xC377: 1879, - 0xC3A1: 1880, - 0xC3A2: 1881, - 0xC3A5: 1882, - 0xC3A8: 1883, - 0xC3A9: 1884, - 0xC3AA: 1885, - 0xC3B1: 1886, - 0xC3B3: 1887, - 0xC3B5: 1888, - 0xC3B7: 1889, - 0xC461: 1890, - 0xC462: 1891, - 0xC465: 1892, - 0xC469: 1893, - 0xC471: 1894, - 0xC473: 1895, - 0xC475: 1896, - 0xC477: 1897, - 0xC481: 1898, - 0xC482: 1899, - 0xC485: 1900, - 0xC489: 1901, - 0xC491: 1902, - 0xC493: 1903, - 0xC495: 1904, - 0xC496: 1905, - 0xC497: 1906, - 0xC4A1: 1907, - 0xC4A2: 1908, - 0xC4B7: 1909, - 0xC4E1: 1910, - 0xC4E2: 1911, - 0xC4E5: 1912, - 0xC4E8: 1913, - 0xC4E9: 1914, - 0xC4F1: 1915, - 0xC4F3: 1916, - 0xC4F5: 1917, - 0xC4F6: 1918, - 0xC4F7: 1919, - 0xC541: 1920, - 0xC542: 1921, - 0xC545: 1922, - 0xC549: 1923, - 0xC551: 1924, - 0xC553: 1925, - 0xC555: 1926, - 0xC557: 1927, - 0xC561: 1928, - 0xC565: 1929, - 0xC569: 1930, - 0xC571: 1931, - 0xC573: 1932, - 0xC575: 1933, - 0xC576: 1934, - 0xC577: 1935, - 0xC581: 1936, - 0xC5A1: 1937, - 0xC5A2: 1938, - 0xC5A5: 1939, - 0xC5A9: 1940, - 0xC5B1: 1941, - 0xC5B3: 1942, - 0xC5B5: 1943, - 0xC5B7: 1944, - 0xC5C1: 1945, - 0xC5C2: 1946, - 0xC5C5: 1947, - 0xC5C9: 1948, - 0xC5D1: 1949, - 0xC5D7: 1950, - 0xC5E1: 1951, - 0xC5F7: 1952, - 0xC641: 1953, - 0xC649: 1954, - 0xC661: 1955, - 0xC681: 1956, - 0xC682: 1957, - 0xC685: 1958, - 0xC689: 1959, - 0xC691: 1960, - 0xC693: 1961, - 0xC695: 1962, - 0xC697: 1963, - 0xC6A1: 1964, - 0xC6A5: 1965, - 0xC6A9: 1966, - 0xC6B7: 1967, - 0xC6C1: 1968, - 0xC6D7: 1969, - 0xC6E1: 1970, - 0xC6E2: 1971, - 0xC6E5: 1972, - 0xC6E9: 1973, - 0xC6F1: 1974, - 0xC6F3: 1975, - 0xC6F5: 1976, - 0xC6F7: 1977, - 0xC741: 1978, - 0xC745: 1979, - 0xC749: 1980, - 0xC751: 1981, - 0xC761: 1982, - 0xC762: 1983, - 0xC765: 1984, - 0xC769: 1985, - 0xC771: 1986, - 0xC773: 1987, - 0xC777: 1988, - 0xC7A1: 1989, - 0xC7A2: 1990, - 0xC7A5: 1991, - 0xC7A9: 1992, - 0xC7B1: 1993, - 0xC7B3: 1994, - 0xC7B5: 1995, - 0xC7B7: 1996, - 0xC861: 1997, - 0xC862: 1998, - 0xC865: 1999, - 0xC869: 2000, - 0xC86A: 2001, - 0xC871: 2002, - 0xC873: 2003, - 0xC875: 2004, - 0xC876: 2005, - 0xC877: 2006, - 0xC881: 2007, - 0xC882: 2008, - 0xC885: 2009, - 0xC889: 2010, - 0xC891: 2011, - 0xC893: 2012, - 0xC895: 2013, - 0xC896: 2014, - 0xC897: 2015, - 0xC8A1: 2016, - 0xC8B7: 2017, - 0xC8E1: 2018, - 0xC8E2: 2019, - 0xC8E5: 2020, - 0xC8E9: 2021, - 0xC8EB: 2022, - 0xC8F1: 2023, - 0xC8F3: 2024, - 0xC8F5: 2025, - 0xC8F6: 2026, - 0xC8F7: 2027, - 0xC941: 2028, - 0xC942: 2029, - 0xC945: 2030, - 0xC949: 2031, - 0xC951: 2032, - 0xC953: 2033, - 0xC955: 2034, - 0xC957: 2035, - 0xC961: 2036, - 0xC965: 2037, - 0xC976: 2038, - 0xC981: 2039, - 0xC985: 2040, - 0xC9A1: 2041, - 0xC9A2: 2042, - 0xC9A5: 2043, - 0xC9A9: 2044, - 0xC9B1: 2045, - 0xC9B3: 2046, - 0xC9B5: 2047, - 0xC9B7: 2048, - 0xC9BC: 2049, - 0xC9C1: 2050, - 0xC9C5: 2051, - 0xC9E1: 2052, - 0xCA41: 2053, - 0xCA45: 2054, - 0xCA55: 2055, - 0xCA57: 2056, - 0xCA61: 2057, - 0xCA81: 2058, - 0xCA82: 2059, - 0xCA85: 2060, - 0xCA89: 2061, - 0xCA91: 2062, - 0xCA93: 2063, - 0xCA95: 2064, - 0xCA97: 2065, - 0xCAA1: 2066, - 0xCAB6: 2067, - 0xCAC1: 2068, - 0xCAE1: 2069, - 0xCAE2: 2070, - 0xCAE5: 2071, - 0xCAE9: 2072, - 0xCAF1: 2073, - 0xCAF3: 2074, - 0xCAF7: 2075, - 0xCB41: 2076, - 0xCB45: 2077, - 0xCB49: 2078, - 0xCB51: 2079, - 0xCB57: 2080, - 0xCB61: 2081, - 0xCB62: 2082, - 0xCB65: 2083, - 0xCB68: 2084, - 0xCB69: 2085, - 0xCB6B: 2086, - 0xCB71: 2087, - 0xCB73: 2088, - 0xCB75: 2089, - 0xCB81: 2090, - 0xCB85: 2091, - 0xCB89: 2092, - 0xCB91: 2093, - 0xCB93: 2094, - 0xCBA1: 2095, - 0xCBA2: 2096, - 0xCBA5: 2097, - 0xCBA9: 2098, - 0xCBB1: 2099, - 0xCBB3: 2100, - 0xCBB5: 2101, - 0xCBB7: 2102, - 0xCC61: 2103, - 0xCC62: 2104, - 0xCC63: 2105, - 0xCC65: 2106, - 0xCC69: 2107, - 0xCC6B: 2108, - 0xCC71: 2109, - 0xCC73: 2110, - 0xCC75: 2111, - 0xCC76: 2112, - 0xCC77: 2113, - 0xCC7B: 2114, - 0xCC81: 2115, - 0xCC82: 2116, - 0xCC85: 2117, - 0xCC89: 2118, - 0xCC91: 2119, - 0xCC93: 2120, - 0xCC95: 2121, - 0xCC96: 2122, - 0xCC97: 2123, - 0xCCA1: 2124, - 0xCCA2: 2125, - 0xCCE1: 2126, - 0xCCE2: 2127, - 0xCCE5: 2128, - 0xCCE9: 2129, - 0xCCF1: 2130, - 0xCCF3: 2131, - 0xCCF5: 2132, - 0xCCF6: 2133, - 0xCCF7: 2134, - 0xCD41: 2135, - 0xCD42: 2136, - 0xCD45: 2137, - 0xCD49: 2138, - 0xCD51: 2139, - 0xCD53: 2140, - 0xCD55: 2141, - 0xCD57: 2142, - 0xCD61: 2143, - 0xCD65: 2144, - 0xCD69: 2145, - 0xCD71: 2146, - 0xCD73: 2147, - 0xCD76: 2148, - 0xCD77: 2149, - 0xCD81: 2150, - 0xCD89: 2151, - 0xCD93: 2152, - 0xCD95: 2153, - 0xCDA1: 2154, - 0xCDA2: 2155, - 0xCDA5: 2156, - 0xCDA9: 2157, - 0xCDB1: 2158, - 0xCDB3: 2159, - 0xCDB5: 2160, - 0xCDB7: 2161, - 0xCDC1: 2162, - 0xCDD7: 2163, - 0xCE41: 2164, - 0xCE45: 2165, - 0xCE61: 2166, - 0xCE65: 2167, - 0xCE69: 2168, - 0xCE73: 2169, - 0xCE75: 2170, - 0xCE81: 2171, - 0xCE82: 2172, - 0xCE85: 2173, - 0xCE88: 2174, - 0xCE89: 2175, - 0xCE8B: 2176, - 0xCE91: 2177, - 0xCE93: 2178, - 0xCE95: 2179, - 0xCE97: 2180, - 0xCEA1: 2181, - 0xCEB7: 2182, - 0xCEE1: 2183, - 0xCEE5: 2184, - 0xCEE9: 2185, - 0xCEF1: 2186, - 0xCEF5: 2187, - 0xCF41: 2188, - 0xCF45: 2189, - 0xCF49: 2190, - 0xCF51: 2191, - 0xCF55: 2192, - 0xCF57: 2193, - 0xCF61: 2194, - 0xCF65: 2195, - 0xCF69: 2196, - 0xCF71: 2197, - 0xCF73: 2198, - 0xCF75: 2199, - 0xCFA1: 2200, - 0xCFA2: 2201, - 0xCFA5: 2202, - 0xCFA9: 2203, - 0xCFB1: 2204, - 0xCFB3: 2205, - 0xCFB5: 2206, - 0xCFB7: 2207, - 0xD061: 2208, - 0xD062: 2209, - 0xD065: 2210, - 0xD069: 2211, - 0xD06E: 2212, - 0xD071: 2213, - 0xD073: 2214, - 0xD075: 2215, - 0xD077: 2216, - 0xD081: 2217, - 0xD082: 2218, - 0xD085: 2219, - 0xD089: 2220, - 0xD091: 2221, - 0xD093: 2222, - 0xD095: 2223, - 0xD096: 2224, - 0xD097: 2225, - 0xD0A1: 2226, - 0xD0B7: 2227, - 0xD0E1: 2228, - 0xD0E2: 2229, - 0xD0E5: 2230, - 0xD0E9: 2231, - 0xD0EB: 2232, - 0xD0F1: 2233, - 0xD0F3: 2234, - 0xD0F5: 2235, - 0xD0F7: 2236, - 0xD141: 2237, - 0xD142: 2238, - 0xD145: 2239, - 0xD149: 2240, - 0xD151: 2241, - 0xD153: 2242, - 0xD155: 2243, - 0xD157: 2244, - 0xD161: 2245, - 0xD162: 2246, - 0xD165: 2247, - 0xD169: 2248, - 0xD171: 2249, - 0xD173: 2250, - 0xD175: 2251, - 0xD176: 2252, - 0xD177: 2253, - 0xD181: 2254, - 0xD185: 2255, - 0xD189: 2256, - 0xD193: 2257, - 0xD1A1: 2258, - 0xD1A2: 2259, - 0xD1A5: 2260, - 0xD1A9: 2261, - 0xD1AE: 2262, - 0xD1B1: 2263, - 0xD1B3: 2264, - 0xD1B5: 2265, - 0xD1B7: 2266, - 0xD1BB: 2267, - 0xD1C1: 2268, - 0xD1C2: 2269, - 0xD1C5: 2270, - 0xD1C9: 2271, - 0xD1D5: 2272, - 0xD1D7: 2273, - 0xD1E1: 2274, - 0xD1E2: 2275, - 0xD1E5: 2276, - 0xD1F5: 2277, - 0xD1F7: 2278, - 0xD241: 2279, - 0xD242: 2280, - 0xD245: 2281, - 0xD249: 2282, - 0xD253: 2283, - 0xD255: 2284, - 0xD257: 2285, - 0xD261: 2286, - 0xD265: 2287, - 0xD269: 2288, - 0xD273: 2289, - 0xD275: 2290, - 0xD281: 2291, - 0xD282: 2292, - 0xD285: 2293, - 0xD289: 2294, - 0xD28E: 2295, - 0xD291: 2296, - 0xD295: 2297, - 0xD297: 2298, - 0xD2A1: 2299, - 0xD2A5: 2300, - 0xD2A9: 2301, - 0xD2B1: 2302, - 0xD2B7: 2303, - 0xD2C1: 2304, - 0xD2C2: 2305, - 0xD2C5: 2306, - 0xD2C9: 2307, - 0xD2D7: 2308, - 0xD2E1: 2309, - 0xD2E2: 2310, - 0xD2E5: 2311, - 0xD2E9: 2312, - 0xD2F1: 2313, - 0xD2F3: 2314, - 0xD2F5: 2315, - 0xD2F7: 2316, - 0xD341: 2317, - 0xD342: 2318, - 0xD345: 2319, - 0xD349: 2320, - 0xD351: 2321, - 0xD355: 2322, - 0xD357: 2323, - 0xD361: 2324, - 0xD362: 2325, - 0xD365: 2326, - 0xD367: 2327, - 0xD368: 2328, - 0xD369: 2329, - 0xD36A: 2330, - 0xD371: 2331, - 0xD373: 2332, - 0xD375: 2333, - 0xD377: 2334, - 0xD37B: 2335, - 0xD381: 2336, - 0xD385: 2337, - 0xD389: 2338, - 0xD391: 2339, - 0xD393: 2340, - 0xD397: 2341, - 0xD3A1: 2342, - 0xD3A2: 2343, - 0xD3A5: 2344, - 0xD3A9: 2345, - 0xD3B1: 2346, - 0xD3B3: 2347, - 0xD3B5: 2348, - 0xD3B7: 2349, -} diff --git a/libs/chardet/johabprober.py b/libs/chardet/johabprober.py deleted file mode 100644 index d7364ba61..000000000 --- a/libs/chardet/johabprober.py +++ /dev/null @@ -1,47 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .chardistribution import JOHABDistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import JOHAB_SM_MODEL - - -class JOHABProber(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(JOHAB_SM_MODEL) - self.distribution_analyzer = JOHABDistributionAnalysis() - self.reset() - - @property - def charset_name(self) -> str: - return "Johab" - - @property - def language(self) -> str: - return "Korean" diff --git a/libs/chardet/jpcntx.py b/libs/chardet/jpcntx.py deleted file mode 100644 index 2f53bdda0..000000000 --- a/libs/chardet/jpcntx.py +++ /dev/null @@ -1,238 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import List, Tuple, Union - -# This is hiragana 2-char sequence table, the number in each cell represents its frequency category -# fmt: off -jp2_char_context = ( - (0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1), - (2, 4, 0, 4, 0, 3, 0, 4, 0, 3, 4, 4, 4, 2, 4, 3, 3, 4, 3, 2, 3, 3, 4, 2, 3, 3, 3, 2, 4, 1, 4, 3, 3, 1, 5, 4, 3, 4, 3, 4, 3, 5, 3, 0, 3, 5, 4, 2, 0, 3, 1, 0, 3, 3, 0, 3, 3, 0, 1, 1, 0, 4, 3, 0, 3, 3, 0, 4, 0, 2, 0, 3, 5, 5, 5, 5, 4, 0, 4, 1, 0, 3, 4), - (0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2), - (0, 4, 0, 5, 0, 5, 0, 4, 0, 4, 5, 4, 4, 3, 5, 3, 5, 1, 5, 3, 4, 3, 4, 4, 3, 4, 3, 3, 4, 3, 5, 4, 4, 3, 5, 5, 3, 5, 5, 5, 3, 5, 5, 3, 4, 5, 5, 3, 1, 3, 2, 0, 3, 4, 0, 4, 2, 0, 4, 2, 1, 5, 3, 2, 3, 5, 0, 4, 0, 2, 0, 5, 4, 4, 5, 4, 5, 0, 4, 0, 0, 4, 4), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - (0, 3, 0, 4, 0, 3, 0, 3, 0, 4, 5, 4, 3, 3, 3, 3, 4, 3, 5, 4, 4, 3, 5, 4, 4, 3, 4, 3, 4, 4, 4, 4, 5, 3, 4, 4, 3, 4, 5, 5, 4, 5, 5, 1, 4, 5, 4, 3, 0, 3, 3, 1, 3, 3, 0, 4, 4, 0, 3, 3, 1, 5, 3, 3, 3, 5, 0, 4, 0, 3, 0, 4, 4, 3, 4, 3, 3, 0, 4, 1, 1, 3, 4), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - (0, 4, 0, 3, 0, 3, 0, 4, 0, 3, 4, 4, 3, 2, 2, 1, 2, 1, 3, 1, 3, 3, 3, 3, 3, 4, 3, 1, 3, 3, 5, 3, 3, 0, 4, 3, 0, 5, 4, 3, 3, 5, 4, 4, 3, 4, 4, 5, 0, 1, 2, 0, 1, 2, 0, 2, 2, 0, 1, 0, 0, 5, 2, 2, 1, 4, 0, 3, 0, 1, 0, 4, 4, 3, 5, 4, 3, 0, 2, 1, 0, 4, 3), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - (0, 3, 0, 5, 0, 4, 0, 2, 1, 4, 4, 2, 4, 1, 4, 2, 4, 2, 4, 3, 3, 3, 4, 3, 3, 3, 3, 1, 4, 2, 3, 3, 3, 1, 4, 4, 1, 1, 1, 4, 3, 3, 2, 0, 2, 4, 3, 2, 0, 3, 3, 0, 3, 1, 1, 0, 0, 0, 3, 3, 0, 4, 2, 2, 3, 4, 0, 4, 0, 3, 0, 4, 4, 5, 3, 4, 4, 0, 3, 0, 0, 1, 4), - (1, 4, 0, 4, 0, 4, 0, 4, 0, 3, 5, 4, 4, 3, 4, 3, 5, 4, 3, 3, 4, 3, 5, 4, 4, 4, 4, 3, 4, 2, 4, 3, 3, 1, 5, 4, 3, 2, 4, 5, 4, 5, 5, 4, 4, 5, 4, 4, 0, 3, 2, 2, 3, 3, 0, 4, 3, 1, 3, 2, 1, 4, 3, 3, 4, 5, 0, 3, 0, 2, 0, 4, 5, 5, 4, 5, 4, 0, 4, 0, 0, 5, 4), - (0, 5, 0, 5, 0, 4, 0, 3, 0, 4, 4, 3, 4, 3, 3, 3, 4, 0, 4, 4, 4, 3, 4, 3, 4, 3, 3, 1, 4, 2, 4, 3, 4, 0, 5, 4, 1, 4, 5, 4, 4, 5, 3, 2, 4, 3, 4, 3, 2, 4, 1, 3, 3, 3, 2, 3, 2, 0, 4, 3, 3, 4, 3, 3, 3, 4, 0, 4, 0, 3, 0, 4, 5, 4, 4, 4, 3, 0, 4, 1, 0, 1, 3), - (0, 3, 1, 4, 0, 3, 0, 2, 0, 3, 4, 4, 3, 1, 4, 2, 3, 3, 4, 3, 4, 3, 4, 3, 4, 4, 3, 2, 3, 1, 5, 4, 4, 1, 4, 4, 3, 5, 4, 4, 3, 5, 5, 4, 3, 4, 4, 3, 1, 2, 3, 1, 2, 2, 0, 3, 2, 0, 3, 1, 0, 5, 3, 3, 3, 4, 3, 3, 3, 3, 4, 4, 4, 4, 5, 4, 2, 0, 3, 3, 2, 4, 3), - (0, 2, 0, 3, 0, 1, 0, 1, 0, 0, 3, 2, 0, 0, 2, 0, 1, 0, 2, 1, 3, 3, 3, 1, 2, 3, 1, 0, 1, 0, 4, 2, 1, 1, 3, 3, 0, 4, 3, 3, 1, 4, 3, 3, 0, 3, 3, 2, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 4, 1, 0, 2, 3, 2, 2, 2, 1, 3, 3, 3, 4, 4, 3, 2, 0, 3, 1, 0, 3, 3), - (0, 4, 0, 4, 0, 3, 0, 3, 0, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 3, 4, 2, 4, 3, 4, 3, 3, 2, 4, 3, 4, 5, 4, 1, 4, 5, 3, 5, 4, 5, 3, 5, 4, 0, 3, 5, 5, 3, 1, 3, 3, 2, 2, 3, 0, 3, 4, 1, 3, 3, 2, 4, 3, 3, 3, 4, 0, 4, 0, 3, 0, 4, 5, 4, 4, 5, 3, 0, 4, 1, 0, 3, 4), - (0, 2, 0, 3, 0, 3, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 3, 0, 3, 0, 3, 0, 1, 3, 1, 0, 3, 1, 3, 3, 3, 1, 3, 3, 3, 0, 1, 3, 1, 3, 4, 0, 0, 3, 1, 1, 0, 3, 2, 0, 0, 0, 0, 1, 3, 0, 1, 0, 0, 3, 3, 2, 0, 3, 0, 0, 0, 0, 0, 3, 4, 3, 4, 3, 3, 0, 3, 0, 0, 2, 3), - (2, 3, 0, 3, 0, 2, 0, 1, 0, 3, 3, 4, 3, 1, 3, 1, 1, 1, 3, 1, 4, 3, 4, 3, 3, 3, 0, 0, 3, 1, 5, 4, 3, 1, 4, 3, 2, 5, 5, 4, 4, 4, 4, 3, 3, 4, 4, 4, 0, 2, 1, 1, 3, 2, 0, 1, 2, 0, 0, 1, 0, 4, 1, 3, 3, 3, 0, 3, 0, 1, 0, 4, 4, 4, 5, 5, 3, 0, 2, 0, 0, 4, 4), - (0, 2, 0, 1, 0, 3, 1, 3, 0, 2, 3, 3, 3, 0, 3, 1, 0, 0, 3, 0, 3, 2, 3, 1, 3, 2, 1, 1, 0, 0, 4, 2, 1, 0, 2, 3, 1, 4, 3, 2, 0, 4, 4, 3, 1, 3, 1, 3, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 4, 1, 1, 1, 2, 0, 3, 0, 0, 0, 3, 4, 2, 4, 3, 2, 0, 1, 0, 0, 3, 3), - (0, 1, 0, 4, 0, 5, 0, 4, 0, 2, 4, 4, 2, 3, 3, 2, 3, 3, 5, 3, 3, 3, 4, 3, 4, 2, 3, 0, 4, 3, 3, 3, 4, 1, 4, 3, 2, 1, 5, 5, 3, 4, 5, 1, 3, 5, 4, 2, 0, 3, 3, 0, 1, 3, 0, 4, 2, 0, 1, 3, 1, 4, 3, 3, 3, 3, 0, 3, 0, 1, 0, 3, 4, 4, 4, 5, 5, 0, 3, 0, 1, 4, 5), - (0, 2, 0, 3, 0, 3, 0, 0, 0, 2, 3, 1, 3, 0, 4, 0, 1, 1, 3, 0, 3, 4, 3, 2, 3, 1, 0, 3, 3, 2, 3, 1, 3, 0, 2, 3, 0, 2, 1, 4, 1, 2, 2, 0, 0, 3, 3, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 0, 3, 2, 1, 3, 3, 0, 2, 0, 2, 0, 0, 3, 3, 1, 2, 4, 0, 3, 0, 2, 2, 3), - (2, 4, 0, 5, 0, 4, 0, 4, 0, 2, 4, 4, 4, 3, 4, 3, 3, 3, 1, 2, 4, 3, 4, 3, 4, 4, 5, 0, 3, 3, 3, 3, 2, 0, 4, 3, 1, 4, 3, 4, 1, 4, 4, 3, 3, 4, 4, 3, 1, 2, 3, 0, 4, 2, 0, 4, 1, 0, 3, 3, 0, 4, 3, 3, 3, 4, 0, 4, 0, 2, 0, 3, 5, 3, 4, 5, 2, 0, 3, 0, 0, 4, 5), - (0, 3, 0, 4, 0, 1, 0, 1, 0, 1, 3, 2, 2, 1, 3, 0, 3, 0, 2, 0, 2, 0, 3, 0, 2, 0, 0, 0, 1, 0, 1, 1, 0, 0, 3, 1, 0, 0, 0, 4, 0, 3, 1, 0, 2, 1, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 2, 2, 3, 1, 0, 3, 0, 0, 0, 1, 4, 4, 4, 3, 0, 0, 4, 0, 0, 1, 4), - (1, 4, 1, 5, 0, 3, 0, 3, 0, 4, 5, 4, 4, 3, 5, 3, 3, 4, 4, 3, 4, 1, 3, 3, 3, 3, 2, 1, 4, 1, 5, 4, 3, 1, 4, 4, 3, 5, 4, 4, 3, 5, 4, 3, 3, 4, 4, 4, 0, 3, 3, 1, 2, 3, 0, 3, 1, 0, 3, 3, 0, 5, 4, 4, 4, 4, 4, 4, 3, 3, 5, 4, 4, 3, 3, 5, 4, 0, 3, 2, 0, 4, 4), - (0, 2, 0, 3, 0, 1, 0, 0, 0, 1, 3, 3, 3, 2, 4, 1, 3, 0, 3, 1, 3, 0, 2, 2, 1, 1, 0, 0, 2, 0, 4, 3, 1, 0, 4, 3, 0, 4, 4, 4, 1, 4, 3, 1, 1, 3, 3, 1, 0, 2, 0, 0, 1, 3, 0, 0, 0, 0, 2, 0, 0, 4, 3, 2, 4, 3, 5, 4, 3, 3, 3, 4, 3, 3, 4, 3, 3, 0, 2, 1, 0, 3, 3), - (0, 2, 0, 4, 0, 3, 0, 2, 0, 2, 5, 5, 3, 4, 4, 4, 4, 1, 4, 3, 3, 0, 4, 3, 4, 3, 1, 3, 3, 2, 4, 3, 0, 3, 4, 3, 0, 3, 4, 4, 2, 4, 4, 0, 4, 5, 3, 3, 2, 2, 1, 1, 1, 2, 0, 1, 5, 0, 3, 3, 2, 4, 3, 3, 3, 4, 0, 3, 0, 2, 0, 4, 4, 3, 5, 5, 0, 0, 3, 0, 2, 3, 3), - (0, 3, 0, 4, 0, 3, 0, 1, 0, 3, 4, 3, 3, 1, 3, 3, 3, 0, 3, 1, 3, 0, 4, 3, 3, 1, 1, 0, 3, 0, 3, 3, 0, 0, 4, 4, 0, 1, 5, 4, 3, 3, 5, 0, 3, 3, 4, 3, 0, 2, 0, 1, 1, 1, 0, 1, 3, 0, 1, 2, 1, 3, 3, 2, 3, 3, 0, 3, 0, 1, 0, 1, 3, 3, 4, 4, 1, 0, 1, 2, 2, 1, 3), - (0, 1, 0, 4, 0, 4, 0, 3, 0, 1, 3, 3, 3, 2, 3, 1, 1, 0, 3, 0, 3, 3, 4, 3, 2, 4, 2, 0, 1, 0, 4, 3, 2, 0, 4, 3, 0, 5, 3, 3, 2, 4, 4, 4, 3, 3, 3, 4, 0, 1, 3, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 4, 2, 3, 3, 3, 0, 3, 0, 0, 0, 4, 4, 4, 5, 3, 2, 0, 3, 3, 0, 3, 5), - (0, 2, 0, 3, 0, 0, 0, 3, 0, 1, 3, 0, 2, 0, 0, 0, 1, 0, 3, 1, 1, 3, 3, 0, 0, 3, 0, 0, 3, 0, 2, 3, 1, 0, 3, 1, 0, 3, 3, 2, 0, 4, 2, 2, 0, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 2, 0, 1, 0, 1, 0, 0, 0, 1, 3, 1, 2, 0, 0, 0, 1, 0, 0, 1, 4), - (0, 3, 0, 3, 0, 5, 0, 1, 0, 2, 4, 3, 1, 3, 3, 2, 1, 1, 5, 2, 1, 0, 5, 1, 2, 0, 0, 0, 3, 3, 2, 2, 3, 2, 4, 3, 0, 0, 3, 3, 1, 3, 3, 0, 2, 5, 3, 4, 0, 3, 3, 0, 1, 2, 0, 2, 2, 0, 3, 2, 0, 2, 2, 3, 3, 3, 0, 2, 0, 1, 0, 3, 4, 4, 2, 5, 4, 0, 3, 0, 0, 3, 5), - (0, 3, 0, 3, 0, 3, 0, 1, 0, 3, 3, 3, 3, 0, 3, 0, 2, 0, 2, 1, 1, 0, 2, 0, 1, 0, 0, 0, 2, 1, 0, 0, 1, 0, 3, 2, 0, 0, 3, 3, 1, 2, 3, 1, 0, 3, 3, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 3, 1, 2, 3, 0, 3, 0, 1, 0, 3, 2, 1, 0, 4, 3, 0, 1, 1, 0, 3, 3), - (0, 4, 0, 5, 0, 3, 0, 3, 0, 4, 5, 5, 4, 3, 5, 3, 4, 3, 5, 3, 3, 2, 5, 3, 4, 4, 4, 3, 4, 3, 4, 5, 5, 3, 4, 4, 3, 4, 4, 5, 4, 4, 4, 3, 4, 5, 5, 4, 2, 3, 4, 2, 3, 4, 0, 3, 3, 1, 4, 3, 2, 4, 3, 3, 5, 5, 0, 3, 0, 3, 0, 5, 5, 5, 5, 4, 4, 0, 4, 0, 1, 4, 4), - (0, 4, 0, 4, 0, 3, 0, 3, 0, 3, 5, 4, 4, 2, 3, 2, 5, 1, 3, 2, 5, 1, 4, 2, 3, 2, 3, 3, 4, 3, 3, 3, 3, 2, 5, 4, 1, 3, 3, 5, 3, 4, 4, 0, 4, 4, 3, 1, 1, 3, 1, 0, 2, 3, 0, 2, 3, 0, 3, 0, 0, 4, 3, 1, 3, 4, 0, 3, 0, 2, 0, 4, 4, 4, 3, 4, 5, 0, 4, 0, 0, 3, 4), - (0, 3, 0, 3, 0, 3, 1, 2, 0, 3, 4, 4, 3, 3, 3, 0, 2, 2, 4, 3, 3, 1, 3, 3, 3, 1, 1, 0, 3, 1, 4, 3, 2, 3, 4, 4, 2, 4, 4, 4, 3, 4, 4, 3, 2, 4, 4, 3, 1, 3, 3, 1, 3, 3, 0, 4, 1, 0, 2, 2, 1, 4, 3, 2, 3, 3, 5, 4, 3, 3, 5, 4, 4, 3, 3, 0, 4, 0, 3, 2, 2, 4, 4), - (0, 2, 0, 1, 0, 0, 0, 0, 0, 1, 2, 1, 3, 0, 0, 0, 0, 0, 2, 0, 1, 2, 1, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, 0, 1, 1, 3, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 0, 3, 4, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1), - (0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 4, 1, 4, 0, 3, 0, 4, 0, 3, 0, 4, 0, 3, 0, 3, 0, 4, 1, 5, 1, 4, 0, 0, 3, 0, 5, 0, 5, 2, 0, 1, 0, 0, 0, 2, 1, 4, 0, 1, 3, 0, 0, 3, 0, 0, 3, 1, 1, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0), - (1, 4, 0, 5, 0, 3, 0, 2, 0, 3, 5, 4, 4, 3, 4, 3, 5, 3, 4, 3, 3, 0, 4, 3, 3, 3, 3, 3, 3, 2, 4, 4, 3, 1, 3, 4, 4, 5, 4, 4, 3, 4, 4, 1, 3, 5, 4, 3, 3, 3, 1, 2, 2, 3, 3, 1, 3, 1, 3, 3, 3, 5, 3, 3, 4, 5, 0, 3, 0, 3, 0, 3, 4, 3, 4, 4, 3, 0, 3, 0, 2, 4, 3), - (0, 1, 0, 4, 0, 0, 0, 0, 0, 1, 4, 0, 4, 1, 4, 2, 4, 0, 3, 0, 1, 0, 1, 0, 0, 0, 0, 0, 2, 0, 3, 1, 1, 1, 0, 3, 0, 0, 0, 1, 2, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 3, 0, 0, 0, 0, 3, 2, 0, 2, 2, 0, 1, 0, 0, 0, 2, 3, 2, 3, 3, 0, 0, 0, 0, 2, 1, 0), - (0, 5, 1, 5, 0, 3, 0, 3, 0, 5, 4, 4, 5, 1, 5, 3, 3, 0, 4, 3, 4, 3, 5, 3, 4, 3, 3, 2, 4, 3, 4, 3, 3, 0, 3, 3, 1, 4, 4, 3, 4, 4, 4, 3, 4, 5, 5, 3, 2, 3, 1, 1, 3, 3, 1, 3, 1, 1, 3, 3, 2, 4, 5, 3, 3, 5, 0, 4, 0, 3, 0, 4, 4, 3, 5, 3, 3, 0, 3, 4, 0, 4, 3), - (0, 5, 0, 5, 0, 3, 0, 2, 0, 4, 4, 3, 5, 2, 4, 3, 3, 3, 4, 4, 4, 3, 5, 3, 5, 3, 3, 1, 4, 0, 4, 3, 3, 0, 3, 3, 0, 4, 4, 4, 4, 5, 4, 3, 3, 5, 5, 3, 2, 3, 1, 2, 3, 2, 0, 1, 0, 0, 3, 2, 2, 4, 4, 3, 1, 5, 0, 4, 0, 3, 0, 4, 3, 1, 3, 2, 1, 0, 3, 3, 0, 3, 3), - (0, 4, 0, 5, 0, 5, 0, 4, 0, 4, 5, 5, 5, 3, 4, 3, 3, 2, 5, 4, 4, 3, 5, 3, 5, 3, 4, 0, 4, 3, 4, 4, 3, 2, 4, 4, 3, 4, 5, 4, 4, 5, 5, 0, 3, 5, 5, 4, 1, 3, 3, 2, 3, 3, 1, 3, 1, 0, 4, 3, 1, 4, 4, 3, 4, 5, 0, 4, 0, 2, 0, 4, 3, 4, 4, 3, 3, 0, 4, 0, 0, 5, 5), - (0, 4, 0, 4, 0, 5, 0, 1, 1, 3, 3, 4, 4, 3, 4, 1, 3, 0, 5, 1, 3, 0, 3, 1, 3, 1, 1, 0, 3, 0, 3, 3, 4, 0, 4, 3, 0, 4, 4, 4, 3, 4, 4, 0, 3, 5, 4, 1, 0, 3, 0, 0, 2, 3, 0, 3, 1, 0, 3, 1, 0, 3, 2, 1, 3, 5, 0, 3, 0, 1, 0, 3, 2, 3, 3, 4, 4, 0, 2, 2, 0, 4, 4), - (2, 4, 0, 5, 0, 4, 0, 3, 0, 4, 5, 5, 4, 3, 5, 3, 5, 3, 5, 3, 5, 2, 5, 3, 4, 3, 3, 4, 3, 4, 5, 3, 2, 1, 5, 4, 3, 2, 3, 4, 5, 3, 4, 1, 2, 5, 4, 3, 0, 3, 3, 0, 3, 2, 0, 2, 3, 0, 4, 1, 0, 3, 4, 3, 3, 5, 0, 3, 0, 1, 0, 4, 5, 5, 5, 4, 3, 0, 4, 2, 0, 3, 5), - (0, 5, 0, 4, 0, 4, 0, 2, 0, 5, 4, 3, 4, 3, 4, 3, 3, 3, 4, 3, 4, 2, 5, 3, 5, 3, 4, 1, 4, 3, 4, 4, 4, 0, 3, 5, 0, 4, 4, 4, 4, 5, 3, 1, 3, 4, 5, 3, 3, 3, 3, 3, 3, 3, 0, 2, 2, 0, 3, 3, 2, 4, 3, 3, 3, 5, 3, 4, 1, 3, 3, 5, 3, 2, 0, 0, 0, 0, 4, 3, 1, 3, 3), - (0, 1, 0, 3, 0, 3, 0, 1, 0, 1, 3, 3, 3, 2, 3, 3, 3, 0, 3, 0, 0, 0, 3, 1, 3, 0, 0, 0, 2, 2, 2, 3, 0, 0, 3, 2, 0, 1, 2, 4, 1, 3, 3, 0, 0, 3, 3, 3, 0, 1, 0, 0, 2, 1, 0, 0, 3, 0, 3, 1, 0, 3, 0, 0, 1, 3, 0, 2, 0, 1, 0, 3, 3, 1, 3, 3, 0, 0, 1, 1, 0, 3, 3), - (0, 2, 0, 3, 0, 2, 1, 4, 0, 2, 2, 3, 1, 1, 3, 1, 1, 0, 2, 0, 3, 1, 2, 3, 1, 3, 0, 0, 1, 0, 4, 3, 2, 3, 3, 3, 1, 4, 2, 3, 3, 3, 3, 1, 0, 3, 1, 4, 0, 1, 1, 0, 1, 2, 0, 1, 1, 0, 1, 1, 0, 3, 1, 3, 2, 2, 0, 1, 0, 0, 0, 2, 3, 3, 3, 1, 0, 0, 0, 0, 0, 2, 3), - (0, 5, 0, 4, 0, 5, 0, 2, 0, 4, 5, 5, 3, 3, 4, 3, 3, 1, 5, 4, 4, 2, 4, 4, 4, 3, 4, 2, 4, 3, 5, 5, 4, 3, 3, 4, 3, 3, 5, 5, 4, 5, 5, 1, 3, 4, 5, 3, 1, 4, 3, 1, 3, 3, 0, 3, 3, 1, 4, 3, 1, 4, 5, 3, 3, 5, 0, 4, 0, 3, 0, 5, 3, 3, 1, 4, 3, 0, 4, 0, 1, 5, 3), - (0, 5, 0, 5, 0, 4, 0, 2, 0, 4, 4, 3, 4, 3, 3, 3, 3, 3, 5, 4, 4, 4, 4, 4, 4, 5, 3, 3, 5, 2, 4, 4, 4, 3, 4, 4, 3, 3, 4, 4, 5, 5, 3, 3, 4, 3, 4, 3, 3, 4, 3, 3, 3, 3, 1, 2, 2, 1, 4, 3, 3, 5, 4, 4, 3, 4, 0, 4, 0, 3, 0, 4, 4, 4, 4, 4, 1, 0, 4, 2, 0, 2, 4), - (0, 4, 0, 4, 0, 3, 0, 1, 0, 3, 5, 2, 3, 0, 3, 0, 2, 1, 4, 2, 3, 3, 4, 1, 4, 3, 3, 2, 4, 1, 3, 3, 3, 0, 3, 3, 0, 0, 3, 3, 3, 5, 3, 3, 3, 3, 3, 2, 0, 2, 0, 0, 2, 0, 0, 2, 0, 0, 1, 0, 0, 3, 1, 2, 2, 3, 0, 3, 0, 2, 0, 4, 4, 3, 3, 4, 1, 0, 3, 0, 0, 2, 4), - (0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 3, 1, 3, 0, 3, 2, 0, 0, 0, 1, 0, 3, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 2, 0, 0, 0, 0, 0, 0, 2), - (0, 2, 1, 3, 0, 2, 0, 2, 0, 3, 3, 3, 3, 1, 3, 1, 3, 3, 3, 3, 3, 3, 4, 2, 2, 1, 2, 1, 4, 0, 4, 3, 1, 3, 3, 3, 2, 4, 3, 5, 4, 3, 3, 3, 3, 3, 3, 3, 0, 1, 3, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 4, 2, 0, 2, 3, 0, 3, 3, 0, 3, 3, 4, 2, 3, 1, 4, 0, 1, 2, 0, 2, 3), - (0, 3, 0, 3, 0, 1, 0, 3, 0, 2, 3, 3, 3, 0, 3, 1, 2, 0, 3, 3, 2, 3, 3, 2, 3, 2, 3, 1, 3, 0, 4, 3, 2, 0, 3, 3, 1, 4, 3, 3, 2, 3, 4, 3, 1, 3, 3, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 4, 1, 1, 0, 3, 0, 3, 1, 0, 2, 3, 3, 3, 3, 3, 1, 0, 0, 2, 0, 3, 3), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 3, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 2, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3), - (0, 2, 0, 3, 1, 3, 0, 3, 0, 2, 3, 3, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 1, 3, 0, 2, 3, 1, 1, 4, 3, 3, 2, 3, 3, 1, 2, 2, 4, 1, 3, 3, 0, 1, 4, 2, 3, 0, 1, 3, 0, 3, 0, 0, 1, 3, 0, 2, 0, 0, 3, 3, 2, 1, 3, 0, 3, 0, 2, 0, 3, 4, 4, 4, 3, 1, 0, 3, 0, 0, 3, 3), - (0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 3, 2, 2, 1, 3, 0, 1, 1, 3, 0, 3, 2, 3, 1, 2, 0, 2, 0, 1, 1, 3, 3, 3, 0, 3, 3, 1, 1, 2, 3, 2, 3, 3, 1, 2, 3, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 2, 1, 2, 1, 3, 0, 3, 0, 0, 0, 3, 4, 4, 4, 3, 2, 0, 2, 0, 0, 2, 4), - (0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 3), - (0, 3, 0, 3, 0, 2, 0, 3, 0, 3, 3, 3, 2, 3, 2, 2, 2, 0, 3, 1, 3, 3, 3, 2, 3, 3, 0, 0, 3, 0, 3, 2, 2, 0, 2, 3, 1, 4, 3, 4, 3, 3, 2, 3, 1, 5, 4, 4, 0, 3, 1, 2, 1, 3, 0, 3, 1, 1, 2, 0, 2, 3, 1, 3, 1, 3, 0, 3, 0, 1, 0, 3, 3, 4, 4, 2, 1, 0, 2, 1, 0, 2, 4), - (0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 4, 2, 5, 1, 4, 0, 2, 0, 2, 1, 3, 1, 4, 0, 2, 1, 0, 0, 2, 1, 4, 1, 1, 0, 3, 3, 0, 5, 1, 3, 2, 3, 3, 1, 0, 3, 2, 3, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 1, 0, 3, 0, 2, 0, 1, 0, 3, 3, 3, 4, 3, 3, 0, 0, 0, 0, 2, 3), - (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 3), - (0, 1, 0, 3, 0, 4, 0, 3, 0, 2, 4, 3, 1, 0, 3, 2, 2, 1, 3, 1, 2, 2, 3, 1, 1, 1, 2, 1, 3, 0, 1, 2, 0, 1, 3, 2, 1, 3, 0, 5, 5, 1, 0, 0, 1, 3, 2, 1, 0, 3, 0, 0, 1, 0, 0, 0, 0, 0, 3, 4, 0, 1, 1, 1, 3, 2, 0, 2, 0, 1, 0, 2, 3, 3, 1, 2, 3, 0, 1, 0, 1, 0, 4), - (0, 0, 0, 1, 0, 3, 0, 3, 0, 2, 2, 1, 0, 0, 4, 0, 3, 0, 3, 1, 3, 0, 3, 0, 3, 0, 1, 0, 3, 0, 3, 1, 3, 0, 3, 3, 0, 0, 1, 2, 1, 1, 1, 0, 1, 2, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 2, 0, 0, 2, 0, 0, 0, 0, 2, 3, 3, 3, 3, 0, 0, 0, 0, 1, 4), - (0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 3, 1, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0, 2, 0, 2, 3, 0, 0, 2, 2, 3, 1, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 2, 0, 0, 0, 0, 2, 3), - (2, 4, 0, 5, 0, 5, 0, 4, 0, 3, 4, 3, 3, 3, 4, 3, 3, 3, 4, 3, 4, 4, 5, 4, 5, 5, 5, 2, 3, 0, 5, 5, 4, 1, 5, 4, 3, 1, 5, 4, 3, 4, 4, 3, 3, 4, 3, 3, 0, 3, 2, 0, 2, 3, 0, 3, 0, 0, 3, 3, 0, 5, 3, 2, 3, 3, 0, 3, 0, 3, 0, 3, 4, 5, 4, 5, 3, 0, 4, 3, 0, 3, 4), - (0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 3, 4, 3, 2, 3, 2, 3, 0, 4, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 2, 4, 3, 3, 1, 3, 4, 3, 4, 4, 4, 3, 4, 4, 3, 2, 4, 4, 1, 0, 2, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0, 5, 3, 2, 1, 3, 0, 3, 0, 1, 2, 4, 3, 2, 4, 3, 3, 0, 3, 2, 0, 4, 4), - (0, 3, 0, 3, 0, 1, 0, 0, 0, 1, 4, 3, 3, 2, 3, 1, 3, 1, 4, 2, 3, 2, 4, 2, 3, 4, 3, 0, 2, 2, 3, 3, 3, 0, 3, 3, 3, 0, 3, 4, 1, 3, 3, 0, 3, 4, 3, 3, 0, 1, 1, 0, 1, 0, 0, 0, 4, 0, 3, 0, 0, 3, 1, 2, 1, 3, 0, 4, 0, 1, 0, 4, 3, 3, 4, 3, 3, 0, 2, 0, 0, 3, 3), - (0, 3, 0, 4, 0, 1, 0, 3, 0, 3, 4, 3, 3, 0, 3, 3, 3, 1, 3, 1, 3, 3, 4, 3, 3, 3, 0, 0, 3, 1, 5, 3, 3, 1, 3, 3, 2, 5, 4, 3, 3, 4, 5, 3, 2, 5, 3, 4, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 1, 1, 0, 4, 2, 2, 1, 3, 0, 3, 0, 2, 0, 4, 4, 3, 5, 3, 2, 0, 1, 1, 0, 3, 4), - (0, 5, 0, 4, 0, 5, 0, 2, 0, 4, 4, 3, 3, 2, 3, 3, 3, 1, 4, 3, 4, 1, 5, 3, 4, 3, 4, 0, 4, 2, 4, 3, 4, 1, 5, 4, 0, 4, 4, 4, 4, 5, 4, 1, 3, 5, 4, 2, 1, 4, 1, 1, 3, 2, 0, 3, 1, 0, 3, 2, 1, 4, 3, 3, 3, 4, 0, 4, 0, 3, 0, 4, 4, 4, 3, 3, 3, 0, 4, 2, 0, 3, 4), - (1, 4, 0, 4, 0, 3, 0, 1, 0, 3, 3, 3, 1, 1, 3, 3, 2, 2, 3, 3, 1, 0, 3, 2, 2, 1, 2, 0, 3, 1, 2, 1, 2, 0, 3, 2, 0, 2, 2, 3, 3, 4, 3, 0, 3, 3, 1, 2, 0, 1, 1, 3, 1, 2, 0, 0, 3, 0, 1, 1, 0, 3, 2, 2, 3, 3, 0, 3, 0, 0, 0, 2, 3, 3, 4, 3, 3, 0, 1, 0, 0, 1, 4), - (0, 4, 0, 4, 0, 4, 0, 0, 0, 3, 4, 4, 3, 1, 4, 2, 3, 2, 3, 3, 3, 1, 4, 3, 4, 0, 3, 0, 4, 2, 3, 3, 2, 2, 5, 4, 2, 1, 3, 4, 3, 4, 3, 1, 3, 3, 4, 2, 0, 2, 1, 0, 3, 3, 0, 0, 2, 0, 3, 1, 0, 4, 4, 3, 4, 3, 0, 4, 0, 1, 0, 2, 4, 4, 4, 4, 4, 0, 3, 2, 0, 3, 3), - (0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 2, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2), - (0, 2, 0, 3, 0, 4, 0, 4, 0, 1, 3, 3, 3, 0, 4, 0, 2, 1, 2, 1, 1, 1, 2, 0, 3, 1, 1, 0, 1, 0, 3, 1, 0, 0, 3, 3, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 2, 0, 2, 2, 0, 3, 1, 0, 0, 1, 0, 1, 1, 0, 1, 2, 0, 3, 0, 0, 0, 0, 1, 0, 0, 3, 3, 4, 3, 1, 0, 1, 0, 3, 0, 2), - (0, 0, 0, 3, 0, 5, 0, 0, 0, 0, 1, 0, 2, 0, 3, 1, 0, 1, 3, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 4, 0, 0, 0, 2, 3, 0, 1, 4, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3, 0, 0, 0, 0, 0, 3), - (0, 2, 0, 5, 0, 5, 0, 1, 0, 2, 4, 3, 3, 2, 5, 1, 3, 2, 3, 3, 3, 0, 4, 1, 2, 0, 3, 0, 4, 0, 2, 2, 1, 1, 5, 3, 0, 0, 1, 4, 2, 3, 2, 0, 3, 3, 3, 2, 0, 2, 4, 1, 1, 2, 0, 1, 1, 0, 3, 1, 0, 1, 3, 1, 2, 3, 0, 2, 0, 0, 0, 1, 3, 5, 4, 4, 4, 0, 3, 0, 0, 1, 3), - (0, 4, 0, 5, 0, 4, 0, 4, 0, 4, 5, 4, 3, 3, 4, 3, 3, 3, 4, 3, 4, 4, 5, 3, 4, 5, 4, 2, 4, 2, 3, 4, 3, 1, 4, 4, 1, 3, 5, 4, 4, 5, 5, 4, 4, 5, 5, 5, 2, 3, 3, 1, 4, 3, 1, 3, 3, 0, 3, 3, 1, 4, 3, 4, 4, 4, 0, 3, 0, 4, 0, 3, 3, 4, 4, 5, 0, 0, 4, 3, 0, 4, 5), - (0, 4, 0, 4, 0, 3, 0, 3, 0, 3, 4, 4, 4, 3, 3, 2, 4, 3, 4, 3, 4, 3, 5, 3, 4, 3, 2, 1, 4, 2, 4, 4, 3, 1, 3, 4, 2, 4, 5, 5, 3, 4, 5, 4, 1, 5, 4, 3, 0, 3, 2, 2, 3, 2, 1, 3, 1, 0, 3, 3, 3, 5, 3, 3, 3, 5, 4, 4, 2, 3, 3, 4, 3, 3, 3, 2, 1, 0, 3, 2, 1, 4, 3), - (0, 4, 0, 5, 0, 4, 0, 3, 0, 3, 5, 5, 3, 2, 4, 3, 4, 0, 5, 4, 4, 1, 4, 4, 4, 3, 3, 3, 4, 3, 5, 5, 2, 3, 3, 4, 1, 2, 5, 5, 3, 5, 5, 2, 3, 5, 5, 4, 0, 3, 2, 0, 3, 3, 1, 1, 5, 1, 4, 1, 0, 4, 3, 2, 3, 5, 0, 4, 0, 3, 0, 5, 4, 3, 4, 3, 0, 0, 4, 1, 0, 4, 4), - (1, 3, 0, 4, 0, 2, 0, 2, 0, 2, 5, 5, 3, 3, 3, 3, 3, 0, 4, 2, 3, 4, 4, 4, 3, 4, 0, 0, 3, 4, 5, 4, 3, 3, 3, 3, 2, 5, 5, 4, 5, 5, 5, 4, 3, 5, 5, 5, 1, 3, 1, 0, 1, 0, 0, 3, 2, 0, 4, 2, 0, 5, 2, 3, 2, 4, 1, 3, 0, 3, 0, 4, 5, 4, 5, 4, 3, 0, 4, 2, 0, 5, 4), - (0, 3, 0, 4, 0, 5, 0, 3, 0, 3, 4, 4, 3, 2, 3, 2, 3, 3, 3, 3, 3, 2, 4, 3, 3, 2, 2, 0, 3, 3, 3, 3, 3, 1, 3, 3, 3, 0, 4, 4, 3, 4, 4, 1, 1, 4, 4, 2, 0, 3, 1, 0, 1, 1, 0, 4, 1, 0, 2, 3, 1, 3, 3, 1, 3, 4, 0, 3, 0, 1, 0, 3, 1, 3, 0, 0, 1, 0, 2, 0, 0, 4, 4), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - (0, 3, 0, 3, 0, 2, 0, 3, 0, 1, 5, 4, 3, 3, 3, 1, 4, 2, 1, 2, 3, 4, 4, 2, 4, 4, 5, 0, 3, 1, 4, 3, 4, 0, 4, 3, 3, 3, 2, 3, 2, 5, 3, 4, 3, 2, 2, 3, 0, 0, 3, 0, 2, 1, 0, 1, 2, 0, 0, 0, 0, 2, 1, 1, 3, 1, 0, 2, 0, 4, 0, 3, 4, 4, 4, 5, 2, 0, 2, 0, 0, 1, 3), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 4, 2, 1, 1, 0, 1, 0, 3, 2, 0, 0, 3, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 4, 0, 4, 2, 1, 0, 0, 0, 0, 0, 1), - (0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 2, 0, 2, 1, 0, 0, 1, 2, 1, 0, 1, 1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2), - (0, 4, 0, 4, 0, 4, 0, 3, 0, 4, 4, 3, 4, 2, 4, 3, 2, 0, 4, 4, 4, 3, 5, 3, 5, 3, 3, 2, 4, 2, 4, 3, 4, 3, 1, 4, 0, 2, 3, 4, 4, 4, 3, 3, 3, 4, 4, 4, 3, 4, 1, 3, 4, 3, 2, 1, 2, 1, 3, 3, 3, 4, 4, 3, 3, 5, 0, 4, 0, 3, 0, 4, 3, 3, 3, 2, 1, 0, 3, 0, 0, 3, 3), - (0, 4, 0, 3, 0, 3, 0, 3, 0, 3, 5, 5, 3, 3, 3, 3, 4, 3, 4, 3, 3, 3, 4, 4, 4, 3, 3, 3, 3, 4, 3, 5, 3, 3, 1, 3, 2, 4, 5, 5, 5, 5, 4, 3, 4, 5, 5, 3, 2, 2, 3, 3, 3, 3, 2, 3, 3, 1, 2, 3, 2, 4, 3, 3, 3, 4, 0, 4, 0, 2, 0, 4, 3, 2, 2, 1, 2, 0, 3, 0, 0, 4, 1), -) -# fmt: on - - -class JapaneseContextAnalysis: - NUM_OF_CATEGORY = 6 - DONT_KNOW = -1 - ENOUGH_REL_THRESHOLD = 100 - MAX_REL_THRESHOLD = 1000 - MINIMUM_DATA_THRESHOLD = 4 - - def __init__(self) -> None: - self._total_rel = 0 - self._rel_sample: List[int] = [] - self._need_to_skip_char_num = 0 - self._last_char_order = -1 - self._done = False - self.reset() - - def reset(self) -> None: - self._total_rel = 0 # total sequence received - # category counters, each integer counts sequence in its category - self._rel_sample = [0] * self.NUM_OF_CATEGORY - # if last byte in current buffer is not the last byte of a character, - # we need to know how many bytes to skip in next buffer - self._need_to_skip_char_num = 0 - self._last_char_order = -1 # The order of previous char - # If this flag is set to True, detection is done and conclusion has - # been made - self._done = False - - def feed(self, byte_str: Union[bytes, bytearray], num_bytes: int) -> None: - if self._done: - return - - # The buffer we got is byte oriented, and a character may span in more than one - # buffers. In case the last one or two byte in last buffer is not - # complete, we record how many byte needed to complete that character - # and skip these bytes here. We can choose to record those bytes as - # well and analyse the character once it is complete, but since a - # character will not make much difference, by simply skipping - # this character will simply our logic and improve performance. - i = self._need_to_skip_char_num - while i < num_bytes: - order, char_len = self.get_order(byte_str[i : i + 2]) - i += char_len - if i > num_bytes: - self._need_to_skip_char_num = i - num_bytes - self._last_char_order = -1 - else: - if (order != -1) and (self._last_char_order != -1): - self._total_rel += 1 - if self._total_rel > self.MAX_REL_THRESHOLD: - self._done = True - break - self._rel_sample[ - jp2_char_context[self._last_char_order][order] - ] += 1 - self._last_char_order = order - - def got_enough_data(self) -> bool: - return self._total_rel > self.ENOUGH_REL_THRESHOLD - - def get_confidence(self) -> float: - # This is just one way to calculate confidence. It works well for me. - if self._total_rel > self.MINIMUM_DATA_THRESHOLD: - return (self._total_rel - self._rel_sample[0]) / self._total_rel - return self.DONT_KNOW - - def get_order(self, _: Union[bytes, bytearray]) -> Tuple[int, int]: - return -1, 1 - - -class SJISContextAnalysis(JapaneseContextAnalysis): - def __init__(self) -> None: - super().__init__() - self._charset_name = "SHIFT_JIS" - - @property - def charset_name(self) -> str: - return self._charset_name - - def get_order(self, byte_str: Union[bytes, bytearray]) -> Tuple[int, int]: - if not byte_str: - return -1, 1 - # find out current char's byte length - first_char = byte_str[0] - if (0x81 <= first_char <= 0x9F) or (0xE0 <= first_char <= 0xFC): - char_len = 2 - if (first_char == 0x87) or (0xFA <= first_char <= 0xFC): - self._charset_name = "CP932" - else: - char_len = 1 - - # return its order if it is hiragana - if len(byte_str) > 1: - second_char = byte_str[1] - if (first_char == 202) and (0x9F <= second_char <= 0xF1): - return second_char - 0x9F, char_len - - return -1, char_len - - -class EUCJPContextAnalysis(JapaneseContextAnalysis): - def get_order(self, byte_str: Union[bytes, bytearray]) -> Tuple[int, int]: - if not byte_str: - return -1, 1 - # find out current char's byte length - first_char = byte_str[0] - if (first_char == 0x8E) or (0xA1 <= first_char <= 0xFE): - char_len = 2 - elif first_char == 0x8F: - char_len = 3 - else: - char_len = 1 - - # return its order if it is hiragana - if len(byte_str) > 1: - second_char = byte_str[1] - if (first_char == 0xA4) and (0xA1 <= second_char <= 0xF3): - return second_char - 0xA1, char_len - - return -1, char_len diff --git a/libs/chardet/langbulgarianmodel.py b/libs/chardet/langbulgarianmodel.py deleted file mode 100644 index 2f771bb81..000000000 --- a/libs/chardet/langbulgarianmodel.py +++ /dev/null @@ -1,4649 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -BULGARIAN_LANG_MODEL = { - 63: { # 'e' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 1, # 'б' - 9: 1, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 1, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 0, # 'и' - 26: 1, # 'й' - 12: 1, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 1, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 1, # 'с' - 5: 1, # 'т' - 19: 0, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 45: { # '\xad' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 0, # 'Л' - 38: 1, # 'М' - 36: 0, # 'Н' - 41: 1, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 1, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 1, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 0, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 0, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 0, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 31: { # 'А' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 1, # 'А' - 32: 1, # 'Б' - 35: 2, # 'В' - 43: 1, # 'Г' - 37: 2, # 'Д' - 44: 2, # 'Е' - 55: 1, # 'Ж' - 47: 2, # 'З' - 40: 1, # 'И' - 59: 1, # 'Й' - 33: 1, # 'К' - 46: 2, # 'Л' - 38: 1, # 'М' - 36: 2, # 'Н' - 41: 1, # 'О' - 30: 2, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 1, # 'У' - 48: 2, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 2, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 1, # 'а' - 18: 2, # 'б' - 9: 2, # 'в' - 20: 2, # 'г' - 11: 2, # 'д' - 3: 1, # 'е' - 23: 1, # 'ж' - 15: 2, # 'з' - 2: 0, # 'и' - 26: 2, # 'й' - 12: 2, # 'к' - 10: 3, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 0, # 'о' - 13: 2, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 1, # 'у' - 29: 2, # 'ф' - 25: 1, # 'х' - 22: 1, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 32: { # 'Б' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 2, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 2, # 'Д' - 44: 1, # 'Е' - 55: 1, # 'Ж' - 47: 2, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 2, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 1, # 'У' - 48: 2, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 0, # 'Ш' - 57: 1, # 'Щ' - 61: 2, # 'Ъ' - 60: 1, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 2, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 1, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 35: { # 'В' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 2, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 1, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 2, # 'Ф' - 49: 0, # 'Х' - 53: 1, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 2, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 2, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 2, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 2, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 43: { # 'Г' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 2, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 0, # 'П' - 39: 1, # 'Р' - 28: 1, # 'С' - 34: 0, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 1, # 'Щ' - 61: 1, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 1, # 'б' - 9: 1, # 'в' - 20: 0, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 2, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 1, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 37: { # 'Д' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 2, # 'В' - 43: 1, # 'Г' - 37: 2, # 'Д' - 44: 2, # 'Е' - 55: 2, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 2, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 2, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 2, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 44: { # 'Е' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 1, # 'Б' - 35: 2, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 1, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 1, # 'Й' - 33: 2, # 'К' - 46: 2, # 'Л' - 38: 1, # 'М' - 36: 2, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 1, # 'У' - 48: 2, # 'Ф' - 49: 1, # 'Х' - 53: 2, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 1, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 0, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 2, # 'д' - 3: 0, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 0, # 'и' - 26: 1, # 'й' - 12: 2, # 'к' - 10: 2, # 'л' - 14: 2, # 'м' - 6: 2, # 'н' - 4: 0, # 'о' - 13: 1, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 1, # 'т' - 19: 1, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 55: { # 'Ж' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 1, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 47: { # 'З' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 2, # 'Н' - 41: 1, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 1, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 2, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 1, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 1, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 40: { # 'И' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 1, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 2, # 'Е' - 55: 1, # 'Ж' - 47: 2, # 'З' - 40: 1, # 'И' - 59: 1, # 'Й' - 33: 2, # 'К' - 46: 2, # 'Л' - 38: 2, # 'М' - 36: 2, # 'Н' - 41: 1, # 'О' - 30: 1, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 0, # 'У' - 48: 1, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 1, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 2, # 'Я' - 1: 1, # 'а' - 18: 1, # 'б' - 9: 3, # 'в' - 20: 2, # 'г' - 11: 1, # 'д' - 3: 1, # 'е' - 23: 0, # 'ж' - 15: 3, # 'з' - 2: 0, # 'и' - 26: 1, # 'й' - 12: 1, # 'к' - 10: 2, # 'л' - 14: 2, # 'м' - 6: 2, # 'н' - 4: 0, # 'о' - 13: 1, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 0, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 1, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 59: { # 'Й' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 1, # 'С' - 34: 1, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 1, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 0, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 1, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 0, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 2, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 33: { # 'К' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 0, # 'М' - 36: 2, # 'Н' - 41: 2, # 'О' - 30: 2, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 1, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 1, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 2, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 3, # 'р' - 8: 1, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 46: { # 'Л' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 2, # 'Г' - 37: 1, # 'Д' - 44: 2, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 0, # 'Р' - 28: 1, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 1, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 1, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 2, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 38: { # 'М' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 2, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 1, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 2, # 'л' - 14: 0, # 'м' - 6: 2, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 36: { # 'Н' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 2, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 2, # 'Д' - 44: 2, # 'Е' - 55: 1, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 1, # 'Й' - 33: 2, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 1, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 1, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 2, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 41: { # 'О' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 1, # 'Б' - 35: 2, # 'В' - 43: 1, # 'Г' - 37: 2, # 'Д' - 44: 1, # 'Е' - 55: 1, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 1, # 'Й' - 33: 2, # 'К' - 46: 2, # 'Л' - 38: 2, # 'М' - 36: 2, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 1, # 'Х' - 53: 0, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 1, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 1, # 'а' - 18: 2, # 'б' - 9: 2, # 'в' - 20: 2, # 'г' - 11: 1, # 'д' - 3: 1, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 0, # 'и' - 26: 1, # 'й' - 12: 2, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 0, # 'о' - 13: 2, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 3, # 'т' - 19: 1, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 1, # 'ц' - 21: 2, # 'ч' - 27: 0, # 'ш' - 24: 2, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 30: { # 'П' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 2, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 2, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 3, # 'л' - 14: 0, # 'м' - 6: 1, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 3, # 'р' - 8: 1, # 'с' - 5: 1, # 'т' - 19: 2, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 39: { # 'Р' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 2, # 'Г' - 37: 2, # 'Д' - 44: 2, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 0, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 2, # 'П' - 39: 1, # 'Р' - 28: 1, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 1, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 1, # 'с' - 5: 0, # 'т' - 19: 3, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 28: { # 'С' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 3, # 'А' - 32: 2, # 'Б' - 35: 2, # 'В' - 43: 1, # 'Г' - 37: 2, # 'Д' - 44: 2, # 'Е' - 55: 1, # 'Ж' - 47: 1, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 2, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 2, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 2, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 1, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 2, # 'к' - 10: 3, # 'л' - 14: 2, # 'м' - 6: 1, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 2, # 'р' - 8: 0, # 'с' - 5: 3, # 'т' - 19: 2, # 'у' - 29: 2, # 'ф' - 25: 1, # 'х' - 22: 1, # 'ц' - 21: 1, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 34: { # 'Т' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 2, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 2, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 2, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 2, # 'О' - 30: 1, # 'П' - 39: 2, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 1, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 1, # 'Ъ' - 60: 0, # 'Ю' - 56: 1, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 1, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 1, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 3, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 51: { # 'У' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 1, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 2, # 'Е' - 55: 1, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 0, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 1, # 'С' - 34: 2, # 'Т' - 51: 0, # 'У' - 48: 1, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 1, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 2, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 2, # 'и' - 26: 1, # 'й' - 12: 2, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 2, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 2, # 'с' - 5: 1, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 2, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 48: { # 'Ф' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 2, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 1, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 2, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 2, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 49: { # 'Х' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 1, # 'П' - 39: 1, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 1, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 1, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 0, # 'н' - 4: 2, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 53: { # 'Ц' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 2, # 'И' - 59: 0, # 'Й' - 33: 2, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 1, # 'Р' - 28: 2, # 'С' - 34: 0, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 2, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 1, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 50: { # 'Ч' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 2, # 'А' - 32: 1, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 1, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 2, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 1, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 54: { # 'Ш' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 1, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 1, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 1, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 2, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 2, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 1, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 57: { # 'Щ' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 1, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 1, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 1, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 1, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 61: { # 'Ъ' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 1, # 'Д' - 44: 0, # 'Е' - 55: 1, # 'Ж' - 47: 1, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 2, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 0, # 'О' - 30: 1, # 'П' - 39: 2, # 'Р' - 28: 1, # 'С' - 34: 1, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 1, # 'Х' - 53: 1, # 'Ц' - 50: 1, # 'Ч' - 54: 1, # 'Ш' - 57: 1, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 0, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 0, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 1, # 'л' - 14: 0, # 'м' - 6: 1, # 'н' - 4: 0, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 60: { # 'Ю' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 1, # 'Б' - 35: 0, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 0, # 'Е' - 55: 1, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 0, # 'М' - 36: 1, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 1, # 'Р' - 28: 1, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 1, # 'б' - 9: 1, # 'в' - 20: 2, # 'г' - 11: 1, # 'д' - 3: 0, # 'е' - 23: 2, # 'ж' - 15: 1, # 'з' - 2: 1, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 0, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 1, # 'с' - 5: 1, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 56: { # 'Я' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 1, # 'Б' - 35: 1, # 'В' - 43: 1, # 'Г' - 37: 1, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 1, # 'Л' - 38: 1, # 'М' - 36: 1, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 1, # 'С' - 34: 2, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 1, # 'б' - 9: 1, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 0, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 1, # 'и' - 26: 1, # 'й' - 12: 1, # 'к' - 10: 1, # 'л' - 14: 2, # 'м' - 6: 2, # 'н' - 4: 0, # 'о' - 13: 2, # 'п' - 7: 1, # 'р' - 8: 1, # 'с' - 5: 1, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 1: { # 'а' - 63: 1, # 'e' - 45: 1, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 1, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 1, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 3, # 'з' - 2: 3, # 'и' - 26: 3, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 2, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 3, # 'ф' - 25: 3, # 'х' - 22: 3, # 'ц' - 21: 3, # 'ч' - 27: 3, # 'ш' - 24: 3, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 18: { # 'б' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 3, # 'в' - 20: 1, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 3, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 1, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 0, # 'т' - 19: 3, # 'у' - 29: 0, # 'ф' - 25: 2, # 'х' - 22: 1, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 3, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 9: { # 'в' - 63: 1, # 'e' - 45: 1, # '\xad' - 31: 0, # 'А' - 32: 1, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 0, # 'в' - 20: 2, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 3, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 2, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 3, # 'ч' - 27: 2, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 20: { # 'г' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 3, # 'л' - 14: 1, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 1, # 'п' - 7: 3, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 3, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 11: { # 'д' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 2, # 'б' - 9: 3, # 'в' - 20: 2, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 2, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 1, # 'т' - 19: 3, # 'у' - 29: 1, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 3: { # 'е' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 2, # 'е' - 23: 3, # 'ж' - 15: 3, # 'з' - 2: 2, # 'и' - 26: 3, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 2, # 'у' - 29: 3, # 'ф' - 25: 3, # 'х' - 22: 3, # 'ц' - 21: 3, # 'ч' - 27: 3, # 'ш' - 24: 3, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 23: { # 'ж' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 2, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 3, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 1, # 'с' - 5: 1, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 1, # 'ц' - 21: 1, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 15: { # 'з' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 1, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 2, # 'ш' - 24: 1, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 2: { # 'и' - 63: 1, # 'e' - 45: 1, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 1, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 1, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 1, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 1, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 3, # 'з' - 2: 3, # 'и' - 26: 3, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 2, # 'у' - 29: 3, # 'ф' - 25: 3, # 'х' - 22: 3, # 'ц' - 21: 3, # 'ч' - 27: 3, # 'ш' - 24: 3, # 'щ' - 17: 2, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 26: { # 'й' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 1, # 'а' - 18: 2, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 2, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 2, # 'з' - 2: 1, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 2, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 2, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 1, # 'у' - 29: 2, # 'ф' - 25: 1, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 12: { # 'к' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 1, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 1, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 3, # 'в' - 20: 2, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 2, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 3, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 1, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 3, # 'ц' - 21: 2, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 10: { # 'л' - 63: 1, # 'e' - 45: 1, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 1, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 2, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 1, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 2, # 'п' - 7: 2, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 2, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 2, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 2, # 'ь' - 42: 3, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 14: { # 'м' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 1, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 2, # 'к' - 10: 3, # 'л' - 14: 1, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 1, # 'т' - 19: 3, # 'у' - 29: 2, # 'ф' - 25: 1, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 2, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 6: { # 'н' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 1, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 2, # 'б' - 9: 2, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 2, # 'ж' - 15: 2, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 1, # 'п' - 7: 2, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 3, # 'ф' - 25: 2, # 'х' - 22: 3, # 'ц' - 21: 3, # 'ч' - 27: 2, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 2, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 4: { # 'о' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 2, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 3, # 'з' - 2: 3, # 'и' - 26: 3, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 2, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 2, # 'у' - 29: 3, # 'ф' - 25: 3, # 'х' - 22: 3, # 'ц' - 21: 3, # 'ч' - 27: 3, # 'ш' - 24: 3, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 13: { # 'п' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 1, # 'й' - 12: 2, # 'к' - 10: 3, # 'л' - 14: 1, # 'м' - 6: 2, # 'н' - 4: 3, # 'о' - 13: 1, # 'п' - 7: 3, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 3, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 7: { # 'р' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 3, # 'е' - 23: 3, # 'ж' - 15: 2, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 2, # 'п' - 7: 1, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 2, # 'ф' - 25: 3, # 'х' - 22: 3, # 'ц' - 21: 2, # 'ч' - 27: 3, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 1, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 8: { # 'с' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 2, # 'б' - 9: 3, # 'в' - 20: 2, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 1, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 2, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 2, # 'ш' - 24: 0, # 'щ' - 17: 3, # 'ъ' - 52: 2, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 5: { # 'т' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 2, # 'г' - 11: 2, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 2, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 3, # 'у' - 29: 1, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 2, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 3, # 'ъ' - 52: 2, # 'ь' - 42: 2, # 'ю' - 16: 3, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 19: { # 'у' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 2, # 'е' - 23: 3, # 'ж' - 15: 3, # 'з' - 2: 2, # 'и' - 26: 2, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 2, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 1, # 'у' - 29: 2, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 3, # 'ч' - 27: 3, # 'ш' - 24: 2, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 29: { # 'ф' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 1, # 'в' - 20: 1, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 2, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 2, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 2, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 25: { # 'х' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 3, # 'в' - 20: 0, # 'г' - 11: 1, # 'д' - 3: 2, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 2, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 1, # 'п' - 7: 3, # 'р' - 8: 1, # 'с' - 5: 2, # 'т' - 19: 3, # 'у' - 29: 0, # 'ф' - 25: 1, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 22: { # 'ц' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 2, # 'в' - 20: 1, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 1, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 2, # 'к' - 10: 1, # 'л' - 14: 1, # 'м' - 6: 1, # 'н' - 4: 2, # 'о' - 13: 1, # 'п' - 7: 1, # 'р' - 8: 1, # 'с' - 5: 1, # 'т' - 19: 2, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 1, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 0, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 21: { # 'ч' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 1, # 'б' - 9: 3, # 'в' - 20: 1, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 1, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 2, # 'л' - 14: 2, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 2, # 'р' - 8: 0, # 'с' - 5: 2, # 'т' - 19: 3, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 1, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 27: { # 'ш' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 2, # 'в' - 20: 0, # 'г' - 11: 1, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 3, # 'к' - 10: 2, # 'л' - 14: 1, # 'м' - 6: 3, # 'н' - 4: 2, # 'о' - 13: 2, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 1, # 'т' - 19: 2, # 'у' - 29: 1, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 1, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 2, # 'ъ' - 52: 1, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 24: { # 'щ' - 63: 1, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 3, # 'а' - 18: 0, # 'б' - 9: 1, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 3, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 3, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 2, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 1, # 'р' - 8: 0, # 'с' - 5: 2, # 'т' - 19: 3, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 1, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 2, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 17: { # 'ъ' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 1, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 3, # 'г' - 11: 3, # 'д' - 3: 2, # 'е' - 23: 3, # 'ж' - 15: 3, # 'з' - 2: 1, # 'и' - 26: 2, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 3, # 'о' - 13: 3, # 'п' - 7: 3, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 1, # 'у' - 29: 1, # 'ф' - 25: 2, # 'х' - 22: 2, # 'ц' - 21: 3, # 'ч' - 27: 2, # 'ш' - 24: 3, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 2, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 52: { # 'ь' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 1, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 0, # 'и' - 26: 0, # 'й' - 12: 1, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 1, # 'н' - 4: 3, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 1, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 1, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 1, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 42: { # 'ю' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 1, # 'а' - 18: 2, # 'б' - 9: 1, # 'в' - 20: 2, # 'г' - 11: 2, # 'д' - 3: 1, # 'е' - 23: 2, # 'ж' - 15: 2, # 'з' - 2: 1, # 'и' - 26: 1, # 'й' - 12: 2, # 'к' - 10: 2, # 'л' - 14: 2, # 'м' - 6: 2, # 'н' - 4: 1, # 'о' - 13: 1, # 'п' - 7: 2, # 'р' - 8: 2, # 'с' - 5: 2, # 'т' - 19: 1, # 'у' - 29: 1, # 'ф' - 25: 1, # 'х' - 22: 2, # 'ц' - 21: 3, # 'ч' - 27: 1, # 'ш' - 24: 1, # 'щ' - 17: 1, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 16: { # 'я' - 63: 0, # 'e' - 45: 1, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 3, # 'б' - 9: 3, # 'в' - 20: 2, # 'г' - 11: 3, # 'д' - 3: 2, # 'е' - 23: 1, # 'ж' - 15: 2, # 'з' - 2: 1, # 'и' - 26: 2, # 'й' - 12: 3, # 'к' - 10: 3, # 'л' - 14: 3, # 'м' - 6: 3, # 'н' - 4: 1, # 'о' - 13: 2, # 'п' - 7: 2, # 'р' - 8: 3, # 'с' - 5: 3, # 'т' - 19: 1, # 'у' - 29: 1, # 'ф' - 25: 3, # 'х' - 22: 2, # 'ц' - 21: 1, # 'ч' - 27: 1, # 'ш' - 24: 2, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 1, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 58: { # 'є' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 0, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 0, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 0, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, - 62: { # '№' - 63: 0, # 'e' - 45: 0, # '\xad' - 31: 0, # 'А' - 32: 0, # 'Б' - 35: 0, # 'В' - 43: 0, # 'Г' - 37: 0, # 'Д' - 44: 0, # 'Е' - 55: 0, # 'Ж' - 47: 0, # 'З' - 40: 0, # 'И' - 59: 0, # 'Й' - 33: 0, # 'К' - 46: 0, # 'Л' - 38: 0, # 'М' - 36: 0, # 'Н' - 41: 0, # 'О' - 30: 0, # 'П' - 39: 0, # 'Р' - 28: 0, # 'С' - 34: 0, # 'Т' - 51: 0, # 'У' - 48: 0, # 'Ф' - 49: 0, # 'Х' - 53: 0, # 'Ц' - 50: 0, # 'Ч' - 54: 0, # 'Ш' - 57: 0, # 'Щ' - 61: 0, # 'Ъ' - 60: 0, # 'Ю' - 56: 0, # 'Я' - 1: 0, # 'а' - 18: 0, # 'б' - 9: 0, # 'в' - 20: 0, # 'г' - 11: 0, # 'д' - 3: 0, # 'е' - 23: 0, # 'ж' - 15: 0, # 'з' - 2: 0, # 'и' - 26: 0, # 'й' - 12: 0, # 'к' - 10: 0, # 'л' - 14: 0, # 'м' - 6: 0, # 'н' - 4: 0, # 'о' - 13: 0, # 'п' - 7: 0, # 'р' - 8: 0, # 'с' - 5: 0, # 'т' - 19: 0, # 'у' - 29: 0, # 'ф' - 25: 0, # 'х' - 22: 0, # 'ц' - 21: 0, # 'ч' - 27: 0, # 'ш' - 24: 0, # 'щ' - 17: 0, # 'ъ' - 52: 0, # 'ь' - 42: 0, # 'ю' - 16: 0, # 'я' - 58: 0, # 'є' - 62: 0, # '№' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -ISO_8859_5_BULGARIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 77, # 'A' - 66: 90, # 'B' - 67: 99, # 'C' - 68: 100, # 'D' - 69: 72, # 'E' - 70: 109, # 'F' - 71: 107, # 'G' - 72: 101, # 'H' - 73: 79, # 'I' - 74: 185, # 'J' - 75: 81, # 'K' - 76: 102, # 'L' - 77: 76, # 'M' - 78: 94, # 'N' - 79: 82, # 'O' - 80: 110, # 'P' - 81: 186, # 'Q' - 82: 108, # 'R' - 83: 91, # 'S' - 84: 74, # 'T' - 85: 119, # 'U' - 86: 84, # 'V' - 87: 96, # 'W' - 88: 111, # 'X' - 89: 187, # 'Y' - 90: 115, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 65, # 'a' - 98: 69, # 'b' - 99: 70, # 'c' - 100: 66, # 'd' - 101: 63, # 'e' - 102: 68, # 'f' - 103: 112, # 'g' - 104: 103, # 'h' - 105: 92, # 'i' - 106: 194, # 'j' - 107: 104, # 'k' - 108: 95, # 'l' - 109: 86, # 'm' - 110: 87, # 'n' - 111: 71, # 'o' - 112: 116, # 'p' - 113: 195, # 'q' - 114: 85, # 'r' - 115: 93, # 's' - 116: 97, # 't' - 117: 113, # 'u' - 118: 196, # 'v' - 119: 197, # 'w' - 120: 198, # 'x' - 121: 199, # 'y' - 122: 200, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 194, # '\x80' - 129: 195, # '\x81' - 130: 196, # '\x82' - 131: 197, # '\x83' - 132: 198, # '\x84' - 133: 199, # '\x85' - 134: 200, # '\x86' - 135: 201, # '\x87' - 136: 202, # '\x88' - 137: 203, # '\x89' - 138: 204, # '\x8a' - 139: 205, # '\x8b' - 140: 206, # '\x8c' - 141: 207, # '\x8d' - 142: 208, # '\x8e' - 143: 209, # '\x8f' - 144: 210, # '\x90' - 145: 211, # '\x91' - 146: 212, # '\x92' - 147: 213, # '\x93' - 148: 214, # '\x94' - 149: 215, # '\x95' - 150: 216, # '\x96' - 151: 217, # '\x97' - 152: 218, # '\x98' - 153: 219, # '\x99' - 154: 220, # '\x9a' - 155: 221, # '\x9b' - 156: 222, # '\x9c' - 157: 223, # '\x9d' - 158: 224, # '\x9e' - 159: 225, # '\x9f' - 160: 81, # '\xa0' - 161: 226, # 'Ё' - 162: 227, # 'Ђ' - 163: 228, # 'Ѓ' - 164: 229, # 'Є' - 165: 230, # 'Ѕ' - 166: 105, # 'І' - 167: 231, # 'Ї' - 168: 232, # 'Ј' - 169: 233, # 'Љ' - 170: 234, # 'Њ' - 171: 235, # 'Ћ' - 172: 236, # 'Ќ' - 173: 45, # '\xad' - 174: 237, # 'Ў' - 175: 238, # 'Џ' - 176: 31, # 'А' - 177: 32, # 'Б' - 178: 35, # 'В' - 179: 43, # 'Г' - 180: 37, # 'Д' - 181: 44, # 'Е' - 182: 55, # 'Ж' - 183: 47, # 'З' - 184: 40, # 'И' - 185: 59, # 'Й' - 186: 33, # 'К' - 187: 46, # 'Л' - 188: 38, # 'М' - 189: 36, # 'Н' - 190: 41, # 'О' - 191: 30, # 'П' - 192: 39, # 'Р' - 193: 28, # 'С' - 194: 34, # 'Т' - 195: 51, # 'У' - 196: 48, # 'Ф' - 197: 49, # 'Х' - 198: 53, # 'Ц' - 199: 50, # 'Ч' - 200: 54, # 'Ш' - 201: 57, # 'Щ' - 202: 61, # 'Ъ' - 203: 239, # 'Ы' - 204: 67, # 'Ь' - 205: 240, # 'Э' - 206: 60, # 'Ю' - 207: 56, # 'Я' - 208: 1, # 'а' - 209: 18, # 'б' - 210: 9, # 'в' - 211: 20, # 'г' - 212: 11, # 'д' - 213: 3, # 'е' - 214: 23, # 'ж' - 215: 15, # 'з' - 216: 2, # 'и' - 217: 26, # 'й' - 218: 12, # 'к' - 219: 10, # 'л' - 220: 14, # 'м' - 221: 6, # 'н' - 222: 4, # 'о' - 223: 13, # 'п' - 224: 7, # 'р' - 225: 8, # 'с' - 226: 5, # 'т' - 227: 19, # 'у' - 228: 29, # 'ф' - 229: 25, # 'х' - 230: 22, # 'ц' - 231: 21, # 'ч' - 232: 27, # 'ш' - 233: 24, # 'щ' - 234: 17, # 'ъ' - 235: 75, # 'ы' - 236: 52, # 'ь' - 237: 241, # 'э' - 238: 42, # 'ю' - 239: 16, # 'я' - 240: 62, # '№' - 241: 242, # 'ё' - 242: 243, # 'ђ' - 243: 244, # 'ѓ' - 244: 58, # 'є' - 245: 245, # 'ѕ' - 246: 98, # 'і' - 247: 246, # 'ї' - 248: 247, # 'ј' - 249: 248, # 'љ' - 250: 249, # 'њ' - 251: 250, # 'ћ' - 252: 251, # 'ќ' - 253: 91, # '§' - 254: 252, # 'ў' - 255: 253, # 'џ' -} - -ISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel( - charset_name="ISO-8859-5", - language="Bulgarian", - char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER, - language_model=BULGARIAN_LANG_MODEL, - typical_positive_ratio=0.969392, - keep_ascii_letters=False, - alphabet="АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", -) - -WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 77, # 'A' - 66: 90, # 'B' - 67: 99, # 'C' - 68: 100, # 'D' - 69: 72, # 'E' - 70: 109, # 'F' - 71: 107, # 'G' - 72: 101, # 'H' - 73: 79, # 'I' - 74: 185, # 'J' - 75: 81, # 'K' - 76: 102, # 'L' - 77: 76, # 'M' - 78: 94, # 'N' - 79: 82, # 'O' - 80: 110, # 'P' - 81: 186, # 'Q' - 82: 108, # 'R' - 83: 91, # 'S' - 84: 74, # 'T' - 85: 119, # 'U' - 86: 84, # 'V' - 87: 96, # 'W' - 88: 111, # 'X' - 89: 187, # 'Y' - 90: 115, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 65, # 'a' - 98: 69, # 'b' - 99: 70, # 'c' - 100: 66, # 'd' - 101: 63, # 'e' - 102: 68, # 'f' - 103: 112, # 'g' - 104: 103, # 'h' - 105: 92, # 'i' - 106: 194, # 'j' - 107: 104, # 'k' - 108: 95, # 'l' - 109: 86, # 'm' - 110: 87, # 'n' - 111: 71, # 'o' - 112: 116, # 'p' - 113: 195, # 'q' - 114: 85, # 'r' - 115: 93, # 's' - 116: 97, # 't' - 117: 113, # 'u' - 118: 196, # 'v' - 119: 197, # 'w' - 120: 198, # 'x' - 121: 199, # 'y' - 122: 200, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 206, # 'Ђ' - 129: 207, # 'Ѓ' - 130: 208, # '‚' - 131: 209, # 'ѓ' - 132: 210, # '„' - 133: 211, # '…' - 134: 212, # '†' - 135: 213, # '‡' - 136: 120, # '€' - 137: 214, # '‰' - 138: 215, # 'Љ' - 139: 216, # '‹' - 140: 217, # 'Њ' - 141: 218, # 'Ќ' - 142: 219, # 'Ћ' - 143: 220, # 'Џ' - 144: 221, # 'ђ' - 145: 78, # '‘' - 146: 64, # '’' - 147: 83, # '“' - 148: 121, # '”' - 149: 98, # '•' - 150: 117, # '–' - 151: 105, # '—' - 152: 222, # None - 153: 223, # '™' - 154: 224, # 'љ' - 155: 225, # '›' - 156: 226, # 'њ' - 157: 227, # 'ќ' - 158: 228, # 'ћ' - 159: 229, # 'џ' - 160: 88, # '\xa0' - 161: 230, # 'Ў' - 162: 231, # 'ў' - 163: 232, # 'Ј' - 164: 233, # '¤' - 165: 122, # 'Ґ' - 166: 89, # '¦' - 167: 106, # '§' - 168: 234, # 'Ё' - 169: 235, # '©' - 170: 236, # 'Є' - 171: 237, # '«' - 172: 238, # '¬' - 173: 45, # '\xad' - 174: 239, # '®' - 175: 240, # 'Ї' - 176: 73, # '°' - 177: 80, # '±' - 178: 118, # 'І' - 179: 114, # 'і' - 180: 241, # 'ґ' - 181: 242, # 'µ' - 182: 243, # '¶' - 183: 244, # '·' - 184: 245, # 'ё' - 185: 62, # '№' - 186: 58, # 'є' - 187: 246, # '»' - 188: 247, # 'ј' - 189: 248, # 'Ѕ' - 190: 249, # 'ѕ' - 191: 250, # 'ї' - 192: 31, # 'А' - 193: 32, # 'Б' - 194: 35, # 'В' - 195: 43, # 'Г' - 196: 37, # 'Д' - 197: 44, # 'Е' - 198: 55, # 'Ж' - 199: 47, # 'З' - 200: 40, # 'И' - 201: 59, # 'Й' - 202: 33, # 'К' - 203: 46, # 'Л' - 204: 38, # 'М' - 205: 36, # 'Н' - 206: 41, # 'О' - 207: 30, # 'П' - 208: 39, # 'Р' - 209: 28, # 'С' - 210: 34, # 'Т' - 211: 51, # 'У' - 212: 48, # 'Ф' - 213: 49, # 'Х' - 214: 53, # 'Ц' - 215: 50, # 'Ч' - 216: 54, # 'Ш' - 217: 57, # 'Щ' - 218: 61, # 'Ъ' - 219: 251, # 'Ы' - 220: 67, # 'Ь' - 221: 252, # 'Э' - 222: 60, # 'Ю' - 223: 56, # 'Я' - 224: 1, # 'а' - 225: 18, # 'б' - 226: 9, # 'в' - 227: 20, # 'г' - 228: 11, # 'д' - 229: 3, # 'е' - 230: 23, # 'ж' - 231: 15, # 'з' - 232: 2, # 'и' - 233: 26, # 'й' - 234: 12, # 'к' - 235: 10, # 'л' - 236: 14, # 'м' - 237: 6, # 'н' - 238: 4, # 'о' - 239: 13, # 'п' - 240: 7, # 'р' - 241: 8, # 'с' - 242: 5, # 'т' - 243: 19, # 'у' - 244: 29, # 'ф' - 245: 25, # 'х' - 246: 22, # 'ц' - 247: 21, # 'ч' - 248: 27, # 'ш' - 249: 24, # 'щ' - 250: 17, # 'ъ' - 251: 75, # 'ы' - 252: 52, # 'ь' - 253: 253, # 'э' - 254: 42, # 'ю' - 255: 16, # 'я' -} - -WINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel( - charset_name="windows-1251", - language="Bulgarian", - char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER, - language_model=BULGARIAN_LANG_MODEL, - typical_positive_ratio=0.969392, - keep_ascii_letters=False, - alphabet="АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", -) diff --git a/libs/chardet/langgreekmodel.py b/libs/chardet/langgreekmodel.py deleted file mode 100644 index 0471d8bb1..000000000 --- a/libs/chardet/langgreekmodel.py +++ /dev/null @@ -1,4397 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -GREEK_LANG_MODEL = { - 60: { # 'e' - 60: 2, # 'e' - 55: 1, # 'o' - 58: 2, # 't' - 36: 1, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 1, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 55: { # 'o' - 60: 0, # 'e' - 55: 2, # 'o' - 58: 2, # 't' - 36: 1, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 1, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 1, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 58: { # 't' - 60: 2, # 'e' - 55: 1, # 'o' - 58: 1, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 1, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 36: { # '·' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 61: { # 'Ά' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 1, # 'γ' - 21: 2, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 1, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 46: { # 'Έ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 2, # 'β' - 20: 2, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 2, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 0, # 'ο' - 9: 2, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 1, # 'σ' - 2: 2, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 3, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 54: { # 'Ό' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 2, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 2, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 2, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 31: { # 'Α' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 2, # 'Β' - 43: 2, # 'Γ' - 41: 1, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 2, # 'Θ' - 47: 2, # 'Ι' - 44: 2, # 'Κ' - 53: 2, # 'Λ' - 38: 2, # 'Μ' - 49: 2, # 'Ν' - 59: 1, # 'Ξ' - 39: 0, # 'Ο' - 35: 2, # 'Π' - 48: 2, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 2, # 'Υ' - 56: 2, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 2, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 1, # 'θ' - 5: 0, # 'ι' - 11: 2, # 'κ' - 16: 3, # 'λ' - 10: 2, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 0, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 2, # 'ς' - 7: 2, # 'σ' - 2: 0, # 'τ' - 12: 3, # 'υ' - 28: 2, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 2, # 'ύ' - 27: 0, # 'ώ' - }, - 51: { # 'Β' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 1, # 'Ε' - 40: 1, # 'Η' - 52: 0, # 'Θ' - 47: 1, # 'Ι' - 44: 0, # 'Κ' - 53: 1, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 2, # 'ή' - 15: 0, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 43: { # 'Γ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 1, # 'Α' - 51: 0, # 'Β' - 43: 2, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 1, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 1, # 'Κ' - 53: 1, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 1, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 2, # 'Υ' - 56: 0, # 'Φ' - 50: 1, # 'Χ' - 57: 2, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 41: { # 'Δ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 2, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 2, # 'ή' - 15: 2, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 1, # 'ό' - 26: 2, # 'ύ' - 27: 2, # 'ώ' - }, - 34: { # 'Ε' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 2, # 'Γ' - 41: 2, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 2, # 'Κ' - 53: 2, # 'Λ' - 38: 2, # 'Μ' - 49: 2, # 'Ν' - 59: 1, # 'Ξ' - 39: 0, # 'Ο' - 35: 2, # 'Π' - 48: 2, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 2, # 'Υ' - 56: 0, # 'Φ' - 50: 2, # 'Χ' - 57: 2, # 'Ω' - 17: 3, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 3, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 3, # 'γ' - 21: 2, # 'δ' - 3: 1, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 1, # 'θ' - 5: 2, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 2, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 0, # 'ο' - 9: 3, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 2, # 'σ' - 2: 2, # 'τ' - 12: 2, # 'υ' - 28: 2, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 1, # 'ύ' - 27: 0, # 'ώ' - }, - 40: { # 'Η' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 1, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 2, # 'Θ' - 47: 0, # 'Ι' - 44: 2, # 'Κ' - 53: 0, # 'Λ' - 38: 2, # 'Μ' - 49: 2, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 2, # 'Π' - 48: 2, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 1, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 1, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 1, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 52: { # 'Θ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 1, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 1, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 2, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 2, # 'ύ' - 27: 0, # 'ώ' - }, - 47: { # 'Ι' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 1, # 'Β' - 43: 1, # 'Γ' - 41: 2, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 2, # 'Κ' - 53: 2, # 'Λ' - 38: 2, # 'Μ' - 49: 2, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 0, # 'Υ' - 56: 2, # 'Φ' - 50: 0, # 'Χ' - 57: 2, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 2, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 1, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 2, # 'σ' - 2: 1, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 1, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 44: { # 'Κ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 1, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 1, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 0, # 'Σ' - 33: 1, # 'Τ' - 45: 2, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 1, # 'Ω' - 17: 3, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 2, # 'ό' - 26: 2, # 'ύ' - 27: 2, # 'ώ' - }, - 53: { # 'Λ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 2, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 2, # 'Σ' - 33: 0, # 'Τ' - 45: 2, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 2, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 0, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 1, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 2, # 'ό' - 26: 2, # 'ύ' - 27: 0, # 'ώ' - }, - 38: { # 'Μ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 2, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 2, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 2, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 2, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 3, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 2, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 49: { # 'Ν' - 60: 2, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 2, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 2, # 'Ω' - 17: 0, # 'ά' - 18: 2, # 'έ' - 22: 0, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 1, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 1, # 'ω' - 19: 2, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 59: { # 'Ξ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 1, # 'Ε' - 40: 1, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 1, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 2, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 39: { # 'Ο' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 1, # 'Β' - 43: 2, # 'Γ' - 41: 2, # 'Δ' - 34: 2, # 'Ε' - 40: 1, # 'Η' - 52: 2, # 'Θ' - 47: 2, # 'Ι' - 44: 2, # 'Κ' - 53: 2, # 'Λ' - 38: 2, # 'Μ' - 49: 2, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 2, # 'Π' - 48: 2, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 2, # 'Υ' - 56: 2, # 'Φ' - 50: 2, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 2, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 2, # 'κ' - 16: 2, # 'λ' - 10: 2, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 2, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 2, # 'τ' - 12: 2, # 'υ' - 28: 1, # 'φ' - 23: 1, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 2, # 'ύ' - 27: 0, # 'ώ' - }, - 35: { # 'Π' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 2, # 'Λ' - 38: 1, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 0, # 'Σ' - 33: 1, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 1, # 'Χ' - 57: 2, # 'Ω' - 17: 2, # 'ά' - 18: 1, # 'έ' - 22: 1, # 'ή' - 15: 2, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 2, # 'ό' - 26: 0, # 'ύ' - 27: 3, # 'ώ' - }, - 48: { # 'Ρ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 1, # 'Γ' - 41: 1, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 2, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 0, # 'Σ' - 33: 1, # 'Τ' - 45: 1, # 'Υ' - 56: 0, # 'Φ' - 50: 1, # 'Χ' - 57: 1, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 2, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 1, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 2, # 'ύ' - 27: 0, # 'ώ' - }, - 37: { # 'Σ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 1, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 2, # 'Κ' - 53: 0, # 'Λ' - 38: 2, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 2, # 'Υ' - 56: 0, # 'Φ' - 50: 2, # 'Χ' - 57: 2, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 2, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 2, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 2, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 2, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 2, # 'ύ' - 27: 2, # 'ώ' - }, - 33: { # 'Τ' - 60: 0, # 'e' - 55: 1, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 2, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 2, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 0, # 'Σ' - 33: 1, # 'Τ' - 45: 1, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 2, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 0, # 'ή' - 15: 2, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 2, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 2, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 2, # 'ό' - 26: 2, # 'ύ' - 27: 3, # 'ώ' - }, - 45: { # 'Υ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 2, # 'Γ' - 41: 0, # 'Δ' - 34: 1, # 'Ε' - 40: 2, # 'Η' - 52: 2, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 1, # 'Λ' - 38: 2, # 'Μ' - 49: 2, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 2, # 'Π' - 48: 1, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 1, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 3, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 56: { # 'Φ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 1, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 1, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 2, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 2, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 1, # 'ύ' - 27: 1, # 'ώ' - }, - 50: { # 'Χ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 1, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 2, # 'Ε' - 40: 2, # 'Η' - 52: 0, # 'Θ' - 47: 2, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 1, # 'Ν' - 59: 0, # 'Ξ' - 39: 1, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 1, # 'Χ' - 57: 1, # 'Ω' - 17: 2, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 2, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 2, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 57: { # 'Ω' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 1, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 1, # 'Λ' - 38: 0, # 'Μ' - 49: 2, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 2, # 'Ρ' - 37: 2, # 'Σ' - 33: 2, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 2, # 'ρ' - 14: 2, # 'ς' - 7: 2, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 1, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 17: { # 'ά' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 3, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 3, # 'ε' - 32: 3, # 'ζ' - 13: 0, # 'η' - 25: 3, # 'θ' - 5: 2, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 0, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 3, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 18: { # 'έ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 3, # 'α' - 29: 2, # 'β' - 20: 3, # 'γ' - 21: 2, # 'δ' - 3: 3, # 'ε' - 32: 2, # 'ζ' - 13: 0, # 'η' - 25: 3, # 'θ' - 5: 0, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 3, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 22: { # 'ή' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 1, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 3, # 'θ' - 5: 0, # 'ι' - 11: 3, # 'κ' - 16: 2, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 0, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 15: { # 'ί' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 3, # 'α' - 29: 2, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 3, # 'ε' - 32: 3, # 'ζ' - 13: 3, # 'η' - 25: 3, # 'θ' - 5: 0, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 1, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 3, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 1: { # 'α' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 2, # 'έ' - 22: 0, # 'ή' - 15: 3, # 'ί' - 1: 0, # 'α' - 29: 3, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 2, # 'ε' - 32: 3, # 'ζ' - 13: 1, # 'η' - 25: 3, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 2, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 0, # 'ω' - 19: 2, # 'ό' - 26: 2, # 'ύ' - 27: 0, # 'ώ' - }, - 29: { # 'β' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 2, # 'έ' - 22: 3, # 'ή' - 15: 2, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 2, # 'γ' - 21: 2, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 3, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 2, # 'ό' - 26: 2, # 'ύ' - 27: 2, # 'ώ' - }, - 20: { # 'γ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 3, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 3, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 2, # 'ύ' - 27: 3, # 'ώ' - }, - 21: { # 'δ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 3: { # 'ε' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 3, # 'ί' - 1: 2, # 'α' - 29: 3, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 2, # 'ε' - 32: 2, # 'ζ' - 13: 0, # 'η' - 25: 3, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 2, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 3, # 'ω' - 19: 2, # 'ό' - 26: 3, # 'ύ' - 27: 2, # 'ώ' - }, - 32: { # 'ζ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 2, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 1, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 2, # 'ό' - 26: 0, # 'ύ' - 27: 2, # 'ώ' - }, - 13: { # 'η' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 3, # 'γ' - 21: 2, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 3, # 'θ' - 5: 0, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 0, # 'ο' - 9: 2, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 25: { # 'θ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 2, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 1, # 'λ' - 10: 3, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 5: { # 'ι' - 60: 0, # 'e' - 55: 1, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 1, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 0, # 'ί' - 1: 3, # 'α' - 29: 3, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 3, # 'ε' - 32: 2, # 'ζ' - 13: 3, # 'η' - 25: 3, # 'θ' - 5: 0, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 0, # 'ύ' - 27: 3, # 'ώ' - }, - 11: { # 'κ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 3, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 2, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 2, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 2, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 16: { # 'λ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 1, # 'β' - 20: 2, # 'γ' - 21: 1, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 2, # 'θ' - 5: 3, # 'ι' - 11: 2, # 'κ' - 16: 3, # 'λ' - 10: 2, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 2, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 10: { # 'μ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 1, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 3, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 2, # 'υ' - 28: 3, # 'φ' - 23: 0, # 'χ' - 42: 2, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 2, # 'ύ' - 27: 2, # 'ώ' - }, - 6: { # 'ν' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 3, # 'δ' - 3: 3, # 'ε' - 32: 2, # 'ζ' - 13: 3, # 'η' - 25: 3, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 1, # 'λ' - 10: 0, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 30: { # 'ξ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 2, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 2, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 2, # 'ό' - 26: 3, # 'ύ' - 27: 1, # 'ώ' - }, - 4: { # 'ο' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 2, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 2, # 'α' - 29: 3, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 3, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 2, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 2, # 'ω' - 19: 1, # 'ό' - 26: 3, # 'ύ' - 27: 2, # 'ώ' - }, - 9: { # 'π' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 3, # 'λ' - 10: 0, # 'μ' - 6: 2, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 2, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 2, # 'ύ' - 27: 3, # 'ώ' - }, - 8: { # 'ρ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 2, # 'β' - 20: 3, # 'γ' - 21: 2, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 3, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 1, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 3, # 'ο' - 9: 2, # 'π' - 8: 2, # 'ρ' - 14: 0, # 'ς' - 7: 2, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 14: { # 'ς' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 2, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 0, # 'θ' - 5: 0, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 0, # 'τ' - 12: 0, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 7: { # 'σ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 3, # 'β' - 20: 0, # 'γ' - 21: 2, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 3, # 'θ' - 5: 3, # 'ι' - 11: 3, # 'κ' - 16: 2, # 'λ' - 10: 3, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 3, # 'φ' - 23: 3, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 2, # 'ώ' - }, - 2: { # 'τ' - 60: 0, # 'e' - 55: 2, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 2, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 3, # 'ι' - 11: 2, # 'κ' - 16: 2, # 'λ' - 10: 3, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 2, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 12: { # 'υ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 3, # 'ή' - 15: 2, # 'ί' - 1: 3, # 'α' - 29: 2, # 'β' - 20: 3, # 'γ' - 21: 2, # 'δ' - 3: 2, # 'ε' - 32: 2, # 'ζ' - 13: 2, # 'η' - 25: 3, # 'θ' - 5: 2, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 3, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 2, # 'ω' - 19: 2, # 'ό' - 26: 0, # 'ύ' - 27: 2, # 'ώ' - }, - 28: { # 'φ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 3, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 2, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 0, # 'μ' - 6: 1, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 1, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 2, # 'ύ' - 27: 2, # 'ώ' - }, - 23: { # 'χ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 3, # 'ά' - 18: 2, # 'έ' - 22: 3, # 'ή' - 15: 3, # 'ί' - 1: 3, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 2, # 'θ' - 5: 3, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 2, # 'μ' - 6: 3, # 'ν' - 30: 0, # 'ξ' - 4: 3, # 'ο' - 9: 0, # 'π' - 8: 3, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 3, # 'τ' - 12: 3, # 'υ' - 28: 0, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 3, # 'ω' - 19: 3, # 'ό' - 26: 3, # 'ύ' - 27: 3, # 'ώ' - }, - 42: { # 'ψ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 2, # 'ά' - 18: 2, # 'έ' - 22: 1, # 'ή' - 15: 2, # 'ί' - 1: 2, # 'α' - 29: 0, # 'β' - 20: 0, # 'γ' - 21: 0, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 3, # 'η' - 25: 0, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 0, # 'λ' - 10: 0, # 'μ' - 6: 0, # 'ν' - 30: 0, # 'ξ' - 4: 2, # 'ο' - 9: 0, # 'π' - 8: 0, # 'ρ' - 14: 0, # 'ς' - 7: 0, # 'σ' - 2: 2, # 'τ' - 12: 1, # 'υ' - 28: 0, # 'φ' - 23: 0, # 'χ' - 42: 0, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 24: { # 'ω' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 1, # 'ά' - 18: 0, # 'έ' - 22: 2, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 2, # 'β' - 20: 3, # 'γ' - 21: 2, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 0, # 'η' - 25: 3, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 0, # 'ξ' - 4: 0, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 2, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 19: { # 'ό' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 3, # 'β' - 20: 3, # 'γ' - 21: 3, # 'δ' - 3: 1, # 'ε' - 32: 2, # 'ζ' - 13: 2, # 'η' - 25: 2, # 'θ' - 5: 2, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 1, # 'ξ' - 4: 2, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 3, # 'χ' - 42: 2, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 26: { # 'ύ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 2, # 'α' - 29: 2, # 'β' - 20: 2, # 'γ' - 21: 1, # 'δ' - 3: 3, # 'ε' - 32: 0, # 'ζ' - 13: 2, # 'η' - 25: 3, # 'θ' - 5: 0, # 'ι' - 11: 3, # 'κ' - 16: 3, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 2, # 'ξ' - 4: 3, # 'ο' - 9: 3, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 2, # 'φ' - 23: 2, # 'χ' - 42: 2, # 'ψ' - 24: 2, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, - 27: { # 'ώ' - 60: 0, # 'e' - 55: 0, # 'o' - 58: 0, # 't' - 36: 0, # '·' - 61: 0, # 'Ά' - 46: 0, # 'Έ' - 54: 0, # 'Ό' - 31: 0, # 'Α' - 51: 0, # 'Β' - 43: 0, # 'Γ' - 41: 0, # 'Δ' - 34: 0, # 'Ε' - 40: 0, # 'Η' - 52: 0, # 'Θ' - 47: 0, # 'Ι' - 44: 0, # 'Κ' - 53: 0, # 'Λ' - 38: 0, # 'Μ' - 49: 0, # 'Ν' - 59: 0, # 'Ξ' - 39: 0, # 'Ο' - 35: 0, # 'Π' - 48: 0, # 'Ρ' - 37: 0, # 'Σ' - 33: 0, # 'Τ' - 45: 0, # 'Υ' - 56: 0, # 'Φ' - 50: 0, # 'Χ' - 57: 0, # 'Ω' - 17: 0, # 'ά' - 18: 0, # 'έ' - 22: 0, # 'ή' - 15: 0, # 'ί' - 1: 0, # 'α' - 29: 1, # 'β' - 20: 0, # 'γ' - 21: 3, # 'δ' - 3: 0, # 'ε' - 32: 0, # 'ζ' - 13: 1, # 'η' - 25: 2, # 'θ' - 5: 2, # 'ι' - 11: 0, # 'κ' - 16: 2, # 'λ' - 10: 3, # 'μ' - 6: 3, # 'ν' - 30: 1, # 'ξ' - 4: 0, # 'ο' - 9: 2, # 'π' - 8: 3, # 'ρ' - 14: 3, # 'ς' - 7: 3, # 'σ' - 2: 3, # 'τ' - 12: 0, # 'υ' - 28: 1, # 'φ' - 23: 1, # 'χ' - 42: 0, # 'ψ' - 24: 0, # 'ω' - 19: 0, # 'ό' - 26: 0, # 'ύ' - 27: 0, # 'ώ' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -WINDOWS_1253_GREEK_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 82, # 'A' - 66: 100, # 'B' - 67: 104, # 'C' - 68: 94, # 'D' - 69: 98, # 'E' - 70: 101, # 'F' - 71: 116, # 'G' - 72: 102, # 'H' - 73: 111, # 'I' - 74: 187, # 'J' - 75: 117, # 'K' - 76: 92, # 'L' - 77: 88, # 'M' - 78: 113, # 'N' - 79: 85, # 'O' - 80: 79, # 'P' - 81: 118, # 'Q' - 82: 105, # 'R' - 83: 83, # 'S' - 84: 67, # 'T' - 85: 114, # 'U' - 86: 119, # 'V' - 87: 95, # 'W' - 88: 99, # 'X' - 89: 109, # 'Y' - 90: 188, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 72, # 'a' - 98: 70, # 'b' - 99: 80, # 'c' - 100: 81, # 'd' - 101: 60, # 'e' - 102: 96, # 'f' - 103: 93, # 'g' - 104: 89, # 'h' - 105: 68, # 'i' - 106: 120, # 'j' - 107: 97, # 'k' - 108: 77, # 'l' - 109: 86, # 'm' - 110: 69, # 'n' - 111: 55, # 'o' - 112: 78, # 'p' - 113: 115, # 'q' - 114: 65, # 'r' - 115: 66, # 's' - 116: 58, # 't' - 117: 76, # 'u' - 118: 106, # 'v' - 119: 103, # 'w' - 120: 87, # 'x' - 121: 107, # 'y' - 122: 112, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 255, # '€' - 129: 255, # None - 130: 255, # '‚' - 131: 255, # 'ƒ' - 132: 255, # '„' - 133: 255, # '…' - 134: 255, # '†' - 135: 255, # '‡' - 136: 255, # None - 137: 255, # '‰' - 138: 255, # None - 139: 255, # '‹' - 140: 255, # None - 141: 255, # None - 142: 255, # None - 143: 255, # None - 144: 255, # None - 145: 255, # '‘' - 146: 255, # '’' - 147: 255, # '“' - 148: 255, # '”' - 149: 255, # '•' - 150: 255, # '–' - 151: 255, # '—' - 152: 255, # None - 153: 255, # '™' - 154: 255, # None - 155: 255, # '›' - 156: 255, # None - 157: 255, # None - 158: 255, # None - 159: 255, # None - 160: 253, # '\xa0' - 161: 233, # '΅' - 162: 61, # 'Ά' - 163: 253, # '£' - 164: 253, # '¤' - 165: 253, # '¥' - 166: 253, # '¦' - 167: 253, # '§' - 168: 253, # '¨' - 169: 253, # '©' - 170: 253, # None - 171: 253, # '«' - 172: 253, # '¬' - 173: 74, # '\xad' - 174: 253, # '®' - 175: 253, # '―' - 176: 253, # '°' - 177: 253, # '±' - 178: 253, # '²' - 179: 253, # '³' - 180: 247, # '΄' - 181: 253, # 'µ' - 182: 253, # '¶' - 183: 36, # '·' - 184: 46, # 'Έ' - 185: 71, # 'Ή' - 186: 73, # 'Ί' - 187: 253, # '»' - 188: 54, # 'Ό' - 189: 253, # '½' - 190: 108, # 'Ύ' - 191: 123, # 'Ώ' - 192: 110, # 'ΐ' - 193: 31, # 'Α' - 194: 51, # 'Β' - 195: 43, # 'Γ' - 196: 41, # 'Δ' - 197: 34, # 'Ε' - 198: 91, # 'Ζ' - 199: 40, # 'Η' - 200: 52, # 'Θ' - 201: 47, # 'Ι' - 202: 44, # 'Κ' - 203: 53, # 'Λ' - 204: 38, # 'Μ' - 205: 49, # 'Ν' - 206: 59, # 'Ξ' - 207: 39, # 'Ο' - 208: 35, # 'Π' - 209: 48, # 'Ρ' - 210: 250, # None - 211: 37, # 'Σ' - 212: 33, # 'Τ' - 213: 45, # 'Υ' - 214: 56, # 'Φ' - 215: 50, # 'Χ' - 216: 84, # 'Ψ' - 217: 57, # 'Ω' - 218: 120, # 'Ϊ' - 219: 121, # 'Ϋ' - 220: 17, # 'ά' - 221: 18, # 'έ' - 222: 22, # 'ή' - 223: 15, # 'ί' - 224: 124, # 'ΰ' - 225: 1, # 'α' - 226: 29, # 'β' - 227: 20, # 'γ' - 228: 21, # 'δ' - 229: 3, # 'ε' - 230: 32, # 'ζ' - 231: 13, # 'η' - 232: 25, # 'θ' - 233: 5, # 'ι' - 234: 11, # 'κ' - 235: 16, # 'λ' - 236: 10, # 'μ' - 237: 6, # 'ν' - 238: 30, # 'ξ' - 239: 4, # 'ο' - 240: 9, # 'π' - 241: 8, # 'ρ' - 242: 14, # 'ς' - 243: 7, # 'σ' - 244: 2, # 'τ' - 245: 12, # 'υ' - 246: 28, # 'φ' - 247: 23, # 'χ' - 248: 42, # 'ψ' - 249: 24, # 'ω' - 250: 64, # 'ϊ' - 251: 75, # 'ϋ' - 252: 19, # 'ό' - 253: 26, # 'ύ' - 254: 27, # 'ώ' - 255: 253, # None -} - -WINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel( - charset_name="windows-1253", - language="Greek", - char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER, - language_model=GREEK_LANG_MODEL, - typical_positive_ratio=0.982851, - keep_ascii_letters=False, - alphabet="ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ", -) - -ISO_8859_7_GREEK_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 82, # 'A' - 66: 100, # 'B' - 67: 104, # 'C' - 68: 94, # 'D' - 69: 98, # 'E' - 70: 101, # 'F' - 71: 116, # 'G' - 72: 102, # 'H' - 73: 111, # 'I' - 74: 187, # 'J' - 75: 117, # 'K' - 76: 92, # 'L' - 77: 88, # 'M' - 78: 113, # 'N' - 79: 85, # 'O' - 80: 79, # 'P' - 81: 118, # 'Q' - 82: 105, # 'R' - 83: 83, # 'S' - 84: 67, # 'T' - 85: 114, # 'U' - 86: 119, # 'V' - 87: 95, # 'W' - 88: 99, # 'X' - 89: 109, # 'Y' - 90: 188, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 72, # 'a' - 98: 70, # 'b' - 99: 80, # 'c' - 100: 81, # 'd' - 101: 60, # 'e' - 102: 96, # 'f' - 103: 93, # 'g' - 104: 89, # 'h' - 105: 68, # 'i' - 106: 120, # 'j' - 107: 97, # 'k' - 108: 77, # 'l' - 109: 86, # 'm' - 110: 69, # 'n' - 111: 55, # 'o' - 112: 78, # 'p' - 113: 115, # 'q' - 114: 65, # 'r' - 115: 66, # 's' - 116: 58, # 't' - 117: 76, # 'u' - 118: 106, # 'v' - 119: 103, # 'w' - 120: 87, # 'x' - 121: 107, # 'y' - 122: 112, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 255, # '\x80' - 129: 255, # '\x81' - 130: 255, # '\x82' - 131: 255, # '\x83' - 132: 255, # '\x84' - 133: 255, # '\x85' - 134: 255, # '\x86' - 135: 255, # '\x87' - 136: 255, # '\x88' - 137: 255, # '\x89' - 138: 255, # '\x8a' - 139: 255, # '\x8b' - 140: 255, # '\x8c' - 141: 255, # '\x8d' - 142: 255, # '\x8e' - 143: 255, # '\x8f' - 144: 255, # '\x90' - 145: 255, # '\x91' - 146: 255, # '\x92' - 147: 255, # '\x93' - 148: 255, # '\x94' - 149: 255, # '\x95' - 150: 255, # '\x96' - 151: 255, # '\x97' - 152: 255, # '\x98' - 153: 255, # '\x99' - 154: 255, # '\x9a' - 155: 255, # '\x9b' - 156: 255, # '\x9c' - 157: 255, # '\x9d' - 158: 255, # '\x9e' - 159: 255, # '\x9f' - 160: 253, # '\xa0' - 161: 233, # '‘' - 162: 90, # '’' - 163: 253, # '£' - 164: 253, # '€' - 165: 253, # '₯' - 166: 253, # '¦' - 167: 253, # '§' - 168: 253, # '¨' - 169: 253, # '©' - 170: 253, # 'ͺ' - 171: 253, # '«' - 172: 253, # '¬' - 173: 74, # '\xad' - 174: 253, # None - 175: 253, # '―' - 176: 253, # '°' - 177: 253, # '±' - 178: 253, # '²' - 179: 253, # '³' - 180: 247, # '΄' - 181: 248, # '΅' - 182: 61, # 'Ά' - 183: 36, # '·' - 184: 46, # 'Έ' - 185: 71, # 'Ή' - 186: 73, # 'Ί' - 187: 253, # '»' - 188: 54, # 'Ό' - 189: 253, # '½' - 190: 108, # 'Ύ' - 191: 123, # 'Ώ' - 192: 110, # 'ΐ' - 193: 31, # 'Α' - 194: 51, # 'Β' - 195: 43, # 'Γ' - 196: 41, # 'Δ' - 197: 34, # 'Ε' - 198: 91, # 'Ζ' - 199: 40, # 'Η' - 200: 52, # 'Θ' - 201: 47, # 'Ι' - 202: 44, # 'Κ' - 203: 53, # 'Λ' - 204: 38, # 'Μ' - 205: 49, # 'Ν' - 206: 59, # 'Ξ' - 207: 39, # 'Ο' - 208: 35, # 'Π' - 209: 48, # 'Ρ' - 210: 250, # None - 211: 37, # 'Σ' - 212: 33, # 'Τ' - 213: 45, # 'Υ' - 214: 56, # 'Φ' - 215: 50, # 'Χ' - 216: 84, # 'Ψ' - 217: 57, # 'Ω' - 218: 120, # 'Ϊ' - 219: 121, # 'Ϋ' - 220: 17, # 'ά' - 221: 18, # 'έ' - 222: 22, # 'ή' - 223: 15, # 'ί' - 224: 124, # 'ΰ' - 225: 1, # 'α' - 226: 29, # 'β' - 227: 20, # 'γ' - 228: 21, # 'δ' - 229: 3, # 'ε' - 230: 32, # 'ζ' - 231: 13, # 'η' - 232: 25, # 'θ' - 233: 5, # 'ι' - 234: 11, # 'κ' - 235: 16, # 'λ' - 236: 10, # 'μ' - 237: 6, # 'ν' - 238: 30, # 'ξ' - 239: 4, # 'ο' - 240: 9, # 'π' - 241: 8, # 'ρ' - 242: 14, # 'ς' - 243: 7, # 'σ' - 244: 2, # 'τ' - 245: 12, # 'υ' - 246: 28, # 'φ' - 247: 23, # 'χ' - 248: 42, # 'ψ' - 249: 24, # 'ω' - 250: 64, # 'ϊ' - 251: 75, # 'ϋ' - 252: 19, # 'ό' - 253: 26, # 'ύ' - 254: 27, # 'ώ' - 255: 253, # None -} - -ISO_8859_7_GREEK_MODEL = SingleByteCharSetModel( - charset_name="ISO-8859-7", - language="Greek", - char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER, - language_model=GREEK_LANG_MODEL, - typical_positive_ratio=0.982851, - keep_ascii_letters=False, - alphabet="ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ", -) diff --git a/libs/chardet/langhebrewmodel.py b/libs/chardet/langhebrewmodel.py deleted file mode 100644 index 86b3c5e64..000000000 --- a/libs/chardet/langhebrewmodel.py +++ /dev/null @@ -1,4380 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -HEBREW_LANG_MODEL = { - 50: { # 'a' - 50: 0, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 2, # 'l' - 54: 2, # 'n' - 49: 0, # 'o' - 51: 2, # 'r' - 43: 1, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 1, # 'ק' - 7: 0, # 'ר' - 10: 1, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 60: { # 'c' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 0, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 0, # 'n' - 49: 1, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 61: { # 'd' - 50: 1, # 'a' - 60: 0, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 1, # 'n' - 49: 2, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 0, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 1, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 42: { # 'e' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 2, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 2, # 'l' - 54: 2, # 'n' - 49: 1, # 'o' - 51: 2, # 'r' - 43: 2, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 1, # '–' - 52: 2, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 53: { # 'i' - 50: 1, # 'a' - 60: 2, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 0, # 'i' - 56: 1, # 'l' - 54: 2, # 'n' - 49: 2, # 'o' - 51: 1, # 'r' - 43: 2, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 56: { # 'l' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 2, # 'e' - 53: 2, # 'i' - 56: 2, # 'l' - 54: 1, # 'n' - 49: 1, # 'o' - 51: 0, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 54: { # 'n' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 1, # 'n' - 49: 1, # 'o' - 51: 0, # 'r' - 43: 1, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 2, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 49: { # 'o' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 2, # 'n' - 49: 1, # 'o' - 51: 2, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 51: { # 'r' - 50: 2, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 2, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 1, # 'n' - 49: 2, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 2, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 43: { # 's' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 0, # 'd' - 42: 2, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 1, # 'n' - 49: 1, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 2, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 44: { # 't' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 0, # 'd' - 42: 2, # 'e' - 53: 2, # 'i' - 56: 1, # 'l' - 54: 0, # 'n' - 49: 1, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 1, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 2, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 63: { # 'u' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 1, # 'n' - 49: 0, # 'o' - 51: 1, # 'r' - 43: 2, # 's' - 44: 1, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 34: { # '\xa0' - 50: 1, # 'a' - 60: 0, # 'c' - 61: 1, # 'd' - 42: 0, # 'e' - 53: 1, # 'i' - 56: 0, # 'l' - 54: 1, # 'n' - 49: 1, # 'o' - 51: 0, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 0, # 'u' - 34: 2, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 1, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 2, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 2, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 1, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 55: { # '´' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 1, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 2, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 1, # 'ן' - 12: 1, # 'נ' - 19: 1, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 48: { # '¼' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 1, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 39: { # '½' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 57: { # '¾' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 30: { # 'ְ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 1, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 1, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 2, # 'ג' - 16: 2, # 'ד' - 3: 2, # 'ה' - 2: 2, # 'ו' - 24: 2, # 'ז' - 14: 2, # 'ח' - 22: 2, # 'ט' - 1: 2, # 'י' - 25: 2, # 'ך' - 15: 2, # 'כ' - 4: 2, # 'ל' - 11: 1, # 'ם' - 6: 2, # 'מ' - 23: 0, # 'ן' - 12: 2, # 'נ' - 19: 2, # 'ס' - 13: 2, # 'ע' - 26: 0, # 'ף' - 18: 2, # 'פ' - 27: 0, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 59: { # 'ֱ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 1, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 1, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 2, # 'ל' - 11: 0, # 'ם' - 6: 2, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 41: { # 'ֲ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 2, # 'ב' - 20: 1, # 'ג' - 16: 2, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 1, # 'י' - 25: 1, # 'ך' - 15: 1, # 'כ' - 4: 2, # 'ל' - 11: 0, # 'ם' - 6: 2, # 'מ' - 23: 0, # 'ן' - 12: 2, # 'נ' - 19: 1, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 2, # 'צ' - 17: 1, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 33: { # 'ִ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 1, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 1, # 'ִ' - 37: 0, # 'ֵ' - 36: 1, # 'ֶ' - 31: 0, # 'ַ' - 29: 1, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 1, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 2, # 'ב' - 20: 2, # 'ג' - 16: 2, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 2, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 2, # 'כ' - 4: 2, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 2, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 2, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 37: { # 'ֵ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 1, # 'ֶ' - 31: 1, # 'ַ' - 29: 1, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 1, # 'ג' - 16: 2, # 'ד' - 3: 2, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 2, # 'ח' - 22: 1, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 1, # 'כ' - 4: 2, # 'ל' - 11: 2, # 'ם' - 6: 1, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 1, # 'ס' - 13: 2, # 'ע' - 26: 1, # 'ף' - 18: 1, # 'פ' - 27: 1, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 36: { # 'ֶ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 1, # 'ֶ' - 31: 1, # 'ַ' - 29: 1, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 1, # 'ג' - 16: 2, # 'ד' - 3: 2, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 2, # 'ח' - 22: 1, # 'ט' - 1: 2, # 'י' - 25: 2, # 'ך' - 15: 1, # 'כ' - 4: 2, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 2, # 'ס' - 13: 1, # 'ע' - 26: 1, # 'ף' - 18: 1, # 'פ' - 27: 2, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 31: { # 'ַ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 1, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 1, # 'ֶ' - 31: 0, # 'ַ' - 29: 2, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 2, # 'ג' - 16: 2, # 'ד' - 3: 2, # 'ה' - 2: 1, # 'ו' - 24: 2, # 'ז' - 14: 2, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 2, # 'כ' - 4: 2, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 2, # 'ס' - 13: 2, # 'ע' - 26: 2, # 'ף' - 18: 2, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 29: { # 'ָ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 1, # 'ַ' - 29: 2, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 1, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 2, # 'ג' - 16: 2, # 'ד' - 3: 3, # 'ה' - 2: 2, # 'ו' - 24: 2, # 'ז' - 14: 2, # 'ח' - 22: 1, # 'ט' - 1: 2, # 'י' - 25: 2, # 'ך' - 15: 2, # 'כ' - 4: 2, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 1, # 'ס' - 13: 2, # 'ע' - 26: 1, # 'ף' - 18: 2, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 35: { # 'ֹ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 1, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 1, # 'ג' - 16: 2, # 'ד' - 3: 2, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 1, # 'י' - 25: 1, # 'ך' - 15: 2, # 'כ' - 4: 2, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 2, # 'ס' - 13: 2, # 'ע' - 26: 1, # 'ף' - 18: 2, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 62: { # 'ֻ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 1, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 2, # 'ל' - 11: 1, # 'ם' - 6: 1, # 'מ' - 23: 1, # 'ן' - 12: 1, # 'נ' - 19: 1, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 28: { # 'ּ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 3, # 'ְ' - 59: 0, # 'ֱ' - 41: 1, # 'ֲ' - 33: 3, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 3, # 'ַ' - 29: 3, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 0, # 'ּ' - 38: 2, # 'ׁ' - 45: 1, # 'ׂ' - 9: 2, # 'א' - 8: 2, # 'ב' - 20: 1, # 'ג' - 16: 2, # 'ד' - 3: 1, # 'ה' - 2: 2, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 2, # 'י' - 25: 2, # 'ך' - 15: 2, # 'כ' - 4: 2, # 'ל' - 11: 1, # 'ם' - 6: 2, # 'מ' - 23: 1, # 'ן' - 12: 2, # 'נ' - 19: 1, # 'ס' - 13: 2, # 'ע' - 26: 1, # 'ף' - 18: 1, # 'פ' - 27: 1, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 2, # 'ר' - 10: 2, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 38: { # 'ׁ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 2, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 45: { # 'ׂ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 1, # 'ֵ' - 36: 2, # 'ֶ' - 31: 1, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 1, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 2, # 'ו' - 24: 0, # 'ז' - 14: 1, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 1, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 0, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 0, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 9: { # 'א' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 2, # 'ֱ' - 41: 2, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 2, # 'ע' - 26: 3, # 'ף' - 18: 3, # 'פ' - 27: 1, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 8: { # 'ב' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 1, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 3, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 1, # 'ף' - 18: 3, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 1, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 20: { # 'ג' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 2, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 1, # 'ִ' - 37: 1, # 'ֵ' - 36: 1, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 0, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 3, # 'ב' - 20: 2, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 2, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 1, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 2, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 2, # 'פ' - 27: 1, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 16: { # 'ד' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 1, # 'ז' - 14: 2, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 2, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 2, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 0, # 'ץ' - 21: 2, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 3: { # 'ה' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 1, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 1, # 'ְ' - 59: 1, # 'ֱ' - 41: 2, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 3, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 0, # 'ף' - 18: 3, # 'פ' - 27: 1, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 1, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 2: { # 'ו' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 1, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 1, # 'ֵ' - 36: 1, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 3, # 'ֹ' - 62: 0, # 'ֻ' - 28: 3, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 3, # 'ף' - 18: 3, # 'פ' - 27: 3, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 1, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 24: { # 'ז' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 1, # 'ֲ' - 33: 1, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 2, # 'ב' - 20: 2, # 'ג' - 16: 2, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 2, # 'ז' - 14: 2, # 'ח' - 22: 1, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 2, # 'נ' - 19: 1, # 'ס' - 13: 2, # 'ע' - 26: 1, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 2, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 1, # 'ש' - 5: 2, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 14: { # 'ח' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 1, # 'ֱ' - 41: 2, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 3, # 'ב' - 20: 2, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 2, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 2, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 1, # 'ע' - 26: 2, # 'ף' - 18: 2, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 22: { # 'ט' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 1, # 'ֵ' - 36: 1, # 'ֶ' - 31: 2, # 'ַ' - 29: 1, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 1, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 1, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 2, # 'ז' - 14: 3, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 2, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 2, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 3, # 'ר' - 10: 2, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 1: { # 'י' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 1, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 3, # 'ף' - 18: 3, # 'פ' - 27: 3, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 1, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 25: { # 'ך' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 2, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 1, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 1, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 1, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 15: { # 'כ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 3, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 2, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 2, # 'ע' - 26: 3, # 'ף' - 18: 3, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 2, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 4: { # 'ל' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 3, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 1, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 11: { # 'ם' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 1, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 0, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 1, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 6: { # 'מ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 0, # 'ף' - 18: 3, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 23: { # 'ן' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 1, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 0, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 1, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 1, # 'ס' - 13: 1, # 'ע' - 26: 1, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 1, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 1, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 12: { # 'נ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 19: { # 'ס' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 1, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 1, # 'ָ' - 35: 1, # 'ֹ' - 62: 2, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 1, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 2, # 'ס' - 13: 3, # 'ע' - 26: 3, # 'ף' - 18: 3, # 'פ' - 27: 0, # 'ץ' - 21: 2, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 1, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 13: { # 'ע' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 1, # 'ְ' - 59: 1, # 'ֱ' - 41: 2, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 1, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 2, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 2, # 'ע' - 26: 1, # 'ף' - 18: 2, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 26: { # 'ף' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 1, # 'ו' - 24: 0, # 'ז' - 14: 1, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 1, # 'ס' - 13: 0, # 'ע' - 26: 1, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 1, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 18: { # 'פ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 1, # 'ֵ' - 36: 2, # 'ֶ' - 31: 1, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 2, # 'ב' - 20: 3, # 'ג' - 16: 2, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 2, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 2, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 2, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 27: { # 'ץ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 1, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 0, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 21: { # 'צ' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 1, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 2, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 1, # 'ז' - 14: 3, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 1, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 1, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 2, # 'ץ' - 21: 2, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 0, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 17: { # 'ק' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 1, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 1, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 2, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 2, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 1, # 'ך' - 15: 1, # 'כ' - 4: 3, # 'ל' - 11: 2, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 2, # 'ץ' - 21: 3, # 'צ' - 17: 2, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 7: { # 'ר' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 2, # '´' - 48: 1, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 1, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 2, # 'ֹ' - 62: 1, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 3, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 3, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 3, # 'ץ' - 21: 3, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 10: { # 'ש' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 1, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 1, # 'ִ' - 37: 1, # 'ֵ' - 36: 1, # 'ֶ' - 31: 1, # 'ַ' - 29: 1, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 3, # 'ׁ' - 45: 2, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 3, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 2, # 'ז' - 14: 3, # 'ח' - 22: 3, # 'ט' - 1: 3, # 'י' - 25: 3, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 2, # 'ן' - 12: 3, # 'נ' - 19: 2, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 1, # '…' - }, - 5: { # 'ת' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 1, # '\xa0' - 55: 0, # '´' - 48: 1, # '¼' - 39: 1, # '½' - 57: 0, # '¾' - 30: 2, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 2, # 'ִ' - 37: 2, # 'ֵ' - 36: 2, # 'ֶ' - 31: 2, # 'ַ' - 29: 2, # 'ָ' - 35: 1, # 'ֹ' - 62: 1, # 'ֻ' - 28: 2, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 3, # 'א' - 8: 3, # 'ב' - 20: 3, # 'ג' - 16: 2, # 'ד' - 3: 3, # 'ה' - 2: 3, # 'ו' - 24: 2, # 'ז' - 14: 3, # 'ח' - 22: 2, # 'ט' - 1: 3, # 'י' - 25: 2, # 'ך' - 15: 3, # 'כ' - 4: 3, # 'ל' - 11: 3, # 'ם' - 6: 3, # 'מ' - 23: 3, # 'ן' - 12: 3, # 'נ' - 19: 2, # 'ס' - 13: 3, # 'ע' - 26: 2, # 'ף' - 18: 3, # 'פ' - 27: 1, # 'ץ' - 21: 2, # 'צ' - 17: 3, # 'ק' - 7: 3, # 'ר' - 10: 3, # 'ש' - 5: 3, # 'ת' - 32: 1, # '–' - 52: 1, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, - 32: { # '–' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 1, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 1, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 0, # 'ז' - 14: 1, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 1, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 1, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 52: { # '’' - 50: 1, # 'a' - 60: 0, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 1, # 'r' - 43: 2, # 's' - 44: 2, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 1, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 47: { # '“' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 1, # 'l' - 54: 1, # 'n' - 49: 1, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 1, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 2, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 1, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 1, # 'ח' - 22: 1, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 1, # 'ס' - 13: 1, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 1, # 'צ' - 17: 1, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 46: { # '”' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 1, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 1, # 'ב' - 20: 1, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 1, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 0, # '†' - 40: 0, # '…' - }, - 58: { # '†' - 50: 0, # 'a' - 60: 0, # 'c' - 61: 0, # 'd' - 42: 0, # 'e' - 53: 0, # 'i' - 56: 0, # 'l' - 54: 0, # 'n' - 49: 0, # 'o' - 51: 0, # 'r' - 43: 0, # 's' - 44: 0, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 0, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 0, # 'ה' - 2: 0, # 'ו' - 24: 0, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 0, # 'י' - 25: 0, # 'ך' - 15: 0, # 'כ' - 4: 0, # 'ל' - 11: 0, # 'ם' - 6: 0, # 'מ' - 23: 0, # 'ן' - 12: 0, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 0, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 0, # 'ר' - 10: 0, # 'ש' - 5: 0, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 0, # '”' - 58: 2, # '†' - 40: 0, # '…' - }, - 40: { # '…' - 50: 1, # 'a' - 60: 1, # 'c' - 61: 1, # 'd' - 42: 1, # 'e' - 53: 1, # 'i' - 56: 0, # 'l' - 54: 1, # 'n' - 49: 0, # 'o' - 51: 1, # 'r' - 43: 1, # 's' - 44: 1, # 't' - 63: 0, # 'u' - 34: 0, # '\xa0' - 55: 0, # '´' - 48: 0, # '¼' - 39: 0, # '½' - 57: 0, # '¾' - 30: 0, # 'ְ' - 59: 0, # 'ֱ' - 41: 0, # 'ֲ' - 33: 0, # 'ִ' - 37: 0, # 'ֵ' - 36: 0, # 'ֶ' - 31: 0, # 'ַ' - 29: 0, # 'ָ' - 35: 0, # 'ֹ' - 62: 0, # 'ֻ' - 28: 0, # 'ּ' - 38: 0, # 'ׁ' - 45: 0, # 'ׂ' - 9: 1, # 'א' - 8: 0, # 'ב' - 20: 0, # 'ג' - 16: 0, # 'ד' - 3: 1, # 'ה' - 2: 1, # 'ו' - 24: 1, # 'ז' - 14: 0, # 'ח' - 22: 0, # 'ט' - 1: 1, # 'י' - 25: 0, # 'ך' - 15: 1, # 'כ' - 4: 1, # 'ל' - 11: 0, # 'ם' - 6: 1, # 'מ' - 23: 0, # 'ן' - 12: 1, # 'נ' - 19: 0, # 'ס' - 13: 0, # 'ע' - 26: 0, # 'ף' - 18: 1, # 'פ' - 27: 0, # 'ץ' - 21: 0, # 'צ' - 17: 0, # 'ק' - 7: 1, # 'ר' - 10: 1, # 'ש' - 5: 1, # 'ת' - 32: 0, # '–' - 52: 0, # '’' - 47: 0, # '“' - 46: 1, # '”' - 58: 0, # '†' - 40: 2, # '…' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -WINDOWS_1255_HEBREW_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 69, # 'A' - 66: 91, # 'B' - 67: 79, # 'C' - 68: 80, # 'D' - 69: 92, # 'E' - 70: 89, # 'F' - 71: 97, # 'G' - 72: 90, # 'H' - 73: 68, # 'I' - 74: 111, # 'J' - 75: 112, # 'K' - 76: 82, # 'L' - 77: 73, # 'M' - 78: 95, # 'N' - 79: 85, # 'O' - 80: 78, # 'P' - 81: 121, # 'Q' - 82: 86, # 'R' - 83: 71, # 'S' - 84: 67, # 'T' - 85: 102, # 'U' - 86: 107, # 'V' - 87: 84, # 'W' - 88: 114, # 'X' - 89: 103, # 'Y' - 90: 115, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 50, # 'a' - 98: 74, # 'b' - 99: 60, # 'c' - 100: 61, # 'd' - 101: 42, # 'e' - 102: 76, # 'f' - 103: 70, # 'g' - 104: 64, # 'h' - 105: 53, # 'i' - 106: 105, # 'j' - 107: 93, # 'k' - 108: 56, # 'l' - 109: 65, # 'm' - 110: 54, # 'n' - 111: 49, # 'o' - 112: 66, # 'p' - 113: 110, # 'q' - 114: 51, # 'r' - 115: 43, # 's' - 116: 44, # 't' - 117: 63, # 'u' - 118: 81, # 'v' - 119: 77, # 'w' - 120: 98, # 'x' - 121: 75, # 'y' - 122: 108, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 124, # '€' - 129: 202, # None - 130: 203, # '‚' - 131: 204, # 'ƒ' - 132: 205, # '„' - 133: 40, # '…' - 134: 58, # '†' - 135: 206, # '‡' - 136: 207, # 'ˆ' - 137: 208, # '‰' - 138: 209, # None - 139: 210, # '‹' - 140: 211, # None - 141: 212, # None - 142: 213, # None - 143: 214, # None - 144: 215, # None - 145: 83, # '‘' - 146: 52, # '’' - 147: 47, # '“' - 148: 46, # '”' - 149: 72, # '•' - 150: 32, # '–' - 151: 94, # '—' - 152: 216, # '˜' - 153: 113, # '™' - 154: 217, # None - 155: 109, # '›' - 156: 218, # None - 157: 219, # None - 158: 220, # None - 159: 221, # None - 160: 34, # '\xa0' - 161: 116, # '¡' - 162: 222, # '¢' - 163: 118, # '£' - 164: 100, # '₪' - 165: 223, # '¥' - 166: 224, # '¦' - 167: 117, # '§' - 168: 119, # '¨' - 169: 104, # '©' - 170: 125, # '×' - 171: 225, # '«' - 172: 226, # '¬' - 173: 87, # '\xad' - 174: 99, # '®' - 175: 227, # '¯' - 176: 106, # '°' - 177: 122, # '±' - 178: 123, # '²' - 179: 228, # '³' - 180: 55, # '´' - 181: 229, # 'µ' - 182: 230, # '¶' - 183: 101, # '·' - 184: 231, # '¸' - 185: 232, # '¹' - 186: 120, # '÷' - 187: 233, # '»' - 188: 48, # '¼' - 189: 39, # '½' - 190: 57, # '¾' - 191: 234, # '¿' - 192: 30, # 'ְ' - 193: 59, # 'ֱ' - 194: 41, # 'ֲ' - 195: 88, # 'ֳ' - 196: 33, # 'ִ' - 197: 37, # 'ֵ' - 198: 36, # 'ֶ' - 199: 31, # 'ַ' - 200: 29, # 'ָ' - 201: 35, # 'ֹ' - 202: 235, # None - 203: 62, # 'ֻ' - 204: 28, # 'ּ' - 205: 236, # 'ֽ' - 206: 126, # '־' - 207: 237, # 'ֿ' - 208: 238, # '׀' - 209: 38, # 'ׁ' - 210: 45, # 'ׂ' - 211: 239, # '׃' - 212: 240, # 'װ' - 213: 241, # 'ױ' - 214: 242, # 'ײ' - 215: 243, # '׳' - 216: 127, # '״' - 217: 244, # None - 218: 245, # None - 219: 246, # None - 220: 247, # None - 221: 248, # None - 222: 249, # None - 223: 250, # None - 224: 9, # 'א' - 225: 8, # 'ב' - 226: 20, # 'ג' - 227: 16, # 'ד' - 228: 3, # 'ה' - 229: 2, # 'ו' - 230: 24, # 'ז' - 231: 14, # 'ח' - 232: 22, # 'ט' - 233: 1, # 'י' - 234: 25, # 'ך' - 235: 15, # 'כ' - 236: 4, # 'ל' - 237: 11, # 'ם' - 238: 6, # 'מ' - 239: 23, # 'ן' - 240: 12, # 'נ' - 241: 19, # 'ס' - 242: 13, # 'ע' - 243: 26, # 'ף' - 244: 18, # 'פ' - 245: 27, # 'ץ' - 246: 21, # 'צ' - 247: 17, # 'ק' - 248: 7, # 'ר' - 249: 10, # 'ש' - 250: 5, # 'ת' - 251: 251, # None - 252: 252, # None - 253: 128, # '\u200e' - 254: 96, # '\u200f' - 255: 253, # None -} - -WINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel( - charset_name="windows-1255", - language="Hebrew", - char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER, - language_model=HEBREW_LANG_MODEL, - typical_positive_ratio=0.984004, - keep_ascii_letters=False, - alphabet="אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ", -) diff --git a/libs/chardet/langhungarianmodel.py b/libs/chardet/langhungarianmodel.py deleted file mode 100644 index bd6630a05..000000000 --- a/libs/chardet/langhungarianmodel.py +++ /dev/null @@ -1,4649 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -HUNGARIAN_LANG_MODEL = { - 28: { # 'A' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 2, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 2, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 2, # 'K' - 41: 2, # 'L' - 34: 1, # 'M' - 35: 2, # 'N' - 47: 1, # 'O' - 46: 2, # 'P' - 43: 2, # 'R' - 33: 2, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 2, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 1, # 'j' - 7: 2, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 2, # 'n' - 8: 0, # 'o' - 23: 2, # 'p' - 10: 2, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 1, # 'u' - 19: 1, # 'v' - 62: 1, # 'x' - 16: 0, # 'y' - 11: 3, # 'z' - 51: 1, # 'Á' - 44: 0, # 'É' - 61: 1, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 40: { # 'B' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 0, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 3, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 2, # 'i' - 22: 1, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 3, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 54: { # 'C' - 28: 1, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 0, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 2, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 0, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 1, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 3, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 1, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 45: { # 'D' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 0, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 0, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 3, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 1, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 1, # 'o' - 23: 0, # 'p' - 10: 2, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 2, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 1, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 0, # 'ű' - }, - 32: { # 'E' - 28: 1, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 2, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 2, # 'K' - 41: 2, # 'L' - 34: 2, # 'M' - 35: 2, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 2, # 'R' - 33: 2, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 1, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 3, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 2, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 2, # 's' - 3: 1, # 't' - 21: 2, # 'u' - 19: 1, # 'v' - 62: 1, # 'x' - 16: 0, # 'y' - 11: 3, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 0, # 'Ú' - 63: 1, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 1, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 50: { # 'F' - 28: 1, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 0, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 0, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 0, # 'V' - 55: 1, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 1, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 2, # 'i' - 22: 1, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 2, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 0, # 'Ú' - 63: 1, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 2, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 49: { # 'G' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 2, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 1, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 2, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 2, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 0, # 'ű' - }, - 38: { # 'H' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 0, # 'D' - 32: 1, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 1, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 1, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 1, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 0, # 'V' - 55: 1, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 2, # 'i' - 22: 1, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 0, # 'n' - 8: 3, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 2, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 2, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 1, # 'é' - 30: 2, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 39: { # 'I' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 2, # 'K' - 41: 2, # 'L' - 34: 1, # 'M' - 35: 2, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 2, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 2, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 0, # 'e' - 27: 1, # 'f' - 12: 2, # 'g' - 20: 1, # 'h' - 9: 0, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 1, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 2, # 's' - 3: 2, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 53: { # 'J' - 28: 2, # 'A' - 40: 0, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 1, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 1, # 'o' - 23: 0, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 2, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 2, # 'ó' - 24: 2, # 'ö' - 31: 1, # 'ú' - 29: 0, # 'ü' - 42: 1, # 'ő' - 56: 0, # 'ű' - }, - 36: { # 'K' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 0, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 1, # 'f' - 12: 0, # 'g' - 20: 1, # 'h' - 9: 3, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 2, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 2, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 2, # 'ö' - 31: 1, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 0, # 'ű' - }, - 41: { # 'L' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 2, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 3, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 2, # 'i' - 22: 1, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 2, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 0, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 34: { # 'M' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 0, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 3, # 'a' - 18: 0, # 'b' - 26: 1, # 'c' - 17: 0, # 'd' - 1: 3, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 3, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 3, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 2, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 1, # 'ű' - }, - 35: { # 'N' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 2, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 2, # 'Y' - 52: 1, # 'Z' - 2: 3, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 3, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 2, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 2, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 1, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 1, # 'ő' - 56: 0, # 'ű' - }, - 47: { # 'O' - 28: 1, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 2, # 'K' - 41: 2, # 'L' - 34: 2, # 'M' - 35: 2, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 2, # 'R' - 33: 2, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 1, # 'j' - 7: 2, # 'k' - 6: 2, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 1, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 1, # 's' - 3: 2, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 1, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 1, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 46: { # 'P' - 28: 1, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 0, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 2, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 1, # 'f' - 12: 0, # 'g' - 20: 1, # 'h' - 9: 2, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 2, # 'r' - 5: 1, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 0, # 'Ú' - 63: 1, # 'Ü' - 14: 3, # 'á' - 15: 2, # 'é' - 30: 0, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 0, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 0, # 'ű' - }, - 43: { # 'R' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 2, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 1, # 'h' - 9: 2, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 2, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 2, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 33: { # 'S' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 2, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 3, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 1, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 1, # 'h' - 9: 2, # 'i' - 22: 0, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 1, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 1, # 't' - 21: 1, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 3, # 'z' - 51: 2, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 37: { # 'T' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 1, # 'P' - 43: 2, # 'R' - 33: 1, # 'S' - 37: 2, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 2, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 1, # 'h' - 9: 2, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 0, # 't' - 21: 2, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 1, # 'z' - 51: 2, # 'Á' - 44: 2, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 2, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 57: { # 'U' - 28: 1, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 2, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 1, # 'e' - 27: 0, # 'f' - 12: 2, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 1, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 48: { # 'V' - 28: 2, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 0, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 2, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 2, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 2, # 'o' - 23: 0, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 2, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 0, # 'Ú' - 63: 1, # 'Ü' - 14: 2, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 0, # 'ó' - 24: 1, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 55: { # 'Y' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 1, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 2, # 'Z' - 2: 1, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 1, # 'd' - 1: 1, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 8: 1, # 'o' - 23: 1, # 'p' - 10: 0, # 'r' - 5: 0, # 's' - 3: 0, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 1, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 52: { # 'Z' - 28: 2, # 'A' - 40: 1, # 'B' - 54: 0, # 'C' - 45: 1, # 'D' - 32: 2, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 2, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 2, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 2, # 'S' - 37: 1, # 'T' - 57: 1, # 'U' - 48: 1, # 'V' - 55: 1, # 'Y' - 52: 1, # 'Z' - 2: 1, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 1, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 1, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 8: 1, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 2, # 's' - 3: 0, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 2, # 'Á' - 44: 1, # 'É' - 61: 1, # 'Í' - 58: 1, # 'Ó' - 59: 1, # 'Ö' - 60: 1, # 'Ú' - 63: 1, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 2: { # 'a' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 2, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 2, # 'o' - 23: 3, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 1, # 'x' - 16: 2, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 18: { # 'b' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 2, # 'k' - 6: 2, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 2, # 's' - 3: 1, # 't' - 21: 3, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 3, # 'ó' - 24: 2, # 'ö' - 31: 2, # 'ú' - 29: 2, # 'ü' - 42: 2, # 'ő' - 56: 1, # 'ű' - }, - 26: { # 'c' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 1, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 1, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 1, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 1, # 'j' - 7: 2, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 3, # 's' - 3: 2, # 't' - 21: 2, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 2, # 'á' - 15: 2, # 'é' - 30: 2, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 17: { # 'd' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 2, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 2, # 'k' - 6: 1, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 2, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 3, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 2, # 'ú' - 29: 2, # 'ü' - 42: 2, # 'ő' - 56: 1, # 'ű' - }, - 1: { # 'e' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 2, # 'e' - 27: 3, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 2, # 'o' - 23: 3, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 2, # 'u' - 19: 3, # 'v' - 62: 2, # 'x' - 16: 2, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 27: { # 'f' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 3, # 'o' - 23: 0, # 'p' - 10: 3, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 2, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 0, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 3, # 'ö' - 31: 1, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 12: { # 'g' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 2, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 2, # 'k' - 6: 3, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 3, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 3, # 'ó' - 24: 2, # 'ö' - 31: 2, # 'ú' - 29: 2, # 'ü' - 42: 2, # 'ő' - 56: 1, # 'ű' - }, - 20: { # 'h' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 0, # 'd' - 1: 3, # 'e' - 27: 0, # 'f' - 12: 1, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 3, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 2, # 's' - 3: 1, # 't' - 21: 3, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 2, # 'y' - 11: 0, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 3, # 'í' - 25: 2, # 'ó' - 24: 2, # 'ö' - 31: 2, # 'ú' - 29: 1, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 9: { # 'i' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 3, # 'e' - 27: 3, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 2, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 2, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 1, # 'x' - 16: 1, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 3, # 'ó' - 24: 1, # 'ö' - 31: 2, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 1, # 'ű' - }, - 22: { # 'j' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 2, # 'b' - 26: 1, # 'c' - 17: 3, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 2, # 'h' - 9: 1, # 'i' - 22: 2, # 'j' - 7: 2, # 'k' - 6: 2, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 2, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 1, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 3, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 7: { # 'k' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 1, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 2, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 3, # 'í' - 25: 2, # 'ó' - 24: 3, # 'ö' - 31: 1, # 'ú' - 29: 3, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 6: { # 'l' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 1, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 1, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 2, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 3, # 'e' - 27: 3, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 2, # 'p' - 10: 2, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 3, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 3, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 2, # 'ú' - 29: 2, # 'ü' - 42: 3, # 'ő' - 56: 1, # 'ű' - }, - 13: { # 'm' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 1, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 8: 3, # 'o' - 23: 3, # 'p' - 10: 2, # 'r' - 5: 2, # 's' - 3: 2, # 't' - 21: 3, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 2, # 'ó' - 24: 2, # 'ö' - 31: 2, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 2, # 'ű' - }, - 4: { # 'n' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 2, # 'p' - 10: 2, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 2, # 'v' - 62: 1, # 'x' - 16: 3, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 2, # 'ó' - 24: 3, # 'ö' - 31: 2, # 'ú' - 29: 3, # 'ü' - 42: 2, # 'ő' - 56: 1, # 'ű' - }, - 8: { # 'o' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 1, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 2, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 2, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 1, # 'o' - 23: 3, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 2, # 'u' - 19: 3, # 'v' - 62: 1, # 'x' - 16: 1, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 23: { # 'p' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 1, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 2, # 'k' - 6: 3, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 8: 3, # 'o' - 23: 3, # 'p' - 10: 3, # 'r' - 5: 2, # 's' - 3: 2, # 't' - 21: 3, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 2, # 'ó' - 24: 2, # 'ö' - 31: 1, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 10: { # 'r' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 1, # 'x' - 16: 2, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 3, # 'ú' - 29: 3, # 'ü' - 42: 2, # 'ő' - 56: 2, # 'ű' - }, - 5: { # 's' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 2, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 2, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 1, # 'j' - 7: 3, # 'k' - 6: 2, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 3, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 3, # 'ú' - 29: 3, # 'ü' - 42: 2, # 'ő' - 56: 1, # 'ű' - }, - 3: { # 't' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 1, # 'g' - 20: 3, # 'h' - 9: 3, # 'i' - 22: 3, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 3, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 3, # 'ú' - 29: 3, # 'ü' - 42: 3, # 'ő' - 56: 2, # 'ű' - }, - 21: { # 'u' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 2, # 'b' - 26: 2, # 'c' - 17: 3, # 'd' - 1: 2, # 'e' - 27: 1, # 'f' - 12: 3, # 'g' - 20: 2, # 'h' - 9: 2, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 1, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 1, # 'u' - 19: 3, # 'v' - 62: 1, # 'x' - 16: 1, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 2, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 0, # 'ö' - 31: 1, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 19: { # 'v' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 2, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 3, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 1, # 'r' - 5: 2, # 's' - 3: 2, # 't' - 21: 2, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 2, # 'ó' - 24: 2, # 'ö' - 31: 1, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 1, # 'ű' - }, - 62: { # 'x' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 0, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 1, # 'i' - 22: 0, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 1, # 'o' - 23: 1, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 1, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 1, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 16: { # 'y' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 2, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 3, # 'e' - 27: 2, # 'f' - 12: 2, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 2, # 'j' - 7: 2, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 2, # 'p' - 10: 2, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 2, # 'í' - 25: 2, # 'ó' - 24: 3, # 'ö' - 31: 2, # 'ú' - 29: 2, # 'ü' - 42: 1, # 'ő' - 56: 2, # 'ű' - }, - 11: { # 'z' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 3, # 'a' - 18: 2, # 'b' - 26: 1, # 'c' - 17: 3, # 'd' - 1: 3, # 'e' - 27: 1, # 'f' - 12: 2, # 'g' - 20: 2, # 'h' - 9: 3, # 'i' - 22: 1, # 'j' - 7: 3, # 'k' - 6: 2, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 3, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 3, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 3, # 'á' - 15: 3, # 'é' - 30: 3, # 'í' - 25: 3, # 'ó' - 24: 3, # 'ö' - 31: 2, # 'ú' - 29: 3, # 'ü' - 42: 2, # 'ő' - 56: 1, # 'ű' - }, - 51: { # 'Á' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 0, # 'E' - 50: 1, # 'F' - 49: 2, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 2, # 'L' - 34: 1, # 'M' - 35: 2, # 'N' - 47: 0, # 'O' - 46: 1, # 'P' - 43: 2, # 'R' - 33: 2, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 0, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 1, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 44: { # 'É' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 1, # 'E' - 50: 0, # 'F' - 49: 2, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 2, # 'L' - 34: 1, # 'M' - 35: 2, # 'N' - 47: 0, # 'O' - 46: 1, # 'P' - 43: 2, # 'R' - 33: 2, # 'S' - 37: 2, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 0, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 2, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 3, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 0, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 61: { # 'Í' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 0, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 1, # 'J' - 36: 0, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 0, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 2, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 1, # 'm' - 4: 0, # 'n' - 8: 0, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 0, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 58: { # 'Ó' - 28: 1, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 0, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 1, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 2, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 0, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 2, # 'h' - 9: 0, # 'i' - 22: 0, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 0, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 1, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 59: { # 'Ö' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 0, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 0, # 'O' - 46: 1, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 0, # 'b' - 26: 1, # 'c' - 17: 1, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 0, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 0, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 8: 0, # 'o' - 23: 0, # 'p' - 10: 2, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 60: { # 'Ú' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 1, # 'C' - 45: 1, # 'D' - 32: 0, # 'E' - 50: 1, # 'F' - 49: 1, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 0, # 'b' - 26: 0, # 'c' - 17: 0, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 2, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 2, # 'j' - 7: 0, # 'k' - 6: 0, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 8: 0, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 0, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 0, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 63: { # 'Ü' - 28: 0, # 'A' - 40: 1, # 'B' - 54: 0, # 'C' - 45: 1, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 1, # 'G' - 38: 1, # 'H' - 39: 0, # 'I' - 53: 1, # 'J' - 36: 1, # 'K' - 41: 1, # 'L' - 34: 1, # 'M' - 35: 1, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 1, # 'R' - 33: 1, # 'S' - 37: 1, # 'T' - 57: 0, # 'U' - 48: 1, # 'V' - 55: 0, # 'Y' - 52: 1, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 0, # 'c' - 17: 1, # 'd' - 1: 0, # 'e' - 27: 0, # 'f' - 12: 1, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 0, # 'j' - 7: 0, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 8: 0, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 1, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 14: { # 'á' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 3, # 'b' - 26: 3, # 'c' - 17: 3, # 'd' - 1: 1, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 2, # 'h' - 9: 2, # 'i' - 22: 3, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 1, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 2, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 1, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 2, # 'é' - 30: 1, # 'í' - 25: 0, # 'ó' - 24: 1, # 'ö' - 31: 0, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 15: { # 'é' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 3, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 3, # 'g' - 20: 3, # 'h' - 9: 2, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 1, # 'o' - 23: 3, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 0, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 30: { # 'í' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 0, # 'a' - 18: 1, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 0, # 'e' - 27: 1, # 'f' - 12: 3, # 'g' - 20: 0, # 'h' - 9: 0, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 2, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 2, # 's' - 3: 3, # 't' - 21: 0, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 25: { # 'ó' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 2, # 'a' - 18: 3, # 'b' - 26: 2, # 'c' - 17: 3, # 'd' - 1: 1, # 'e' - 27: 2, # 'f' - 12: 2, # 'g' - 20: 2, # 'h' - 9: 2, # 'i' - 22: 2, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 8: 1, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 1, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 0, # 'ó' - 24: 1, # 'ö' - 31: 1, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 24: { # 'ö' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 0, # 'a' - 18: 3, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 0, # 'e' - 27: 1, # 'f' - 12: 2, # 'g' - 20: 1, # 'h' - 9: 0, # 'i' - 22: 1, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 8: 0, # 'o' - 23: 2, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 3, # 't' - 21: 0, # 'u' - 19: 3, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 3, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 31: { # 'ú' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 1, # 'b' - 26: 2, # 'c' - 17: 1, # 'd' - 1: 1, # 'e' - 27: 2, # 'f' - 12: 3, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 3, # 'j' - 7: 1, # 'k' - 6: 3, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 3, # 'r' - 5: 3, # 's' - 3: 2, # 't' - 21: 1, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 1, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 29: { # 'ü' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 1, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 3, # 'g' - 20: 2, # 'h' - 9: 1, # 'i' - 22: 1, # 'j' - 7: 3, # 'k' - 6: 3, # 'l' - 13: 1, # 'm' - 4: 3, # 'n' - 8: 0, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 2, # 's' - 3: 2, # 't' - 21: 0, # 'u' - 19: 2, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 1, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 42: { # 'ő' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 2, # 'b' - 26: 1, # 'c' - 17: 2, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 1, # 'j' - 7: 2, # 'k' - 6: 3, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 8: 1, # 'o' - 23: 1, # 'p' - 10: 2, # 'r' - 5: 2, # 's' - 3: 2, # 't' - 21: 1, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 1, # 'é' - 30: 1, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 1, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, - 56: { # 'ű' - 28: 0, # 'A' - 40: 0, # 'B' - 54: 0, # 'C' - 45: 0, # 'D' - 32: 0, # 'E' - 50: 0, # 'F' - 49: 0, # 'G' - 38: 0, # 'H' - 39: 0, # 'I' - 53: 0, # 'J' - 36: 0, # 'K' - 41: 0, # 'L' - 34: 0, # 'M' - 35: 0, # 'N' - 47: 0, # 'O' - 46: 0, # 'P' - 43: 0, # 'R' - 33: 0, # 'S' - 37: 0, # 'T' - 57: 0, # 'U' - 48: 0, # 'V' - 55: 0, # 'Y' - 52: 0, # 'Z' - 2: 1, # 'a' - 18: 1, # 'b' - 26: 0, # 'c' - 17: 1, # 'd' - 1: 1, # 'e' - 27: 1, # 'f' - 12: 1, # 'g' - 20: 1, # 'h' - 9: 1, # 'i' - 22: 1, # 'j' - 7: 1, # 'k' - 6: 1, # 'l' - 13: 0, # 'm' - 4: 2, # 'n' - 8: 0, # 'o' - 23: 0, # 'p' - 10: 1, # 'r' - 5: 1, # 's' - 3: 1, # 't' - 21: 0, # 'u' - 19: 1, # 'v' - 62: 0, # 'x' - 16: 0, # 'y' - 11: 2, # 'z' - 51: 0, # 'Á' - 44: 0, # 'É' - 61: 0, # 'Í' - 58: 0, # 'Ó' - 59: 0, # 'Ö' - 60: 0, # 'Ú' - 63: 0, # 'Ü' - 14: 0, # 'á' - 15: 0, # 'é' - 30: 0, # 'í' - 25: 0, # 'ó' - 24: 0, # 'ö' - 31: 0, # 'ú' - 29: 0, # 'ü' - 42: 0, # 'ő' - 56: 0, # 'ű' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 28, # 'A' - 66: 40, # 'B' - 67: 54, # 'C' - 68: 45, # 'D' - 69: 32, # 'E' - 70: 50, # 'F' - 71: 49, # 'G' - 72: 38, # 'H' - 73: 39, # 'I' - 74: 53, # 'J' - 75: 36, # 'K' - 76: 41, # 'L' - 77: 34, # 'M' - 78: 35, # 'N' - 79: 47, # 'O' - 80: 46, # 'P' - 81: 72, # 'Q' - 82: 43, # 'R' - 83: 33, # 'S' - 84: 37, # 'T' - 85: 57, # 'U' - 86: 48, # 'V' - 87: 64, # 'W' - 88: 68, # 'X' - 89: 55, # 'Y' - 90: 52, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 2, # 'a' - 98: 18, # 'b' - 99: 26, # 'c' - 100: 17, # 'd' - 101: 1, # 'e' - 102: 27, # 'f' - 103: 12, # 'g' - 104: 20, # 'h' - 105: 9, # 'i' - 106: 22, # 'j' - 107: 7, # 'k' - 108: 6, # 'l' - 109: 13, # 'm' - 110: 4, # 'n' - 111: 8, # 'o' - 112: 23, # 'p' - 113: 67, # 'q' - 114: 10, # 'r' - 115: 5, # 's' - 116: 3, # 't' - 117: 21, # 'u' - 118: 19, # 'v' - 119: 65, # 'w' - 120: 62, # 'x' - 121: 16, # 'y' - 122: 11, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 161, # '€' - 129: 162, # None - 130: 163, # '‚' - 131: 164, # None - 132: 165, # '„' - 133: 166, # '…' - 134: 167, # '†' - 135: 168, # '‡' - 136: 169, # None - 137: 170, # '‰' - 138: 171, # 'Š' - 139: 172, # '‹' - 140: 173, # 'Ś' - 141: 174, # 'Ť' - 142: 175, # 'Ž' - 143: 176, # 'Ź' - 144: 177, # None - 145: 178, # '‘' - 146: 179, # '’' - 147: 180, # '“' - 148: 78, # '”' - 149: 181, # '•' - 150: 69, # '–' - 151: 182, # '—' - 152: 183, # None - 153: 184, # '™' - 154: 185, # 'š' - 155: 186, # '›' - 156: 187, # 'ś' - 157: 188, # 'ť' - 158: 189, # 'ž' - 159: 190, # 'ź' - 160: 191, # '\xa0' - 161: 192, # 'ˇ' - 162: 193, # '˘' - 163: 194, # 'Ł' - 164: 195, # '¤' - 165: 196, # 'Ą' - 166: 197, # '¦' - 167: 76, # '§' - 168: 198, # '¨' - 169: 199, # '©' - 170: 200, # 'Ş' - 171: 201, # '«' - 172: 202, # '¬' - 173: 203, # '\xad' - 174: 204, # '®' - 175: 205, # 'Ż' - 176: 81, # '°' - 177: 206, # '±' - 178: 207, # '˛' - 179: 208, # 'ł' - 180: 209, # '´' - 181: 210, # 'µ' - 182: 211, # '¶' - 183: 212, # '·' - 184: 213, # '¸' - 185: 214, # 'ą' - 186: 215, # 'ş' - 187: 216, # '»' - 188: 217, # 'Ľ' - 189: 218, # '˝' - 190: 219, # 'ľ' - 191: 220, # 'ż' - 192: 221, # 'Ŕ' - 193: 51, # 'Á' - 194: 83, # 'Â' - 195: 222, # 'Ă' - 196: 80, # 'Ä' - 197: 223, # 'Ĺ' - 198: 224, # 'Ć' - 199: 225, # 'Ç' - 200: 226, # 'Č' - 201: 44, # 'É' - 202: 227, # 'Ę' - 203: 228, # 'Ë' - 204: 229, # 'Ě' - 205: 61, # 'Í' - 206: 230, # 'Î' - 207: 231, # 'Ď' - 208: 232, # 'Đ' - 209: 233, # 'Ń' - 210: 234, # 'Ň' - 211: 58, # 'Ó' - 212: 235, # 'Ô' - 213: 66, # 'Ő' - 214: 59, # 'Ö' - 215: 236, # '×' - 216: 237, # 'Ř' - 217: 238, # 'Ů' - 218: 60, # 'Ú' - 219: 70, # 'Ű' - 220: 63, # 'Ü' - 221: 239, # 'Ý' - 222: 240, # 'Ţ' - 223: 241, # 'ß' - 224: 84, # 'ŕ' - 225: 14, # 'á' - 226: 75, # 'â' - 227: 242, # 'ă' - 228: 71, # 'ä' - 229: 82, # 'ĺ' - 230: 243, # 'ć' - 231: 73, # 'ç' - 232: 244, # 'č' - 233: 15, # 'é' - 234: 85, # 'ę' - 235: 79, # 'ë' - 236: 86, # 'ě' - 237: 30, # 'í' - 238: 77, # 'î' - 239: 87, # 'ď' - 240: 245, # 'đ' - 241: 246, # 'ń' - 242: 247, # 'ň' - 243: 25, # 'ó' - 244: 74, # 'ô' - 245: 42, # 'ő' - 246: 24, # 'ö' - 247: 248, # '÷' - 248: 249, # 'ř' - 249: 250, # 'ů' - 250: 31, # 'ú' - 251: 56, # 'ű' - 252: 29, # 'ü' - 253: 251, # 'ý' - 254: 252, # 'ţ' - 255: 253, # '˙' -} - -WINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel( - charset_name="windows-1250", - language="Hungarian", - char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER, - language_model=HUNGARIAN_LANG_MODEL, - typical_positive_ratio=0.947368, - keep_ascii_letters=True, - alphabet="ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű", -) - -ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 28, # 'A' - 66: 40, # 'B' - 67: 54, # 'C' - 68: 45, # 'D' - 69: 32, # 'E' - 70: 50, # 'F' - 71: 49, # 'G' - 72: 38, # 'H' - 73: 39, # 'I' - 74: 53, # 'J' - 75: 36, # 'K' - 76: 41, # 'L' - 77: 34, # 'M' - 78: 35, # 'N' - 79: 47, # 'O' - 80: 46, # 'P' - 81: 71, # 'Q' - 82: 43, # 'R' - 83: 33, # 'S' - 84: 37, # 'T' - 85: 57, # 'U' - 86: 48, # 'V' - 87: 64, # 'W' - 88: 68, # 'X' - 89: 55, # 'Y' - 90: 52, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 2, # 'a' - 98: 18, # 'b' - 99: 26, # 'c' - 100: 17, # 'd' - 101: 1, # 'e' - 102: 27, # 'f' - 103: 12, # 'g' - 104: 20, # 'h' - 105: 9, # 'i' - 106: 22, # 'j' - 107: 7, # 'k' - 108: 6, # 'l' - 109: 13, # 'm' - 110: 4, # 'n' - 111: 8, # 'o' - 112: 23, # 'p' - 113: 67, # 'q' - 114: 10, # 'r' - 115: 5, # 's' - 116: 3, # 't' - 117: 21, # 'u' - 118: 19, # 'v' - 119: 65, # 'w' - 120: 62, # 'x' - 121: 16, # 'y' - 122: 11, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 159, # '\x80' - 129: 160, # '\x81' - 130: 161, # '\x82' - 131: 162, # '\x83' - 132: 163, # '\x84' - 133: 164, # '\x85' - 134: 165, # '\x86' - 135: 166, # '\x87' - 136: 167, # '\x88' - 137: 168, # '\x89' - 138: 169, # '\x8a' - 139: 170, # '\x8b' - 140: 171, # '\x8c' - 141: 172, # '\x8d' - 142: 173, # '\x8e' - 143: 174, # '\x8f' - 144: 175, # '\x90' - 145: 176, # '\x91' - 146: 177, # '\x92' - 147: 178, # '\x93' - 148: 179, # '\x94' - 149: 180, # '\x95' - 150: 181, # '\x96' - 151: 182, # '\x97' - 152: 183, # '\x98' - 153: 184, # '\x99' - 154: 185, # '\x9a' - 155: 186, # '\x9b' - 156: 187, # '\x9c' - 157: 188, # '\x9d' - 158: 189, # '\x9e' - 159: 190, # '\x9f' - 160: 191, # '\xa0' - 161: 192, # 'Ą' - 162: 193, # '˘' - 163: 194, # 'Ł' - 164: 195, # '¤' - 165: 196, # 'Ľ' - 166: 197, # 'Ś' - 167: 75, # '§' - 168: 198, # '¨' - 169: 199, # 'Š' - 170: 200, # 'Ş' - 171: 201, # 'Ť' - 172: 202, # 'Ź' - 173: 203, # '\xad' - 174: 204, # 'Ž' - 175: 205, # 'Ż' - 176: 79, # '°' - 177: 206, # 'ą' - 178: 207, # '˛' - 179: 208, # 'ł' - 180: 209, # '´' - 181: 210, # 'ľ' - 182: 211, # 'ś' - 183: 212, # 'ˇ' - 184: 213, # '¸' - 185: 214, # 'š' - 186: 215, # 'ş' - 187: 216, # 'ť' - 188: 217, # 'ź' - 189: 218, # '˝' - 190: 219, # 'ž' - 191: 220, # 'ż' - 192: 221, # 'Ŕ' - 193: 51, # 'Á' - 194: 81, # 'Â' - 195: 222, # 'Ă' - 196: 78, # 'Ä' - 197: 223, # 'Ĺ' - 198: 224, # 'Ć' - 199: 225, # 'Ç' - 200: 226, # 'Č' - 201: 44, # 'É' - 202: 227, # 'Ę' - 203: 228, # 'Ë' - 204: 229, # 'Ě' - 205: 61, # 'Í' - 206: 230, # 'Î' - 207: 231, # 'Ď' - 208: 232, # 'Đ' - 209: 233, # 'Ń' - 210: 234, # 'Ň' - 211: 58, # 'Ó' - 212: 235, # 'Ô' - 213: 66, # 'Ő' - 214: 59, # 'Ö' - 215: 236, # '×' - 216: 237, # 'Ř' - 217: 238, # 'Ů' - 218: 60, # 'Ú' - 219: 69, # 'Ű' - 220: 63, # 'Ü' - 221: 239, # 'Ý' - 222: 240, # 'Ţ' - 223: 241, # 'ß' - 224: 82, # 'ŕ' - 225: 14, # 'á' - 226: 74, # 'â' - 227: 242, # 'ă' - 228: 70, # 'ä' - 229: 80, # 'ĺ' - 230: 243, # 'ć' - 231: 72, # 'ç' - 232: 244, # 'č' - 233: 15, # 'é' - 234: 83, # 'ę' - 235: 77, # 'ë' - 236: 84, # 'ě' - 237: 30, # 'í' - 238: 76, # 'î' - 239: 85, # 'ď' - 240: 245, # 'đ' - 241: 246, # 'ń' - 242: 247, # 'ň' - 243: 25, # 'ó' - 244: 73, # 'ô' - 245: 42, # 'ő' - 246: 24, # 'ö' - 247: 248, # '÷' - 248: 249, # 'ř' - 249: 250, # 'ů' - 250: 31, # 'ú' - 251: 56, # 'ű' - 252: 29, # 'ü' - 253: 251, # 'ý' - 254: 252, # 'ţ' - 255: 253, # '˙' -} - -ISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel( - charset_name="ISO-8859-2", - language="Hungarian", - char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER, - language_model=HUNGARIAN_LANG_MODEL, - typical_positive_ratio=0.947368, - keep_ascii_letters=True, - alphabet="ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű", -) diff --git a/libs/chardet/langrussianmodel.py b/libs/chardet/langrussianmodel.py deleted file mode 100644 index 0d5b17844..000000000 --- a/libs/chardet/langrussianmodel.py +++ /dev/null @@ -1,5725 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -RUSSIAN_LANG_MODEL = { - 37: { # 'А' - 37: 0, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 1, # 'Ж' - 51: 1, # 'З' - 42: 1, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 2, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 1, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 1, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 1, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 0, # 'е' - 24: 1, # 'ж' - 20: 1, # 'з' - 4: 0, # 'и' - 23: 1, # 'й' - 11: 2, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 0, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 2, # 'у' - 39: 2, # 'ф' - 26: 2, # 'х' - 28: 0, # 'ц' - 22: 1, # 'ч' - 25: 2, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 44: { # 'Б' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 1, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 2, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 33: { # 'В' - 37: 2, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 1, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 2, # 'а' - 21: 1, # 'б' - 10: 1, # 'в' - 19: 1, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 2, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 2, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 1, # 'ц' - 22: 2, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 1, # 'ъ' - 18: 3, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 0, # 'ю' - 16: 1, # 'я' - }, - 46: { # 'Г' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 2, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 1, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 1, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 41: { # 'Д' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 2, # 'Е' - 56: 1, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 2, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 3, # 'ж' - 20: 1, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 1, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 48: { # 'Е' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 1, # 'Ж' - 51: 1, # 'З' - 42: 1, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 2, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 2, # 'Р' - 32: 2, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 1, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 0, # 'а' - 21: 0, # 'б' - 10: 2, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 2, # 'е' - 24: 1, # 'ж' - 20: 1, # 'з' - 4: 0, # 'и' - 23: 2, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 1, # 'н' - 1: 0, # 'о' - 15: 1, # 'п' - 9: 1, # 'р' - 7: 3, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 2, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 56: { # 'Ж' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 1, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 1, # 'б' - 10: 0, # 'в' - 19: 1, # 'г' - 13: 1, # 'д' - 2: 2, # 'е' - 24: 1, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 1, # 'м' - 5: 0, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 2, # 'ю' - 16: 0, # 'я' - }, - 51: { # 'З' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 0, # 'г' - 13: 2, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 1, # 'л' - 12: 1, # 'м' - 5: 2, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 1, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 1, # 'я' - }, - 42: { # 'И' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 2, # 'Е' - 56: 1, # 'Ж' - 51: 1, # 'З' - 42: 1, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 2, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 1, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 1, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 2, # 'з' - 4: 1, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 1, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 1, # 'у' - 39: 1, # 'ф' - 26: 2, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 60: { # 'Й' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 1, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 0, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 36: { # 'К' - 37: 2, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 1, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 2, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 1, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 0, # 'м' - 5: 1, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 49: { # 'Л' - 37: 2, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 1, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 1, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 0, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 0, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 1, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 1, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 1, # 'л' - 12: 0, # 'м' - 5: 1, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 2, # 'ю' - 16: 1, # 'я' - }, - 38: { # 'М' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 1, # 'Ф' - 55: 1, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 0, # 'Ь' - 47: 1, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 1, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 1, # 'л' - 12: 1, # 'м' - 5: 2, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 1, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 31: { # 'Н' - 37: 2, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 1, # 'З' - 42: 2, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 1, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 1, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 3, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 2, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 34: { # 'О' - 37: 0, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 2, # 'Д' - 48: 1, # 'Е' - 56: 1, # 'Ж' - 51: 1, # 'З' - 42: 1, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 2, # 'Л' - 38: 1, # 'М' - 31: 2, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 2, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 1, # 'Ф' - 55: 1, # 'Х' - 58: 0, # 'Ц' - 50: 1, # 'Ч' - 57: 1, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 1, # 'а' - 21: 2, # 'б' - 10: 1, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 0, # 'е' - 24: 1, # 'ж' - 20: 1, # 'з' - 4: 0, # 'и' - 23: 1, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 3, # 'н' - 1: 0, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 1, # 'у' - 39: 1, # 'ф' - 26: 2, # 'х' - 28: 1, # 'ц' - 22: 2, # 'ч' - 25: 2, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 35: { # 'П' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 2, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 1, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 0, # 'м' - 5: 1, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 3, # 'р' - 7: 1, # 'с' - 6: 1, # 'т' - 14: 2, # 'у' - 39: 1, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 2, # 'ь' - 30: 1, # 'э' - 27: 0, # 'ю' - 16: 2, # 'я' - }, - 45: { # 'Р' - 37: 2, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 2, # 'Е' - 56: 1, # 'Ж' - 51: 0, # 'З' - 42: 2, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 2, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 1, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 1, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 1, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 2, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 2, # 'я' - }, - 32: { # 'С' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 2, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 1, # 'Ч' - 57: 1, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 1, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 2, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 1, # 'ж' - 20: 1, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 2, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 1, # 'с' - 6: 3, # 'т' - 14: 2, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 1, # 'ц' - 22: 1, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 1, # 'ъ' - 18: 1, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 40: { # 'Т' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 2, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 1, # 'Ь' - 47: 1, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 1, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 1, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 52: { # 'У' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 1, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 0, # 'Ц' - 50: 1, # 'Ч' - 57: 1, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 1, # 'Ю' - 43: 0, # 'Я' - 3: 1, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 2, # 'д' - 2: 1, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 2, # 'и' - 23: 1, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 1, # 'н' - 1: 2, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 0, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 1, # 'ц' - 22: 2, # 'ч' - 25: 1, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 53: { # 'Ф' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 0, # 'с' - 6: 1, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 55: { # 'Х' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 2, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 0, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 1, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 1, # 'ь' - 30: 1, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 58: { # 'Ц' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 1, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 1, # 'а' - 21: 0, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 0, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 1, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 50: { # 'Ч' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 1, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 1, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 1, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 1, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 3, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 1, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 57: { # 'Ш' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 1, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 0, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 1, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 1, # 'н' - 1: 2, # 'о' - 15: 2, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 2, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 63: { # 'Щ' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 1, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 1, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 1, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 1, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 1, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 1, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 62: { # 'Ы' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 1, # 'Ц' - 50: 0, # 'Ч' - 57: 1, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 0, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 0, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 0, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 61: { # 'Ь' - 37: 0, # 'А' - 44: 1, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 0, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 1, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 1, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 1, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 1, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 0, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 0, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 0, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 0, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 47: { # 'Э' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 1, # 'Й' - 36: 1, # 'К' - 49: 1, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 1, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 1, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 2, # 'д' - 2: 0, # 'е' - 24: 1, # 'ж' - 20: 0, # 'з' - 4: 0, # 'и' - 23: 2, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 0, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 1, # 'с' - 6: 3, # 'т' - 14: 1, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 59: { # 'Ю' - 37: 1, # 'А' - 44: 1, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 1, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 0, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 1, # 'б' - 10: 0, # 'в' - 19: 1, # 'г' - 13: 1, # 'д' - 2: 0, # 'е' - 24: 1, # 'ж' - 20: 0, # 'з' - 4: 0, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 2, # 'н' - 1: 0, # 'о' - 15: 1, # 'п' - 9: 1, # 'р' - 7: 1, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 43: { # 'Я' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 1, # 'В' - 46: 1, # 'Г' - 41: 0, # 'Д' - 48: 1, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 1, # 'С' - 40: 1, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 1, # 'Х' - 58: 0, # 'Ц' - 50: 1, # 'Ч' - 57: 0, # 'Ш' - 63: 1, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 1, # 'Ю' - 43: 1, # 'Я' - 3: 0, # 'а' - 21: 1, # 'б' - 10: 1, # 'в' - 19: 1, # 'г' - 13: 1, # 'д' - 2: 0, # 'е' - 24: 0, # 'ж' - 20: 1, # 'з' - 4: 0, # 'и' - 23: 1, # 'й' - 11: 1, # 'к' - 8: 1, # 'л' - 12: 1, # 'м' - 5: 2, # 'н' - 1: 0, # 'о' - 15: 1, # 'п' - 9: 1, # 'р' - 7: 1, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 0, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 3: { # 'а' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 1, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 3, # 'з' - 4: 3, # 'и' - 23: 3, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 2, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 3, # 'ц' - 22: 3, # 'ч' - 25: 3, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 2, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 21: { # 'б' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 1, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 1, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 0, # 'ф' - 26: 2, # 'х' - 28: 1, # 'ц' - 22: 1, # 'ч' - 25: 2, # 'ш' - 29: 3, # 'щ' - 54: 2, # 'ъ' - 18: 3, # 'ы' - 17: 2, # 'ь' - 30: 1, # 'э' - 27: 2, # 'ю' - 16: 3, # 'я' - }, - 10: { # 'в' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 2, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 1, # 'ж' - 20: 3, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 2, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 3, # 'ш' - 29: 2, # 'щ' - 54: 2, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 3, # 'я' - }, - 19: { # 'г' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 3, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 1, # 'ц' - 22: 2, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 1, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 13: { # 'д' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 3, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 2, # 'х' - 28: 3, # 'ц' - 22: 2, # 'ч' - 25: 2, # 'ш' - 29: 1, # 'щ' - 54: 2, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 1, # 'э' - 27: 2, # 'ю' - 16: 3, # 'я' - }, - 2: { # 'е' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 3, # 'з' - 4: 2, # 'и' - 23: 3, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 2, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 3, # 'ц' - 22: 3, # 'ч' - 25: 3, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 2, # 'ю' - 16: 3, # 'я' - }, - 24: { # 'ж' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 1, # 'в' - 19: 2, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 3, # 'н' - 1: 2, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 1, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 0, # 'х' - 28: 1, # 'ц' - 22: 2, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 2, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 20: { # 'з' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 3, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 1, # 'ц' - 22: 2, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 2, # 'ъ' - 18: 3, # 'ы' - 17: 2, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 3, # 'я' - }, - 4: { # 'и' - 37: 1, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 3, # 'з' - 4: 3, # 'и' - 23: 3, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 2, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 3, # 'ц' - 22: 3, # 'ч' - 25: 3, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 2, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 23: { # 'й' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 1, # 'а' - 21: 1, # 'б' - 10: 1, # 'в' - 19: 2, # 'г' - 13: 3, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 2, # 'з' - 4: 1, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 2, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 1, # 'у' - 39: 2, # 'ф' - 26: 1, # 'х' - 28: 2, # 'ц' - 22: 3, # 'ч' - 25: 2, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 2, # 'я' - }, - 11: { # 'к' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 3, # 'в' - 19: 1, # 'г' - 13: 1, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 3, # 'л' - 12: 1, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 2, # 'х' - 28: 2, # 'ц' - 22: 1, # 'ч' - 25: 2, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 1, # 'ы' - 17: 1, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 8: { # 'л' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 3, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 1, # 'р' - 7: 3, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 2, # 'х' - 28: 1, # 'ц' - 22: 3, # 'ч' - 25: 2, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 1, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 12: { # 'м' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 2, # 'г' - 13: 1, # 'д' - 2: 3, # 'е' - 24: 1, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 2, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 1, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 2, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 3, # 'я' - }, - 5: { # 'н' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 2, # 'х' - 28: 3, # 'ц' - 22: 3, # 'ч' - 25: 2, # 'ш' - 29: 2, # 'щ' - 54: 1, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 1, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 1: { # 'о' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 3, # 'з' - 4: 3, # 'и' - 23: 3, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 2, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 2, # 'ц' - 22: 3, # 'ч' - 25: 3, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 2, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 15: { # 'п' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 3, # 'л' - 12: 1, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 3, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 0, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 1, # 'ш' - 29: 1, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 2, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 3, # 'я' - }, - 9: { # 'р' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 2, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 3, # 'ш' - 29: 2, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 2, # 'э' - 27: 2, # 'ю' - 16: 3, # 'я' - }, - 7: { # 'с' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 1, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 3, # 'в' - 19: 2, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 2, # 'ц' - 22: 3, # 'ч' - 25: 2, # 'ш' - 29: 1, # 'щ' - 54: 2, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 2, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 6: { # 'т' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 2, # 'б' - 10: 3, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 1, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 2, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 2, # 'ш' - 29: 2, # 'щ' - 54: 2, # 'ъ' - 18: 3, # 'ы' - 17: 3, # 'ь' - 30: 2, # 'э' - 27: 2, # 'ю' - 16: 3, # 'я' - }, - 14: { # 'у' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 3, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 3, # 'з' - 4: 2, # 'и' - 23: 2, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 2, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 1, # 'у' - 39: 2, # 'ф' - 26: 3, # 'х' - 28: 2, # 'ц' - 22: 3, # 'ч' - 25: 3, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 2, # 'э' - 27: 3, # 'ю' - 16: 2, # 'я' - }, - 39: { # 'ф' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 0, # 'в' - 19: 1, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 1, # 'н' - 1: 3, # 'о' - 15: 1, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 2, # 'у' - 39: 2, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 1, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 2, # 'ы' - 17: 1, # 'ь' - 30: 2, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 26: { # 'х' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 3, # 'в' - 19: 1, # 'г' - 13: 1, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 1, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 1, # 'п' - 9: 3, # 'р' - 7: 2, # 'с' - 6: 2, # 'т' - 14: 2, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 1, # 'ц' - 22: 1, # 'ч' - 25: 2, # 'ш' - 29: 0, # 'щ' - 54: 1, # 'ъ' - 18: 0, # 'ы' - 17: 1, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 28: { # 'ц' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 1, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 1, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 2, # 'к' - 8: 1, # 'л' - 12: 1, # 'м' - 5: 1, # 'н' - 1: 3, # 'о' - 15: 0, # 'п' - 9: 1, # 'р' - 7: 0, # 'с' - 6: 1, # 'т' - 14: 3, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 1, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 3, # 'ы' - 17: 1, # 'ь' - 30: 0, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 22: { # 'ч' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 1, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 2, # 'л' - 12: 1, # 'м' - 5: 3, # 'н' - 1: 2, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 1, # 'с' - 6: 3, # 'т' - 14: 3, # 'у' - 39: 1, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 1, # 'ч' - 25: 2, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 3, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 25: { # 'ш' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 1, # 'б' - 10: 2, # 'в' - 19: 1, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 2, # 'м' - 5: 3, # 'н' - 1: 3, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 1, # 'с' - 6: 2, # 'т' - 14: 3, # 'у' - 39: 2, # 'ф' - 26: 1, # 'х' - 28: 1, # 'ц' - 22: 1, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 3, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 0, # 'я' - }, - 29: { # 'щ' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 3, # 'а' - 21: 0, # 'б' - 10: 1, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 3, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 3, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 1, # 'м' - 5: 2, # 'н' - 1: 1, # 'о' - 15: 0, # 'п' - 9: 2, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 2, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 2, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 0, # 'я' - }, - 54: { # 'ъ' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 0, # 'б' - 10: 0, # 'в' - 19: 0, # 'г' - 13: 0, # 'д' - 2: 2, # 'е' - 24: 0, # 'ж' - 20: 0, # 'з' - 4: 0, # 'и' - 23: 0, # 'й' - 11: 0, # 'к' - 8: 0, # 'л' - 12: 0, # 'м' - 5: 0, # 'н' - 1: 0, # 'о' - 15: 0, # 'п' - 9: 0, # 'р' - 7: 0, # 'с' - 6: 0, # 'т' - 14: 0, # 'у' - 39: 0, # 'ф' - 26: 0, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 0, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 1, # 'ю' - 16: 2, # 'я' - }, - 18: { # 'ы' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 3, # 'б' - 10: 3, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 2, # 'и' - 23: 3, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 1, # 'о' - 15: 3, # 'п' - 9: 3, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 1, # 'у' - 39: 0, # 'ф' - 26: 3, # 'х' - 28: 2, # 'ц' - 22: 3, # 'ч' - 25: 3, # 'ш' - 29: 2, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 0, # 'ю' - 16: 2, # 'я' - }, - 17: { # 'ь' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 2, # 'б' - 10: 2, # 'в' - 19: 2, # 'г' - 13: 2, # 'д' - 2: 3, # 'е' - 24: 1, # 'ж' - 20: 3, # 'з' - 4: 2, # 'и' - 23: 0, # 'й' - 11: 3, # 'к' - 8: 0, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 2, # 'о' - 15: 2, # 'п' - 9: 1, # 'р' - 7: 3, # 'с' - 6: 2, # 'т' - 14: 0, # 'у' - 39: 2, # 'ф' - 26: 1, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 3, # 'ш' - 29: 2, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 3, # 'ю' - 16: 3, # 'я' - }, - 30: { # 'э' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 1, # 'М' - 31: 1, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 1, # 'Р' - 32: 1, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 1, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 1, # 'б' - 10: 1, # 'в' - 19: 1, # 'г' - 13: 2, # 'д' - 2: 1, # 'е' - 24: 0, # 'ж' - 20: 1, # 'з' - 4: 0, # 'и' - 23: 2, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 0, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 2, # 'с' - 6: 3, # 'т' - 14: 1, # 'у' - 39: 2, # 'ф' - 26: 1, # 'х' - 28: 0, # 'ц' - 22: 0, # 'ч' - 25: 1, # 'ш' - 29: 0, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 1, # 'ю' - 16: 1, # 'я' - }, - 27: { # 'ю' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 2, # 'а' - 21: 3, # 'б' - 10: 1, # 'в' - 19: 2, # 'г' - 13: 3, # 'д' - 2: 1, # 'е' - 24: 2, # 'ж' - 20: 2, # 'з' - 4: 1, # 'и' - 23: 1, # 'й' - 11: 2, # 'к' - 8: 2, # 'л' - 12: 2, # 'м' - 5: 2, # 'н' - 1: 1, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 0, # 'у' - 39: 1, # 'ф' - 26: 2, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 2, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 1, # 'э' - 27: 2, # 'ю' - 16: 1, # 'я' - }, - 16: { # 'я' - 37: 0, # 'А' - 44: 0, # 'Б' - 33: 0, # 'В' - 46: 0, # 'Г' - 41: 0, # 'Д' - 48: 0, # 'Е' - 56: 0, # 'Ж' - 51: 0, # 'З' - 42: 0, # 'И' - 60: 0, # 'Й' - 36: 0, # 'К' - 49: 0, # 'Л' - 38: 0, # 'М' - 31: 0, # 'Н' - 34: 0, # 'О' - 35: 0, # 'П' - 45: 0, # 'Р' - 32: 0, # 'С' - 40: 0, # 'Т' - 52: 0, # 'У' - 53: 0, # 'Ф' - 55: 0, # 'Х' - 58: 0, # 'Ц' - 50: 0, # 'Ч' - 57: 0, # 'Ш' - 63: 0, # 'Щ' - 62: 0, # 'Ы' - 61: 0, # 'Ь' - 47: 0, # 'Э' - 59: 0, # 'Ю' - 43: 0, # 'Я' - 3: 0, # 'а' - 21: 2, # 'б' - 10: 3, # 'в' - 19: 2, # 'г' - 13: 3, # 'д' - 2: 3, # 'е' - 24: 3, # 'ж' - 20: 3, # 'з' - 4: 2, # 'и' - 23: 2, # 'й' - 11: 3, # 'к' - 8: 3, # 'л' - 12: 3, # 'м' - 5: 3, # 'н' - 1: 0, # 'о' - 15: 2, # 'п' - 9: 2, # 'р' - 7: 3, # 'с' - 6: 3, # 'т' - 14: 1, # 'у' - 39: 1, # 'ф' - 26: 3, # 'х' - 28: 2, # 'ц' - 22: 2, # 'ч' - 25: 2, # 'ш' - 29: 3, # 'щ' - 54: 0, # 'ъ' - 18: 0, # 'ы' - 17: 0, # 'ь' - 30: 0, # 'э' - 27: 2, # 'ю' - 16: 2, # 'я' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -IBM866_RUSSIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 142, # 'A' - 66: 143, # 'B' - 67: 144, # 'C' - 68: 145, # 'D' - 69: 146, # 'E' - 70: 147, # 'F' - 71: 148, # 'G' - 72: 149, # 'H' - 73: 150, # 'I' - 74: 151, # 'J' - 75: 152, # 'K' - 76: 74, # 'L' - 77: 153, # 'M' - 78: 75, # 'N' - 79: 154, # 'O' - 80: 155, # 'P' - 81: 156, # 'Q' - 82: 157, # 'R' - 83: 158, # 'S' - 84: 159, # 'T' - 85: 160, # 'U' - 86: 161, # 'V' - 87: 162, # 'W' - 88: 163, # 'X' - 89: 164, # 'Y' - 90: 165, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 71, # 'a' - 98: 172, # 'b' - 99: 66, # 'c' - 100: 173, # 'd' - 101: 65, # 'e' - 102: 174, # 'f' - 103: 76, # 'g' - 104: 175, # 'h' - 105: 64, # 'i' - 106: 176, # 'j' - 107: 177, # 'k' - 108: 77, # 'l' - 109: 72, # 'm' - 110: 178, # 'n' - 111: 69, # 'o' - 112: 67, # 'p' - 113: 179, # 'q' - 114: 78, # 'r' - 115: 73, # 's' - 116: 180, # 't' - 117: 181, # 'u' - 118: 79, # 'v' - 119: 182, # 'w' - 120: 183, # 'x' - 121: 184, # 'y' - 122: 185, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 37, # 'А' - 129: 44, # 'Б' - 130: 33, # 'В' - 131: 46, # 'Г' - 132: 41, # 'Д' - 133: 48, # 'Е' - 134: 56, # 'Ж' - 135: 51, # 'З' - 136: 42, # 'И' - 137: 60, # 'Й' - 138: 36, # 'К' - 139: 49, # 'Л' - 140: 38, # 'М' - 141: 31, # 'Н' - 142: 34, # 'О' - 143: 35, # 'П' - 144: 45, # 'Р' - 145: 32, # 'С' - 146: 40, # 'Т' - 147: 52, # 'У' - 148: 53, # 'Ф' - 149: 55, # 'Х' - 150: 58, # 'Ц' - 151: 50, # 'Ч' - 152: 57, # 'Ш' - 153: 63, # 'Щ' - 154: 70, # 'Ъ' - 155: 62, # 'Ы' - 156: 61, # 'Ь' - 157: 47, # 'Э' - 158: 59, # 'Ю' - 159: 43, # 'Я' - 160: 3, # 'а' - 161: 21, # 'б' - 162: 10, # 'в' - 163: 19, # 'г' - 164: 13, # 'д' - 165: 2, # 'е' - 166: 24, # 'ж' - 167: 20, # 'з' - 168: 4, # 'и' - 169: 23, # 'й' - 170: 11, # 'к' - 171: 8, # 'л' - 172: 12, # 'м' - 173: 5, # 'н' - 174: 1, # 'о' - 175: 15, # 'п' - 176: 191, # '░' - 177: 192, # '▒' - 178: 193, # '▓' - 179: 194, # '│' - 180: 195, # '┤' - 181: 196, # '╡' - 182: 197, # '╢' - 183: 198, # '╖' - 184: 199, # '╕' - 185: 200, # '╣' - 186: 201, # '║' - 187: 202, # '╗' - 188: 203, # '╝' - 189: 204, # '╜' - 190: 205, # '╛' - 191: 206, # '┐' - 192: 207, # '└' - 193: 208, # '┴' - 194: 209, # '┬' - 195: 210, # '├' - 196: 211, # '─' - 197: 212, # '┼' - 198: 213, # '╞' - 199: 214, # '╟' - 200: 215, # '╚' - 201: 216, # '╔' - 202: 217, # '╩' - 203: 218, # '╦' - 204: 219, # '╠' - 205: 220, # '═' - 206: 221, # '╬' - 207: 222, # '╧' - 208: 223, # '╨' - 209: 224, # '╤' - 210: 225, # '╥' - 211: 226, # '╙' - 212: 227, # '╘' - 213: 228, # '╒' - 214: 229, # '╓' - 215: 230, # '╫' - 216: 231, # '╪' - 217: 232, # '┘' - 218: 233, # '┌' - 219: 234, # '█' - 220: 235, # '▄' - 221: 236, # '▌' - 222: 237, # '▐' - 223: 238, # '▀' - 224: 9, # 'р' - 225: 7, # 'с' - 226: 6, # 'т' - 227: 14, # 'у' - 228: 39, # 'ф' - 229: 26, # 'х' - 230: 28, # 'ц' - 231: 22, # 'ч' - 232: 25, # 'ш' - 233: 29, # 'щ' - 234: 54, # 'ъ' - 235: 18, # 'ы' - 236: 17, # 'ь' - 237: 30, # 'э' - 238: 27, # 'ю' - 239: 16, # 'я' - 240: 239, # 'Ё' - 241: 68, # 'ё' - 242: 240, # 'Є' - 243: 241, # 'є' - 244: 242, # 'Ї' - 245: 243, # 'ї' - 246: 244, # 'Ў' - 247: 245, # 'ў' - 248: 246, # '°' - 249: 247, # '∙' - 250: 248, # '·' - 251: 249, # '√' - 252: 250, # '№' - 253: 251, # '¤' - 254: 252, # '■' - 255: 255, # '\xa0' -} - -IBM866_RUSSIAN_MODEL = SingleByteCharSetModel( - charset_name="IBM866", - language="Russian", - char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER, - language_model=RUSSIAN_LANG_MODEL, - typical_positive_ratio=0.976601, - keep_ascii_letters=False, - alphabet="ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё", -) - -WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 142, # 'A' - 66: 143, # 'B' - 67: 144, # 'C' - 68: 145, # 'D' - 69: 146, # 'E' - 70: 147, # 'F' - 71: 148, # 'G' - 72: 149, # 'H' - 73: 150, # 'I' - 74: 151, # 'J' - 75: 152, # 'K' - 76: 74, # 'L' - 77: 153, # 'M' - 78: 75, # 'N' - 79: 154, # 'O' - 80: 155, # 'P' - 81: 156, # 'Q' - 82: 157, # 'R' - 83: 158, # 'S' - 84: 159, # 'T' - 85: 160, # 'U' - 86: 161, # 'V' - 87: 162, # 'W' - 88: 163, # 'X' - 89: 164, # 'Y' - 90: 165, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 71, # 'a' - 98: 172, # 'b' - 99: 66, # 'c' - 100: 173, # 'd' - 101: 65, # 'e' - 102: 174, # 'f' - 103: 76, # 'g' - 104: 175, # 'h' - 105: 64, # 'i' - 106: 176, # 'j' - 107: 177, # 'k' - 108: 77, # 'l' - 109: 72, # 'm' - 110: 178, # 'n' - 111: 69, # 'o' - 112: 67, # 'p' - 113: 179, # 'q' - 114: 78, # 'r' - 115: 73, # 's' - 116: 180, # 't' - 117: 181, # 'u' - 118: 79, # 'v' - 119: 182, # 'w' - 120: 183, # 'x' - 121: 184, # 'y' - 122: 185, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 191, # 'Ђ' - 129: 192, # 'Ѓ' - 130: 193, # '‚' - 131: 194, # 'ѓ' - 132: 195, # '„' - 133: 196, # '…' - 134: 197, # '†' - 135: 198, # '‡' - 136: 199, # '€' - 137: 200, # '‰' - 138: 201, # 'Љ' - 139: 202, # '‹' - 140: 203, # 'Њ' - 141: 204, # 'Ќ' - 142: 205, # 'Ћ' - 143: 206, # 'Џ' - 144: 207, # 'ђ' - 145: 208, # '‘' - 146: 209, # '’' - 147: 210, # '“' - 148: 211, # '”' - 149: 212, # '•' - 150: 213, # '–' - 151: 214, # '—' - 152: 215, # None - 153: 216, # '™' - 154: 217, # 'љ' - 155: 218, # '›' - 156: 219, # 'њ' - 157: 220, # 'ќ' - 158: 221, # 'ћ' - 159: 222, # 'џ' - 160: 223, # '\xa0' - 161: 224, # 'Ў' - 162: 225, # 'ў' - 163: 226, # 'Ј' - 164: 227, # '¤' - 165: 228, # 'Ґ' - 166: 229, # '¦' - 167: 230, # '§' - 168: 231, # 'Ё' - 169: 232, # '©' - 170: 233, # 'Є' - 171: 234, # '«' - 172: 235, # '¬' - 173: 236, # '\xad' - 174: 237, # '®' - 175: 238, # 'Ї' - 176: 239, # '°' - 177: 240, # '±' - 178: 241, # 'І' - 179: 242, # 'і' - 180: 243, # 'ґ' - 181: 244, # 'µ' - 182: 245, # '¶' - 183: 246, # '·' - 184: 68, # 'ё' - 185: 247, # '№' - 186: 248, # 'є' - 187: 249, # '»' - 188: 250, # 'ј' - 189: 251, # 'Ѕ' - 190: 252, # 'ѕ' - 191: 253, # 'ї' - 192: 37, # 'А' - 193: 44, # 'Б' - 194: 33, # 'В' - 195: 46, # 'Г' - 196: 41, # 'Д' - 197: 48, # 'Е' - 198: 56, # 'Ж' - 199: 51, # 'З' - 200: 42, # 'И' - 201: 60, # 'Й' - 202: 36, # 'К' - 203: 49, # 'Л' - 204: 38, # 'М' - 205: 31, # 'Н' - 206: 34, # 'О' - 207: 35, # 'П' - 208: 45, # 'Р' - 209: 32, # 'С' - 210: 40, # 'Т' - 211: 52, # 'У' - 212: 53, # 'Ф' - 213: 55, # 'Х' - 214: 58, # 'Ц' - 215: 50, # 'Ч' - 216: 57, # 'Ш' - 217: 63, # 'Щ' - 218: 70, # 'Ъ' - 219: 62, # 'Ы' - 220: 61, # 'Ь' - 221: 47, # 'Э' - 222: 59, # 'Ю' - 223: 43, # 'Я' - 224: 3, # 'а' - 225: 21, # 'б' - 226: 10, # 'в' - 227: 19, # 'г' - 228: 13, # 'д' - 229: 2, # 'е' - 230: 24, # 'ж' - 231: 20, # 'з' - 232: 4, # 'и' - 233: 23, # 'й' - 234: 11, # 'к' - 235: 8, # 'л' - 236: 12, # 'м' - 237: 5, # 'н' - 238: 1, # 'о' - 239: 15, # 'п' - 240: 9, # 'р' - 241: 7, # 'с' - 242: 6, # 'т' - 243: 14, # 'у' - 244: 39, # 'ф' - 245: 26, # 'х' - 246: 28, # 'ц' - 247: 22, # 'ч' - 248: 25, # 'ш' - 249: 29, # 'щ' - 250: 54, # 'ъ' - 251: 18, # 'ы' - 252: 17, # 'ь' - 253: 30, # 'э' - 254: 27, # 'ю' - 255: 16, # 'я' -} - -WINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel( - charset_name="windows-1251", - language="Russian", - char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER, - language_model=RUSSIAN_LANG_MODEL, - typical_positive_ratio=0.976601, - keep_ascii_letters=False, - alphabet="ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё", -) - -IBM855_RUSSIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 142, # 'A' - 66: 143, # 'B' - 67: 144, # 'C' - 68: 145, # 'D' - 69: 146, # 'E' - 70: 147, # 'F' - 71: 148, # 'G' - 72: 149, # 'H' - 73: 150, # 'I' - 74: 151, # 'J' - 75: 152, # 'K' - 76: 74, # 'L' - 77: 153, # 'M' - 78: 75, # 'N' - 79: 154, # 'O' - 80: 155, # 'P' - 81: 156, # 'Q' - 82: 157, # 'R' - 83: 158, # 'S' - 84: 159, # 'T' - 85: 160, # 'U' - 86: 161, # 'V' - 87: 162, # 'W' - 88: 163, # 'X' - 89: 164, # 'Y' - 90: 165, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 71, # 'a' - 98: 172, # 'b' - 99: 66, # 'c' - 100: 173, # 'd' - 101: 65, # 'e' - 102: 174, # 'f' - 103: 76, # 'g' - 104: 175, # 'h' - 105: 64, # 'i' - 106: 176, # 'j' - 107: 177, # 'k' - 108: 77, # 'l' - 109: 72, # 'm' - 110: 178, # 'n' - 111: 69, # 'o' - 112: 67, # 'p' - 113: 179, # 'q' - 114: 78, # 'r' - 115: 73, # 's' - 116: 180, # 't' - 117: 181, # 'u' - 118: 79, # 'v' - 119: 182, # 'w' - 120: 183, # 'x' - 121: 184, # 'y' - 122: 185, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 191, # 'ђ' - 129: 192, # 'Ђ' - 130: 193, # 'ѓ' - 131: 194, # 'Ѓ' - 132: 68, # 'ё' - 133: 195, # 'Ё' - 134: 196, # 'є' - 135: 197, # 'Є' - 136: 198, # 'ѕ' - 137: 199, # 'Ѕ' - 138: 200, # 'і' - 139: 201, # 'І' - 140: 202, # 'ї' - 141: 203, # 'Ї' - 142: 204, # 'ј' - 143: 205, # 'Ј' - 144: 206, # 'љ' - 145: 207, # 'Љ' - 146: 208, # 'њ' - 147: 209, # 'Њ' - 148: 210, # 'ћ' - 149: 211, # 'Ћ' - 150: 212, # 'ќ' - 151: 213, # 'Ќ' - 152: 214, # 'ў' - 153: 215, # 'Ў' - 154: 216, # 'џ' - 155: 217, # 'Џ' - 156: 27, # 'ю' - 157: 59, # 'Ю' - 158: 54, # 'ъ' - 159: 70, # 'Ъ' - 160: 3, # 'а' - 161: 37, # 'А' - 162: 21, # 'б' - 163: 44, # 'Б' - 164: 28, # 'ц' - 165: 58, # 'Ц' - 166: 13, # 'д' - 167: 41, # 'Д' - 168: 2, # 'е' - 169: 48, # 'Е' - 170: 39, # 'ф' - 171: 53, # 'Ф' - 172: 19, # 'г' - 173: 46, # 'Г' - 174: 218, # '«' - 175: 219, # '»' - 176: 220, # '░' - 177: 221, # '▒' - 178: 222, # '▓' - 179: 223, # '│' - 180: 224, # '┤' - 181: 26, # 'х' - 182: 55, # 'Х' - 183: 4, # 'и' - 184: 42, # 'И' - 185: 225, # '╣' - 186: 226, # '║' - 187: 227, # '╗' - 188: 228, # '╝' - 189: 23, # 'й' - 190: 60, # 'Й' - 191: 229, # '┐' - 192: 230, # '└' - 193: 231, # '┴' - 194: 232, # '┬' - 195: 233, # '├' - 196: 234, # '─' - 197: 235, # '┼' - 198: 11, # 'к' - 199: 36, # 'К' - 200: 236, # '╚' - 201: 237, # '╔' - 202: 238, # '╩' - 203: 239, # '╦' - 204: 240, # '╠' - 205: 241, # '═' - 206: 242, # '╬' - 207: 243, # '¤' - 208: 8, # 'л' - 209: 49, # 'Л' - 210: 12, # 'м' - 211: 38, # 'М' - 212: 5, # 'н' - 213: 31, # 'Н' - 214: 1, # 'о' - 215: 34, # 'О' - 216: 15, # 'п' - 217: 244, # '┘' - 218: 245, # '┌' - 219: 246, # '█' - 220: 247, # '▄' - 221: 35, # 'П' - 222: 16, # 'я' - 223: 248, # '▀' - 224: 43, # 'Я' - 225: 9, # 'р' - 226: 45, # 'Р' - 227: 7, # 'с' - 228: 32, # 'С' - 229: 6, # 'т' - 230: 40, # 'Т' - 231: 14, # 'у' - 232: 52, # 'У' - 233: 24, # 'ж' - 234: 56, # 'Ж' - 235: 10, # 'в' - 236: 33, # 'В' - 237: 17, # 'ь' - 238: 61, # 'Ь' - 239: 249, # '№' - 240: 250, # '\xad' - 241: 18, # 'ы' - 242: 62, # 'Ы' - 243: 20, # 'з' - 244: 51, # 'З' - 245: 25, # 'ш' - 246: 57, # 'Ш' - 247: 30, # 'э' - 248: 47, # 'Э' - 249: 29, # 'щ' - 250: 63, # 'Щ' - 251: 22, # 'ч' - 252: 50, # 'Ч' - 253: 251, # '§' - 254: 252, # '■' - 255: 255, # '\xa0' -} - -IBM855_RUSSIAN_MODEL = SingleByteCharSetModel( - charset_name="IBM855", - language="Russian", - char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER, - language_model=RUSSIAN_LANG_MODEL, - typical_positive_ratio=0.976601, - keep_ascii_letters=False, - alphabet="ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё", -) - -KOI8_R_RUSSIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 142, # 'A' - 66: 143, # 'B' - 67: 144, # 'C' - 68: 145, # 'D' - 69: 146, # 'E' - 70: 147, # 'F' - 71: 148, # 'G' - 72: 149, # 'H' - 73: 150, # 'I' - 74: 151, # 'J' - 75: 152, # 'K' - 76: 74, # 'L' - 77: 153, # 'M' - 78: 75, # 'N' - 79: 154, # 'O' - 80: 155, # 'P' - 81: 156, # 'Q' - 82: 157, # 'R' - 83: 158, # 'S' - 84: 159, # 'T' - 85: 160, # 'U' - 86: 161, # 'V' - 87: 162, # 'W' - 88: 163, # 'X' - 89: 164, # 'Y' - 90: 165, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 71, # 'a' - 98: 172, # 'b' - 99: 66, # 'c' - 100: 173, # 'd' - 101: 65, # 'e' - 102: 174, # 'f' - 103: 76, # 'g' - 104: 175, # 'h' - 105: 64, # 'i' - 106: 176, # 'j' - 107: 177, # 'k' - 108: 77, # 'l' - 109: 72, # 'm' - 110: 178, # 'n' - 111: 69, # 'o' - 112: 67, # 'p' - 113: 179, # 'q' - 114: 78, # 'r' - 115: 73, # 's' - 116: 180, # 't' - 117: 181, # 'u' - 118: 79, # 'v' - 119: 182, # 'w' - 120: 183, # 'x' - 121: 184, # 'y' - 122: 185, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 191, # '─' - 129: 192, # '│' - 130: 193, # '┌' - 131: 194, # '┐' - 132: 195, # '└' - 133: 196, # '┘' - 134: 197, # '├' - 135: 198, # '┤' - 136: 199, # '┬' - 137: 200, # '┴' - 138: 201, # '┼' - 139: 202, # '▀' - 140: 203, # '▄' - 141: 204, # '█' - 142: 205, # '▌' - 143: 206, # '▐' - 144: 207, # '░' - 145: 208, # '▒' - 146: 209, # '▓' - 147: 210, # '⌠' - 148: 211, # '■' - 149: 212, # '∙' - 150: 213, # '√' - 151: 214, # '≈' - 152: 215, # '≤' - 153: 216, # '≥' - 154: 217, # '\xa0' - 155: 218, # '⌡' - 156: 219, # '°' - 157: 220, # '²' - 158: 221, # '·' - 159: 222, # '÷' - 160: 223, # '═' - 161: 224, # '║' - 162: 225, # '╒' - 163: 68, # 'ё' - 164: 226, # '╓' - 165: 227, # '╔' - 166: 228, # '╕' - 167: 229, # '╖' - 168: 230, # '╗' - 169: 231, # '╘' - 170: 232, # '╙' - 171: 233, # '╚' - 172: 234, # '╛' - 173: 235, # '╜' - 174: 236, # '╝' - 175: 237, # '╞' - 176: 238, # '╟' - 177: 239, # '╠' - 178: 240, # '╡' - 179: 241, # 'Ё' - 180: 242, # '╢' - 181: 243, # '╣' - 182: 244, # '╤' - 183: 245, # '╥' - 184: 246, # '╦' - 185: 247, # '╧' - 186: 248, # '╨' - 187: 249, # '╩' - 188: 250, # '╪' - 189: 251, # '╫' - 190: 252, # '╬' - 191: 253, # '©' - 192: 27, # 'ю' - 193: 3, # 'а' - 194: 21, # 'б' - 195: 28, # 'ц' - 196: 13, # 'д' - 197: 2, # 'е' - 198: 39, # 'ф' - 199: 19, # 'г' - 200: 26, # 'х' - 201: 4, # 'и' - 202: 23, # 'й' - 203: 11, # 'к' - 204: 8, # 'л' - 205: 12, # 'м' - 206: 5, # 'н' - 207: 1, # 'о' - 208: 15, # 'п' - 209: 16, # 'я' - 210: 9, # 'р' - 211: 7, # 'с' - 212: 6, # 'т' - 213: 14, # 'у' - 214: 24, # 'ж' - 215: 10, # 'в' - 216: 17, # 'ь' - 217: 18, # 'ы' - 218: 20, # 'з' - 219: 25, # 'ш' - 220: 30, # 'э' - 221: 29, # 'щ' - 222: 22, # 'ч' - 223: 54, # 'ъ' - 224: 59, # 'Ю' - 225: 37, # 'А' - 226: 44, # 'Б' - 227: 58, # 'Ц' - 228: 41, # 'Д' - 229: 48, # 'Е' - 230: 53, # 'Ф' - 231: 46, # 'Г' - 232: 55, # 'Х' - 233: 42, # 'И' - 234: 60, # 'Й' - 235: 36, # 'К' - 236: 49, # 'Л' - 237: 38, # 'М' - 238: 31, # 'Н' - 239: 34, # 'О' - 240: 35, # 'П' - 241: 43, # 'Я' - 242: 45, # 'Р' - 243: 32, # 'С' - 244: 40, # 'Т' - 245: 52, # 'У' - 246: 56, # 'Ж' - 247: 33, # 'В' - 248: 61, # 'Ь' - 249: 62, # 'Ы' - 250: 51, # 'З' - 251: 57, # 'Ш' - 252: 47, # 'Э' - 253: 63, # 'Щ' - 254: 50, # 'Ч' - 255: 70, # 'Ъ' -} - -KOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel( - charset_name="KOI8-R", - language="Russian", - char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER, - language_model=RUSSIAN_LANG_MODEL, - typical_positive_ratio=0.976601, - keep_ascii_letters=False, - alphabet="ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё", -) - -MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 142, # 'A' - 66: 143, # 'B' - 67: 144, # 'C' - 68: 145, # 'D' - 69: 146, # 'E' - 70: 147, # 'F' - 71: 148, # 'G' - 72: 149, # 'H' - 73: 150, # 'I' - 74: 151, # 'J' - 75: 152, # 'K' - 76: 74, # 'L' - 77: 153, # 'M' - 78: 75, # 'N' - 79: 154, # 'O' - 80: 155, # 'P' - 81: 156, # 'Q' - 82: 157, # 'R' - 83: 158, # 'S' - 84: 159, # 'T' - 85: 160, # 'U' - 86: 161, # 'V' - 87: 162, # 'W' - 88: 163, # 'X' - 89: 164, # 'Y' - 90: 165, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 71, # 'a' - 98: 172, # 'b' - 99: 66, # 'c' - 100: 173, # 'd' - 101: 65, # 'e' - 102: 174, # 'f' - 103: 76, # 'g' - 104: 175, # 'h' - 105: 64, # 'i' - 106: 176, # 'j' - 107: 177, # 'k' - 108: 77, # 'l' - 109: 72, # 'm' - 110: 178, # 'n' - 111: 69, # 'o' - 112: 67, # 'p' - 113: 179, # 'q' - 114: 78, # 'r' - 115: 73, # 's' - 116: 180, # 't' - 117: 181, # 'u' - 118: 79, # 'v' - 119: 182, # 'w' - 120: 183, # 'x' - 121: 184, # 'y' - 122: 185, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 37, # 'А' - 129: 44, # 'Б' - 130: 33, # 'В' - 131: 46, # 'Г' - 132: 41, # 'Д' - 133: 48, # 'Е' - 134: 56, # 'Ж' - 135: 51, # 'З' - 136: 42, # 'И' - 137: 60, # 'Й' - 138: 36, # 'К' - 139: 49, # 'Л' - 140: 38, # 'М' - 141: 31, # 'Н' - 142: 34, # 'О' - 143: 35, # 'П' - 144: 45, # 'Р' - 145: 32, # 'С' - 146: 40, # 'Т' - 147: 52, # 'У' - 148: 53, # 'Ф' - 149: 55, # 'Х' - 150: 58, # 'Ц' - 151: 50, # 'Ч' - 152: 57, # 'Ш' - 153: 63, # 'Щ' - 154: 70, # 'Ъ' - 155: 62, # 'Ы' - 156: 61, # 'Ь' - 157: 47, # 'Э' - 158: 59, # 'Ю' - 159: 43, # 'Я' - 160: 191, # '†' - 161: 192, # '°' - 162: 193, # 'Ґ' - 163: 194, # '£' - 164: 195, # '§' - 165: 196, # '•' - 166: 197, # '¶' - 167: 198, # 'І' - 168: 199, # '®' - 169: 200, # '©' - 170: 201, # '™' - 171: 202, # 'Ђ' - 172: 203, # 'ђ' - 173: 204, # '≠' - 174: 205, # 'Ѓ' - 175: 206, # 'ѓ' - 176: 207, # '∞' - 177: 208, # '±' - 178: 209, # '≤' - 179: 210, # '≥' - 180: 211, # 'і' - 181: 212, # 'µ' - 182: 213, # 'ґ' - 183: 214, # 'Ј' - 184: 215, # 'Є' - 185: 216, # 'є' - 186: 217, # 'Ї' - 187: 218, # 'ї' - 188: 219, # 'Љ' - 189: 220, # 'љ' - 190: 221, # 'Њ' - 191: 222, # 'њ' - 192: 223, # 'ј' - 193: 224, # 'Ѕ' - 194: 225, # '¬' - 195: 226, # '√' - 196: 227, # 'ƒ' - 197: 228, # '≈' - 198: 229, # '∆' - 199: 230, # '«' - 200: 231, # '»' - 201: 232, # '…' - 202: 233, # '\xa0' - 203: 234, # 'Ћ' - 204: 235, # 'ћ' - 205: 236, # 'Ќ' - 206: 237, # 'ќ' - 207: 238, # 'ѕ' - 208: 239, # '–' - 209: 240, # '—' - 210: 241, # '“' - 211: 242, # '”' - 212: 243, # '‘' - 213: 244, # '’' - 214: 245, # '÷' - 215: 246, # '„' - 216: 247, # 'Ў' - 217: 248, # 'ў' - 218: 249, # 'Џ' - 219: 250, # 'џ' - 220: 251, # '№' - 221: 252, # 'Ё' - 222: 68, # 'ё' - 223: 16, # 'я' - 224: 3, # 'а' - 225: 21, # 'б' - 226: 10, # 'в' - 227: 19, # 'г' - 228: 13, # 'д' - 229: 2, # 'е' - 230: 24, # 'ж' - 231: 20, # 'з' - 232: 4, # 'и' - 233: 23, # 'й' - 234: 11, # 'к' - 235: 8, # 'л' - 236: 12, # 'м' - 237: 5, # 'н' - 238: 1, # 'о' - 239: 15, # 'п' - 240: 9, # 'р' - 241: 7, # 'с' - 242: 6, # 'т' - 243: 14, # 'у' - 244: 39, # 'ф' - 245: 26, # 'х' - 246: 28, # 'ц' - 247: 22, # 'ч' - 248: 25, # 'ш' - 249: 29, # 'щ' - 250: 54, # 'ъ' - 251: 18, # 'ы' - 252: 17, # 'ь' - 253: 30, # 'э' - 254: 27, # 'ю' - 255: 255, # '€' -} - -MACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel( - charset_name="MacCyrillic", - language="Russian", - char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER, - language_model=RUSSIAN_LANG_MODEL, - typical_positive_ratio=0.976601, - keep_ascii_letters=False, - alphabet="ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё", -) - -ISO_8859_5_RUSSIAN_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 142, # 'A' - 66: 143, # 'B' - 67: 144, # 'C' - 68: 145, # 'D' - 69: 146, # 'E' - 70: 147, # 'F' - 71: 148, # 'G' - 72: 149, # 'H' - 73: 150, # 'I' - 74: 151, # 'J' - 75: 152, # 'K' - 76: 74, # 'L' - 77: 153, # 'M' - 78: 75, # 'N' - 79: 154, # 'O' - 80: 155, # 'P' - 81: 156, # 'Q' - 82: 157, # 'R' - 83: 158, # 'S' - 84: 159, # 'T' - 85: 160, # 'U' - 86: 161, # 'V' - 87: 162, # 'W' - 88: 163, # 'X' - 89: 164, # 'Y' - 90: 165, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 71, # 'a' - 98: 172, # 'b' - 99: 66, # 'c' - 100: 173, # 'd' - 101: 65, # 'e' - 102: 174, # 'f' - 103: 76, # 'g' - 104: 175, # 'h' - 105: 64, # 'i' - 106: 176, # 'j' - 107: 177, # 'k' - 108: 77, # 'l' - 109: 72, # 'm' - 110: 178, # 'n' - 111: 69, # 'o' - 112: 67, # 'p' - 113: 179, # 'q' - 114: 78, # 'r' - 115: 73, # 's' - 116: 180, # 't' - 117: 181, # 'u' - 118: 79, # 'v' - 119: 182, # 'w' - 120: 183, # 'x' - 121: 184, # 'y' - 122: 185, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 191, # '\x80' - 129: 192, # '\x81' - 130: 193, # '\x82' - 131: 194, # '\x83' - 132: 195, # '\x84' - 133: 196, # '\x85' - 134: 197, # '\x86' - 135: 198, # '\x87' - 136: 199, # '\x88' - 137: 200, # '\x89' - 138: 201, # '\x8a' - 139: 202, # '\x8b' - 140: 203, # '\x8c' - 141: 204, # '\x8d' - 142: 205, # '\x8e' - 143: 206, # '\x8f' - 144: 207, # '\x90' - 145: 208, # '\x91' - 146: 209, # '\x92' - 147: 210, # '\x93' - 148: 211, # '\x94' - 149: 212, # '\x95' - 150: 213, # '\x96' - 151: 214, # '\x97' - 152: 215, # '\x98' - 153: 216, # '\x99' - 154: 217, # '\x9a' - 155: 218, # '\x9b' - 156: 219, # '\x9c' - 157: 220, # '\x9d' - 158: 221, # '\x9e' - 159: 222, # '\x9f' - 160: 223, # '\xa0' - 161: 224, # 'Ё' - 162: 225, # 'Ђ' - 163: 226, # 'Ѓ' - 164: 227, # 'Є' - 165: 228, # 'Ѕ' - 166: 229, # 'І' - 167: 230, # 'Ї' - 168: 231, # 'Ј' - 169: 232, # 'Љ' - 170: 233, # 'Њ' - 171: 234, # 'Ћ' - 172: 235, # 'Ќ' - 173: 236, # '\xad' - 174: 237, # 'Ў' - 175: 238, # 'Џ' - 176: 37, # 'А' - 177: 44, # 'Б' - 178: 33, # 'В' - 179: 46, # 'Г' - 180: 41, # 'Д' - 181: 48, # 'Е' - 182: 56, # 'Ж' - 183: 51, # 'З' - 184: 42, # 'И' - 185: 60, # 'Й' - 186: 36, # 'К' - 187: 49, # 'Л' - 188: 38, # 'М' - 189: 31, # 'Н' - 190: 34, # 'О' - 191: 35, # 'П' - 192: 45, # 'Р' - 193: 32, # 'С' - 194: 40, # 'Т' - 195: 52, # 'У' - 196: 53, # 'Ф' - 197: 55, # 'Х' - 198: 58, # 'Ц' - 199: 50, # 'Ч' - 200: 57, # 'Ш' - 201: 63, # 'Щ' - 202: 70, # 'Ъ' - 203: 62, # 'Ы' - 204: 61, # 'Ь' - 205: 47, # 'Э' - 206: 59, # 'Ю' - 207: 43, # 'Я' - 208: 3, # 'а' - 209: 21, # 'б' - 210: 10, # 'в' - 211: 19, # 'г' - 212: 13, # 'д' - 213: 2, # 'е' - 214: 24, # 'ж' - 215: 20, # 'з' - 216: 4, # 'и' - 217: 23, # 'й' - 218: 11, # 'к' - 219: 8, # 'л' - 220: 12, # 'м' - 221: 5, # 'н' - 222: 1, # 'о' - 223: 15, # 'п' - 224: 9, # 'р' - 225: 7, # 'с' - 226: 6, # 'т' - 227: 14, # 'у' - 228: 39, # 'ф' - 229: 26, # 'х' - 230: 28, # 'ц' - 231: 22, # 'ч' - 232: 25, # 'ш' - 233: 29, # 'щ' - 234: 54, # 'ъ' - 235: 18, # 'ы' - 236: 17, # 'ь' - 237: 30, # 'э' - 238: 27, # 'ю' - 239: 16, # 'я' - 240: 239, # '№' - 241: 68, # 'ё' - 242: 240, # 'ђ' - 243: 241, # 'ѓ' - 244: 242, # 'є' - 245: 243, # 'ѕ' - 246: 244, # 'і' - 247: 245, # 'ї' - 248: 246, # 'ј' - 249: 247, # 'љ' - 250: 248, # 'њ' - 251: 249, # 'ћ' - 252: 250, # 'ќ' - 253: 251, # '§' - 254: 252, # 'ў' - 255: 255, # 'џ' -} - -ISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel( - charset_name="ISO-8859-5", - language="Russian", - char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER, - language_model=RUSSIAN_LANG_MODEL, - typical_positive_ratio=0.976601, - keep_ascii_letters=False, - alphabet="ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё", -) diff --git a/libs/chardet/langthaimodel.py b/libs/chardet/langthaimodel.py deleted file mode 100644 index 883fdb1ea..000000000 --- a/libs/chardet/langthaimodel.py +++ /dev/null @@ -1,4380 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -THAI_LANG_MODEL = { - 5: { # 'ก' - 5: 2, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 2, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 3, # 'ฎ' - 57: 2, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 2, # 'ณ' - 20: 2, # 'ด' - 19: 3, # 'ต' - 44: 0, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 1, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 1, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 2, # 'ม' - 16: 1, # 'ย' - 2: 3, # 'ร' - 61: 2, # 'ฤ' - 15: 3, # 'ล' - 12: 3, # 'ว' - 42: 2, # 'ศ' - 46: 3, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 3, # 'อ' - 63: 1, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 3, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 0, # 'ึ' - 27: 2, # 'ื' - 32: 2, # 'ุ' - 35: 1, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 1, # 'ใ' - 33: 2, # 'ไ' - 50: 1, # 'ๆ' - 37: 3, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 30: { # 'ข' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 1, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 2, # 'ณ' - 20: 0, # 'ด' - 19: 2, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 1, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 2, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 1, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 2, # 'ี' - 40: 3, # 'ึ' - 27: 1, # 'ื' - 32: 1, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 2, # '่' - 7: 3, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 24: { # 'ค' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 2, # 'ค' - 8: 2, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 2, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 0, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 2, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 3, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 2, # 'า' - 36: 3, # 'ำ' - 23: 3, # 'ิ' - 13: 2, # 'ี' - 40: 0, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 2, # 'ู' - 11: 1, # 'เ' - 28: 0, # 'แ' - 41: 3, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 8: { # 'ง' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 3, # 'ค' - 8: 2, # 'ง' - 26: 2, # 'จ' - 52: 1, # 'ฉ' - 34: 2, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 3, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 1, # 'ฝ' - 31: 2, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 2, # 'ม' - 16: 1, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 2, # 'ว' - 42: 2, # 'ศ' - 46: 1, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 1, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 1, # 'ื' - 32: 1, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 3, # 'ๆ' - 37: 0, # '็' - 6: 2, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 26: { # 'จ' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 0, # 'ค' - 8: 2, # 'ง' - 26: 3, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 1, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 1, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 1, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 1, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 3, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 3, # 'ำ' - 23: 2, # 'ิ' - 13: 1, # 'ี' - 40: 3, # 'ึ' - 27: 1, # 'ื' - 32: 3, # 'ุ' - 35: 2, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 2, # '่' - 7: 2, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 52: { # 'ฉ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 3, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 3, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 1, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 1, # 'ั' - 1: 1, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 1, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 34: { # 'ช' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 1, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 1, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 1, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 2, # 'ั' - 1: 3, # 'า' - 36: 1, # 'ำ' - 23: 3, # 'ิ' - 13: 2, # 'ี' - 40: 0, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 1, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 51: { # 'ซ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 1, # 'ั' - 1: 1, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 2, # 'ี' - 40: 3, # 'ึ' - 27: 2, # 'ื' - 32: 1, # 'ุ' - 35: 1, # 'ู' - 11: 1, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 1, # '่' - 7: 2, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 47: { # 'ญ' - 5: 1, # 'ก' - 30: 1, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 3, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 1, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 2, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 2, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 0, # 'ไ' - 50: 1, # 'ๆ' - 37: 0, # '็' - 6: 2, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 58: { # 'ฎ' - 5: 2, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 1, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 2, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 57: { # 'ฏ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 3, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 49: { # 'ฐ' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 2, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 53: { # 'ฑ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 55: { # 'ฒ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 43: { # 'ณ' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 3, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 3, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 1, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 3, # 'ะ' - 10: 0, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 2, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 20: { # 'ด' - 5: 2, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 1, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 2, # 'ม' - 16: 3, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 3, # 'ั' - 1: 2, # 'า' - 36: 2, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 1, # 'ึ' - 27: 2, # 'ื' - 32: 3, # 'ุ' - 35: 2, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 2, # 'ๆ' - 37: 2, # '็' - 6: 1, # '่' - 7: 3, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 19: { # 'ต' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 1, # 'ต' - 44: 2, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 1, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 2, # 'ภ' - 9: 1, # 'ม' - 16: 1, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 3, # 'ส' - 21: 0, # 'ห' - 4: 3, # 'อ' - 63: 1, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 2, # 'ำ' - 23: 3, # 'ิ' - 13: 2, # 'ี' - 40: 1, # 'ึ' - 27: 1, # 'ื' - 32: 3, # 'ุ' - 35: 2, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 1, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 2, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 44: { # 'ถ' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 2, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 2, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 1, # 'ี' - 40: 3, # 'ึ' - 27: 2, # 'ื' - 32: 2, # 'ุ' - 35: 3, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 2, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 14: { # 'ท' - 5: 1, # 'ก' - 30: 1, # 'ข' - 24: 3, # 'ค' - 8: 1, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 3, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 2, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 3, # 'ย' - 2: 3, # 'ร' - 61: 1, # 'ฤ' - 15: 1, # 'ล' - 12: 2, # 'ว' - 42: 3, # 'ศ' - 46: 1, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 3, # 'ำ' - 23: 2, # 'ิ' - 13: 3, # 'ี' - 40: 2, # 'ึ' - 27: 1, # 'ื' - 32: 3, # 'ุ' - 35: 1, # 'ู' - 11: 0, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 48: { # 'ธ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 1, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 2, # 'า' - 36: 0, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 2, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 3: { # 'น' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 3, # 'ค' - 8: 1, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 1, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 2, # 'ถ' - 14: 3, # 'ท' - 48: 3, # 'ธ' - 3: 2, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 0, # 'ฝ' - 31: 2, # 'พ' - 54: 1, # 'ฟ' - 45: 1, # 'ภ' - 9: 2, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 1, # 'ฤ' - 15: 2, # 'ล' - 12: 3, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 3, # 'อ' - 63: 1, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 3, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 3, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 2, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 3, # 'โ' - 29: 3, # 'ใ' - 33: 3, # 'ไ' - 50: 2, # 'ๆ' - 37: 1, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 17: { # 'บ' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 1, # 'ง' - 26: 1, # 'จ' - 52: 1, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 3, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 1, # 'ฟ' - 45: 1, # 'ภ' - 9: 1, # 'ม' - 16: 0, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 3, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 2, # 'อ' - 63: 1, # 'ฯ' - 22: 0, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 2, # 'ำ' - 23: 2, # 'ิ' - 13: 2, # 'ี' - 40: 0, # 'ึ' - 27: 2, # 'ื' - 32: 3, # 'ุ' - 35: 2, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 2, # '่' - 7: 2, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 25: { # 'ป' - 5: 2, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 1, # 'ฎ' - 57: 3, # 'ฏ' - 49: 1, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 1, # 'ต' - 44: 1, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 0, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 1, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 0, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 1, # 'ษ' - 18: 2, # 'ส' - 21: 1, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 3, # 'ั' - 1: 1, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 3, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 1, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 2, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 2, # 'ไ' - 50: 0, # 'ๆ' - 37: 3, # '็' - 6: 1, # '่' - 7: 2, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 39: { # 'ผ' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 1, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 2, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 1, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 1, # 'ื' - 32: 0, # 'ุ' - 35: 3, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 1, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 62: { # 'ฝ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 1, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 1, # 'ี' - 40: 2, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 2, # '่' - 7: 1, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 31: { # 'พ' - 5: 1, # 'ก' - 30: 1, # 'ข' - 24: 1, # 'ค' - 8: 1, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 1, # 'ณ' - 20: 1, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 2, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 0, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 2, # 'ย' - 2: 3, # 'ร' - 61: 2, # 'ฤ' - 15: 2, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 1, # 'ห' - 4: 2, # 'อ' - 63: 1, # 'ฯ' - 22: 0, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 3, # 'ิ' - 13: 2, # 'ี' - 40: 1, # 'ึ' - 27: 3, # 'ื' - 32: 1, # 'ุ' - 35: 2, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 1, # '็' - 6: 0, # '่' - 7: 1, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 54: { # 'ฟ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 2, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 2, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 1, # 'ื' - 32: 1, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 2, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 45: { # 'ภ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 3, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 2, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 9: { # 'ม' - 5: 2, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 2, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 1, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 3, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 2, # 'ม' - 16: 1, # 'ย' - 2: 2, # 'ร' - 61: 2, # 'ฤ' - 15: 2, # 'ล' - 12: 2, # 'ว' - 42: 1, # 'ศ' - 46: 1, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 0, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 3, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 2, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 1, # 'ๆ' - 37: 1, # '็' - 6: 3, # '่' - 7: 2, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 16: { # 'ย' - 5: 3, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 2, # 'ช' - 51: 0, # 'ซ' - 47: 2, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 2, # 'ม' - 16: 0, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 3, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 1, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 3, # 'ี' - 40: 1, # 'ึ' - 27: 2, # 'ื' - 32: 2, # 'ุ' - 35: 3, # 'ู' - 11: 2, # 'เ' - 28: 1, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 2, # 'ๆ' - 37: 1, # '็' - 6: 3, # '่' - 7: 2, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 2: { # 'ร' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 2, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 3, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 3, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 3, # 'ถ' - 14: 3, # 'ท' - 48: 1, # 'ธ' - 3: 2, # 'น' - 17: 2, # 'บ' - 25: 3, # 'ป' - 39: 2, # 'ผ' - 62: 1, # 'ฝ' - 31: 2, # 'พ' - 54: 1, # 'ฟ' - 45: 1, # 'ภ' - 9: 3, # 'ม' - 16: 2, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 3, # 'ว' - 42: 2, # 'ศ' - 46: 2, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 3, # 'อ' - 63: 1, # 'ฯ' - 22: 3, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 2, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 3, # 'ู' - 11: 3, # 'เ' - 28: 3, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 3, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 3, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 61: { # 'ฤ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 2, # 'ต' - 44: 0, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 2, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 15: { # 'ล' - 5: 2, # 'ก' - 30: 3, # 'ข' - 24: 1, # 'ค' - 8: 3, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 1, # 'ม' - 16: 3, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 1, # 'ห' - 4: 3, # 'อ' - 63: 2, # 'ฯ' - 22: 3, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 2, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 2, # 'ึ' - 27: 3, # 'ื' - 32: 2, # 'ุ' - 35: 3, # 'ู' - 11: 2, # 'เ' - 28: 1, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 2, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 12: { # 'ว' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 1, # 'ค' - 8: 3, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 1, # 'ณ' - 20: 2, # 'ด' - 19: 1, # 'ต' - 44: 1, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 1, # 'ฟ' - 45: 0, # 'ภ' - 9: 3, # 'ม' - 16: 3, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 3, # 'ิ' - 13: 2, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 2, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 1, # 'ใ' - 33: 2, # 'ไ' - 50: 1, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 42: { # 'ศ' - 5: 1, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 1, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 2, # 'ว' - 42: 1, # 'ศ' - 46: 2, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 2, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 2, # 'ิ' - 13: 0, # 'ี' - 40: 3, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 2, # 'ู' - 11: 0, # 'เ' - 28: 1, # 'แ' - 41: 0, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 46: { # 'ษ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 2, # 'ฎ' - 57: 1, # 'ฏ' - 49: 2, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 3, # 'ณ' - 20: 0, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 1, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 2, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 18: { # 'ส' - 5: 2, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 2, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 3, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 2, # 'ภ' - 9: 3, # 'ม' - 16: 1, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 2, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 3, # 'ำ' - 23: 3, # 'ิ' - 13: 3, # 'ี' - 40: 2, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 3, # 'ู' - 11: 2, # 'เ' - 28: 0, # 'แ' - 41: 1, # 'โ' - 29: 0, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 1, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 21: { # 'ห' - 5: 3, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 1, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 2, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 3, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 0, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 3, # 'ม' - 16: 2, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 0, # 'ำ' - 23: 1, # 'ิ' - 13: 1, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 1, # 'ุ' - 35: 1, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 3, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 4: { # 'อ' - 5: 3, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 1, # 'ฟ' - 45: 1, # 'ภ' - 9: 3, # 'ม' - 16: 3, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 2, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 2, # 'ะ' - 10: 3, # 'ั' - 1: 3, # 'า' - 36: 2, # 'ำ' - 23: 2, # 'ิ' - 13: 3, # 'ี' - 40: 0, # 'ึ' - 27: 3, # 'ื' - 32: 3, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 1, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 1, # 'ๆ' - 37: 1, # '็' - 6: 2, # '่' - 7: 2, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 63: { # 'ฯ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 22: { # 'ะ' - 5: 3, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 1, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 3, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 1, # 'ถ' - 14: 3, # 'ท' - 48: 1, # 'ธ' - 3: 2, # 'น' - 17: 3, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 2, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 3, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 2, # 'อ' - 63: 1, # 'ฯ' - 22: 1, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 10: { # 'ั' - 5: 3, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 3, # 'ง' - 26: 3, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 3, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 2, # 'ฐ' - 53: 0, # 'ฑ' - 55: 3, # 'ฒ' - 43: 3, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 0, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 2, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 3, # 'ม' - 16: 3, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 3, # 'ว' - 42: 2, # 'ศ' - 46: 0, # 'ษ' - 18: 3, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 1: { # 'า' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 3, # 'ค' - 8: 3, # 'ง' - 26: 3, # 'จ' - 52: 0, # 'ฉ' - 34: 3, # 'ช' - 51: 1, # 'ซ' - 47: 2, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 3, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 1, # 'ถ' - 14: 3, # 'ท' - 48: 2, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 1, # 'ฝ' - 31: 3, # 'พ' - 54: 1, # 'ฟ' - 45: 1, # 'ภ' - 9: 3, # 'ม' - 16: 3, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 3, # 'ว' - 42: 2, # 'ศ' - 46: 3, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 2, # 'อ' - 63: 1, # 'ฯ' - 22: 3, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 1, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 36: { # 'ำ' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 3, # 'ค' - 8: 2, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 1, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 1, # 'ต' - 44: 1, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 1, # 'บ' - 25: 1, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 1, # 'ม' - 16: 0, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 3, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 23: { # 'ิ' - 5: 3, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 3, # 'จ' - 52: 0, # 'ฉ' - 34: 3, # 'ช' - 51: 0, # 'ซ' - 47: 2, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 1, # 'ถ' - 14: 3, # 'ท' - 48: 3, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 0, # 'ฝ' - 31: 3, # 'พ' - 54: 1, # 'ฟ' - 45: 2, # 'ภ' - 9: 3, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 3, # 'ว' - 42: 3, # 'ศ' - 46: 2, # 'ษ' - 18: 2, # 'ส' - 21: 3, # 'ห' - 4: 1, # 'อ' - 63: 1, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 1, # 'แ' - 41: 1, # 'โ' - 29: 1, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 2, # '้' - 38: 2, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 13: { # 'ี' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 1, # 'ผ' - 62: 0, # 'ฝ' - 31: 2, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 3, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 2, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 1, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 1, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 40: { # 'ึ' - 5: 3, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 3, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 1, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 27: { # 'ื' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 3, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 32: { # 'ุ' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 3, # 'ค' - 8: 3, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 2, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 1, # 'ฒ' - 43: 3, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 1, # 'ธ' - 3: 2, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 1, # 'ภ' - 9: 3, # 'ม' - 16: 1, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 1, # 'ว' - 42: 1, # 'ศ' - 46: 2, # 'ษ' - 18: 1, # 'ส' - 21: 1, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 0, # 'แ' - 41: 1, # 'โ' - 29: 0, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 2, # '้' - 38: 1, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 35: { # 'ู' - 5: 3, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 2, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 2, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 1, # 'ณ' - 20: 2, # 'ด' - 19: 2, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 2, # 'น' - 17: 0, # 'บ' - 25: 3, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 1, # 'แ' - 41: 1, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 3, # '่' - 7: 3, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 11: { # 'เ' - 5: 3, # 'ก' - 30: 3, # 'ข' - 24: 3, # 'ค' - 8: 2, # 'ง' - 26: 3, # 'จ' - 52: 3, # 'ฉ' - 34: 3, # 'ช' - 51: 2, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 1, # 'ณ' - 20: 3, # 'ด' - 19: 3, # 'ต' - 44: 1, # 'ถ' - 14: 3, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 3, # 'ป' - 39: 2, # 'ผ' - 62: 1, # 'ฝ' - 31: 3, # 'พ' - 54: 1, # 'ฟ' - 45: 3, # 'ภ' - 9: 3, # 'ม' - 16: 2, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 3, # 'ว' - 42: 2, # 'ศ' - 46: 0, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 28: { # 'แ' - 5: 3, # 'ก' - 30: 2, # 'ข' - 24: 2, # 'ค' - 8: 1, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 3, # 'ต' - 44: 2, # 'ถ' - 14: 3, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 2, # 'ป' - 39: 3, # 'ผ' - 62: 0, # 'ฝ' - 31: 2, # 'พ' - 54: 2, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 41: { # 'โ' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 1, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 2, # 'ต' - 44: 0, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 1, # 'บ' - 25: 3, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 1, # 'ฟ' - 45: 1, # 'ภ' - 9: 1, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 3, # 'ล' - 12: 0, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 0, # 'ห' - 4: 2, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 29: { # 'ใ' - 5: 2, # 'ก' - 30: 0, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 3, # 'จ' - 52: 0, # 'ฉ' - 34: 3, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 1, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 3, # 'ส' - 21: 3, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 33: { # 'ไ' - 5: 1, # 'ก' - 30: 2, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 3, # 'ด' - 19: 1, # 'ต' - 44: 0, # 'ถ' - 14: 3, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 1, # 'บ' - 25: 3, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 2, # 'ฟ' - 45: 0, # 'ภ' - 9: 3, # 'ม' - 16: 0, # 'ย' - 2: 3, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 3, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 2, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 50: { # 'ๆ' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 37: { # '็' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 2, # 'ง' - 26: 3, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 1, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 2, # 'ต' - 44: 0, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 3, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 1, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 2, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 0, # 'ห' - 4: 1, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 1, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 6: { # '่' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 1, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 1, # 'ธ' - 3: 3, # 'น' - 17: 1, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 1, # 'ฝ' - 31: 1, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 3, # 'ม' - 16: 3, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 2, # 'ล' - 12: 3, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 1, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 1, # 'ะ' - 10: 0, # 'ั' - 1: 3, # 'า' - 36: 2, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 3, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 1, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 7: { # '้' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 2, # 'ค' - 8: 3, # 'ง' - 26: 2, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 1, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 1, # 'ด' - 19: 2, # 'ต' - 44: 1, # 'ถ' - 14: 2, # 'ท' - 48: 0, # 'ธ' - 3: 3, # 'น' - 17: 2, # 'บ' - 25: 2, # 'ป' - 39: 2, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 1, # 'ฟ' - 45: 0, # 'ภ' - 9: 3, # 'ม' - 16: 2, # 'ย' - 2: 2, # 'ร' - 61: 0, # 'ฤ' - 15: 1, # 'ล' - 12: 3, # 'ว' - 42: 1, # 'ศ' - 46: 0, # 'ษ' - 18: 2, # 'ส' - 21: 2, # 'ห' - 4: 3, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 3, # 'า' - 36: 2, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 2, # 'ใ' - 33: 2, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 38: { # '์' - 5: 2, # 'ก' - 30: 1, # 'ข' - 24: 1, # 'ค' - 8: 0, # 'ง' - 26: 1, # 'จ' - 52: 0, # 'ฉ' - 34: 1, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 2, # 'ด' - 19: 1, # 'ต' - 44: 1, # 'ถ' - 14: 1, # 'ท' - 48: 0, # 'ธ' - 3: 1, # 'น' - 17: 1, # 'บ' - 25: 1, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 1, # 'พ' - 54: 1, # 'ฟ' - 45: 0, # 'ภ' - 9: 2, # 'ม' - 16: 0, # 'ย' - 2: 1, # 'ร' - 61: 1, # 'ฤ' - 15: 1, # 'ล' - 12: 1, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 1, # 'ส' - 21: 1, # 'ห' - 4: 2, # 'อ' - 63: 1, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 2, # 'เ' - 28: 2, # 'แ' - 41: 1, # 'โ' - 29: 1, # 'ใ' - 33: 1, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 0, # '๑' - 59: 0, # '๒' - 60: 0, # '๕' - }, - 56: { # '๑' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 2, # '๑' - 59: 1, # '๒' - 60: 1, # '๕' - }, - 59: { # '๒' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 1, # '๑' - 59: 1, # '๒' - 60: 3, # '๕' - }, - 60: { # '๕' - 5: 0, # 'ก' - 30: 0, # 'ข' - 24: 0, # 'ค' - 8: 0, # 'ง' - 26: 0, # 'จ' - 52: 0, # 'ฉ' - 34: 0, # 'ช' - 51: 0, # 'ซ' - 47: 0, # 'ญ' - 58: 0, # 'ฎ' - 57: 0, # 'ฏ' - 49: 0, # 'ฐ' - 53: 0, # 'ฑ' - 55: 0, # 'ฒ' - 43: 0, # 'ณ' - 20: 0, # 'ด' - 19: 0, # 'ต' - 44: 0, # 'ถ' - 14: 0, # 'ท' - 48: 0, # 'ธ' - 3: 0, # 'น' - 17: 0, # 'บ' - 25: 0, # 'ป' - 39: 0, # 'ผ' - 62: 0, # 'ฝ' - 31: 0, # 'พ' - 54: 0, # 'ฟ' - 45: 0, # 'ภ' - 9: 0, # 'ม' - 16: 0, # 'ย' - 2: 0, # 'ร' - 61: 0, # 'ฤ' - 15: 0, # 'ล' - 12: 0, # 'ว' - 42: 0, # 'ศ' - 46: 0, # 'ษ' - 18: 0, # 'ส' - 21: 0, # 'ห' - 4: 0, # 'อ' - 63: 0, # 'ฯ' - 22: 0, # 'ะ' - 10: 0, # 'ั' - 1: 0, # 'า' - 36: 0, # 'ำ' - 23: 0, # 'ิ' - 13: 0, # 'ี' - 40: 0, # 'ึ' - 27: 0, # 'ื' - 32: 0, # 'ุ' - 35: 0, # 'ู' - 11: 0, # 'เ' - 28: 0, # 'แ' - 41: 0, # 'โ' - 29: 0, # 'ใ' - 33: 0, # 'ไ' - 50: 0, # 'ๆ' - 37: 0, # '็' - 6: 0, # '่' - 7: 0, # '้' - 38: 0, # '์' - 56: 2, # '๑' - 59: 1, # '๒' - 60: 0, # '๕' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -TIS_620_THAI_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 254, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 254, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 253, # ' ' - 33: 253, # '!' - 34: 253, # '"' - 35: 253, # '#' - 36: 253, # '$' - 37: 253, # '%' - 38: 253, # '&' - 39: 253, # "'" - 40: 253, # '(' - 41: 253, # ')' - 42: 253, # '*' - 43: 253, # '+' - 44: 253, # ',' - 45: 253, # '-' - 46: 253, # '.' - 47: 253, # '/' - 48: 252, # '0' - 49: 252, # '1' - 50: 252, # '2' - 51: 252, # '3' - 52: 252, # '4' - 53: 252, # '5' - 54: 252, # '6' - 55: 252, # '7' - 56: 252, # '8' - 57: 252, # '9' - 58: 253, # ':' - 59: 253, # ';' - 60: 253, # '<' - 61: 253, # '=' - 62: 253, # '>' - 63: 253, # '?' - 64: 253, # '@' - 65: 182, # 'A' - 66: 106, # 'B' - 67: 107, # 'C' - 68: 100, # 'D' - 69: 183, # 'E' - 70: 184, # 'F' - 71: 185, # 'G' - 72: 101, # 'H' - 73: 94, # 'I' - 74: 186, # 'J' - 75: 187, # 'K' - 76: 108, # 'L' - 77: 109, # 'M' - 78: 110, # 'N' - 79: 111, # 'O' - 80: 188, # 'P' - 81: 189, # 'Q' - 82: 190, # 'R' - 83: 89, # 'S' - 84: 95, # 'T' - 85: 112, # 'U' - 86: 113, # 'V' - 87: 191, # 'W' - 88: 192, # 'X' - 89: 193, # 'Y' - 90: 194, # 'Z' - 91: 253, # '[' - 92: 253, # '\\' - 93: 253, # ']' - 94: 253, # '^' - 95: 253, # '_' - 96: 253, # '`' - 97: 64, # 'a' - 98: 72, # 'b' - 99: 73, # 'c' - 100: 114, # 'd' - 101: 74, # 'e' - 102: 115, # 'f' - 103: 116, # 'g' - 104: 102, # 'h' - 105: 81, # 'i' - 106: 201, # 'j' - 107: 117, # 'k' - 108: 90, # 'l' - 109: 103, # 'm' - 110: 78, # 'n' - 111: 82, # 'o' - 112: 96, # 'p' - 113: 202, # 'q' - 114: 91, # 'r' - 115: 79, # 's' - 116: 84, # 't' - 117: 104, # 'u' - 118: 105, # 'v' - 119: 97, # 'w' - 120: 98, # 'x' - 121: 92, # 'y' - 122: 203, # 'z' - 123: 253, # '{' - 124: 253, # '|' - 125: 253, # '}' - 126: 253, # '~' - 127: 253, # '\x7f' - 128: 209, # '\x80' - 129: 210, # '\x81' - 130: 211, # '\x82' - 131: 212, # '\x83' - 132: 213, # '\x84' - 133: 88, # '\x85' - 134: 214, # '\x86' - 135: 215, # '\x87' - 136: 216, # '\x88' - 137: 217, # '\x89' - 138: 218, # '\x8a' - 139: 219, # '\x8b' - 140: 220, # '\x8c' - 141: 118, # '\x8d' - 142: 221, # '\x8e' - 143: 222, # '\x8f' - 144: 223, # '\x90' - 145: 224, # '\x91' - 146: 99, # '\x92' - 147: 85, # '\x93' - 148: 83, # '\x94' - 149: 225, # '\x95' - 150: 226, # '\x96' - 151: 227, # '\x97' - 152: 228, # '\x98' - 153: 229, # '\x99' - 154: 230, # '\x9a' - 155: 231, # '\x9b' - 156: 232, # '\x9c' - 157: 233, # '\x9d' - 158: 234, # '\x9e' - 159: 235, # '\x9f' - 160: 236, # None - 161: 5, # 'ก' - 162: 30, # 'ข' - 163: 237, # 'ฃ' - 164: 24, # 'ค' - 165: 238, # 'ฅ' - 166: 75, # 'ฆ' - 167: 8, # 'ง' - 168: 26, # 'จ' - 169: 52, # 'ฉ' - 170: 34, # 'ช' - 171: 51, # 'ซ' - 172: 119, # 'ฌ' - 173: 47, # 'ญ' - 174: 58, # 'ฎ' - 175: 57, # 'ฏ' - 176: 49, # 'ฐ' - 177: 53, # 'ฑ' - 178: 55, # 'ฒ' - 179: 43, # 'ณ' - 180: 20, # 'ด' - 181: 19, # 'ต' - 182: 44, # 'ถ' - 183: 14, # 'ท' - 184: 48, # 'ธ' - 185: 3, # 'น' - 186: 17, # 'บ' - 187: 25, # 'ป' - 188: 39, # 'ผ' - 189: 62, # 'ฝ' - 190: 31, # 'พ' - 191: 54, # 'ฟ' - 192: 45, # 'ภ' - 193: 9, # 'ม' - 194: 16, # 'ย' - 195: 2, # 'ร' - 196: 61, # 'ฤ' - 197: 15, # 'ล' - 198: 239, # 'ฦ' - 199: 12, # 'ว' - 200: 42, # 'ศ' - 201: 46, # 'ษ' - 202: 18, # 'ส' - 203: 21, # 'ห' - 204: 76, # 'ฬ' - 205: 4, # 'อ' - 206: 66, # 'ฮ' - 207: 63, # 'ฯ' - 208: 22, # 'ะ' - 209: 10, # 'ั' - 210: 1, # 'า' - 211: 36, # 'ำ' - 212: 23, # 'ิ' - 213: 13, # 'ี' - 214: 40, # 'ึ' - 215: 27, # 'ื' - 216: 32, # 'ุ' - 217: 35, # 'ู' - 218: 86, # 'ฺ' - 219: 240, # None - 220: 241, # None - 221: 242, # None - 222: 243, # None - 223: 244, # '฿' - 224: 11, # 'เ' - 225: 28, # 'แ' - 226: 41, # 'โ' - 227: 29, # 'ใ' - 228: 33, # 'ไ' - 229: 245, # 'ๅ' - 230: 50, # 'ๆ' - 231: 37, # '็' - 232: 6, # '่' - 233: 7, # '้' - 234: 67, # '๊' - 235: 77, # '๋' - 236: 38, # '์' - 237: 93, # 'ํ' - 238: 246, # '๎' - 239: 247, # '๏' - 240: 68, # '๐' - 241: 56, # '๑' - 242: 59, # '๒' - 243: 65, # '๓' - 244: 69, # '๔' - 245: 60, # '๕' - 246: 70, # '๖' - 247: 80, # '๗' - 248: 71, # '๘' - 249: 87, # '๙' - 250: 248, # '๚' - 251: 249, # '๛' - 252: 250, # None - 253: 251, # None - 254: 252, # None - 255: 253, # None -} - -TIS_620_THAI_MODEL = SingleByteCharSetModel( - charset_name="TIS-620", - language="Thai", - char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER, - language_model=THAI_LANG_MODEL, - typical_positive_ratio=0.926386, - keep_ascii_letters=False, - alphabet="กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛", -) diff --git a/libs/chardet/langturkishmodel.py b/libs/chardet/langturkishmodel.py deleted file mode 100644 index 64c94336c..000000000 --- a/libs/chardet/langturkishmodel.py +++ /dev/null @@ -1,4380 +0,0 @@ -from chardet.sbcharsetprober import SingleByteCharSetModel - -# 3: Positive -# 2: Likely -# 1: Unlikely -# 0: Negative - -TURKISH_LANG_MODEL = { - 23: { # 'A' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 1, # 'h' - 3: 1, # 'i' - 24: 0, # 'j' - 10: 2, # 'k' - 5: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 1, # 'r' - 8: 1, # 's' - 9: 1, # 't' - 14: 1, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 37: { # 'B' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 2, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 1, # 'Ş' - 19: 1, # 'ş' - }, - 47: { # 'C' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 1, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 1, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 2, # 'j' - 10: 1, # 'k' - 5: 2, # 'l' - 13: 2, # 'm' - 4: 2, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 2, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 1, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 39: { # 'D' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 1, # 'l' - 13: 3, # 'm' - 4: 0, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 1, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 1, # 'Ş' - 19: 0, # 'ş' - }, - 29: { # 'E' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 1, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 0, # 'h' - 3: 1, # 'i' - 24: 1, # 'j' - 10: 0, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 1, # 's' - 9: 1, # 't' - 14: 1, # 'u' - 32: 1, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 52: { # 'F' - 23: 0, # 'A' - 37: 1, # 'B' - 47: 1, # 'C' - 39: 1, # 'D' - 29: 1, # 'E' - 52: 2, # 'F' - 36: 0, # 'G' - 45: 2, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 1, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 1, # 'b' - 28: 1, # 'c' - 12: 1, # 'd' - 2: 0, # 'e' - 18: 1, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 2, # 'i' - 24: 1, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 1, # 'm' - 4: 2, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 2, # 'r' - 8: 1, # 's' - 9: 1, # 't' - 14: 1, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 1, # 'Ö' - 55: 2, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 2, # 'ş' - }, - 36: { # 'G' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 2, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 2, # 'N' - 42: 1, # 'O' - 48: 1, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 1, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 1, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 0, # 'r' - 8: 1, # 's' - 9: 1, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 1, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 2, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 45: { # 'H' - 23: 0, # 'A' - 37: 1, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 2, # 'G' - 45: 1, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 1, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 2, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 2, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 1, # 'o' - 26: 1, # 'p' - 7: 1, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 2, # 'ğ' - 41: 1, # 'İ' - 6: 0, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 53: { # 'I' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 2, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 1, # 'Ş' - 19: 1, # 'ş' - }, - 60: { # 'J' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 1, # 'd' - 2: 0, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 1, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 1, # 's' - 9: 0, # 't' - 14: 0, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 16: { # 'K' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 1, # 'e' - 18: 3, # 'f' - 27: 3, # 'g' - 25: 3, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 0, # 'u' - 32: 3, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 1, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 49: { # 'L' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 2, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 2, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 0, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 2, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 2, # 'n' - 15: 1, # 'o' - 26: 1, # 'p' - 7: 1, # 'r' - 8: 1, # 's' - 9: 1, # 't' - 14: 0, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 2, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 1, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 20: { # 'M' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 1, # 'h' - 3: 2, # 'i' - 24: 2, # 'j' - 10: 2, # 'k' - 5: 2, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 3, # 'r' - 8: 0, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 46: { # 'N' - 23: 0, # 'A' - 37: 1, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 2, # 'j' - 10: 1, # 'k' - 5: 1, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 1, # 'o' - 26: 1, # 'p' - 7: 1, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 1, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 1, # 'İ' - 6: 2, # 'ı' - 40: 1, # 'Ş' - 19: 1, # 'ş' - }, - 42: { # 'O' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 1, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 0, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 2, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 2, # 'İ' - 6: 1, # 'ı' - 40: 1, # 'Ş' - 19: 1, # 'ş' - }, - 48: { # 'P' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 2, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 2, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 2, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 0, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 44: { # 'R' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 1, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 2, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 1, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 1, # 'Ş' - 19: 1, # 'ş' - }, - 35: { # 'S' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 1, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 1, # 'l' - 13: 2, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 1, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 2, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 31: { # 'T' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 0, # 'c' - 12: 1, # 'd' - 2: 3, # 'e' - 18: 2, # 'f' - 27: 2, # 'g' - 25: 0, # 'h' - 3: 1, # 'i' - 24: 1, # 'j' - 10: 2, # 'k' - 5: 2, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 2, # 'r' - 8: 0, # 's' - 9: 2, # 't' - 14: 2, # 'u' - 32: 1, # 'v' - 57: 1, # 'w' - 58: 1, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 51: { # 'U' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 1, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 1, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 38: { # 'V' - 23: 1, # 'A' - 37: 1, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 1, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 2, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 2, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 1, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 1, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 3, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 62: { # 'W' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 0, # 'd' - 2: 0, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 0, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 0, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 43: { # 'Y' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 0, # 'G' - 45: 1, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 2, # 'N' - 42: 0, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 1, # 'j' - 10: 1, # 'k' - 5: 1, # 'l' - 13: 3, # 'm' - 4: 0, # 'n' - 15: 2, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 2, # 'Ö' - 55: 1, # 'Ü' - 59: 1, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 0, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 56: { # 'Z' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 2, # 'Z' - 1: 2, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 2, # 'i' - 24: 1, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 1, # 'r' - 8: 1, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 1: { # 'a' - 23: 3, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 1, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 3, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 2, # 'Z' - 1: 2, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 2, # 'e' - 18: 3, # 'f' - 27: 3, # 'g' - 25: 3, # 'h' - 3: 3, # 'i' - 24: 3, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 3, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 3, # 'v' - 57: 2, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 1, # 'î' - 34: 1, # 'ö' - 17: 3, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 21: { # 'b' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 3, # 'g' - 25: 1, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 3, # 'p' - 7: 1, # 'r' - 8: 2, # 's' - 9: 2, # 't' - 14: 2, # 'u' - 32: 1, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 28: { # 'c' - 23: 0, # 'A' - 37: 1, # 'B' - 47: 1, # 'C' - 39: 1, # 'D' - 29: 2, # 'E' - 52: 0, # 'F' - 36: 2, # 'G' - 45: 2, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 2, # 'T' - 51: 2, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 3, # 'Y' - 56: 0, # 'Z' - 1: 1, # 'a' - 21: 1, # 'b' - 28: 2, # 'c' - 12: 2, # 'd' - 2: 1, # 'e' - 18: 1, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 3, # 'i' - 24: 1, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 2, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 1, # 'u' - 32: 0, # 'v' - 57: 1, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 1, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 1, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 1, # 'î' - 34: 2, # 'ö' - 17: 2, # 'ü' - 30: 2, # 'ğ' - 41: 1, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 2, # 'ş' - }, - 12: { # 'd' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 2, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 1, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 1, # 'f' - 27: 3, # 'g' - 25: 3, # 'h' - 3: 2, # 'i' - 24: 3, # 'j' - 10: 2, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 2, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 1, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 3, # 'y' - 22: 1, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 2: { # 'e' - 23: 2, # 'A' - 37: 0, # 'B' - 47: 2, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 1, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 1, # 'R' - 35: 0, # 'S' - 31: 3, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 2, # 'e' - 18: 3, # 'f' - 27: 3, # 'g' - 25: 3, # 'h' - 3: 3, # 'i' - 24: 3, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 3, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 3, # 'v' - 57: 2, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 1, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 3, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 18: { # 'f' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 2, # 'f' - 27: 1, # 'g' - 25: 1, # 'h' - 3: 1, # 'i' - 24: 1, # 'j' - 10: 1, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 1, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 1, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 27: { # 'g' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 1, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 1, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 1, # 'h' - 3: 2, # 'i' - 24: 3, # 'j' - 10: 2, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 2, # 'r' - 8: 2, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 1, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 25: { # 'h' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 2, # 'h' - 3: 2, # 'i' - 24: 3, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 1, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 1, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 3: { # 'i' - 23: 2, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 0, # 'N' - 42: 1, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 1, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 2, # 'f' - 27: 3, # 'g' - 25: 1, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 3, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 1, # 'w' - 58: 1, # 'x' - 11: 3, # 'y' - 22: 1, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 1, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 3, # 'ü' - 30: 0, # 'ğ' - 41: 1, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 24: { # 'j' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 2, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 1, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 2, # 'f' - 27: 1, # 'g' - 25: 1, # 'h' - 3: 2, # 'i' - 24: 1, # 'j' - 10: 2, # 'k' - 5: 2, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 2, # 'r' - 8: 3, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 2, # 'x' - 11: 1, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 10: { # 'k' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 3, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 3, # 'e' - 18: 1, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 2, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 3, # 'p' - 7: 2, # 'r' - 8: 2, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 3, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 5: { # 'l' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 1, # 'e' - 18: 3, # 'f' - 27: 3, # 'g' - 25: 2, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 1, # 'l' - 13: 1, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 2, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 13: { # 'm' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 3, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 2, # 'e' - 18: 3, # 'f' - 27: 3, # 'g' - 25: 3, # 'h' - 3: 3, # 'i' - 24: 3, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 2, # 'u' - 32: 2, # 'v' - 57: 1, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 3, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 4: { # 'n' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 2, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 1, # 'f' - 27: 2, # 'g' - 25: 3, # 'h' - 3: 2, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 3, # 'p' - 7: 2, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 2, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 15: { # 'o' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 2, # 'L' - 20: 0, # 'M' - 46: 2, # 'N' - 42: 1, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 1, # 'i' - 24: 2, # 'j' - 10: 1, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 2, # 'o' - 26: 0, # 'p' - 7: 1, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 2, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 2, # 'ğ' - 41: 2, # 'İ' - 6: 3, # 'ı' - 40: 2, # 'Ş' - 19: 2, # 'ş' - }, - 26: { # 'p' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 1, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 1, # 'h' - 3: 2, # 'i' - 24: 3, # 'j' - 10: 1, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 2, # 'r' - 8: 1, # 's' - 9: 1, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 1, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 7: { # 'r' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 1, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 2, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 1, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 3, # 'h' - 3: 2, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 3, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 8: { # 's' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 2, # 'i' - 24: 3, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 3, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 2, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 9: { # 't' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 2, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 2, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 3, # 'v' - 57: 0, # 'w' - 58: 2, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 14: { # 'u' - 23: 3, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 2, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 3, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 2, # 'Z' - 1: 2, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 2, # 'e' - 18: 2, # 'f' - 27: 3, # 'g' - 25: 3, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 3, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 2, # 'v' - 57: 2, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 3, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 32: { # 'v' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 1, # 'j' - 10: 1, # 'k' - 5: 3, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 1, # 'r' - 8: 2, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 1, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 57: { # 'w' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 1, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 1, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 1, # 's' - 9: 0, # 't' - 14: 1, # 'u' - 32: 0, # 'v' - 57: 2, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 58: { # 'x' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 1, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 1, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 2, # 'i' - 24: 2, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 2, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 1, # 'r' - 8: 2, # 's' - 9: 1, # 't' - 14: 0, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 11: { # 'y' - 23: 1, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 2, # 'i' - 24: 1, # 'j' - 10: 2, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 2, # 'r' - 8: 1, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 1, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 3, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 22: { # 'z' - 23: 2, # 'A' - 37: 2, # 'B' - 47: 1, # 'C' - 39: 2, # 'D' - 29: 3, # 'E' - 52: 1, # 'F' - 36: 2, # 'G' - 45: 2, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 2, # 'N' - 42: 2, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 3, # 'T' - 51: 2, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 1, # 'Z' - 1: 1, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 2, # 'd' - 2: 2, # 'e' - 18: 3, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 2, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 0, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 3, # 'y' - 22: 2, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 2, # 'Ü' - 59: 1, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 2, # 'ü' - 30: 2, # 'ğ' - 41: 1, # 'İ' - 6: 3, # 'ı' - 40: 1, # 'Ş' - 19: 2, # 'ş' - }, - 63: { # '·' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 0, # 'd' - 2: 1, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 0, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 54: { # 'Ç' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 1, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 1, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 1, # 'b' - 28: 0, # 'c' - 12: 1, # 'd' - 2: 0, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 0, # 'h' - 3: 3, # 'i' - 24: 0, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 2, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 2, # 'r' - 8: 0, # 's' - 9: 1, # 't' - 14: 0, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 2, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 50: { # 'Ö' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 1, # 'D' - 29: 2, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 2, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 1, # 'N' - 42: 2, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 2, # 'd' - 2: 0, # 'e' - 18: 1, # 'f' - 27: 1, # 'g' - 25: 1, # 'h' - 3: 2, # 'i' - 24: 0, # 'j' - 10: 2, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 3, # 'n' - 15: 2, # 'o' - 26: 2, # 'p' - 7: 3, # 'r' - 8: 1, # 's' - 9: 2, # 't' - 14: 0, # 'u' - 32: 1, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 2, # 'ü' - 30: 1, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 55: { # 'Ü' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 1, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 1, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 1, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 1, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 1, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 0, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 59: { # 'â' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 0, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 2, # 'm' - 4: 0, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 2, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 1, # 'Ş' - 19: 0, # 'ş' - }, - 33: { # 'ç' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 3, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 0, # 'Z' - 1: 0, # 'a' - 21: 3, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 0, # 'e' - 18: 2, # 'f' - 27: 1, # 'g' - 25: 3, # 'h' - 3: 3, # 'i' - 24: 0, # 'j' - 10: 3, # 'k' - 5: 0, # 'l' - 13: 0, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 3, # 'r' - 8: 2, # 's' - 9: 3, # 't' - 14: 0, # 'u' - 32: 2, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 1, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 61: { # 'î' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 0, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 0, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 2, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 1, # 'j' - 10: 0, # 'k' - 5: 0, # 'l' - 13: 1, # 'm' - 4: 1, # 'n' - 15: 0, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 1, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 1, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 1, # 'î' - 34: 0, # 'ö' - 17: 0, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 1, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 34: { # 'ö' - 23: 0, # 'A' - 37: 1, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 1, # 'G' - 45: 1, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 1, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 2, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 1, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 2, # 'c' - 12: 1, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 2, # 'h' - 3: 1, # 'i' - 24: 2, # 'j' - 10: 1, # 'k' - 5: 2, # 'l' - 13: 3, # 'm' - 4: 2, # 'n' - 15: 2, # 'o' - 26: 0, # 'p' - 7: 0, # 'r' - 8: 3, # 's' - 9: 1, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 1, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 0, # 'ü' - 30: 2, # 'ğ' - 41: 1, # 'İ' - 6: 1, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 17: { # 'ü' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 0, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 1, # 'J' - 16: 1, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 0, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 0, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 0, # 'c' - 12: 1, # 'd' - 2: 3, # 'e' - 18: 1, # 'f' - 27: 2, # 'g' - 25: 0, # 'h' - 3: 1, # 'i' - 24: 1, # 'j' - 10: 2, # 'k' - 5: 3, # 'l' - 13: 2, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 2, # 'p' - 7: 2, # 'r' - 8: 3, # 's' - 9: 2, # 't' - 14: 3, # 'u' - 32: 1, # 'v' - 57: 1, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 2, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 30: { # 'ğ' - 23: 0, # 'A' - 37: 2, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 1, # 'M' - 46: 2, # 'N' - 42: 2, # 'O' - 48: 1, # 'P' - 44: 1, # 'R' - 35: 0, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 2, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 0, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 2, # 'e' - 18: 0, # 'f' - 27: 0, # 'g' - 25: 0, # 'h' - 3: 0, # 'i' - 24: 3, # 'j' - 10: 1, # 'k' - 5: 2, # 'l' - 13: 3, # 'm' - 4: 0, # 'n' - 15: 1, # 'o' - 26: 0, # 'p' - 7: 1, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 2, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 2, # 'İ' - 6: 2, # 'ı' - 40: 2, # 'Ş' - 19: 1, # 'ş' - }, - 41: { # 'İ' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 1, # 'D' - 29: 1, # 'E' - 52: 0, # 'F' - 36: 2, # 'G' - 45: 2, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 2, # 'P' - 44: 0, # 'R' - 35: 1, # 'S' - 31: 1, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 0, # 'Z' - 1: 1, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 2, # 'd' - 2: 1, # 'e' - 18: 0, # 'f' - 27: 3, # 'g' - 25: 2, # 'h' - 3: 2, # 'i' - 24: 2, # 'j' - 10: 2, # 'k' - 5: 0, # 'l' - 13: 1, # 'm' - 4: 3, # 'n' - 15: 1, # 'o' - 26: 1, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 2, # 't' - 14: 0, # 'u' - 32: 0, # 'v' - 57: 1, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 1, # 'Ü' - 59: 1, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 1, # 'ö' - 17: 1, # 'ü' - 30: 2, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 1, # 'ş' - }, - 6: { # 'ı' - 23: 2, # 'A' - 37: 0, # 'B' - 47: 0, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 2, # 'J' - 16: 3, # 'K' - 49: 0, # 'L' - 20: 3, # 'M' - 46: 1, # 'N' - 42: 0, # 'O' - 48: 0, # 'P' - 44: 0, # 'R' - 35: 0, # 'S' - 31: 2, # 'T' - 51: 0, # 'U' - 38: 0, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 1, # 'Z' - 1: 3, # 'a' - 21: 2, # 'b' - 28: 1, # 'c' - 12: 3, # 'd' - 2: 3, # 'e' - 18: 3, # 'f' - 27: 3, # 'g' - 25: 2, # 'h' - 3: 3, # 'i' - 24: 3, # 'j' - 10: 3, # 'k' - 5: 3, # 'l' - 13: 3, # 'm' - 4: 3, # 'n' - 15: 0, # 'o' - 26: 3, # 'p' - 7: 3, # 'r' - 8: 3, # 's' - 9: 3, # 't' - 14: 3, # 'u' - 32: 3, # 'v' - 57: 1, # 'w' - 58: 1, # 'x' - 11: 3, # 'y' - 22: 0, # 'z' - 63: 1, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 2, # 'ç' - 61: 0, # 'î' - 34: 0, # 'ö' - 17: 3, # 'ü' - 30: 0, # 'ğ' - 41: 0, # 'İ' - 6: 3, # 'ı' - 40: 0, # 'Ş' - 19: 0, # 'ş' - }, - 40: { # 'Ş' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 1, # 'D' - 29: 1, # 'E' - 52: 0, # 'F' - 36: 1, # 'G' - 45: 2, # 'H' - 53: 1, # 'I' - 60: 0, # 'J' - 16: 0, # 'K' - 49: 0, # 'L' - 20: 2, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 2, # 'P' - 44: 2, # 'R' - 35: 1, # 'S' - 31: 1, # 'T' - 51: 0, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 2, # 'Y' - 56: 1, # 'Z' - 1: 0, # 'a' - 21: 2, # 'b' - 28: 0, # 'c' - 12: 2, # 'd' - 2: 0, # 'e' - 18: 3, # 'f' - 27: 0, # 'g' - 25: 2, # 'h' - 3: 3, # 'i' - 24: 2, # 'j' - 10: 1, # 'k' - 5: 0, # 'l' - 13: 1, # 'm' - 4: 3, # 'n' - 15: 2, # 'o' - 26: 0, # 'p' - 7: 3, # 'r' - 8: 2, # 's' - 9: 2, # 't' - 14: 1, # 'u' - 32: 3, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 2, # 'y' - 22: 0, # 'z' - 63: 0, # '·' - 54: 0, # 'Ç' - 50: 0, # 'Ö' - 55: 1, # 'Ü' - 59: 0, # 'â' - 33: 0, # 'ç' - 61: 0, # 'î' - 34: 2, # 'ö' - 17: 1, # 'ü' - 30: 2, # 'ğ' - 41: 0, # 'İ' - 6: 2, # 'ı' - 40: 1, # 'Ş' - 19: 2, # 'ş' - }, - 19: { # 'ş' - 23: 0, # 'A' - 37: 0, # 'B' - 47: 1, # 'C' - 39: 0, # 'D' - 29: 0, # 'E' - 52: 2, # 'F' - 36: 1, # 'G' - 45: 0, # 'H' - 53: 0, # 'I' - 60: 0, # 'J' - 16: 3, # 'K' - 49: 2, # 'L' - 20: 0, # 'M' - 46: 1, # 'N' - 42: 1, # 'O' - 48: 1, # 'P' - 44: 1, # 'R' - 35: 1, # 'S' - 31: 0, # 'T' - 51: 1, # 'U' - 38: 1, # 'V' - 62: 0, # 'W' - 43: 1, # 'Y' - 56: 0, # 'Z' - 1: 3, # 'a' - 21: 1, # 'b' - 28: 2, # 'c' - 12: 0, # 'd' - 2: 3, # 'e' - 18: 0, # 'f' - 27: 2, # 'g' - 25: 1, # 'h' - 3: 1, # 'i' - 24: 0, # 'j' - 10: 2, # 'k' - 5: 2, # 'l' - 13: 3, # 'm' - 4: 0, # 'n' - 15: 0, # 'o' - 26: 1, # 'p' - 7: 3, # 'r' - 8: 0, # 's' - 9: 0, # 't' - 14: 3, # 'u' - 32: 0, # 'v' - 57: 0, # 'w' - 58: 0, # 'x' - 11: 0, # 'y' - 22: 2, # 'z' - 63: 0, # '·' - 54: 1, # 'Ç' - 50: 2, # 'Ö' - 55: 0, # 'Ü' - 59: 0, # 'â' - 33: 1, # 'ç' - 61: 1, # 'î' - 34: 2, # 'ö' - 17: 0, # 'ü' - 30: 1, # 'ğ' - 41: 1, # 'İ' - 6: 1, # 'ı' - 40: 1, # 'Ş' - 19: 1, # 'ş' - }, -} - -# 255: Undefined characters that did not exist in training text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 -# 251: Control characters - -# Character Mapping Table(s): -ISO_8859_9_TURKISH_CHAR_TO_ORDER = { - 0: 255, # '\x00' - 1: 255, # '\x01' - 2: 255, # '\x02' - 3: 255, # '\x03' - 4: 255, # '\x04' - 5: 255, # '\x05' - 6: 255, # '\x06' - 7: 255, # '\x07' - 8: 255, # '\x08' - 9: 255, # '\t' - 10: 255, # '\n' - 11: 255, # '\x0b' - 12: 255, # '\x0c' - 13: 255, # '\r' - 14: 255, # '\x0e' - 15: 255, # '\x0f' - 16: 255, # '\x10' - 17: 255, # '\x11' - 18: 255, # '\x12' - 19: 255, # '\x13' - 20: 255, # '\x14' - 21: 255, # '\x15' - 22: 255, # '\x16' - 23: 255, # '\x17' - 24: 255, # '\x18' - 25: 255, # '\x19' - 26: 255, # '\x1a' - 27: 255, # '\x1b' - 28: 255, # '\x1c' - 29: 255, # '\x1d' - 30: 255, # '\x1e' - 31: 255, # '\x1f' - 32: 255, # ' ' - 33: 255, # '!' - 34: 255, # '"' - 35: 255, # '#' - 36: 255, # '$' - 37: 255, # '%' - 38: 255, # '&' - 39: 255, # "'" - 40: 255, # '(' - 41: 255, # ')' - 42: 255, # '*' - 43: 255, # '+' - 44: 255, # ',' - 45: 255, # '-' - 46: 255, # '.' - 47: 255, # '/' - 48: 255, # '0' - 49: 255, # '1' - 50: 255, # '2' - 51: 255, # '3' - 52: 255, # '4' - 53: 255, # '5' - 54: 255, # '6' - 55: 255, # '7' - 56: 255, # '8' - 57: 255, # '9' - 58: 255, # ':' - 59: 255, # ';' - 60: 255, # '<' - 61: 255, # '=' - 62: 255, # '>' - 63: 255, # '?' - 64: 255, # '@' - 65: 23, # 'A' - 66: 37, # 'B' - 67: 47, # 'C' - 68: 39, # 'D' - 69: 29, # 'E' - 70: 52, # 'F' - 71: 36, # 'G' - 72: 45, # 'H' - 73: 53, # 'I' - 74: 60, # 'J' - 75: 16, # 'K' - 76: 49, # 'L' - 77: 20, # 'M' - 78: 46, # 'N' - 79: 42, # 'O' - 80: 48, # 'P' - 81: 69, # 'Q' - 82: 44, # 'R' - 83: 35, # 'S' - 84: 31, # 'T' - 85: 51, # 'U' - 86: 38, # 'V' - 87: 62, # 'W' - 88: 65, # 'X' - 89: 43, # 'Y' - 90: 56, # 'Z' - 91: 255, # '[' - 92: 255, # '\\' - 93: 255, # ']' - 94: 255, # '^' - 95: 255, # '_' - 96: 255, # '`' - 97: 1, # 'a' - 98: 21, # 'b' - 99: 28, # 'c' - 100: 12, # 'd' - 101: 2, # 'e' - 102: 18, # 'f' - 103: 27, # 'g' - 104: 25, # 'h' - 105: 3, # 'i' - 106: 24, # 'j' - 107: 10, # 'k' - 108: 5, # 'l' - 109: 13, # 'm' - 110: 4, # 'n' - 111: 15, # 'o' - 112: 26, # 'p' - 113: 64, # 'q' - 114: 7, # 'r' - 115: 8, # 's' - 116: 9, # 't' - 117: 14, # 'u' - 118: 32, # 'v' - 119: 57, # 'w' - 120: 58, # 'x' - 121: 11, # 'y' - 122: 22, # 'z' - 123: 255, # '{' - 124: 255, # '|' - 125: 255, # '}' - 126: 255, # '~' - 127: 255, # '\x7f' - 128: 180, # '\x80' - 129: 179, # '\x81' - 130: 178, # '\x82' - 131: 177, # '\x83' - 132: 176, # '\x84' - 133: 175, # '\x85' - 134: 174, # '\x86' - 135: 173, # '\x87' - 136: 172, # '\x88' - 137: 171, # '\x89' - 138: 170, # '\x8a' - 139: 169, # '\x8b' - 140: 168, # '\x8c' - 141: 167, # '\x8d' - 142: 166, # '\x8e' - 143: 165, # '\x8f' - 144: 164, # '\x90' - 145: 163, # '\x91' - 146: 162, # '\x92' - 147: 161, # '\x93' - 148: 160, # '\x94' - 149: 159, # '\x95' - 150: 101, # '\x96' - 151: 158, # '\x97' - 152: 157, # '\x98' - 153: 156, # '\x99' - 154: 155, # '\x9a' - 155: 154, # '\x9b' - 156: 153, # '\x9c' - 157: 152, # '\x9d' - 158: 151, # '\x9e' - 159: 106, # '\x9f' - 160: 150, # '\xa0' - 161: 149, # '¡' - 162: 148, # '¢' - 163: 147, # '£' - 164: 146, # '¤' - 165: 145, # '¥' - 166: 144, # '¦' - 167: 100, # '§' - 168: 143, # '¨' - 169: 142, # '©' - 170: 141, # 'ª' - 171: 140, # '«' - 172: 139, # '¬' - 173: 138, # '\xad' - 174: 137, # '®' - 175: 136, # '¯' - 176: 94, # '°' - 177: 80, # '±' - 178: 93, # '²' - 179: 135, # '³' - 180: 105, # '´' - 181: 134, # 'µ' - 182: 133, # '¶' - 183: 63, # '·' - 184: 132, # '¸' - 185: 131, # '¹' - 186: 130, # 'º' - 187: 129, # '»' - 188: 128, # '¼' - 189: 127, # '½' - 190: 126, # '¾' - 191: 125, # '¿' - 192: 124, # 'À' - 193: 104, # 'Á' - 194: 73, # 'Â' - 195: 99, # 'Ã' - 196: 79, # 'Ä' - 197: 85, # 'Å' - 198: 123, # 'Æ' - 199: 54, # 'Ç' - 200: 122, # 'È' - 201: 98, # 'É' - 202: 92, # 'Ê' - 203: 121, # 'Ë' - 204: 120, # 'Ì' - 205: 91, # 'Í' - 206: 103, # 'Î' - 207: 119, # 'Ï' - 208: 68, # 'Ğ' - 209: 118, # 'Ñ' - 210: 117, # 'Ò' - 211: 97, # 'Ó' - 212: 116, # 'Ô' - 213: 115, # 'Õ' - 214: 50, # 'Ö' - 215: 90, # '×' - 216: 114, # 'Ø' - 217: 113, # 'Ù' - 218: 112, # 'Ú' - 219: 111, # 'Û' - 220: 55, # 'Ü' - 221: 41, # 'İ' - 222: 40, # 'Ş' - 223: 86, # 'ß' - 224: 89, # 'à' - 225: 70, # 'á' - 226: 59, # 'â' - 227: 78, # 'ã' - 228: 71, # 'ä' - 229: 82, # 'å' - 230: 88, # 'æ' - 231: 33, # 'ç' - 232: 77, # 'è' - 233: 66, # 'é' - 234: 84, # 'ê' - 235: 83, # 'ë' - 236: 110, # 'ì' - 237: 75, # 'í' - 238: 61, # 'î' - 239: 96, # 'ï' - 240: 30, # 'ğ' - 241: 67, # 'ñ' - 242: 109, # 'ò' - 243: 74, # 'ó' - 244: 87, # 'ô' - 245: 102, # 'õ' - 246: 34, # 'ö' - 247: 95, # '÷' - 248: 81, # 'ø' - 249: 108, # 'ù' - 250: 76, # 'ú' - 251: 72, # 'û' - 252: 17, # 'ü' - 253: 6, # 'ı' - 254: 19, # 'ş' - 255: 107, # 'ÿ' -} - -ISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel( - charset_name="ISO-8859-9", - language="Turkish", - char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER, - language_model=TURKISH_LANG_MODEL, - typical_positive_ratio=0.97029, - keep_ascii_letters=True, - alphabet="ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş", -) diff --git a/libs/chardet/latin1prober.py b/libs/chardet/latin1prober.py deleted file mode 100644 index 59a01d91b..000000000 --- a/libs/chardet/latin1prober.py +++ /dev/null @@ -1,147 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import List, Union - -from .charsetprober import CharSetProber -from .enums import ProbingState - -FREQ_CAT_NUM = 4 - -UDF = 0 # undefined -OTH = 1 # other -ASC = 2 # ascii capital letter -ASS = 3 # ascii small letter -ACV = 4 # accent capital vowel -ACO = 5 # accent capital other -ASV = 6 # accent small vowel -ASO = 7 # accent small other -CLASS_NUM = 8 # total classes - -# fmt: off -Latin1_CharToClass = ( - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 00 - 07 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 08 - 0F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 10 - 17 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 18 - 1F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 20 - 27 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 28 - 2F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 30 - 37 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 38 - 3F - OTH, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 40 - 47 - ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 48 - 4F - ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 50 - 57 - ASC, ASC, ASC, OTH, OTH, OTH, OTH, OTH, # 58 - 5F - OTH, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 60 - 67 - ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 68 - 6F - ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 70 - 77 - ASS, ASS, ASS, OTH, OTH, OTH, OTH, OTH, # 78 - 7F - OTH, UDF, OTH, ASO, OTH, OTH, OTH, OTH, # 80 - 87 - OTH, OTH, ACO, OTH, ACO, UDF, ACO, UDF, # 88 - 8F - UDF, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 90 - 97 - OTH, OTH, ASO, OTH, ASO, UDF, ASO, ACO, # 98 - 9F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # A0 - A7 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # A8 - AF - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # B0 - B7 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # B8 - BF - ACV, ACV, ACV, ACV, ACV, ACV, ACO, ACO, # C0 - C7 - ACV, ACV, ACV, ACV, ACV, ACV, ACV, ACV, # C8 - CF - ACO, ACO, ACV, ACV, ACV, ACV, ACV, OTH, # D0 - D7 - ACV, ACV, ACV, ACV, ACV, ACO, ACO, ACO, # D8 - DF - ASV, ASV, ASV, ASV, ASV, ASV, ASO, ASO, # E0 - E7 - ASV, ASV, ASV, ASV, ASV, ASV, ASV, ASV, # E8 - EF - ASO, ASO, ASV, ASV, ASV, ASV, ASV, OTH, # F0 - F7 - ASV, ASV, ASV, ASV, ASV, ASO, ASO, ASO, # F8 - FF -) - -# 0 : illegal -# 1 : very unlikely -# 2 : normal -# 3 : very likely -Latin1ClassModel = ( -# UDF OTH ASC ASS ACV ACO ASV ASO - 0, 0, 0, 0, 0, 0, 0, 0, # UDF - 0, 3, 3, 3, 3, 3, 3, 3, # OTH - 0, 3, 3, 3, 3, 3, 3, 3, # ASC - 0, 3, 3, 3, 1, 1, 3, 3, # ASS - 0, 3, 3, 3, 1, 2, 1, 2, # ACV - 0, 3, 3, 3, 3, 3, 3, 3, # ACO - 0, 3, 1, 3, 1, 1, 1, 3, # ASV - 0, 3, 1, 3, 1, 1, 3, 3, # ASO -) -# fmt: on - - -class Latin1Prober(CharSetProber): - def __init__(self) -> None: - super().__init__() - self._last_char_class = OTH - self._freq_counter: List[int] = [] - self.reset() - - def reset(self) -> None: - self._last_char_class = OTH - self._freq_counter = [0] * FREQ_CAT_NUM - super().reset() - - @property - def charset_name(self) -> str: - return "ISO-8859-1" - - @property - def language(self) -> str: - return "" - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - byte_str = self.remove_xml_tags(byte_str) - for c in byte_str: - char_class = Latin1_CharToClass[c] - freq = Latin1ClassModel[(self._last_char_class * CLASS_NUM) + char_class] - if freq == 0: - self._state = ProbingState.NOT_ME - break - self._freq_counter[freq] += 1 - self._last_char_class = char_class - - return self.state - - def get_confidence(self) -> float: - if self.state == ProbingState.NOT_ME: - return 0.01 - - total = sum(self._freq_counter) - confidence = ( - 0.0 - if total < 0.01 - else (self._freq_counter[3] - self._freq_counter[1] * 20.0) / total - ) - confidence = max(confidence, 0.0) - # lower the confidence of latin1 so that other more accurate - # detector can take priority. - confidence *= 0.73 - return confidence diff --git a/libs/chardet/macromanprober.py b/libs/chardet/macromanprober.py deleted file mode 100644 index 1425d10ec..000000000 --- a/libs/chardet/macromanprober.py +++ /dev/null @@ -1,162 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# This code was modified from latin1prober.py by Rob Speer . -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Rob Speer - adapt to MacRoman encoding -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import List, Union - -from .charsetprober import CharSetProber -from .enums import ProbingState - -FREQ_CAT_NUM = 4 - -UDF = 0 # undefined -OTH = 1 # other -ASC = 2 # ascii capital letter -ASS = 3 # ascii small letter -ACV = 4 # accent capital vowel -ACO = 5 # accent capital other -ASV = 6 # accent small vowel -ASO = 7 # accent small other -ODD = 8 # character that is unlikely to appear -CLASS_NUM = 9 # total classes - -# The change from Latin1 is that we explicitly look for extended characters -# that are infrequently-occurring symbols, and consider them to always be -# improbable. This should let MacRoman get out of the way of more likely -# encodings in most situations. - -# fmt: off -MacRoman_CharToClass = ( - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 00 - 07 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 08 - 0F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 10 - 17 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 18 - 1F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 20 - 27 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 28 - 2F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 30 - 37 - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 38 - 3F - OTH, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 40 - 47 - ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 48 - 4F - ASC, ASC, ASC, ASC, ASC, ASC, ASC, ASC, # 50 - 57 - ASC, ASC, ASC, OTH, OTH, OTH, OTH, OTH, # 58 - 5F - OTH, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 60 - 67 - ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 68 - 6F - ASS, ASS, ASS, ASS, ASS, ASS, ASS, ASS, # 70 - 77 - ASS, ASS, ASS, OTH, OTH, OTH, OTH, OTH, # 78 - 7F - ACV, ACV, ACO, ACV, ACO, ACV, ACV, ASV, # 80 - 87 - ASV, ASV, ASV, ASV, ASV, ASO, ASV, ASV, # 88 - 8F - ASV, ASV, ASV, ASV, ASV, ASV, ASO, ASV, # 90 - 97 - ASV, ASV, ASV, ASV, ASV, ASV, ASV, ASV, # 98 - 9F - OTH, OTH, OTH, OTH, OTH, OTH, OTH, ASO, # A0 - A7 - OTH, OTH, ODD, ODD, OTH, OTH, ACV, ACV, # A8 - AF - OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # B0 - B7 - OTH, OTH, OTH, OTH, OTH, OTH, ASV, ASV, # B8 - BF - OTH, OTH, ODD, OTH, ODD, OTH, OTH, OTH, # C0 - C7 - OTH, OTH, OTH, ACV, ACV, ACV, ACV, ASV, # C8 - CF - OTH, OTH, OTH, OTH, OTH, OTH, OTH, ODD, # D0 - D7 - ASV, ACV, ODD, OTH, OTH, OTH, OTH, OTH, # D8 - DF - OTH, OTH, OTH, OTH, OTH, ACV, ACV, ACV, # E0 - E7 - ACV, ACV, ACV, ACV, ACV, ACV, ACV, ACV, # E8 - EF - ODD, ACV, ACV, ACV, ACV, ASV, ODD, ODD, # F0 - F7 - ODD, ODD, ODD, ODD, ODD, ODD, ODD, ODD, # F8 - FF -) - -# 0 : illegal -# 1 : very unlikely -# 2 : normal -# 3 : very likely -MacRomanClassModel = ( -# UDF OTH ASC ASS ACV ACO ASV ASO ODD - 0, 0, 0, 0, 0, 0, 0, 0, 0, # UDF - 0, 3, 3, 3, 3, 3, 3, 3, 1, # OTH - 0, 3, 3, 3, 3, 3, 3, 3, 1, # ASC - 0, 3, 3, 3, 1, 1, 3, 3, 1, # ASS - 0, 3, 3, 3, 1, 2, 1, 2, 1, # ACV - 0, 3, 3, 3, 3, 3, 3, 3, 1, # ACO - 0, 3, 1, 3, 1, 1, 1, 3, 1, # ASV - 0, 3, 1, 3, 1, 1, 3, 3, 1, # ASO - 0, 1, 1, 1, 1, 1, 1, 1, 1, # ODD -) -# fmt: on - - -class MacRomanProber(CharSetProber): - def __init__(self) -> None: - super().__init__() - self._last_char_class = OTH - self._freq_counter: List[int] = [] - self.reset() - - def reset(self) -> None: - self._last_char_class = OTH - self._freq_counter = [0] * FREQ_CAT_NUM - - # express the prior that MacRoman is a somewhat rare encoding; - # this can be done by starting out in a slightly improbable state - # that must be overcome - self._freq_counter[2] = 10 - - super().reset() - - @property - def charset_name(self) -> str: - return "MacRoman" - - @property - def language(self) -> str: - return "" - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - byte_str = self.remove_xml_tags(byte_str) - for c in byte_str: - char_class = MacRoman_CharToClass[c] - freq = MacRomanClassModel[(self._last_char_class * CLASS_NUM) + char_class] - if freq == 0: - self._state = ProbingState.NOT_ME - break - self._freq_counter[freq] += 1 - self._last_char_class = char_class - - return self.state - - def get_confidence(self) -> float: - if self.state == ProbingState.NOT_ME: - return 0.01 - - total = sum(self._freq_counter) - confidence = ( - 0.0 - if total < 0.01 - else (self._freq_counter[3] - self._freq_counter[1] * 20.0) / total - ) - confidence = max(confidence, 0.0) - # lower the confidence of MacRoman so that other more accurate - # detector can take priority. - confidence *= 0.73 - return confidence diff --git a/libs/chardet/mbcharsetprober.py b/libs/chardet/mbcharsetprober.py deleted file mode 100644 index 666307e8f..000000000 --- a/libs/chardet/mbcharsetprober.py +++ /dev/null @@ -1,95 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# Proofpoint, Inc. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Optional, Union - -from .chardistribution import CharDistributionAnalysis -from .charsetprober import CharSetProber -from .codingstatemachine import CodingStateMachine -from .enums import LanguageFilter, MachineState, ProbingState - - -class MultiByteCharSetProber(CharSetProber): - """ - MultiByteCharSetProber - """ - - def __init__(self, lang_filter: LanguageFilter = LanguageFilter.NONE) -> None: - super().__init__(lang_filter=lang_filter) - self.distribution_analyzer: Optional[CharDistributionAnalysis] = None - self.coding_sm: Optional[CodingStateMachine] = None - self._last_char = bytearray(b"\0\0") - - def reset(self) -> None: - super().reset() - if self.coding_sm: - self.coding_sm.reset() - if self.distribution_analyzer: - self.distribution_analyzer.reset() - self._last_char = bytearray(b"\0\0") - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - assert self.coding_sm is not None - assert self.distribution_analyzer is not None - - for i, byte in enumerate(byte_str): - coding_state = self.coding_sm.next_state(byte) - if coding_state == MachineState.ERROR: - self.logger.debug( - "%s %s prober hit error at byte %s", - self.charset_name, - self.language, - i, - ) - self._state = ProbingState.NOT_ME - break - if coding_state == MachineState.ITS_ME: - self._state = ProbingState.FOUND_IT - break - if coding_state == MachineState.START: - char_len = self.coding_sm.get_current_charlen() - if i == 0: - self._last_char[1] = byte - self.distribution_analyzer.feed(self._last_char, char_len) - else: - self.distribution_analyzer.feed(byte_str[i - 1 : i + 1], char_len) - - self._last_char[0] = byte_str[-1] - - if self.state == ProbingState.DETECTING: - if self.distribution_analyzer.got_enough_data() and ( - self.get_confidence() > self.SHORTCUT_THRESHOLD - ): - self._state = ProbingState.FOUND_IT - - return self.state - - def get_confidence(self) -> float: - assert self.distribution_analyzer is not None - return self.distribution_analyzer.get_confidence() diff --git a/libs/chardet/mbcsgroupprober.py b/libs/chardet/mbcsgroupprober.py deleted file mode 100644 index 6cb9cc7b3..000000000 --- a/libs/chardet/mbcsgroupprober.py +++ /dev/null @@ -1,57 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# Proofpoint, Inc. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .big5prober import Big5Prober -from .charsetgroupprober import CharSetGroupProber -from .cp949prober import CP949Prober -from .enums import LanguageFilter -from .eucjpprober import EUCJPProber -from .euckrprober import EUCKRProber -from .euctwprober import EUCTWProber -from .gb2312prober import GB2312Prober -from .johabprober import JOHABProber -from .sjisprober import SJISProber -from .utf8prober import UTF8Prober - - -class MBCSGroupProber(CharSetGroupProber): - def __init__(self, lang_filter: LanguageFilter = LanguageFilter.NONE) -> None: - super().__init__(lang_filter=lang_filter) - self.probers = [ - UTF8Prober(), - SJISProber(), - EUCJPProber(), - GB2312Prober(), - EUCKRProber(), - CP949Prober(), - Big5Prober(), - EUCTWProber(), - JOHABProber(), - ] - self.reset() diff --git a/libs/chardet/mbcssm.py b/libs/chardet/mbcssm.py deleted file mode 100644 index 7bbe97e66..000000000 --- a/libs/chardet/mbcssm.py +++ /dev/null @@ -1,661 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .codingstatemachinedict import CodingStateMachineDict -from .enums import MachineState - -# BIG5 - -# fmt: off -BIG5_CLS = ( - 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 #allow 0x00 as legal value - 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f - 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17 - 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f - 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27 - 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f - 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37 - 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f - 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47 - 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f - 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57 - 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f - 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67 - 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f - 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77 - 2, 2, 2, 2, 2, 2, 2, 1, # 78 - 7f - 4, 4, 4, 4, 4, 4, 4, 4, # 80 - 87 - 4, 4, 4, 4, 4, 4, 4, 4, # 88 - 8f - 4, 4, 4, 4, 4, 4, 4, 4, # 90 - 97 - 4, 4, 4, 4, 4, 4, 4, 4, # 98 - 9f - 4, 3, 3, 3, 3, 3, 3, 3, # a0 - a7 - 3, 3, 3, 3, 3, 3, 3, 3, # a8 - af - 3, 3, 3, 3, 3, 3, 3, 3, # b0 - b7 - 3, 3, 3, 3, 3, 3, 3, 3, # b8 - bf - 3, 3, 3, 3, 3, 3, 3, 3, # c0 - c7 - 3, 3, 3, 3, 3, 3, 3, 3, # c8 - cf - 3, 3, 3, 3, 3, 3, 3, 3, # d0 - d7 - 3, 3, 3, 3, 3, 3, 3, 3, # d8 - df - 3, 3, 3, 3, 3, 3, 3, 3, # e0 - e7 - 3, 3, 3, 3, 3, 3, 3, 3, # e8 - ef - 3, 3, 3, 3, 3, 3, 3, 3, # f0 - f7 - 3, 3, 3, 3, 3, 3, 3, 0 # f8 - ff -) - -BIG5_ST = ( - MachineState.ERROR,MachineState.START,MachineState.START, 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,#08-0f - MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START#10-17 -) -# fmt: on - -BIG5_CHAR_LEN_TABLE = (0, 1, 1, 2, 0) - -BIG5_SM_MODEL: CodingStateMachineDict = { - "class_table": BIG5_CLS, - "class_factor": 5, - "state_table": BIG5_ST, - "char_len_table": BIG5_CHAR_LEN_TABLE, - "name": "Big5", -} - -# CP949 -# fmt: off -CP949_CLS = ( - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, # 00 - 0f - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, # 10 - 1f - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 2f - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 3f - 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, # 40 - 4f - 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, # 50 - 5f - 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, # 60 - 6f - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, # 70 - 7f - 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, # 80 - 8f - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, # 90 - 9f - 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, # a0 - af - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, # b0 - bf - 7, 7, 7, 7, 7, 7, 9, 2, 2, 3, 2, 2, 2, 2, 2, 2, # c0 - cf - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # d0 - df - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # e0 - ef - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, # f0 - ff -) - -CP949_ST = ( -#cls= 0 1 2 3 4 5 6 7 8 9 # previous state = - MachineState.ERROR,MachineState.START, 3,MachineState.ERROR,MachineState.START,MachineState.START, 4, 5,MachineState.ERROR, 6, # MachineState.START - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, # MachineState.ERROR - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME, # MachineState.ITS_ME - MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START, # 3 - MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START, # 4 - MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START, # 5 - MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START, # 6 -) -# fmt: on - -CP949_CHAR_LEN_TABLE = (0, 1, 2, 0, 1, 1, 2, 2, 0, 2) - -CP949_SM_MODEL: CodingStateMachineDict = { - "class_table": CP949_CLS, - "class_factor": 10, - "state_table": CP949_ST, - "char_len_table": CP949_CHAR_LEN_TABLE, - "name": "CP949", -} - -# EUC-JP -# fmt: off -EUCJP_CLS = ( - 4, 4, 4, 4, 4, 4, 4, 4, # 00 - 07 - 4, 4, 4, 4, 4, 4, 5, 5, # 08 - 0f - 4, 4, 4, 4, 4, 4, 4, 4, # 10 - 17 - 4, 4, 4, 5, 4, 4, 4, 4, # 18 - 1f - 4, 4, 4, 4, 4, 4, 4, 4, # 20 - 27 - 4, 4, 4, 4, 4, 4, 4, 4, # 28 - 2f - 4, 4, 4, 4, 4, 4, 4, 4, # 30 - 37 - 4, 4, 4, 4, 4, 4, 4, 4, # 38 - 3f - 4, 4, 4, 4, 4, 4, 4, 4, # 40 - 47 - 4, 4, 4, 4, 4, 4, 4, 4, # 48 - 4f - 4, 4, 4, 4, 4, 4, 4, 4, # 50 - 57 - 4, 4, 4, 4, 4, 4, 4, 4, # 58 - 5f - 4, 4, 4, 4, 4, 4, 4, 4, # 60 - 67 - 4, 4, 4, 4, 4, 4, 4, 4, # 68 - 6f - 4, 4, 4, 4, 4, 4, 4, 4, # 70 - 77 - 4, 4, 4, 4, 4, 4, 4, 4, # 78 - 7f - 5, 5, 5, 5, 5, 5, 5, 5, # 80 - 87 - 5, 5, 5, 5, 5, 5, 1, 3, # 88 - 8f - 5, 5, 5, 5, 5, 5, 5, 5, # 90 - 97 - 5, 5, 5, 5, 5, 5, 5, 5, # 98 - 9f - 5, 2, 2, 2, 2, 2, 2, 2, # a0 - a7 - 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af - 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7 - 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf - 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7 - 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf - 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7 - 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df - 0, 0, 0, 0, 0, 0, 0, 0, # e0 - e7 - 0, 0, 0, 0, 0, 0, 0, 0, # e8 - ef - 0, 0, 0, 0, 0, 0, 0, 0, # f0 - f7 - 0, 0, 0, 0, 0, 0, 0, 5 # f8 - ff -) - -EUCJP_ST = ( - 3, 4, 3, 5,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.START,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#10-17 - MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 3,MachineState.ERROR,#18-1f - 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START#20-27 -) -# fmt: on - -EUCJP_CHAR_LEN_TABLE = (2, 2, 2, 3, 1, 0) - -EUCJP_SM_MODEL: CodingStateMachineDict = { - "class_table": EUCJP_CLS, - "class_factor": 6, - "state_table": EUCJP_ST, - "char_len_table": EUCJP_CHAR_LEN_TABLE, - "name": "EUC-JP", -} - -# EUC-KR -# fmt: off -EUCKR_CLS = ( - 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 - 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f - 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17 - 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f - 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27 - 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f - 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37 - 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f - 1, 1, 1, 1, 1, 1, 1, 1, # 40 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, # 48 - 4f - 1, 1, 1, 1, 1, 1, 1, 1, # 50 - 57 - 1, 1, 1, 1, 1, 1, 1, 1, # 58 - 5f - 1, 1, 1, 1, 1, 1, 1, 1, # 60 - 67 - 1, 1, 1, 1, 1, 1, 1, 1, # 68 - 6f - 1, 1, 1, 1, 1, 1, 1, 1, # 70 - 77 - 1, 1, 1, 1, 1, 1, 1, 1, # 78 - 7f - 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87 - 0, 0, 0, 0, 0, 0, 0, 0, # 88 - 8f - 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97 - 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f - 0, 2, 2, 2, 2, 2, 2, 2, # a0 - a7 - 2, 2, 2, 2, 2, 3, 3, 3, # a8 - af - 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7 - 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf - 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7 - 2, 3, 2, 2, 2, 2, 2, 2, # c8 - cf - 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7 - 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df - 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7 - 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef - 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7 - 2, 2, 2, 2, 2, 2, 2, 0 # f8 - ff -) - -EUCKR_ST = ( - MachineState.ERROR,MachineState.START, 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07 - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #08-0f -) -# fmt: on - -EUCKR_CHAR_LEN_TABLE = (0, 1, 2, 0) - -EUCKR_SM_MODEL: CodingStateMachineDict = { - "class_table": EUCKR_CLS, - "class_factor": 4, - "state_table": EUCKR_ST, - "char_len_table": EUCKR_CHAR_LEN_TABLE, - "name": "EUC-KR", -} - -# JOHAB -# fmt: off -JOHAB_CLS = ( - 4,4,4,4,4,4,4,4, # 00 - 07 - 4,4,4,4,4,4,0,0, # 08 - 0f - 4,4,4,4,4,4,4,4, # 10 - 17 - 4,4,4,0,4,4,4,4, # 18 - 1f - 4,4,4,4,4,4,4,4, # 20 - 27 - 4,4,4,4,4,4,4,4, # 28 - 2f - 4,3,3,3,3,3,3,3, # 30 - 37 - 3,3,3,3,3,3,3,3, # 38 - 3f - 3,1,1,1,1,1,1,1, # 40 - 47 - 1,1,1,1,1,1,1,1, # 48 - 4f - 1,1,1,1,1,1,1,1, # 50 - 57 - 1,1,1,1,1,1,1,1, # 58 - 5f - 1,1,1,1,1,1,1,1, # 60 - 67 - 1,1,1,1,1,1,1,1, # 68 - 6f - 1,1,1,1,1,1,1,1, # 70 - 77 - 1,1,1,1,1,1,1,2, # 78 - 7f - 6,6,6,6,8,8,8,8, # 80 - 87 - 8,8,8,8,8,8,8,8, # 88 - 8f - 8,7,7,7,7,7,7,7, # 90 - 97 - 7,7,7,7,7,7,7,7, # 98 - 9f - 7,7,7,7,7,7,7,7, # a0 - a7 - 7,7,7,7,7,7,7,7, # a8 - af - 7,7,7,7,7,7,7,7, # b0 - b7 - 7,7,7,7,7,7,7,7, # b8 - bf - 7,7,7,7,7,7,7,7, # c0 - c7 - 7,7,7,7,7,7,7,7, # c8 - cf - 7,7,7,7,5,5,5,5, # d0 - d7 - 5,9,9,9,9,9,9,5, # d8 - df - 9,9,9,9,9,9,9,9, # e0 - e7 - 9,9,9,9,9,9,9,9, # e8 - ef - 9,9,9,9,9,9,9,9, # f0 - f7 - 9,9,5,5,5,5,5,0 # f8 - ff -) - -JOHAB_ST = ( -# cls = 0 1 2 3 4 5 6 7 8 9 - MachineState.ERROR ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.ERROR ,MachineState.ERROR ,3 ,3 ,4 , # MachineState.START - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME, # MachineState.ITS_ME - MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR , # MachineState.ERROR - MachineState.ERROR ,MachineState.START ,MachineState.START ,MachineState.ERROR ,MachineState.ERROR ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.START , # 3 - MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START , # 4 -) -# fmt: on - -JOHAB_CHAR_LEN_TABLE = (0, 1, 1, 1, 1, 0, 0, 2, 2, 2) - -JOHAB_SM_MODEL: CodingStateMachineDict = { - "class_table": JOHAB_CLS, - "class_factor": 10, - "state_table": JOHAB_ST, - "char_len_table": JOHAB_CHAR_LEN_TABLE, - "name": "Johab", -} - -# EUC-TW -# fmt: off -EUCTW_CLS = ( - 2, 2, 2, 2, 2, 2, 2, 2, # 00 - 07 - 2, 2, 2, 2, 2, 2, 0, 0, # 08 - 0f - 2, 2, 2, 2, 2, 2, 2, 2, # 10 - 17 - 2, 2, 2, 0, 2, 2, 2, 2, # 18 - 1f - 2, 2, 2, 2, 2, 2, 2, 2, # 20 - 27 - 2, 2, 2, 2, 2, 2, 2, 2, # 28 - 2f - 2, 2, 2, 2, 2, 2, 2, 2, # 30 - 37 - 2, 2, 2, 2, 2, 2, 2, 2, # 38 - 3f - 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47 - 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f - 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57 - 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f - 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67 - 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f - 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77 - 2, 2, 2, 2, 2, 2, 2, 2, # 78 - 7f - 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87 - 0, 0, 0, 0, 0, 0, 6, 0, # 88 - 8f - 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97 - 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f - 0, 3, 4, 4, 4, 4, 4, 4, # a0 - a7 - 5, 5, 1, 1, 1, 1, 1, 1, # a8 - af - 1, 1, 1, 1, 1, 1, 1, 1, # b0 - b7 - 1, 1, 1, 1, 1, 1, 1, 1, # b8 - bf - 1, 1, 3, 1, 3, 3, 3, 3, # c0 - c7 - 3, 3, 3, 3, 3, 3, 3, 3, # c8 - cf - 3, 3, 3, 3, 3, 3, 3, 3, # d0 - d7 - 3, 3, 3, 3, 3, 3, 3, 3, # d8 - df - 3, 3, 3, 3, 3, 3, 3, 3, # e0 - e7 - 3, 3, 3, 3, 3, 3, 3, 3, # e8 - ef - 3, 3, 3, 3, 3, 3, 3, 3, # f0 - f7 - 3, 3, 3, 3, 3, 3, 3, 0 # f8 - ff -) - -EUCTW_ST = ( - MachineState.ERROR,MachineState.ERROR,MachineState.START, 3, 3, 3, 4,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.START,MachineState.ERROR,#10-17 - MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#18-1f - 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.START,MachineState.START,#20-27 - MachineState.START,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START #28-2f -) -# fmt: on - -EUCTW_CHAR_LEN_TABLE = (0, 0, 1, 2, 2, 2, 3) - -EUCTW_SM_MODEL: CodingStateMachineDict = { - "class_table": EUCTW_CLS, - "class_factor": 7, - "state_table": EUCTW_ST, - "char_len_table": EUCTW_CHAR_LEN_TABLE, - "name": "x-euc-tw", -} - -# GB2312 -# fmt: off -GB2312_CLS = ( - 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 - 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f - 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17 - 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f - 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27 - 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f - 3, 3, 3, 3, 3, 3, 3, 3, # 30 - 37 - 3, 3, 1, 1, 1, 1, 1, 1, # 38 - 3f - 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47 - 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f - 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57 - 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f - 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67 - 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f - 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77 - 2, 2, 2, 2, 2, 2, 2, 4, # 78 - 7f - 5, 6, 6, 6, 6, 6, 6, 6, # 80 - 87 - 6, 6, 6, 6, 6, 6, 6, 6, # 88 - 8f - 6, 6, 6, 6, 6, 6, 6, 6, # 90 - 97 - 6, 6, 6, 6, 6, 6, 6, 6, # 98 - 9f - 6, 6, 6, 6, 6, 6, 6, 6, # a0 - a7 - 6, 6, 6, 6, 6, 6, 6, 6, # a8 - af - 6, 6, 6, 6, 6, 6, 6, 6, # b0 - b7 - 6, 6, 6, 6, 6, 6, 6, 6, # b8 - bf - 6, 6, 6, 6, 6, 6, 6, 6, # c0 - c7 - 6, 6, 6, 6, 6, 6, 6, 6, # c8 - cf - 6, 6, 6, 6, 6, 6, 6, 6, # d0 - d7 - 6, 6, 6, 6, 6, 6, 6, 6, # d8 - df - 6, 6, 6, 6, 6, 6, 6, 6, # e0 - e7 - 6, 6, 6, 6, 6, 6, 6, 6, # e8 - ef - 6, 6, 6, 6, 6, 6, 6, 6, # f0 - f7 - 6, 6, 6, 6, 6, 6, 6, 0 # f8 - ff -) - -GB2312_ST = ( - MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START, 3,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,#10-17 - 4,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#18-1f - MachineState.ERROR,MachineState.ERROR, 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,#20-27 - MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START #28-2f -) -# fmt: on - -# To be accurate, the length of class 6 can be either 2 or 4. -# But it is not necessary to discriminate between the two since -# it is used for frequency analysis only, and we are validating -# each code range there as well. So it is safe to set it to be -# 2 here. -GB2312_CHAR_LEN_TABLE = (0, 1, 1, 1, 1, 1, 2) - -GB2312_SM_MODEL: CodingStateMachineDict = { - "class_table": GB2312_CLS, - "class_factor": 7, - "state_table": GB2312_ST, - "char_len_table": GB2312_CHAR_LEN_TABLE, - "name": "GB2312", -} - -# Shift_JIS -# fmt: off -SJIS_CLS = ( - 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 - 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f - 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17 - 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f - 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27 - 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f - 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37 - 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f - 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47 - 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f - 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57 - 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f - 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67 - 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f - 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77 - 2, 2, 2, 2, 2, 2, 2, 1, # 78 - 7f - 3, 3, 3, 3, 3, 2, 2, 3, # 80 - 87 - 3, 3, 3, 3, 3, 3, 3, 3, # 88 - 8f - 3, 3, 3, 3, 3, 3, 3, 3, # 90 - 97 - 3, 3, 3, 3, 3, 3, 3, 3, # 98 - 9f - #0xa0 is illegal in sjis encoding, but some pages does - #contain such byte. We need to be more error forgiven. - 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7 - 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af - 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7 - 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf - 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7 - 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf - 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7 - 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df - 3, 3, 3, 3, 3, 3, 3, 3, # e0 - e7 - 3, 3, 3, 3, 3, 4, 4, 4, # e8 - ef - 3, 3, 3, 3, 3, 3, 3, 3, # f0 - f7 - 3, 3, 3, 3, 3, 0, 0, 0, # f8 - ff -) - -SJIS_ST = ( - MachineState.ERROR,MachineState.START,MachineState.START, 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START #10-17 -) -# fmt: on - -SJIS_CHAR_LEN_TABLE = (0, 1, 1, 2, 0, 0) - -SJIS_SM_MODEL: CodingStateMachineDict = { - "class_table": SJIS_CLS, - "class_factor": 6, - "state_table": SJIS_ST, - "char_len_table": SJIS_CHAR_LEN_TABLE, - "name": "Shift_JIS", -} - -# UCS2-BE -# fmt: off -UCS2BE_CLS = ( - 0, 0, 0, 0, 0, 0, 0, 0, # 00 - 07 - 0, 0, 1, 0, 0, 2, 0, 0, # 08 - 0f - 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17 - 0, 0, 0, 3, 0, 0, 0, 0, # 18 - 1f - 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27 - 0, 3, 3, 3, 3, 3, 0, 0, # 28 - 2f - 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37 - 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f - 0, 0, 0, 0, 0, 0, 0, 0, # 40 - 47 - 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f - 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57 - 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f - 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67 - 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f - 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77 - 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f - 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87 - 0, 0, 0, 0, 0, 0, 0, 0, # 88 - 8f - 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97 - 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f - 0, 0, 0, 0, 0, 0, 0, 0, # a0 - a7 - 0, 0, 0, 0, 0, 0, 0, 0, # a8 - af - 0, 0, 0, 0, 0, 0, 0, 0, # b0 - b7 - 0, 0, 0, 0, 0, 0, 0, 0, # b8 - bf - 0, 0, 0, 0, 0, 0, 0, 0, # c0 - c7 - 0, 0, 0, 0, 0, 0, 0, 0, # c8 - cf - 0, 0, 0, 0, 0, 0, 0, 0, # d0 - d7 - 0, 0, 0, 0, 0, 0, 0, 0, # d8 - df - 0, 0, 0, 0, 0, 0, 0, 0, # e0 - e7 - 0, 0, 0, 0, 0, 0, 0, 0, # e8 - ef - 0, 0, 0, 0, 0, 0, 0, 0, # f0 - f7 - 0, 0, 0, 0, 0, 0, 4, 5 # f8 - ff -) - -UCS2BE_ST = ( - 5, 7, 7,MachineState.ERROR, 4, 3,MachineState.ERROR,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f - MachineState.ITS_ME,MachineState.ITS_ME, 6, 6, 6, 6,MachineState.ERROR,MachineState.ERROR,#10-17 - 6, 6, 6, 6, 6,MachineState.ITS_ME, 6, 6,#18-1f - 6, 6, 6, 6, 5, 7, 7,MachineState.ERROR,#20-27 - 5, 8, 6, 6,MachineState.ERROR, 6, 6, 6,#28-2f - 6, 6, 6, 6,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #30-37 -) -# fmt: on - -UCS2BE_CHAR_LEN_TABLE = (2, 2, 2, 0, 2, 2) - -UCS2BE_SM_MODEL: CodingStateMachineDict = { - "class_table": UCS2BE_CLS, - "class_factor": 6, - "state_table": UCS2BE_ST, - "char_len_table": UCS2BE_CHAR_LEN_TABLE, - "name": "UTF-16BE", -} - -# UCS2-LE -# fmt: off -UCS2LE_CLS = ( - 0, 0, 0, 0, 0, 0, 0, 0, # 00 - 07 - 0, 0, 1, 0, 0, 2, 0, 0, # 08 - 0f - 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17 - 0, 0, 0, 3, 0, 0, 0, 0, # 18 - 1f - 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27 - 0, 3, 3, 3, 3, 3, 0, 0, # 28 - 2f - 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37 - 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f - 0, 0, 0, 0, 0, 0, 0, 0, # 40 - 47 - 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f - 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57 - 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f - 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67 - 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f - 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77 - 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f - 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87 - 0, 0, 0, 0, 0, 0, 0, 0, # 88 - 8f - 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97 - 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f - 0, 0, 0, 0, 0, 0, 0, 0, # a0 - a7 - 0, 0, 0, 0, 0, 0, 0, 0, # a8 - af - 0, 0, 0, 0, 0, 0, 0, 0, # b0 - b7 - 0, 0, 0, 0, 0, 0, 0, 0, # b8 - bf - 0, 0, 0, 0, 0, 0, 0, 0, # c0 - c7 - 0, 0, 0, 0, 0, 0, 0, 0, # c8 - cf - 0, 0, 0, 0, 0, 0, 0, 0, # d0 - d7 - 0, 0, 0, 0, 0, 0, 0, 0, # d8 - df - 0, 0, 0, 0, 0, 0, 0, 0, # e0 - e7 - 0, 0, 0, 0, 0, 0, 0, 0, # e8 - ef - 0, 0, 0, 0, 0, 0, 0, 0, # f0 - f7 - 0, 0, 0, 0, 0, 0, 4, 5 # f8 - ff -) - -UCS2LE_ST = ( - 6, 6, 7, 6, 4, 3,MachineState.ERROR,MachineState.ERROR,#00-07 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f - MachineState.ITS_ME,MachineState.ITS_ME, 5, 5, 5,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,#10-17 - 5, 5, 5,MachineState.ERROR, 5,MachineState.ERROR, 6, 6,#18-1f - 7, 6, 8, 8, 5, 5, 5,MachineState.ERROR,#20-27 - 5, 5, 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 5, 5,#28-2f - 5, 5, 5,MachineState.ERROR, 5,MachineState.ERROR,MachineState.START,MachineState.START #30-37 -) -# fmt: on - -UCS2LE_CHAR_LEN_TABLE = (2, 2, 2, 2, 2, 2) - -UCS2LE_SM_MODEL: CodingStateMachineDict = { - "class_table": UCS2LE_CLS, - "class_factor": 6, - "state_table": UCS2LE_ST, - "char_len_table": UCS2LE_CHAR_LEN_TABLE, - "name": "UTF-16LE", -} - -# UTF-8 -# fmt: off -UTF8_CLS = ( - 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 #allow 0x00 as a legal value - 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f - 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17 - 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f - 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27 - 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f - 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37 - 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f - 1, 1, 1, 1, 1, 1, 1, 1, # 40 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, # 48 - 4f - 1, 1, 1, 1, 1, 1, 1, 1, # 50 - 57 - 1, 1, 1, 1, 1, 1, 1, 1, # 58 - 5f - 1, 1, 1, 1, 1, 1, 1, 1, # 60 - 67 - 1, 1, 1, 1, 1, 1, 1, 1, # 68 - 6f - 1, 1, 1, 1, 1, 1, 1, 1, # 70 - 77 - 1, 1, 1, 1, 1, 1, 1, 1, # 78 - 7f - 2, 2, 2, 2, 3, 3, 3, 3, # 80 - 87 - 4, 4, 4, 4, 4, 4, 4, 4, # 88 - 8f - 4, 4, 4, 4, 4, 4, 4, 4, # 90 - 97 - 4, 4, 4, 4, 4, 4, 4, 4, # 98 - 9f - 5, 5, 5, 5, 5, 5, 5, 5, # a0 - a7 - 5, 5, 5, 5, 5, 5, 5, 5, # a8 - af - 5, 5, 5, 5, 5, 5, 5, 5, # b0 - b7 - 5, 5, 5, 5, 5, 5, 5, 5, # b8 - bf - 0, 0, 6, 6, 6, 6, 6, 6, # c0 - c7 - 6, 6, 6, 6, 6, 6, 6, 6, # c8 - cf - 6, 6, 6, 6, 6, 6, 6, 6, # d0 - d7 - 6, 6, 6, 6, 6, 6, 6, 6, # d8 - df - 7, 8, 8, 8, 8, 8, 8, 8, # e0 - e7 - 8, 8, 8, 8, 8, 9, 8, 8, # e8 - ef - 10, 11, 11, 11, 11, 11, 11, 11, # f0 - f7 - 12, 13, 13, 13, 14, 15, 0, 0 # f8 - ff -) - -UTF8_ST = ( - MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 12, 10,#00-07 - 9, 11, 8, 7, 6, 5, 4, 3,#08-0f - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#10-17 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#18-1f - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#20-27 - MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#28-2f - MachineState.ERROR,MachineState.ERROR, 5, 5, 5, 5,MachineState.ERROR,MachineState.ERROR,#30-37 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#38-3f - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 5, 5, 5,MachineState.ERROR,MachineState.ERROR,#40-47 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#48-4f - MachineState.ERROR,MachineState.ERROR, 7, 7, 7, 7,MachineState.ERROR,MachineState.ERROR,#50-57 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#58-5f - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 7, 7,MachineState.ERROR,MachineState.ERROR,#60-67 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#68-6f - MachineState.ERROR,MachineState.ERROR, 9, 9, 9, 9,MachineState.ERROR,MachineState.ERROR,#70-77 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#78-7f - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 9,MachineState.ERROR,MachineState.ERROR,#80-87 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#88-8f - MachineState.ERROR,MachineState.ERROR, 12, 12, 12, 12,MachineState.ERROR,MachineState.ERROR,#90-97 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#98-9f - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 12,MachineState.ERROR,MachineState.ERROR,#a0-a7 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#a8-af - MachineState.ERROR,MachineState.ERROR, 12, 12, 12,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#b0-b7 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#b8-bf - MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,#c0-c7 - MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR #c8-cf -) -# fmt: on - -UTF8_CHAR_LEN_TABLE = (0, 1, 0, 0, 0, 0, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6) - -UTF8_SM_MODEL: CodingStateMachineDict = { - "class_table": UTF8_CLS, - "class_factor": 16, - "state_table": UTF8_ST, - "char_len_table": UTF8_CHAR_LEN_TABLE, - "name": "UTF-8", -} diff --git a/libs/chardet/metadata/languages.py b/libs/chardet/metadata/languages.py deleted file mode 100644 index eb40c5f0c..000000000 --- a/libs/chardet/metadata/languages.py +++ /dev/null @@ -1,352 +0,0 @@ -""" -Metadata about languages used by our model training code for our -SingleByteCharSetProbers. Could be used for other things in the future. - -This code is based on the language metadata from the uchardet project. -""" - -from string import ascii_letters -from typing import List, Optional - -# TODO: Add Ukrainian (KOI8-U) - - -class Language: - """Metadata about a language useful for training models - - :ivar name: The human name for the language, in English. - :type name: str - :ivar iso_code: 2-letter ISO 639-1 if possible, 3-letter ISO code otherwise, - or use another catalog as a last resort. - :type iso_code: str - :ivar use_ascii: Whether or not ASCII letters should be included in trained - models. - :type use_ascii: bool - :ivar charsets: The charsets we want to support and create data for. - :type charsets: list of str - :ivar alphabet: The characters in the language's alphabet. If `use_ascii` is - `True`, you only need to add those not in the ASCII set. - :type alphabet: str - :ivar wiki_start_pages: The Wikipedia pages to start from if we're crawling - Wikipedia for training data. - :type wiki_start_pages: list of str - """ - - def __init__( - self, - name: Optional[str] = None, - iso_code: Optional[str] = None, - use_ascii: bool = True, - charsets: Optional[List[str]] = None, - alphabet: Optional[str] = None, - wiki_start_pages: Optional[List[str]] = None, - ) -> None: - super().__init__() - self.name = name - self.iso_code = iso_code - self.use_ascii = use_ascii - self.charsets = charsets - if self.use_ascii: - if alphabet: - alphabet += ascii_letters - else: - alphabet = ascii_letters - elif not alphabet: - raise ValueError("Must supply alphabet if use_ascii is False") - self.alphabet = "".join(sorted(set(alphabet))) if alphabet else None - self.wiki_start_pages = wiki_start_pages - - def __repr__(self) -> str: - param_str = ", ".join( - f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_") - ) - return f"{self.__class__.__name__}({param_str})" - - -LANGUAGES = { - "Arabic": Language( - name="Arabic", - iso_code="ar", - use_ascii=False, - # We only support encodings that use isolated - # forms, because the current recommendation is - # that the rendering system handles presentation - # forms. This means we purposefully skip IBM864. - charsets=["ISO-8859-6", "WINDOWS-1256", "CP720", "CP864"], - alphabet="ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّ", - wiki_start_pages=["الصفحة_الرئيسية"], - ), - "Belarusian": Language( - name="Belarusian", - iso_code="be", - use_ascii=False, - charsets=["ISO-8859-5", "WINDOWS-1251", "IBM866", "MacCyrillic"], - alphabet="АБВГДЕЁЖЗІЙКЛМНОПРСТУЎФХЦЧШЫЬЭЮЯабвгдеёжзійклмнопрстуўфхцчшыьэюяʼ", - wiki_start_pages=["Галоўная_старонка"], - ), - "Bulgarian": Language( - name="Bulgarian", - iso_code="bg", - use_ascii=False, - charsets=["ISO-8859-5", "WINDOWS-1251", "IBM855"], - alphabet="АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", - wiki_start_pages=["Начална_страница"], - ), - "Czech": Language( - name="Czech", - iso_code="cz", - use_ascii=True, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ", - wiki_start_pages=["Hlavní_strana"], - ), - "Danish": Language( - name="Danish", - iso_code="da", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="æøåÆØÅ", - wiki_start_pages=["Forside"], - ), - "German": Language( - name="German", - iso_code="de", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="äöüßẞÄÖÜ", - wiki_start_pages=["Wikipedia:Hauptseite"], - ), - "Greek": Language( - name="Greek", - iso_code="el", - use_ascii=False, - charsets=["ISO-8859-7", "WINDOWS-1253"], - alphabet="αβγδεζηθικλμνξοπρσςτυφχψωάέήίόύώΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΣΤΥΦΧΨΩΆΈΉΊΌΎΏ", - wiki_start_pages=["Πύλη:Κύρια"], - ), - "English": Language( - name="English", - iso_code="en", - use_ascii=True, - charsets=["ISO-8859-1", "WINDOWS-1252", "MacRoman"], - wiki_start_pages=["Main_Page"], - ), - "Esperanto": Language( - name="Esperanto", - iso_code="eo", - # Q, W, X, and Y not used at all - use_ascii=False, - charsets=["ISO-8859-3"], - alphabet="abcĉdefgĝhĥijĵklmnoprsŝtuŭvzABCĈDEFGĜHĤIJĴKLMNOPRSŜTUŬVZ", - wiki_start_pages=["Vikipedio:Ĉefpaĝo"], - ), - "Spanish": Language( - name="Spanish", - iso_code="es", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="ñáéíóúüÑÁÉÍÓÚÜ", - wiki_start_pages=["Wikipedia:Portada"], - ), - "Estonian": Language( - name="Estonian", - iso_code="et", - use_ascii=False, - charsets=["ISO-8859-4", "ISO-8859-13", "WINDOWS-1257"], - # C, F, Š, Q, W, X, Y, Z, Ž are only for - # loanwords - alphabet="ABDEGHIJKLMNOPRSTUVÕÄÖÜabdeghijklmnoprstuvõäöü", - wiki_start_pages=["Esileht"], - ), - "Finnish": Language( - name="Finnish", - iso_code="fi", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="ÅÄÖŠŽåäöšž", - wiki_start_pages=["Wikipedia:Etusivu"], - ), - "French": Language( - name="French", - iso_code="fr", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="œàâçèéîïùûêŒÀÂÇÈÉÎÏÙÛÊ", - wiki_start_pages=["Wikipédia:Accueil_principal", "Bœuf (animal)"], - ), - "Hebrew": Language( - name="Hebrew", - iso_code="he", - use_ascii=False, - charsets=["ISO-8859-8", "WINDOWS-1255"], - alphabet="אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ", - wiki_start_pages=["עמוד_ראשי"], - ), - "Croatian": Language( - name="Croatian", - iso_code="hr", - # Q, W, X, Y are only used for foreign words. - use_ascii=False, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="abcčćdđefghijklmnoprsštuvzžABCČĆDĐEFGHIJKLMNOPRSŠTUVZŽ", - wiki_start_pages=["Glavna_stranica"], - ), - "Hungarian": Language( - name="Hungarian", - iso_code="hu", - # Q, W, X, Y are only used for foreign words. - use_ascii=False, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="abcdefghijklmnoprstuvzáéíóöőúüűABCDEFGHIJKLMNOPRSTUVZÁÉÍÓÖŐÚÜŰ", - wiki_start_pages=["Kezdőlap"], - ), - "Italian": Language( - name="Italian", - iso_code="it", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="ÀÈÉÌÒÓÙàèéìòóù", - wiki_start_pages=["Pagina_principale"], - ), - "Lithuanian": Language( - name="Lithuanian", - iso_code="lt", - use_ascii=False, - charsets=["ISO-8859-13", "WINDOWS-1257", "ISO-8859-4"], - # Q, W, and X not used at all - alphabet="AĄBCČDEĘĖFGHIĮYJKLMNOPRSŠTUŲŪVZŽaąbcčdeęėfghiįyjklmnoprsštuųūvzž", - wiki_start_pages=["Pagrindinis_puslapis"], - ), - "Latvian": Language( - name="Latvian", - iso_code="lv", - use_ascii=False, - charsets=["ISO-8859-13", "WINDOWS-1257", "ISO-8859-4"], - # Q, W, X, Y are only for loanwords - alphabet="AĀBCČDEĒFGĢHIĪJKĶLĻMNŅOPRSŠTUŪVZŽaābcčdeēfgģhiījkķlļmnņoprsštuūvzž", - wiki_start_pages=["Sākumlapa"], - ), - "Macedonian": Language( - name="Macedonian", - iso_code="mk", - use_ascii=False, - charsets=["ISO-8859-5", "WINDOWS-1251", "MacCyrillic", "IBM855"], - alphabet="АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЦЧЏШабвгдѓежзѕијклљмнњопрстќуфхцчџш", - wiki_start_pages=["Главна_страница"], - ), - "Dutch": Language( - name="Dutch", - iso_code="nl", - use_ascii=True, - charsets=["ISO-8859-1", "WINDOWS-1252", "MacRoman"], - wiki_start_pages=["Hoofdpagina"], - ), - "Polish": Language( - name="Polish", - iso_code="pl", - # Q and X are only used for foreign words. - use_ascii=False, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="AĄBCĆDEĘFGHIJKLŁMNŃOÓPRSŚTUWYZŹŻaąbcćdeęfghijklłmnńoóprsśtuwyzźż", - wiki_start_pages=["Wikipedia:Strona_główna"], - ), - "Portuguese": Language( - name="Portuguese", - iso_code="pt", - use_ascii=True, - charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252", "MacRoman"], - alphabet="ÁÂÃÀÇÉÊÍÓÔÕÚáâãàçéêíóôõú", - wiki_start_pages=["Wikipédia:Página_principal"], - ), - "Romanian": Language( - name="Romanian", - iso_code="ro", - use_ascii=True, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="ăâîșțĂÂÎȘȚ", - wiki_start_pages=["Pagina_principală"], - ), - "Russian": Language( - name="Russian", - iso_code="ru", - use_ascii=False, - charsets=[ - "ISO-8859-5", - "WINDOWS-1251", - "KOI8-R", - "MacCyrillic", - "IBM866", - "IBM855", - ], - alphabet="абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ", - wiki_start_pages=["Заглавная_страница"], - ), - "Slovak": Language( - name="Slovak", - iso_code="sk", - use_ascii=True, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="áäčďéíĺľňóôŕšťúýžÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽ", - wiki_start_pages=["Hlavná_stránka"], - ), - "Slovene": Language( - name="Slovene", - iso_code="sl", - # Q, W, X, Y are only used for foreign words. - use_ascii=False, - charsets=["ISO-8859-2", "WINDOWS-1250"], - alphabet="abcčdefghijklmnoprsštuvzžABCČDEFGHIJKLMNOPRSŠTUVZŽ", - wiki_start_pages=["Glavna_stran"], - ), - # Serbian can be written in both Latin and Cyrillic, but there's no - # simple way to get the Latin alphabet pages from Wikipedia through - # the API, so for now we just support Cyrillic. - "Serbian": Language( - name="Serbian", - iso_code="sr", - alphabet="АБВГДЂЕЖЗИЈКЛЉМНЊОПРСТЋУФХЦЧЏШабвгдђежзијклљмнњопрстћуфхцчџш", - charsets=["ISO-8859-5", "WINDOWS-1251", "MacCyrillic", "IBM855"], - wiki_start_pages=["Главна_страна"], - ), - "Thai": Language( - name="Thai", - iso_code="th", - use_ascii=False, - charsets=["ISO-8859-11", "TIS-620", "CP874"], - alphabet="กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛", - wiki_start_pages=["หน้าหลัก"], - ), - "Turkish": Language( - name="Turkish", - iso_code="tr", - # Q, W, and X are not used by Turkish - use_ascii=False, - charsets=["ISO-8859-3", "ISO-8859-9", "WINDOWS-1254"], - alphabet="abcçdefgğhıijklmnoöprsştuüvyzâîûABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZÂÎÛ", - wiki_start_pages=["Ana_Sayfa"], - ), - "Vietnamese": Language( - name="Vietnamese", - iso_code="vi", - use_ascii=False, - # Windows-1258 is the only common 8-bit - # Vietnamese encoding supported by Python. - # From Wikipedia: - # For systems that lack support for Unicode, - # dozens of 8-bit Vietnamese code pages are - # available.[1] The most common are VISCII - # (TCVN 5712:1993), VPS, and Windows-1258.[3] - # Where ASCII is required, such as when - # ensuring readability in plain text e-mail, - # Vietnamese letters are often encoded - # according to Vietnamese Quoted-Readable - # (VIQR) or VSCII Mnemonic (VSCII-MNEM),[4] - # though usage of either variable-width - # scheme has declined dramatically following - # the adoption of Unicode on the World Wide - # Web. - charsets=["WINDOWS-1258"], - alphabet="aăâbcdđeêghiklmnoôơpqrstuưvxyAĂÂBCDĐEÊGHIKLMNOÔƠPQRSTUƯVXY", - wiki_start_pages=["Chữ_Quốc_ngữ"], - ), -} diff --git a/libs/chardet/models/__init__.py b/libs/chardet/models/__init__.py new file mode 100644 index 000000000..3e94998b4 --- /dev/null +++ b/libs/chardet/models/__init__.py @@ -0,0 +1,348 @@ +"""Model loading and bigram scoring utilities. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +import functools +import importlib.resources +import math +import struct +import warnings +import zlib + +from chardet.registry import REGISTRY, lookup_encoding + +_unpack_uint32 = struct.Struct(">I").unpack_from +_unpack_float64 = struct.Struct(">d").unpack_from +_V2_MAGIC = b"CMD2" + +# Encodings that map to exactly one language, derived from the registry. +# Keyed by canonical name only — callers always use canonical names. +_SINGLE_LANG_MAP: dict[str, str] = {} +for _enc in REGISTRY.values(): + if len(_enc.languages) == 1: + _SINGLE_LANG_MAP[_enc.name] = _enc.languages[0] + + +def _parse_models_bin( + data: bytes, +) -> tuple[dict[str, memoryview], dict[str, float]]: + """Parse the v2 dense zlib-compressed models.bin format. + + :param data: Raw bytes of models.bin (must be non-empty). + :returns: A ``(models, norms)`` tuple. + :raises ValueError: If the data is corrupt or truncated. + """ + try: + if data[:4] != _V2_MAGIC: + msg = "corrupt models.bin: missing CMD2 magic" + raise ValueError(msg) + + offset = 4 # skip magic + (num_models,) = _unpack_uint32(data, offset) + offset += 4 + + if num_models > 10_000: + msg = f"corrupt models.bin: num_models={num_models} exceeds limit" + raise ValueError(msg) + + names: list[str] = [] + norms: dict[str, float] = {} + for _ in range(num_models): + (name_len,) = _unpack_uint32(data, offset) + offset += 4 + if name_len > 256: + msg = f"corrupt models.bin: name_len={name_len} exceeds 256" + raise ValueError(msg) + name = data[offset : offset + name_len].decode("utf-8") + offset += name_len + (norm,) = _unpack_float64(data, offset) + offset += 8 + names.append(name) + norms[name] = norm + + # zlib.decompress is faster than decompressobj; trailing bytes are + # unlikely in bundled data and would not affect correctness since we + # validate decompressed size. train.py uses decompressobj for + # stricter checking during model generation. + blob = zlib.decompress(data[offset:]) + expected_size = num_models * 65536 + if len(blob) != expected_size: + msg = ( + f"corrupt models.bin: decompressed size {len(blob)} " + f"!= expected {expected_size}" + ) + raise ValueError(msg) + + # memoryview slices avoid copies; the blob bytes object is kept + # alive by the functools.cache on _load_models_data(). + mv = memoryview(blob) + models: dict[str, memoryview] = {} + for i, name in enumerate(names): + start = i * 65536 + models[name] = mv[start : start + 65536] + + except zlib.error as e: + msg = f"corrupt models.bin: {e}" + raise ValueError(msg) from e + except (struct.error, UnicodeDecodeError) as e: + msg = f"corrupt models.bin: {e}" + raise ValueError(msg) from e + + return models, norms + + +@functools.cache +def _load_models_data() -> tuple[dict[str, memoryview], dict[str, float]]: + """Load and parse models.bin, returning (models, norms). + + Cached: only reads from disk on first call. + """ + ref = importlib.resources.files("chardet.models").joinpath("models.bin") + data = ref.read_bytes() + + if not data: + warnings.warn( + "chardet models.bin is empty — statistical detection disabled; " + "reinstall chardet to fix", + RuntimeWarning, + stacklevel=2, + ) + return {}, {} + + return _parse_models_bin(data) + + +def load_models() -> dict[str, memoryview]: + """Load all bigram models from the bundled models.bin file. + + Each model is a memoryview of length 65536 (256*256). + Index: (b1 << 8) | b2 -> weight (0-255). + + :returns: A dict mapping model key strings to 65536-byte lookup tables. + """ + return _load_models_data()[0] + + +def _build_enc_index( + models: dict[str, memoryview], +) -> dict[str, list[tuple[str | None, memoryview, str]]]: + """Build a grouped index from a models dict. + + :param models: Mapping of ``"lang/encoding"`` keys to 65536-byte tables. + :returns: Mapping of encoding name to ``[(lang, model, model_key), ...]``. + """ + index: dict[str, list[tuple[str | None, memoryview, str]]] = {} + for key, model in models.items(): + lang, enc = key.split("/", 1) + index.setdefault(enc, []).append((lang, model, key)) + + # Resolve aliases: if a model key uses a non-canonical name, + # copy the entry under the canonical name. + for enc_name in list(index): + canonical = lookup_encoding(enc_name) + if canonical is not None and canonical not in index: + index[canonical] = index[enc_name] + + return index + + +@functools.cache +def get_enc_index() -> dict[str, list[tuple[str | None, memoryview, str]]]: + """Return a pre-grouped index mapping encoding name -> [(lang, model, model_key), ...].""" + return _build_enc_index(load_models()) + + +def infer_language(encoding: str) -> str | None: + """Return the language for a single-language encoding, or None. + + :param encoding: The canonical encoding name. + :returns: An ISO 639-1 language code, or ``None`` if the encoding is + multi-language. + """ + return _SINGLE_LANG_MAP.get(encoding) + + +def has_model_variants(encoding: str) -> bool: + """Return True if the encoding has language variants in the model index. + + :param encoding: The canonical encoding name. + :returns: ``True`` if bigram models exist for this encoding. + """ + return encoding in get_enc_index() + + +def _get_model_norms() -> dict[str, float]: + """Return cached L2 norms for all models, keyed by model key string.""" + return _load_models_data()[1] + + +@functools.cache +def get_idf_weights() -> bytearray: + """Return a 65536-byte IDF weight table for bigram profile construction. + + Loads a precomputed table from ``idf.bin`` (generated at training time). + For each bigram index, the weight reflects how discriminative that bigram + is across all models: + + - Bigrams in every model (common ASCII) → weight 1 (minimal signal) + - Bigrams in one model → weight 255 (maximum signal) + - Bigrams not in any model → weight 1 (unknown, treat as neutral) + """ + ref = importlib.resources.files("chardet.models").joinpath("idf.bin") + data = ref.read_bytes() + if len(data) != 65536: + warnings.warn( + f"chardet idf.bin has wrong size ({len(data)}), " + "falling back to uniform weights", + RuntimeWarning, + stacklevel=2, + ) + return bytearray(b"\x01" * 65536) + return bytearray(data) + + +class BigramProfile: + """Pre-computed bigram frequency distribution for a data sample. + + Computing this once and reusing it across all models reduces per-model + scoring from O(n) to O(distinct_bigrams). + + Stores a dense ``freq`` list of length 65536 indexed by bigram index, plus + a ``nonzero`` list of indices with non-zero frequency for fast iteration. + Each bigram is weighted by its IDF (inverse document frequency) across all + models — bigrams unique to few models get high weight, bigrams common to + all models get weight 1. + """ + + __slots__ = ("freq", "input_norm", "nonzero", "weight_sum") + + def __init__(self, data: bytes) -> None: + """Compute the bigram frequency distribution for *data*. + + Each bigram is weighted by its IDF (inverse document frequency) across + all loaded models. Bigrams unique to few models get high weight; + bigrams common to all models get weight 1. + + :param data: The raw byte data to profile. + """ + total_bigrams = len(data) - 1 + if total_bigrams <= 0: + # Use empty lists (not [0]*65536) to avoid a 256KB allocation + # for no-op profiles. Safe because score_with_profile returns + # early when input_norm == 0.0, so freq is never indexed. + self.freq: list[int] = [] + self.nonzero: list[int] = [] + self.weight_sum: int = 0 + self.input_norm: float = 0.0 + return + + idf = get_idf_weights() + freq: list[int] = [0] * 65536 + nonzero: list[int] = [] + w_sum = 0 + for i in range(total_bigrams): + idx = (data[i] << 8) | data[i + 1] + w = idf[idx] + if freq[idx] == 0: + nonzero.append(idx) + freq[idx] += w + w_sum += w + self.freq = freq + self.nonzero = nonzero + self.weight_sum = w_sum + norm_sq = 0 + for idx in nonzero: + v = freq[idx] + norm_sq += v * v + self.input_norm = math.sqrt(norm_sq) + + @classmethod + def from_weighted_freq(cls, weighted_freq: dict[int, int]) -> "BigramProfile": + """Create a BigramProfile from pre-computed weighted frequencies. + + Computes ``weight_sum`` and ``input_norm`` from *weighted_freq* to + ensure consistency between the stored fields. + + :param weighted_freq: Mapping of bigram index to weighted count. + :returns: A new :class:`BigramProfile` instance. + """ + profile = cls(b"") + freq: list[int] = [0] * 65536 + nonzero: list[int] = [] + for idx, count in weighted_freq.items(): + freq[idx] = count + if count: + nonzero.append(idx) + profile.freq = freq + profile.nonzero = nonzero + profile.weight_sum = sum(weighted_freq.values()) + profile.input_norm = math.sqrt(sum(v * v for v in weighted_freq.values())) + return profile + + +def score_with_profile( + profile: BigramProfile, model: bytearray | memoryview, model_key: str = "" +) -> float: + """Score a pre-computed bigram profile against a single model using cosine similarity.""" + if profile.input_norm == 0.0: + return 0.0 + norms = _get_model_norms() + model_norm = norms.get(model_key) if model_key else None + if model_norm is None: + sq_sum = 0 + for i in range(65536): + v = model[i] + if v: + sq_sum += v * v + model_norm = math.sqrt(sq_sum) + if model_norm == 0.0: + return 0.0 + dot = 0 + freq = profile.freq + for idx in profile.nonzero: + dot += model[idx] * freq[idx] + return dot / (model_norm * profile.input_norm) + + +def score_best_language( + data: bytes, + encoding: str, + profile: BigramProfile | None = None, +) -> tuple[float, str | None]: + """Score data against all language variants of an encoding. + + Returns (best_score, best_language). Uses a pre-grouped index for O(L) + lookup where L is the number of language variants for the encoding. + + If *profile* is provided, it is reused instead of recomputing the bigram + frequency distribution from *data*. + + :param data: The raw byte data to score. + :param encoding: The canonical encoding name to match against. + :param profile: Optional pre-computed :class:`BigramProfile` to reuse. + :returns: A ``(score, language)`` tuple with the best cosine-similarity + score and the corresponding language code (or ``None``). + """ + if not data and profile is None: + return 0.0, None + + index = get_enc_index() + variants = index.get(encoding) + if variants is None: + return 0.0, None + + if profile is None: + profile = BigramProfile(data) + + best_score = 0.0 + best_lang: str | None = None + for lang, model, model_key in variants: + s = score_with_profile(profile, model, model_key) + if s > best_score: + best_score = s + best_lang = lang + + return best_score, best_lang diff --git a/libs/chardet/models/confusion.bin b/libs/chardet/models/confusion.bin new file mode 100644 index 000000000..257c21ed5 Binary files /dev/null and b/libs/chardet/models/confusion.bin differ diff --git a/libs/chardet/models/idf.bin b/libs/chardet/models/idf.bin new file mode 100644 index 000000000..ad0d1e9d1 --- /dev/null +++ b/libs/chardet/models/idf.bin @@ -0,0 +1,35 @@ +tύ'Ou# (!)%#"$Yf^ytpwtwr}wtturafc{mmyKM_ytϹ}}epirUtiyatJuwPm}trSj}oywmZAVVW_NXl}tw_ffBS[We`Zd]arwpWK;V\erSdrdpifolj}jwuo+{`uI,f +  + + +#}Ϲy   K + J/+8     +S + +y.3 +YfQ\]@efeuajehScVZaQ[HIFphmemmppdJXLQL^JeLhGNIWC?AwmiXN\0=KLTLITPoW^`XXm>61HCEEBA8U[WKVCAiNQYOEjIw8fh[a :ùϹϱ%"ùrÊÛÄm{}}oϛϗr}}{{y}oteihtcuu}p_dmmmoy{iVYc`hp`mlwlw}ut}{w᫫ñùϹϹϹϱϫϱùùϹϹϹù}Ϲùùl^`ldhmw_c{jcjjdlde}r`````^```a_```__``^_`^e}c```````ehQϹ5Wd{U႓{o}^\ᱠϠϱϥùϫϗåϹùÛëÛàϗñëϱϹñ"@ñϛwft\{}wᓄ}{{}ϥ}rryu}wpachp{oy}tu`aooiy}y_LNXPZlVatleepfj}wpp}   ϹñϹALI_WᱹϱᱱϹùùÛϹϱùϱᱫààϱàñùpUE+iᱱu}}yuϓfy3E]UdCyUGF3YAl)7i]Ṡ{ryϱwtw}}}twfluychrtpuaQa\XS^`dopljmrmu}lttw}<PM(%t#mrÛtϐyre`upooÂ}ltwf}ϥ{jQ{fy{p{ϫ 5hf6ϫϗϹ᫐ϠÐϹ7Qϱ) /2'+!FùùëϊϫϠrϥñññϫoϥϠÄUVK  + + + + +BùëϹ᫹ṱṱùùᐫùñXm5H 68:?TëϹ᫹ϫùᗥññVV +?ZhfNaillië᫹ùϹϹùᥫϥñZmmpymIrlϹϫϹåϹᠱϱyϓRL! +yyhyùϫùϹùᱹùym%$}ùϫyl';j{}ùϱyo&,yyyy}rpùϹùyjr%2c_aYMQPE{ù ë}L(=E>HIùÛϱϥñ8ùñùpù ùùñùùùϠ_\wϱà__ϱwåå}r`^]^`_``]\l\ϹÍ^^^]^^^a_ᱍ``___d_`Ϲϥa{p_____``__ù__``__`````````````````````Ϡϱùtt]᱓}L`/Â5@JñpN=/XKfB({2.:-[utϫëϹùùùùϱᱹñëϫᥛᥠϹÓṫåJϹύñùùϥ{ùñùϹù+#fᐛëϓwåϹṹyëwϱ,ϱṹTϹ{ϹM29<ET{ëϹϥϱÐÓϛᇹpùùùZNa}ϱ,/ϠùϹϥϫÐåñϹññ}ù᫹ϐëϫϊϥᱠ፠ϥWo.&9{o{X_Mw#W!7AI1ULÍåϱϱṗᗫϹᥥññϹϹϱϱϹùùϱñù.LëRϱϛϹëà᱓ÐëṫύṹᄥϠñÇÛZϹùùùùùϠ#ZV5ς}àᠫñ{ᥛtùϹùë}ñùùñ%1o2l×ñù×ϹëÛϱϐṹ}}ፗëϹWacHÓ'}ϱEiÛ[Tc5VpϱϓùñåëϹϹùwϠù(m)Ϡ×ṥàᱱÓϱṱϹኛύÍC]\tÍù\y"1ϐ.7%1.ÐZ][Zeiadaàm}lRc]Yw\^}{Zj]]fyiñÇ}ycoehaet}ñ×Û``_U_\McXaÊ5ϹVGdϛ/᫂×ñϱ᫓ᱹτÐuṗàùwϹÄϛ;ùI`àu(VXOeXf_`_cwlaThpVdϹRVmlñWYINQXZe\mr`\]f[arYXVdul᥍h`_LZ_Wpfr{"ϹñùùϹCϛQχmϠwo"U[ṹṓ᥹ñϛàrϠ᫠ëë᫂p1ù{ṹùùùñ^åëFLVralfcNh9D8Boëϱṫ>eupSV''0åϱϓϱϥϹ{ϫyÍϫςOᱱu{ᄂ$D;SUύ';Ûñϓñፍuϫϱ}{Çςϐ×à=Vpᥥ5HW>T#LH}ϗϥÇϹϱϹϹùo{åt}ϛύ'81ϹFNᗛ#`{ϠëϐአÐÛϗṹåpϫëϫwϫ}τTy;l%ljråϹϱϛëRùàH9{ϐåύṐϹϱ{ñåyϥtÛùϹ>C)cùϱϱVjucùåo{{Uw[f{ϹϹ]ñϱϹϱmùZùϹu[_}_acϥd_^^^```ad]Ϲpl_^_^^```amr```dfafieupialyad`ar````````ce# `B_7--Jc- !# 2j +Q;'_Vj`SrefUùffSdm\l^Vhu]ñcÍàñ{mj\yhwϹϛϱÓMናrERTcpw}Û`WùùùùùϹ=c {3dQ'Gw7ÛϗϠ×ϹuùtyϠyϥuoVB> + 4\N +#V yQyϹᗄϠëùϹϗorϐϹL ù[w"F!u7+ +W?> V=+.2]$?3[5efÓᛗṠr{Ûùroawu{{uIl6 #Q  "*" + ` +".ϗ{ååùϥlwÛåip{{ëϛåwatëyr{ᐥmP{'ciljU.IaϫÇϓuϛϛχuëy_yϱwm{J +`$uṹeFp7%ZiZ0:ZrG{ϛϠϫÇyñϱtåtly}ϗrÍe9`=Ϲ mlU m{RL0G$!dE^9l×Íë}àå᥇ᠱrñϱup`{{Ó8 \UA=p   .N/  +^ uQ%Âᛥwùϥlå᫹ëᗥùmy}Ϲëññiap{ooimÊë6ciϹϱtN0_HhL"urM]iÐñᓠϛϛ᱇ùåϫϹy{ϐ_ϫ'j(ϹùàϠ\pXUwT]8#7%!%"%%?f<yåϗwÍϫyϹϠù{᱇ϠW^\S_YNaW^9 +tJte 0 2!3 A% 7 +)jB%e_cϱ{Ûϱϛëᗫl{×r_}]ϐwϐmw}: ùup +VK QCHdH$+:>}8}iύùϗàr×ñwp}åÛrcycύuåp}{`"YiVQ,8\ +5 +  +3 +31 M_* [*8]}ÓpϹᱠ{yϗᓗwlrϥÐùr`uyVwru{y@ ecud8Ldd($! +  j @L1᫠ᥫàÐñϹ{ϫùwà᛫ñϗχ}wϗtϫQ<VQY[(eGmEtLwwϹ}ᗗy{uùuuwϓyr}=ϱùϹ) ^-I  ' C j [8Zrëwñ᫓ioϱl_ytQr{op}dtD))mf94 (@ @! _+/T){cuϥϫ}wùṱwᱫ{o{ëåàùme\ϛyru$p}wj3@c 9(^>H +B(!*!  .Ym\ñuύϱṂϱṛÐm{ϊṱñj[}UÓupw{Nf*p%r393": t0rhr1τᐓëϱϥϱtùrϗϹrtϠϛt.NDmK +w} ucDL/JV"yetϓϹϛϥ{rmwu}4oKrrdMe@uϱϹϱW\M;=7o +w=K8LR?3XJRWV?10/21;&8Wcmåùϱϛϱϱϱϗ×ùùÛå1cUyT^(ZjU1ei/tfQUV*fV`]9WuwpϫϱᱥṛÓϥ}ϊÓṥ_a\T_[Ϲ} ypu}Ðw`htåå{afiiltopuohmyooelwcftfourohp{echofmacy_M}ohyd}ϱwl}wtKuwtucuLNyldhd×NVÇ᫫lpåwuO`m`ᱱ}^ùᥠ]}ϱϛ[o^aWrdo]ffXXYQQZ[NYuywPVOVWWX`[Ϡ][\T[ZR[ϓùᗍϥ}}ùᄠiϓϠÛϹ]5{wtLoKly`ùϫaϹϛwptyr{}Xr}]ycjf}YOcWQdX{}XoQjlZ[ÛWfYpSaùùϐϫ×mùñϥëϫVl^ϥϹ]ϓñrw×ϥ`åϓåÓ{TYtSPZ}YP_uZ]ù]^[WϱኍᛛÛϱ{U`f`ᇱ`_ϗÂå]`tVo}cfjRQ^NS\ZRX}}VaPT\Vd]Qe\Y]XñϹϛᐠrϹእᗱàG2r`fhCw`ᱹ_ϊ{?l`dZuj]ddURSPCWQWTyiy{{QOWSVVU\\VWX\\Z]^ϫϹÍϗjÓϱw\mṱmTwÍllw`ùa{ᱠyY[{RVmtZ^JY}w[\ϛd_Ze᱐Ṡàϫ{V`ph^`ccLZfᠠ{etW\VVw[R[{lmUZ[\w^YXXuV᫱ëñᐱϫᛥjSdñc`_ϱϱ`åwi}ϱ]ϹwT_t^Vu]U}{fZ[VVQO\ϠWZVoj^×ëëᠹᱫùᓓ}ϱ[`}^ኹϹ\ᥛ[_rϓ\m`{lY[[Z]\YZY\YYZZZZ[]ëZ^\[l]c[ùåùϹùϫϐϓÓ_ᐄùñṹXÍommϫϊÂϹàyϫϊ፥ÊññåϹÛϹᇠ}፹Ðϓ{SfÐ{l^Ðùϗ}}}ϹϛñϫϹϱϹṹχ_ϠùyQreùñ᫹ṱϹ×ëϛ}}{tw{ñṐᱱϹåϹùùᗛë᛹ÛåϹTϛddù{ϐᗛ}{ñϛṛϹṱùϱϹëùϱåϹÍϱÐϫÛùiLocwdτϱᱱà{uå{ry}{ty{ϹϹϹϹϹÛϹϛ{ύI}lmϹᱥϱñÄÐÇϐùϊṓϛùϱϫϓṫᄗ×åàN×ϛmm{ϹϱϹëϛàύϛ}}åÓùàϹùϹᱱϹ×ύϱϠwKϠ{ay]cñàϊw}ϊϱRy}QwY}yllXuÍ᥊oZwϹϹϹÓᱹτÛëfC`ϛmd^ñ`ύ×ϱphlYpÄhϛ}SVotQy`QZ}c]O\\Yi]V][htVϗàϫϹϹÍå{@`Ójdr`᥹\Êy\^aàùZ}i^ohQUVS[]V\[y}hQ[]^^XaϛVZX[_ZiëùëùÛኍd5à`X}W^ϥÛå`ύàύ\hYQ}mwpyc}}TZp_Ui_^\ytZTZ\[d᱗\e_uTϗϹùÛà}ùwÍ>`ϥw^ṥå^rå\j^c`ùhù^ac[]^ZY[_S[X^[\V^\}^×ùV]]UpU[ëϥϱùëåᠱϗϓjÓëύ?ϫ`Ûm^Ϲ]ñÇc^r[ùwᥱf}ᄊWXYVQ^YQXëUYW[\\X{^ϫᛥX\^W^[V_ùϹϹϛṐϹtÐëϹϫy9eϐwu`ëå`×ϱa[፠ϥYÊ[ic^PtdQXf[\_[R^[\ZtYñϹϐà×à᥍ååÇ\ϐyyϫϱùτëϹᱛϛ\ùàᛠςùñZ`Ð}`àᥱ_pë^p``ññ×W{ey[ji[J[[[]^[[×ñdZ[\\]^w\ϠZ\\V]Z_ϗϫϛϥù`uwϹϱåùñëϱëùϹñϹññNë{ϹϗÍϹñϹëññPϓroϫùϥϊϓṥϱṛϛëÛϓùàᐥàpNëd[ϛåᥥϱϥùϹϹñùᥱϥᐠϹϹṹ^ë}màùñϱñàñϹññϹùñϫùWϐphÛϗϱåùmwϱñëëϥ᠛åϫṹeFuwXmVlyt{{}}tyϹϹϹëϠϱϠϱywϠù[5taKmLiχppypytullfwtmljpÄϫùåñϱñwou}u{wr{ϗÇy{r{{ᠥåc9`rUuWr`à×ϫ_{ᱠ^j^\{Y{ttjtuyϫ[rd[[Z^[_cZ\Ϲr\[\Y[[fZϫyQTN]QM}ϛÍñf{ϗ}×åÐχw{orÍàm?`uYVw`ϫ`tṗϊ`u`WᐫZ{jw]fe\`^`^cj_^ςh]]^_^\åTYUV^OU{ùùϓjaAycuc]}_ù_ëϗwfù\àhϥyτ]]^]]a^`[ϫa[^^Z]]`ù}OTUXuPPVϫϓ}ñϛᄄS{{m?yij{_ë`ϱϓϱùiùϠ]`\ùy{[fëÂVo[r\×ëÊϥùñᐍ{ϹùJ={ojK_]Qå᛹fϹ[]`[ùëj\\iϹÄQiNpÐ{ë}ϫ]ϫϫo2yZay_a{ϫᐱ[[dcYñåi`ϹϱtyNa[SϹùᐇu}ϥϹjD{ctefy`ϛ_ùje\ñip[\^a_yj]Ϡëu[_[]a]ϹϠOUOoTm}ëϹñ{ፂuuy{}ϛe }p^^r_fፐ\wÄϠ]`l[ϫùë{]yuV}lXùëϠùᐊ{ÛᱠmVwha{ϹùϹᱹñùϫϫÄñåϹፂyëd'wdj}{ϥyëϠᛓϹùëù{{uϛϥotuwr{}à[Måwljᐊ{ëϹṱùϱùϱùñ}×å×ϗñÓwiIuje{Ϲ{{{fwjZ[`rcdYyᱹϫϫåᱹϱwwëϹϱϛñoo}åo#ϱwuᐱùåùϥÛṓåàùñùϫϱṹϹϥṹ?3yUUᄫuϱϹùåṹñàϗϠåëϱùϱññϹi/tjo፥ùyUëϹñùÍyàtjϓtëϫϱw^}mmùϱëùϹϹùϥϥùùñùϹw᱊ÍᠹϥåùwGplϹùùϥϹñϫñmÐϫϥùeJ}jh}al`ϥϛÛϹϱϱ×åᠫëϥoᱠኛùñùu\Ϲ{prṹuϱϱϹùϹùùϹϹϫñùϐëreJ}r`ayϥëëϹùϹϹϥyyñ_w}ywϹùϫ}YṠ{u×åñùùϫϫϫwϠùñmwimåṹà}Ϲhy{àṛ᥹᥹hAec}ϱñᱱϫϫϹϹϹ}yùϫilùpϠàhJ}wdetᐱᥛϱñϫϓϹñàùϹϹϹëlC}rZ}aw{ùϗëᛛñùÓyñϹjϓëÛëry×ϫù+#yñaLGw_᫹ùñϱñùñᓥñϹër}ϹᱥåÛùϱñrYᥫ{ilyᓛϥùùϱϹϫàùuÓëᥛϹṱlEulj{ύϱϱùùñϹùϫÓÓñ`{ù᫠×ϫϥṹfNufaw{rùëñϠëë{Ϲdh}×ϛñtZjmùϹϠ᫥ϹϹϱùϹϗëÍ_ùᱱ{ϱϹÛëϹù\ÇuuϹϱϹñṹ}yw}ϛy}ᱹyRcpm`ϥϫ]ϠchY{åuϛw_^```i[ùχj]\\]`VYV\iiÛϹϓpNNPR\UU[UutZVVQmWyYVTfV{eu}ϫϫaϓ}w`dtiϹ[_\ϗ]^`ᓱZ\_V\iQutQPuQTTdQ{tU}w{t}rU\Z[y`]YfrwϥϠñaϱu_ccrcV\]QÛVVu^`{HRXm^yer}[}O^Y\Sm{SV{{h\^hU_[{UXid}ÇÛϱ_ëjup_ù^mcÛàaëWZᥥYÓùÊϫ]Zϱᗛ\pϹ}NpdZQ_o\Vy}e{c_T`]^}ZdZVñϓᱫϫùQjϫom_dϛ}ajÊd{a^lidϫÛ\]\d]Û{[]\Y^ëw}SVQSV^Ru`{ymo]TUQVUVQNRdw`{y]ϥpocpååϛϥë\`[Û^^`VÓuÛñU{`wV\}WpfotZwhÛuYjr}᫹ÛuG{lj`ϫhϹpÛZ]ϊ][[]ñZpϓ}Q[yhUh`Xt{{prw`WWV[VyQSRytur}åñVBϥṂljcϓeñϹ\_\ϓ^åñYaëùëUrpUQ{Vt{{`mppT^jYRZ}{ϱùñVCwϥy[`]T{ëawwhùÄ`_]_\^lϥϱàϠëW[WV^X[wIjZYW^Z^åå^[Z}ᱫϱm᥂wwṹϹåùṹ}}ᓥÐÓϱñ^ëyᱱñùṹ᫫Ç}ëϱϫùà`ϓϱtϹñϊϹ᱗ùϱ}ùåṱ}ñϓ×ϱṫà\ëpmë᛹ñùùùϱ}}àϱùϹϠ}iϠϹyϹëϐϠϹϹϓϹÛù}åϗÛϠeñutϥϹùëϹṹùϹ}{ϗñÊÊñt?}yf{aϱϗùñϱùÓ{}y\}f{uZm_hiu_Xr{uu}pypàύ×ÇàñύCñwmjeifDi}TX}ëWᠱϱXϱϹ]wcyw{ruwjyfoÐ}r{{yᥥ᫹ϱdZuod{`ñl`ϫhϱϹϱ[]t[Ϲṗct^aϱ᱓UjàÛVfUl}wZy{p{aaru^wr}admcϓᱹϱùaFy[{Y``wùùàiϠ\_]ϱϹ]᱄{Y`ùϱϫRuYTUoWWtulR[wWTtwQST{o᫹ùᱹjW{ptfdt`cwåϱaåñ[f_]ϹåϹ^TWϗϛëñVY\^UrmpWu{oVQyUVmu}yWSefmw}}ϹϱjFlie`d}aᛐ{å]`^Ϲϫ`[Ûuñà᥹VdXWTcNhX}}wedtSRZryQVPew_åYÐrp`cṥfñ{}Í_ah_yëh^d`]^\]Zoåϓ᫫hd]]d`Ztpñ{t[\TUV]]ZTcNu}wùÐϗëϫñ{WϠÍtt`flϹe[`^\ñ\ϫ]\aVtϱϫVZYT`^WjrTSV^UYVW[[}ϓùyYwupùᱹϛϫùϱSëytr×piϛ_`ϱ{wiXYu[ϥϹ_ϛXùwñ[c]dVl]w[ϠmVZa]]wV\ZlùϱpÇàϱùëë×ñdùϥùñàcùåϱϱϹϹϹϹṱåëùϱ×ùëϹϹTtoϫñùù᱐ϱṫϹùùàᥠϥñpa{pdϹϓχϱñùñëϫñwëϫ}crtùùùϱàëᥥϹY>t^]UeQ_pl᫫ϓϥùùùñëyϱwåàϱjaiafhhid}wmpurm}}yw}}}yϥM4}aT@RCVi{pt}ph{lmlprohrpϱϹϱùñëϱϹñÄϓ{{i_d`^Yi]a{m]i]ewjlymy_JhddYtTeu``ahϓ]}ᇠ{U]^]m]WQVëχ\_^aHP{iuuQUmZQVY[ZWraV[Ê^UZUVUf}hoHHANceXwocuriom]Luai[T]Va}}`ϱdltϹiëϱT]]\ϫ×^^ϹfNyU_Ϡñ]jYpZdyVXmiSyS^[welFJF?heVwopil}w{weCoiOjNr`w}}frrwtwϹùëñϱy^p^iaϫÊd`ṱϠ}doifhrafcVYNY\hYBFONlmm{{iflot{owrNB^ZTHWIQl``f_ZñQY]}{^yy}_t^\R{wajpd]XVh]mjypjpdrryrmpwuruϠjC{MJcÐ}y[]]Yñù[yw`ljwtr[[ootftpmyr}}{×[DotNmMùu`ϹoåÇ}ϛϱÂëùÛñåà{u}{tåm_fUt^Suyjlprurt{aEfiOwLowc{τ]cϫù\ërotw}ptm]OSiifrorymimtpwyww{{V/ee]?iA]owϹϊjj{{rw_uX{V_X]ëYϹ[ùϱ[ϥpyy{ñÐᗥᱥÂppitmu}rwu{}}{}{lQ{ul^wdo{ù᠐ϓϹùùṫÐñåùùù}y{ttw}{y}}᫠fNhjWjVhϗϛϱùϹϹϹÛàÛñùùÍÛëyapludol{ttptpt{mwu]EmdaRfNariύÊϹϹϫϱ×ϹëÄÛñ᱓uaewoiph{ytumhrwwy}ZHl`_QdQ]ompy}u{tut{rurwϹϥϹùññùϱåϛàϛùñùymptmmyppuyuwuo}u}}eQyml]udm{ϹÊϹùùϹϹùëϹëùùàofmuiohlo{}pt}}y{wyᛄiMwpZi^uùϐϹëùᓥàϠ`ñϱϱo{yj{h{wtip{uww{ϥmMr`tf{dw``\`Mt{OL{}y{[ᠠ{eTᠱuëϱÛëϥϱϱ}h}y}o{t{}u{yrJNMPMNPNNVeSr`l^u[i``_`W\rcṫϫϱϹàϫàrñÛåå{ruuw}uyu{NQNJNOOSS\pHu`tcydw{}````VϱZjϗcùϹwåù᫱{tww}}{}wyNPNMNMQNVXaKt`tTmRl_`^`Wu}rtVTylulytVϫùñϥϠϠᓛϹϓ}añϗåÍc{u}o}tuo{ryLMNJNLQOOSᠹVeup````aϫ]o_ϥϠϫṹϫñϠϱëåñ}twmyPPJNRWZX\ZmGu`}_a````]ϫåY\ë^{àåϹϱñù×ϱϐϥàς}tuyr{OSNO[WYZ[\Ϲ}A`åje``^`WwL]wuwfuoå\ϹùϫϹϫϹᱹëëñùϫϹj{}SUSQZYL[[]}`cpj````e_cϥëϫùϱùùϫϥÂpñ{yyXZSNZWXV\\ñ<ÄcϠfc``\`[ÛSfϓaϥñëñϫùṹϹ}ty{w}QUQOZZVZZX{XÊcτju````c\w^ùϫϛàëñùùϠϓV[YV[_[[[]3mo}ywwyyyyÊùùùϹùϱ×åϹSṓptϹåϹñϹÛϫñëåñṠù᠓ÛëZϊ}tÓrϱϹϱϱᱹÛÛᛠfMÓÂpjëϥϹϫϱùùñϫàùϹ`trϹñႹϹϹëëᱱùyύññϱññ \ No newline at end of file diff --git a/libs/chardet/models/models.bin b/libs/chardet/models/models.bin new file mode 100644 index 000000000..f163308cc Binary files /dev/null and b/libs/chardet/models/models.bin differ diff --git a/libs/chardet/models/training_metadata.yaml b/libs/chardet/models/training_metadata.yaml new file mode 100644 index 000000000..f068fc603 --- /dev/null +++ b/libs/chardet/models/training_metadata.yaml @@ -0,0 +1,3523 @@ +training_date: "2026-03-25T16:07:09Z" +max_samples: 25000 +models: + ar/cp1256: + language: ar + encoding: cp1256 + samples_used: 25000 + bigram_entries: 1945 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ar/cp720: + language: ar + encoding: cp720 + samples_used: 25000 + bigram_entries: 1859 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ar/cp864: + language: ar + encoding: cp864 + samples_used: 25000 + bigram_entries: 1587 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ar/iso8859-6: + language: ar + encoding: iso8859-6 + samples_used: 25000 + bigram_entries: 1865 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ar/utf-8: + language: ar + encoding: utf-8 + samples_used: 25000 + bigram_entries: 725 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + be/cp1251: + language: be + encoding: cp1251 + samples_used: 25000 + bigram_entries: 2375 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + be/cp855: + language: be + encoding: cp855 + samples_used: 25000 + bigram_entries: 2336 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + be/cp866: + language: be + encoding: cp866 + samples_used: 25000 + bigram_entries: 2187 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + be/iso8859-5: + language: be + encoding: iso8859-5 + samples_used: 25000 + bigram_entries: 2251 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + be/mac-cyrillic: + language: be + encoding: mac-cyrillic + samples_used: 25000 + bigram_entries: 2360 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + be/utf-8: + language: be + encoding: utf-8 + samples_used: 25000 + bigram_entries: 739 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + bg/cp1251: + language: bg + encoding: cp1251 + samples_used: 25000 + bigram_entries: 2063 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + bg/cp855: + language: bg + encoding: cp855 + samples_used: 25000 + bigram_entries: 2007 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + bg/cp866: + language: bg + encoding: cp866 + samples_used: 25000 + bigram_entries: 1998 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + bg/iso8859-5: + language: bg + encoding: iso8859-5 + samples_used: 25000 + bigram_entries: 2002 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + bg/mac-cyrillic: + language: bg + encoding: mac-cyrillic + samples_used: 25000 + bigram_entries: 2054 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + bg/utf-8: + language: bg + encoding: utf-8 + samples_used: 25000 + bigram_entries: 658 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + br/cp1140: + language: br + encoding: cp1140 + samples_used: 25000 + bigram_entries: 1435 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/cp1252: + language: br + encoding: cp1252 + samples_used: 25000 + bigram_entries: 951 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/cp500: + language: br + encoding: cp500 + samples_used: 25000 + bigram_entries: 1430 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/cp850: + language: br + encoding: cp850 + samples_used: 25000 + bigram_entries: 921 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/cp858: + language: br + encoding: cp858 + samples_used: 25000 + bigram_entries: 921 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/hp-roman8: + language: br + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 922 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/iso8859-1: + language: br + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 921 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/iso8859-14: + language: br + encoding: iso8859-14 + samples_used: 25000 + bigram_entries: 910 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/iso8859-15: + language: br + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 920 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/mac-roman: + language: br + encoding: mac-roman + samples_used: 25000 + bigram_entries: 950 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + br/utf-8: + language: br + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1034 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 9224 + cs/cp1250: + language: cs + encoding: cp1250 + samples_used: 25000 + bigram_entries: 1754 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cs/cp852: + language: cs + encoding: cp852 + samples_used: 25000 + bigram_entries: 1711 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cs/iso8859-2: + language: cs + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 1707 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cs/mac-latin2: + language: cs + encoding: mac-latin2 + samples_used: 25000 + bigram_entries: 1749 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cs/utf-8: + language: cs + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1450 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/cp1140: + language: cy + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2231 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/cp1252: + language: cy + encoding: cp1252 + samples_used: 25000 + bigram_entries: 886 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/cp500: + language: cy + encoding: cp500 + samples_used: 25000 + bigram_entries: 2215 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/cp850: + language: cy + encoding: cp850 + samples_used: 25000 + bigram_entries: 861 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/cp858: + language: cy + encoding: cp858 + samples_used: 25000 + bigram_entries: 863 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/hp-roman8: + language: cy + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 869 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/iso8859-1: + language: cy + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 861 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/iso8859-14: + language: cy + encoding: iso8859-14 + samples_used: 25000 + bigram_entries: 885 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/iso8859-15: + language: cy + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 863 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/mac-roman: + language: cy + encoding: mac-roman + samples_used: 25000 + bigram_entries: 886 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + cy/utf-8: + language: cy + encoding: utf-8 + samples_used: 25000 + bigram_entries: 926 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp1140: + language: da + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2212 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp1252: + language: da + encoding: cp1252 + samples_used: 25000 + bigram_entries: 957 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp437: + language: da + encoding: cp437 + samples_used: 25000 + bigram_entries: 868 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp500: + language: da + encoding: cp500 + samples_used: 25000 + bigram_entries: 2203 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp850: + language: da + encoding: cp850 + samples_used: 25000 + bigram_entries: 925 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp858: + language: da + encoding: cp858 + samples_used: 25000 + bigram_entries: 926 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/cp865: + language: da + encoding: cp865 + samples_used: 25000 + bigram_entries: 915 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/hp-roman8: + language: da + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 925 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/iso8859-1: + language: da + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 925 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/iso8859-15: + language: da + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 925 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/mac-roman: + language: da + encoding: mac-roman + samples_used: 25000 + bigram_entries: 956 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + da/utf-8: + language: da + encoding: utf-8 + samples_used: 25000 + bigram_entries: 972 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp1140: + language: de + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2474 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp1252: + language: de + encoding: cp1252 + samples_used: 25000 + bigram_entries: 1126 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp273: + language: de + encoding: cp273 + samples_used: 25000 + bigram_entries: 2467 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp437: + language: de + encoding: cp437 + samples_used: 25000 + bigram_entries: 1044 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp500: + language: de + encoding: cp500 + samples_used: 25000 + bigram_entries: 2462 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp850: + language: de + encoding: cp850 + samples_used: 25000 + bigram_entries: 1081 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/cp858: + language: de + encoding: cp858 + samples_used: 25000 + bigram_entries: 1085 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/hp-roman8: + language: de + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 1047 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/iso8859-1: + language: de + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 1081 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/iso8859-15: + language: de + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 1084 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/mac-roman: + language: de + encoding: mac-roman + samples_used: 25000 + bigram_entries: 1094 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + de/utf-8: + language: de + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1139 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/cp1253: + language: el + encoding: cp1253 + samples_used: 25000 + bigram_entries: 2386 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/cp737: + language: el + encoding: cp737 + samples_used: 25000 + bigram_entries: 2240 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/cp869: + language: el + encoding: cp869 + samples_used: 25000 + bigram_entries: 2329 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/cp875: + language: el + encoding: cp875 + samples_used: 25000 + bigram_entries: 2915 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/iso8859-7: + language: el + encoding: iso8859-7 + samples_used: 25000 + bigram_entries: 2333 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/mac-greek: + language: el + encoding: mac-greek + samples_used: 25000 + bigram_entries: 2360 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + el/utf-8: + language: el + encoding: utf-8 + samples_used: 25000 + bigram_entries: 749 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/cp1140: + language: en + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2296 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/cp1252: + language: en + encoding: cp1252 + samples_used: 25000 + bigram_entries: 883 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/cp437: + language: en + encoding: cp437 + samples_used: 25000 + bigram_entries: 810 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/cp500: + language: en + encoding: cp500 + samples_used: 25000 + bigram_entries: 2287 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/cp850: + language: en + encoding: cp850 + samples_used: 25000 + bigram_entries: 812 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/cp858: + language: en + encoding: cp858 + samples_used: 25000 + bigram_entries: 813 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/hp-roman8: + language: en + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 816 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/iso8859-1: + language: en + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 812 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/iso8859-15: + language: en + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 813 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/mac-roman: + language: en + encoding: mac-roman + samples_used: 25000 + bigram_entries: 883 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + en/utf-8: + language: en + encoding: utf-8 + samples_used: 25000 + bigram_entries: 954 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + eo/iso8859-3: + language: eo + encoding: iso8859-3 + samples_used: 25000 + bigram_entries: 1385 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + eo/utf-8: + language: eo + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1779 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/cp1140: + language: es + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2255 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/cp1252: + language: es + encoding: cp1252 + samples_used: 25000 + bigram_entries: 1067 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/cp437: + language: es + encoding: cp437 + samples_used: 25000 + bigram_entries: 1005 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/cp500: + language: es + encoding: cp500 + samples_used: 25000 + bigram_entries: 2240 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/cp850: + language: es + encoding: cp850 + samples_used: 25000 + bigram_entries: 1026 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/cp858: + language: es + encoding: cp858 + samples_used: 25000 + bigram_entries: 1030 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/hp-roman8: + language: es + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 1026 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/iso8859-1: + language: es + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 1026 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/iso8859-15: + language: es + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 1030 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/mac-roman: + language: es + encoding: mac-roman + samples_used: 25000 + bigram_entries: 1066 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + es/utf-8: + language: es + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1025 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + et/cp1257: + language: et + encoding: cp1257 + samples_used: 25000 + bigram_entries: 1096 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + et/cp775: + language: et + encoding: cp775 + samples_used: 25000 + bigram_entries: 1055 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + et/iso8859-13: + language: et + encoding: iso8859-13 + samples_used: 25000 + bigram_entries: 1055 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + et/iso8859-4: + language: et + encoding: iso8859-4 + samples_used: 25000 + bigram_entries: 1032 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + et/utf-8: + language: et + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1124 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fa/cp1256: + language: fa + encoding: cp1256 + samples_used: 25000 + bigram_entries: 1662 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fa/cp720: + language: fa + encoding: cp720 + samples_used: 25000 + bigram_entries: 1401 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fa/iso8859-6: + language: fa + encoding: iso8859-6 + samples_used: 25000 + bigram_entries: 1348 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fa/utf-8: + language: fa + encoding: utf-8 + samples_used: 25000 + bigram_entries: 715 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/cp1140: + language: fi + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2110 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/cp1252: + language: fi + encoding: cp1252 + samples_used: 25000 + bigram_entries: 876 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/cp437: + language: fi + encoding: cp437 + samples_used: 25000 + bigram_entries: 809 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/cp500: + language: fi + encoding: cp500 + samples_used: 25000 + bigram_entries: 2095 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/cp850: + language: fi + encoding: cp850 + samples_used: 25000 + bigram_entries: 838 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/cp858: + language: fi + encoding: cp858 + samples_used: 25000 + bigram_entries: 844 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/hp-roman8: + language: fi + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 813 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/iso8859-1: + language: fi + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 838 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/iso8859-10: + language: fi + encoding: iso8859-10 + samples_used: 25000 + bigram_entries: 830 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/iso8859-15: + language: fi + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 844 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/mac-roman: + language: fi + encoding: mac-roman + samples_used: 25000 + bigram_entries: 851 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fi/utf-8: + language: fi + encoding: utf-8 + samples_used: 25000 + bigram_entries: 913 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp1140: + language: fr + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2298 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp1252: + language: fr + encoding: cp1252 + samples_used: 25000 + bigram_entries: 965 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp437: + language: fr + encoding: cp437 + samples_used: 25000 + bigram_entries: 886 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp500: + language: fr + encoding: cp500 + samples_used: 25000 + bigram_entries: 2282 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp850: + language: fr + encoding: cp850 + samples_used: 25000 + bigram_entries: 895 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp858: + language: fr + encoding: cp858 + samples_used: 25000 + bigram_entries: 900 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/cp863: + language: fr + encoding: cp863 + samples_used: 25000 + bigram_entries: 893 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/hp-roman8: + language: fr + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 894 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/iso8859-1: + language: fr + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 895 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/iso8859-15: + language: fr + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 907 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/mac-roman: + language: fr + encoding: mac-roman + samples_used: 25000 + bigram_entries: 964 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + fr/utf-8: + language: fr + encoding: utf-8 + samples_used: 25000 + bigram_entries: 978 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/cp1140: + language: ga + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2313 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/cp1252: + language: ga + encoding: cp1252 + samples_used: 25000 + bigram_entries: 939 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/cp437: + language: ga + encoding: cp437 + samples_used: 25000 + bigram_entries: 867 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/cp500: + language: ga + encoding: cp500 + samples_used: 25000 + bigram_entries: 2295 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/cp850: + language: ga + encoding: cp850 + samples_used: 25000 + bigram_entries: 914 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/cp858: + language: ga + encoding: cp858 + samples_used: 25000 + bigram_entries: 921 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/hp-roman8: + language: ga + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 915 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/iso8859-1: + language: ga + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 914 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/iso8859-14: + language: ga + encoding: iso8859-14 + samples_used: 25000 + bigram_entries: 906 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/iso8859-15: + language: ga + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 921 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/mac-roman: + language: ga + encoding: mac-roman + samples_used: 25000 + bigram_entries: 939 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ga/utf-8: + language: ga + encoding: utf-8 + samples_used: 25000 + bigram_entries: 858 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + gd/iso8859-14: + language: gd + encoding: iso8859-14 + samples_used: 25000 + bigram_entries: 783 + sources: + culturax: 8388 + madlad400: 16612 + wikipedia: 0 + test_articles_excluded: 20 + gd/utf-8: + language: gd + encoding: utf-8 + samples_used: 25000 + bigram_entries: 867 + sources: + culturax: 8388 + madlad400: 16612 + wikipedia: 0 + test_articles_excluded: 20 + he/cp1255: + language: he + encoding: cp1255 + samples_used: 25000 + bigram_entries: 2116 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + he/cp424: + language: he + encoding: cp424 + samples_used: 25000 + bigram_entries: 2280 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + he/cp856: + language: he + encoding: cp856 + samples_used: 25000 + bigram_entries: 1393 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + he/cp862: + language: he + encoding: cp862 + samples_used: 25000 + bigram_entries: 1391 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + he/iso8859-8: + language: he + encoding: iso8859-8 + samples_used: 25000 + bigram_entries: 1406 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + he/utf-8: + language: he + encoding: utf-8 + samples_used: 25000 + bigram_entries: 812 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hr/cp1250: + language: hr + encoding: cp1250 + samples_used: 25000 + bigram_entries: 679 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hr/cp852: + language: hr + encoding: cp852 + samples_used: 25000 + bigram_entries: 675 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hr/iso8859-16: + language: hr + encoding: iso8859-16 + samples_used: 25000 + bigram_entries: 678 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hr/iso8859-2: + language: hr + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 675 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hr/mac-latin2: + language: hr + encoding: mac-latin2 + samples_used: 25000 + bigram_entries: 675 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hr/utf-8: + language: hr + encoding: utf-8 + samples_used: 25000 + bigram_entries: 673 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hu/cp1250: + language: hu + encoding: cp1250 + samples_used: 25000 + bigram_entries: 1564 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hu/cp852: + language: hu + encoding: cp852 + samples_used: 25000 + bigram_entries: 1529 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hu/iso8859-16: + language: hu + encoding: iso8859-16 + samples_used: 25000 + bigram_entries: 1545 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hu/iso8859-2: + language: hu + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 1525 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hu/mac-latin2: + language: hu + encoding: mac-latin2 + samples_used: 25000 + bigram_entries: 1579 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + hu/utf-8: + language: hu + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1320 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/cp1140: + language: id + encoding: cp1140 + samples_used: 25000 + bigram_entries: 1748 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/cp1252: + language: id + encoding: cp1252 + samples_used: 25000 + bigram_entries: 588 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/cp500: + language: id + encoding: cp500 + samples_used: 25000 + bigram_entries: 1744 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/cp850: + language: id + encoding: cp850 + samples_used: 25000 + bigram_entries: 541 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/cp858: + language: id + encoding: cp858 + samples_used: 25000 + bigram_entries: 541 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/hp-roman8: + language: id + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 543 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/iso8859-1: + language: id + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 541 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/iso8859-15: + language: id + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 541 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/mac-roman: + language: id + encoding: mac-roman + samples_used: 25000 + bigram_entries: 588 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + id/utf-8: + language: id + encoding: utf-8 + samples_used: 25000 + bigram_entries: 598 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/cp1140: + language: is + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2508 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/cp1252: + language: is + encoding: cp1252 + samples_used: 25000 + bigram_entries: 1419 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/cp500: + language: is + encoding: cp500 + samples_used: 25000 + bigram_entries: 2501 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/cp850: + language: is + encoding: cp850 + samples_used: 25000 + bigram_entries: 1394 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/cp858: + language: is + encoding: cp858 + samples_used: 25000 + bigram_entries: 1394 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/cp861: + language: is + encoding: cp861 + samples_used: 25000 + bigram_entries: 1346 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/hp-roman8: + language: is + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 1350 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/iso8859-1: + language: is + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 1394 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/iso8859-10: + language: is + encoding: iso8859-10 + samples_used: 25000 + bigram_entries: 1387 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/iso8859-15: + language: is + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 1394 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/mac-iceland: + language: is + encoding: mac-iceland + samples_used: 25000 + bigram_entries: 1374 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/mac-roman: + language: is + encoding: mac-roman + samples_used: 25000 + bigram_entries: 1211 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + is/utf-8: + language: is + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1082 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/cp1140: + language: it + encoding: cp1140 + samples_used: 25000 + bigram_entries: 1970 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/cp1252: + language: it + encoding: cp1252 + samples_used: 25000 + bigram_entries: 804 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/cp437: + language: it + encoding: cp437 + samples_used: 25000 + bigram_entries: 728 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/cp500: + language: it + encoding: cp500 + samples_used: 25000 + bigram_entries: 1957 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/cp850: + language: it + encoding: cp850 + samples_used: 25000 + bigram_entries: 737 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/cp858: + language: it + encoding: cp858 + samples_used: 25000 + bigram_entries: 742 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/hp-roman8: + language: it + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 736 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/iso8859-1: + language: it + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 737 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/iso8859-15: + language: it + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 741 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/mac-roman: + language: it + encoding: mac-roman + samples_used: 25000 + bigram_entries: 804 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + it/utf-8: + language: it + encoding: utf-8 + samples_used: 25000 + bigram_entries: 827 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/cp932: + language: ja + encoding: cp932 + samples_used: 25000 + bigram_entries: 6845 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/euc_jis_2004: + language: ja + encoding: euc_jis_2004 + samples_used: 25000 + bigram_entries: 6319 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/iso2022_jp_2: + language: ja + encoding: iso2022_jp_2 + samples_used: 25000 + bigram_entries: 2425 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/iso2022_jp_2004: + language: ja + encoding: iso2022_jp_2004 + samples_used: 25000 + bigram_entries: 2426 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/iso2022_jp_ext: + language: ja + encoding: iso2022_jp_ext + samples_used: 25000 + bigram_entries: 2436 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/shift_jis_2004: + language: ja + encoding: shift_jis_2004 + samples_used: 25000 + bigram_entries: 6867 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ja/utf-8: + language: ja + encoding: utf-8 + samples_used: 25000 + bigram_entries: 3664 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + kk/kz1048: + language: kk + encoding: kz1048 + samples_used: 25000 + bigram_entries: 2648 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + kk/ptcp154: + language: kk + encoding: ptcp154 + samples_used: 25000 + bigram_entries: 2601 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + kk/utf-8: + language: kk + encoding: utf-8 + samples_used: 25000 + bigram_entries: 914 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ko/cp949: + language: ko + encoding: cp949 + samples_used: 25000 + bigram_entries: 5140 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ko/euc_kr: + language: ko + encoding: euc_kr + samples_used: 25000 + bigram_entries: 5145 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ko/iso2022_kr: + language: ko + encoding: iso2022_kr + samples_used: 25000 + bigram_entries: 815 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ko/johab: + language: ko + encoding: johab + samples_used: 25000 + bigram_entries: 4776 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ko/utf-8: + language: ko + encoding: utf-8 + samples_used: 25000 + bigram_entries: 2642 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lt/cp1257: + language: lt + encoding: cp1257 + samples_used: 25000 + bigram_entries: 1238 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lt/cp775: + language: lt + encoding: cp775 + samples_used: 25000 + bigram_entries: 1199 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lt/iso8859-13: + language: lt + encoding: iso8859-13 + samples_used: 25000 + bigram_entries: 1199 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lt/iso8859-4: + language: lt + encoding: iso8859-4 + samples_used: 25000 + bigram_entries: 1190 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lt/utf-8: + language: lt + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1223 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lv/cp1257: + language: lv + encoding: cp1257 + samples_used: 25000 + bigram_entries: 1292 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lv/cp775: + language: lv + encoding: cp775 + samples_used: 25000 + bigram_entries: 1264 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lv/iso8859-13: + language: lv + encoding: iso8859-13 + samples_used: 25000 + bigram_entries: 1264 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lv/iso8859-4: + language: lv + encoding: iso8859-4 + samples_used: 25000 + bigram_entries: 1233 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + lv/utf-8: + language: lv + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1185 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mk/cp1251: + language: mk + encoding: cp1251 + samples_used: 25000 + bigram_entries: 1663 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mk/cp855: + language: mk + encoding: cp855 + samples_used: 25000 + bigram_entries: 1644 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mk/cp866: + language: mk + encoding: cp866 + samples_used: 25000 + bigram_entries: 1556 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mk/iso8859-5: + language: mk + encoding: iso8859-5 + samples_used: 25000 + bigram_entries: 1641 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mk/mac-cyrillic: + language: mk + encoding: mac-cyrillic + samples_used: 25000 + bigram_entries: 1661 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mk/utf-8: + language: mk + encoding: utf-8 + samples_used: 25000 + bigram_entries: 533 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/cp1140: + language: ms + encoding: cp1140 + samples_used: 25000 + bigram_entries: 1663 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/cp1252: + language: ms + encoding: cp1252 + samples_used: 25000 + bigram_entries: 687 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/cp500: + language: ms + encoding: cp500 + samples_used: 25000 + bigram_entries: 1654 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/cp850: + language: ms + encoding: cp850 + samples_used: 25000 + bigram_entries: 674 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/cp858: + language: ms + encoding: cp858 + samples_used: 25000 + bigram_entries: 674 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/hp-roman8: + language: ms + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 676 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/iso8859-1: + language: ms + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 674 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/iso8859-15: + language: ms + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 674 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/mac-roman: + language: ms + encoding: mac-roman + samples_used: 25000 + bigram_entries: 687 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ms/utf-8: + language: ms + encoding: utf-8 + samples_used: 25000 + bigram_entries: 763 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mt/iso8859-3: + language: mt + encoding: iso8859-3 + samples_used: 25000 + bigram_entries: 1153 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + mt/utf-8: + language: mt + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1223 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/cp1140: + language: nl + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2074 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/cp1252: + language: nl + encoding: cp1252 + samples_used: 25000 + bigram_entries: 805 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/cp437: + language: nl + encoding: cp437 + samples_used: 25000 + bigram_entries: 755 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/cp500: + language: nl + encoding: cp500 + samples_used: 25000 + bigram_entries: 2060 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/cp850: + language: nl + encoding: cp850 + samples_used: 25000 + bigram_entries: 758 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/cp858: + language: nl + encoding: cp858 + samples_used: 25000 + bigram_entries: 763 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/hp-roman8: + language: nl + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 755 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/iso8859-1: + language: nl + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 758 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/iso8859-15: + language: nl + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 763 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/mac-roman: + language: nl + encoding: mac-roman + samples_used: 25000 + bigram_entries: 803 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + nl/utf-8: + language: nl + encoding: utf-8 + samples_used: 25000 + bigram_entries: 836 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/cp1140: + language: no + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2397 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/cp1252: + language: no + encoding: cp1252 + samples_used: 25000 + bigram_entries: 1029 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/cp500: + language: no + encoding: cp500 + samples_used: 25000 + bigram_entries: 2385 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/cp850: + language: no + encoding: cp850 + samples_used: 25000 + bigram_entries: 1005 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/cp858: + language: no + encoding: cp858 + samples_used: 25000 + bigram_entries: 1007 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/cp865: + language: no + encoding: cp865 + samples_used: 25000 + bigram_entries: 978 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/hp-roman8: + language: no + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 1007 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/iso8859-1: + language: no + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 1005 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/iso8859-15: + language: no + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 1007 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/mac-roman: + language: no + encoding: mac-roman + samples_used: 25000 + bigram_entries: 1029 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + no/utf-8: + language: no + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1029 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pl/cp1250: + language: pl + encoding: cp1250 + samples_used: 25000 + bigram_entries: 1367 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pl/cp852: + language: pl + encoding: cp852 + samples_used: 25000 + bigram_entries: 1330 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pl/iso8859-16: + language: pl + encoding: iso8859-16 + samples_used: 25000 + bigram_entries: 1344 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pl/iso8859-2: + language: pl + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 1324 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pl/mac-latin2: + language: pl + encoding: mac-latin2 + samples_used: 25000 + bigram_entries: 1334 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pl/utf-8: + language: pl + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1382 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp1140: + language: pt + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2274 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp1252: + language: pt + encoding: cp1252 + samples_used: 25000 + bigram_entries: 1135 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp437: + language: pt + encoding: cp437 + samples_used: 25000 + bigram_entries: 1028 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp500: + language: pt + encoding: cp500 + samples_used: 25000 + bigram_entries: 2263 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp850: + language: pt + encoding: cp850 + samples_used: 25000 + bigram_entries: 1096 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp858: + language: pt + encoding: cp858 + samples_used: 25000 + bigram_entries: 1097 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/cp860: + language: pt + encoding: cp860 + samples_used: 25000 + bigram_entries: 1091 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/hp-roman8: + language: pt + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 1093 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/iso8859-1: + language: pt + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 1096 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/iso8859-15: + language: pt + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 1097 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/mac-roman: + language: pt + encoding: mac-roman + samples_used: 25000 + bigram_entries: 1130 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + pt/utf-8: + language: pt + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1000 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ro/cp1250: + language: ro + encoding: cp1250 + samples_used: 25000 + bigram_entries: 927 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ro/cp852: + language: ro + encoding: cp852 + samples_used: 25000 + bigram_entries: 867 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ro/iso8859-16: + language: ro + encoding: iso8859-16 + samples_used: 25000 + bigram_entries: 892 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ro/iso8859-2: + language: ro + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 859 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ro/utf-8: + language: ro + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1047 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/cp1251: + language: ru + encoding: cp1251 + samples_used: 25000 + bigram_entries: 2511 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/cp855: + language: ru + encoding: cp855 + samples_used: 25000 + bigram_entries: 2472 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/cp866: + language: ru + encoding: cp866 + samples_used: 25000 + bigram_entries: 2386 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/iso8859-5: + language: ru + encoding: iso8859-5 + samples_used: 25000 + bigram_entries: 2393 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/koi8-r: + language: ru + encoding: koi8-r + samples_used: 25000 + bigram_entries: 2386 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/mac-cyrillic: + language: ru + encoding: mac-cyrillic + samples_used: 25000 + bigram_entries: 2497 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ru/utf-8: + language: ru + encoding: utf-8 + samples_used: 25000 + bigram_entries: 733 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sk/cp1250: + language: sk + encoding: cp1250 + samples_used: 25000 + bigram_entries: 1685 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sk/cp852: + language: sk + encoding: cp852 + samples_used: 25000 + bigram_entries: 1651 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sk/iso8859-16: + language: sk + encoding: iso8859-16 + samples_used: 25000 + bigram_entries: 1424 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sk/iso8859-2: + language: sk + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 1648 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sk/mac-latin2: + language: sk + encoding: mac-latin2 + samples_used: 25000 + bigram_entries: 1673 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sk/utf-8: + language: sk + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1494 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sl/cp1250: + language: sl + encoding: cp1250 + samples_used: 25000 + bigram_entries: 1006 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sl/cp852: + language: sl + encoding: cp852 + samples_used: 25000 + bigram_entries: 972 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sl/iso8859-16: + language: sl + encoding: iso8859-16 + samples_used: 25000 + bigram_entries: 981 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sl/iso8859-2: + language: sl + encoding: iso8859-2 + samples_used: 25000 + bigram_entries: 924 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sl/mac-latin2: + language: sl + encoding: mac-latin2 + samples_used: 25000 + bigram_entries: 998 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sl/utf-8: + language: sl + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1081 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/cp1250: + language: sr + encoding: cp1250 + samples_used: 25000 + bigram_entries: 119 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/cp1251: + language: sr + encoding: cp1251 + samples_used: 25000 + bigram_entries: 2145 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/cp855: + language: sr + encoding: cp855 + samples_used: 25000 + bigram_entries: 2115 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/cp866: + language: sr + encoding: cp866 + samples_used: 25000 + bigram_entries: 1917 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/iso8859-5: + language: sr + encoding: iso8859-5 + samples_used: 25000 + bigram_entries: 2108 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/mac-cyrillic: + language: sr + encoding: mac-cyrillic + samples_used: 25000 + bigram_entries: 2139 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sr/utf-8: + language: sr + encoding: utf-8 + samples_used: 25000 + bigram_entries: 708 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/cp1140: + language: sv + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2203 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/cp1252: + language: sv + encoding: cp1252 + samples_used: 25000 + bigram_entries: 1076 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/cp437: + language: sv + encoding: cp437 + samples_used: 25000 + bigram_entries: 1038 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/cp500: + language: sv + encoding: cp500 + samples_used: 25000 + bigram_entries: 2197 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/cp850: + language: sv + encoding: cp850 + samples_used: 25000 + bigram_entries: 1044 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/cp858: + language: sv + encoding: cp858 + samples_used: 25000 + bigram_entries: 1044 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/hp-roman8: + language: sv + encoding: hp-roman8 + samples_used: 25000 + bigram_entries: 1045 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/iso8859-1: + language: sv + encoding: iso8859-1 + samples_used: 25000 + bigram_entries: 1044 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/iso8859-15: + language: sv + encoding: iso8859-15 + samples_used: 25000 + bigram_entries: 1043 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/mac-roman: + language: sv + encoding: mac-roman + samples_used: 25000 + bigram_entries: 1075 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + sv/utf-8: + language: sv + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1061 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tg/koi8-t: + language: tg + encoding: koi8-t + samples_used: 25000 + bigram_entries: 2204 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tg/utf-8: + language: tg + encoding: utf-8 + samples_used: 25000 + bigram_entries: 811 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + th/cp874: + language: th + encoding: cp874 + samples_used: 25000 + bigram_entries: 3557 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + th/tis-620: + language: th + encoding: tis-620 + samples_used: 25000 + bigram_entries: 3515 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + th/utf-8: + language: th + encoding: utf-8 + samples_used: 25000 + bigram_entries: 604 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/cp1026: + language: tr + encoding: cp1026 + samples_used: 25000 + bigram_entries: 2384 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/cp1140: + language: tr + encoding: cp1140 + samples_used: 25000 + bigram_entries: 2255 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/cp1254: + language: tr + encoding: cp1254 + samples_used: 25000 + bigram_entries: 1481 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/cp857: + language: tr + encoding: cp857 + samples_used: 25000 + bigram_entries: 1418 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/iso8859-3: + language: tr + encoding: iso8859-3 + samples_used: 25000 + bigram_entries: 1413 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/iso8859-9: + language: tr + encoding: iso8859-9 + samples_used: 25000 + bigram_entries: 1418 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/mac-turkish: + language: tr + encoding: mac-turkish + samples_used: 25000 + bigram_entries: 1474 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + tr/utf-8: + language: tr + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1170 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/cp1125: + language: uk + encoding: cp1125 + samples_used: 25000 + bigram_entries: 2304 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/cp1251: + language: uk + encoding: cp1251 + samples_used: 25000 + bigram_entries: 2442 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/cp855: + language: uk + encoding: cp855 + samples_used: 25000 + bigram_entries: 2393 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/cp866: + language: uk + encoding: cp866 + samples_used: 25000 + bigram_entries: 2075 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/iso8859-5: + language: uk + encoding: iso8859-5 + samples_used: 25000 + bigram_entries: 2312 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/koi8-u: + language: uk + encoding: koi8-u + samples_used: 25000 + bigram_entries: 2302 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/mac-cyrillic: + language: uk + encoding: mac-cyrillic + samples_used: 25000 + bigram_entries: 2424 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + uk/utf-8: + language: uk + encoding: utf-8 + samples_used: 25000 + bigram_entries: 737 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ur/cp1006: + language: ur + encoding: cp1006 + samples_used: 25000 + bigram_entries: 1119 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + ur/utf-8: + language: ur + encoding: utf-8 + samples_used: 25000 + bigram_entries: 667 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + vi/cp1258: + language: vi + encoding: cp1258 + samples_used: 25000 + bigram_entries: 1149 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + vi/utf-8: + language: vi + encoding: utf-8 + samples_used: 25000 + bigram_entries: 1256 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + zh/big5hkscs: + language: zh + encoding: big5hkscs + samples_used: 25000 + bigram_entries: 10253 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + zh/gb18030: + language: zh + encoding: gb18030 + samples_used: 25000 + bigram_entries: 9597 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + zh/hz: + language: zh + encoding: hz + samples_used: 25000 + bigram_entries: 3228 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 + zh/utf-8: + language: zh + encoding: utf-8 + samples_used: 25000 + bigram_entries: 5138 + sources: + culturax: 25000 + madlad400: 0 + wikipedia: 0 + test_articles_excluded: 20 diff --git a/libs/chardet/pipeline/__init__.py b/libs/chardet/pipeline/__init__.py new file mode 100644 index 000000000..8184d6c96 --- /dev/null +++ b/libs/chardet/pipeline/__init__.py @@ -0,0 +1,81 @@ +"""Detection pipeline stages and shared types.""" + +from __future__ import annotations + +import dataclasses +from dataclasses import field +from typing import TypedDict + +#: Confidence for deterministic (non-BOM) detection stages. +#: Used by escape, markup, and utf1632 stages (and by the orchestrator for +#: the binary-detection result). +DETERMINISTIC_CONFIDENCE: float = 0.95 + +#: Byte table for fast non-ASCII counting (C-speed via bytes.translate). +#: Deleting all bytes >= 0x80 and comparing lengths gives the non-ASCII count. +HIGH_BYTES: bytes = bytes(range(0x80, 0x100)) + +#: Bytes considered valid in ASCII text: tab (0x09), newline (0x0A), +#: carriage return (0x0D), and printable ASCII (0x20-0x7E). +#: Used by ``ascii.py`` directly and by ``utf1632.py`` (with null added). +ASCII_TEXT_BYTES: bytes = bytes([0x09, 0x0A, 0x0D, *range(0x20, 0x7F)]) + + +class DetectionDict(TypedDict): + """Dictionary representation of a detection result. + + Returned by :func:`chardet.detect`, :func:`chardet.detect_all`, + and :attr:`chardet.UniversalDetector.result`. + """ + + encoding: str | None + confidence: float + language: str | None + mime_type: str | None + + +@dataclasses.dataclass(frozen=True, slots=True) +class DetectionResult: + """A single encoding detection result. + + Frozen dataclass holding the encoding name, confidence score, and + optional language identifier returned by the detection pipeline. + """ + + encoding: str | None + confidence: float + language: str | None + mime_type: str | None = None + + def to_dict(self) -> DetectionDict: + """Convert this result to a plain dict. + + :returns: A dict with ``'encoding'``, ``'confidence'``, ``'language'``, and ``'mime_type'`` keys. + """ + return { + "encoding": self.encoding, + "confidence": self.confidence, + "language": self.language, + "mime_type": self.mime_type, + } + + +#: Sentinel result for "no detection" — used by the orchestrator for +#: filtered-out fallbacks and by UniversalDetector before close(). +_NONE_RESULT = DetectionResult(encoding=None, confidence=0.0, language=None) + + +@dataclasses.dataclass(slots=True) +class PipelineContext: + """Per-run mutable state for a single pipeline invocation. + + Created once at the start of ``run_pipeline()`` and threaded through + the call chain via function parameters. Each concurrent ``detect()`` + call gets its own context, eliminating the need for module-level + mutable caches. + """ + + analysis_cache: dict[str, tuple[float, int, int]] = field(default_factory=dict) + non_ascii_count: int | None = None + mb_scores: dict[str, float] = field(default_factory=dict) + mb_coverage: dict[str, float] = field(default_factory=dict) diff --git a/libs/chardet/pipeline/ascii.py b/libs/chardet/pipeline/ascii.py new file mode 100644 index 000000000..080fe93c7 --- /dev/null +++ b/libs/chardet/pipeline/ascii.py @@ -0,0 +1,39 @@ +"""Stage 1c: Pure ASCII detection (with null-separator tolerance). + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from chardet.pipeline import ASCII_TEXT_BYTES, DetectionResult + +# Maximum fraction of null bytes to still classify data as ASCII. +# Null-separated CLI output (find -print0, git ls-tree -z) typically has +# 1-3.5% nulls. 5% covers all realistic cases while staying well below +# the UTF-16 guard threshold (15%). +_MAX_NULL_FRACTION = 0.05 + + +def detect_ascii(data: bytes) -> DetectionResult | None: + r"""Return an ASCII result if all bytes are printable ASCII plus common whitespace. + + Tolerates sparse null bytes (``\x00``) up to ``_MAX_NULL_FRACTION`` of + the data, returning confidence 0.99 instead of 1.0 to distinguish from + pure ASCII. + + :param data: The raw byte data to examine. + :returns: A :class:`DetectionResult` for ASCII, or ``None``. + """ + if not data: + return None + remainder = data.translate(None, ASCII_TEXT_BYTES) + if not remainder: + return DetectionResult(encoding="ascii", confidence=1.0, language=None) + # Check if the only non-allowed bytes are null separators + if remainder.replace(b"\x00", b""): + return None # Non-null, non-ASCII bytes present + # All non-allowed bytes are nulls — accept if sparse enough + null_fraction = len(remainder) / len(data) + if null_fraction <= _MAX_NULL_FRACTION: + return DetectionResult(encoding="ascii", confidence=0.99, language=None) + return None diff --git a/libs/chardet/pipeline/binary.py b/libs/chardet/pipeline/binary.py new file mode 100644 index 000000000..53032a797 --- /dev/null +++ b/libs/chardet/pipeline/binary.py @@ -0,0 +1,30 @@ +"""Stage 0: Binary content detection.""" + +from __future__ import annotations + +from chardet._utils import DEFAULT_MAX_BYTES + +# Threshold: if more than this fraction of bytes are binary indicators, it's binary +_BINARY_THRESHOLD = 0.01 + +# Translation table that maps binary-indicator control bytes (0x00-0x08, +# 0x0E-0x1F — excludes \t \n \v \f \r) to None (deleting them) and keeps +# everything else. len(data) - len(translated) gives the count in one +# C-level pass. +_BINARY_DELETE = bytes(range(0x09)) + bytes(range(0x0E, 0x20)) + + +def is_binary(data: bytes, max_bytes: int = DEFAULT_MAX_BYTES) -> bool: + """Return ``True`` if *data* appears to be binary (not text) content. + + :param data: The raw byte data to examine. + :param max_bytes: Maximum number of bytes to scan. + :returns: ``True`` if the data is classified as binary. + """ + data = data[:max_bytes] + if not data: + return False + + clean = data.translate(None, _BINARY_DELETE) + binary_count = len(data) - len(clean) + return binary_count / len(data) > _BINARY_THRESHOLD diff --git a/libs/chardet/pipeline/bom.py b/libs/chardet/pipeline/bom.py new file mode 100644 index 000000000..6ae0fbe35 --- /dev/null +++ b/libs/chardet/pipeline/bom.py @@ -0,0 +1,37 @@ +"""Stage 1a: BOM (Byte Order Mark) detection.""" + +from __future__ import annotations + +from chardet.pipeline import DetectionResult + +# Ordered longest-first so UTF-32 is checked before UTF-16 +# (UTF-32-LE BOM starts with the same bytes as UTF-16-LE BOM) +_BOMS: tuple[tuple[bytes, str], ...] = ( + (b"\x00\x00\xfe\xff", "utf-32"), + (b"\xff\xfe\x00\x00", "utf-32"), + (b"\xef\xbb\xbf", "utf-8-sig"), + (b"\xfe\xff", "utf-16"), + (b"\xff\xfe", "utf-16"), +) + +_UTF32_BOMS: frozenset[bytes] = frozenset({b"\x00\x00\xfe\xff", b"\xff\xfe\x00\x00"}) + + +def detect_bom(data: bytes) -> DetectionResult | None: + """Check for a byte order mark at the start of *data*. + + :param data: The raw byte data to examine. + :returns: A :class:`DetectionResult` with confidence 1.0, or ``None``. + """ + for bom_bytes, encoding in _BOMS: + if data.startswith(bom_bytes): + # UTF-32 BOMs overlap with UTF-16 BOMs (e.g. FF FE 00 00 starts + # with the UTF-16-LE BOM FF FE). Validate that the payload after + # a UTF-32 BOM is a valid number of UTF-32 code units (multiple of + # 4 bytes). If not, skip to let the shorter UTF-16 BOM match. + if bom_bytes in _UTF32_BOMS: + payload_len = len(data) - len(bom_bytes) + if payload_len % 4 != 0: + continue + return DetectionResult(encoding=encoding, confidence=1.0, language=None) + return None diff --git a/libs/chardet/pipeline/confusion.py b/libs/chardet/pipeline/confusion.py new file mode 100644 index 000000000..4fbe6b94d --- /dev/null +++ b/libs/chardet/pipeline/confusion.py @@ -0,0 +1,361 @@ +"""Confusion group resolution for similar single-byte encodings. + +At runtime, loads pre-computed distinguishing byte maps from confusion.bin +and uses them to resolve statistical scoring ties between similar encodings. + +Build-time computation (``compute_confusion_groups``, ``compute_distinguishing_maps``, +``serialize_confusion_data``) lives in ``scripts/confusion_training.py``. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +import functools +import importlib.resources +import struct +import warnings + +from chardet.models import ( + BigramProfile, + get_enc_index, + get_idf_weights, + score_with_profile, +) +from chardet.pipeline import DetectionResult +from chardet.registry import lookup_encoding + +# Type alias for the distinguishing map structure: +# Maps (enc_a, enc_b) -> (distinguishing_byte_set, {byte_val: (cat_a, cat_b)}) +DistinguishingMaps = dict[ + tuple[str, str], + tuple[frozenset[int], dict[int, tuple[str, str]]], +] + +# uint8 -> Unicode general category, inverse of the mapping in +# scripts/confusion_training.py used at serialization time. +_INT_TO_CATEGORY: dict[int, str] = { + 0: "Lu", + 1: "Ll", + 2: "Lt", + 3: "Lm", + 4: "Lo", + 5: "Mn", + 6: "Mc", + 7: "Me", + 8: "Nd", + 9: "Nl", + 10: "No", + 11: "Pc", + 12: "Pd", + 13: "Ps", + 14: "Pe", + 15: "Pi", + 16: "Pf", + 17: "Po", + 18: "Sm", + 19: "Sc", + 20: "Sk", + 21: "So", + 22: "Zs", + 23: "Zl", + 24: "Zp", + 25: "Cc", + 26: "Cf", + 27: "Cs", + 28: "Co", + 29: "Cn", +} + +# Inverse mapping for serialization — used by scripts/confusion_training.py. +_CATEGORY_TO_INT: dict[str, int] = {v: k for k, v in _INT_TO_CATEGORY.items()} + + +def deserialize_confusion_data_from_bytes(data: bytes) -> DistinguishingMaps: + """Load confusion group data from raw bytes. + + :param data: The raw binary content of a confusion.bin file. + :returns: A :data:`DistinguishingMaps` dictionary keyed by encoding pairs. + """ + result: DistinguishingMaps = {} + offset = 0 + (num_pairs,) = struct.unpack_from("!H", data, offset) + offset += 2 + + for _ in range(num_pairs): + (name_a_len,) = struct.unpack_from("!B", data, offset) + offset += 1 + name_a = data[offset : offset + name_a_len].decode("utf-8") + offset += name_a_len + + (name_b_len,) = struct.unpack_from("!B", data, offset) + offset += 1 + name_b = data[offset : offset + name_b_len].decode("utf-8") + offset += name_b_len + + (num_diffs,) = struct.unpack_from("!B", data, offset) + offset += 1 + + diff_bytes_list: list[int] = [] + categories: dict[int, tuple[str, str]] = {} + for _ in range(num_diffs): + bv, cat_a_int, cat_b_int = struct.unpack_from("!BBB", data, offset) + offset += 3 + diff_bytes_list.append(bv) + categories[bv] = ( + _INT_TO_CATEGORY.get(cat_a_int, "Cn"), + _INT_TO_CATEGORY.get(cat_b_int, "Cn"), + ) + result[(name_a, name_b)] = (frozenset(diff_bytes_list), categories) + + return result + + +@functools.cache +def load_confusion_data() -> DistinguishingMaps: + """Load confusion group data from the bundled confusion.bin file. + + :returns: A :data:`DistinguishingMaps` dictionary keyed by encoding pairs. + """ + ref = importlib.resources.files("chardet.models").joinpath("confusion.bin") + raw = ref.read_bytes() + if not raw: + warnings.warn( + "chardet confusion.bin is empty — confusion resolution disabled; " + "reinstall chardet to fix", + RuntimeWarning, + stacklevel=2, + ) + return {} + try: + raw_maps = deserialize_confusion_data_from_bytes(raw) + except (struct.error, UnicodeDecodeError) as e: + msg = f"corrupt confusion.bin: {e}" + raise ValueError(msg) from e + # Normalize keys to canonical codec names so pipeline output matches. + normalized: DistinguishingMaps = {} + for (a, b), value in raw_maps.items(): + norm_a = lookup_encoding(a) or a + norm_b = lookup_encoding(b) or b + normalized[(norm_a, norm_b)] = value + return normalized + + +# Unicode general category preference scores for voting resolution. +# Higher scores indicate more linguistically meaningful characters. +_CATEGORY_PREFERENCE: dict[str, int] = { + "Lu": 10, + "Ll": 10, + "Lt": 10, + "Lm": 9, + "Lo": 9, + "Nd": 8, + "Nl": 7, + "No": 7, + "Pc": 6, + "Pd": 6, + "Ps": 6, + "Pe": 6, + "Pi": 6, + "Pf": 6, + "Po": 6, + "Sc": 5, + "Sm": 5, + "Sk": 4, + "So": 4, + "Zs": 3, + "Zl": 3, + "Zp": 3, + "Cf": 2, + "Cc": 1, + "Co": 1, + "Cs": 0, + "Cn": 0, + "Mn": 5, + "Mc": 5, + "Me": 5, +} + + +def resolve_by_category_voting( + data: bytes, + enc_a: str, + enc_b: str, + diff_bytes: frozenset[int], + categories: dict[int, tuple[str, str]], +) -> str | None: + """Resolve between two encodings using Unicode category voting. + + For each distinguishing byte present in the data, compare the Unicode + general category under each encoding. The encoding whose interpretation + has the higher category preference score gets a vote. The encoding with + more votes wins. + + :param data: The raw byte data to examine. + :param enc_a: First encoding name. + :param enc_b: Second encoding name. + :param diff_bytes: Byte values where the two encodings differ. + :param categories: Mapping of byte value to ``(cat_a, cat_b)`` Unicode + general category pairs. + :returns: The winning encoding name, or ``None`` if tied. + """ + votes_a = 0 + votes_b = 0 + relevant = frozenset(data) & diff_bytes + if not relevant: + return None + for bv in relevant: + cat_a, cat_b = categories[bv] + pref_a = _CATEGORY_PREFERENCE.get(cat_a, 0) + pref_b = _CATEGORY_PREFERENCE.get(cat_b, 0) + if pref_a > pref_b: + votes_a += pref_a - pref_b + elif pref_b > pref_a: + votes_b += pref_b - pref_a + if votes_a > votes_b: + return enc_a + if votes_b > votes_a: + return enc_b + return None + + +def _best_variant_score( + profile: BigramProfile, + index: dict[str, list[tuple[str | None, memoryview, str]]], + enc: str, +) -> float: + """Return the best bigram score across all language variants for *enc*.""" + variants = index.get(enc) + if not variants: + return 0.0 + return max( + score_with_profile(profile, model, model_key) + for _, model, model_key in variants + ) + + +def resolve_by_bigram_rescore( + data: bytes, + enc_a: str, + enc_b: str, + diff_bytes: frozenset[int], +) -> str | None: + """Resolve between two encodings by re-scoring only distinguishing bigrams. + + Builds a focused bigram profile containing only bigrams where at least one + byte is a distinguishing byte, then scores both encodings against their + best language model. + + :param data: The raw byte data to examine. + :param enc_a: First encoding name. + :param enc_b: Second encoding name. + :param diff_bytes: Byte values where the two encodings differ. + :returns: The winning encoding name, or ``None`` if tied. + """ + if len(data) < 2: + return None + + idf = get_idf_weights() + freq: dict[int, int] = {} + for i in range(len(data) - 1): + b1 = data[i] + b2 = data[i + 1] + if b1 not in diff_bytes and b2 not in diff_bytes: + continue + idx = (b1 << 8) | b2 + freq[idx] = freq.get(idx, 0) + idf[idx] + + if not freq: + return None + + profile = BigramProfile.from_weighted_freq(freq) + + index = get_enc_index() + best_a = _best_variant_score(profile, index, enc_a) + best_b = _best_variant_score(profile, index, enc_b) + + if best_a > best_b: + return enc_a + if best_b > best_a: + return enc_b + return None + + +def _find_pair_key( + maps: DistinguishingMaps, + enc_a: str, + enc_b: str, +) -> tuple[str, str] | None: + """Find the canonical key for a pair of encodings in the confusion maps.""" + if (enc_a, enc_b) in maps: + return (enc_a, enc_b) + if (enc_b, enc_a) in maps: + return (enc_b, enc_a) + return None + + +# Maximum confidence gap from the top result for candidates beyond +# position 1 to participate in confusion resolution. +_CONFUSION_BAND = 0.005 + + +def resolve_confusion_groups( + data: bytes, + results: list[DetectionResult], +) -> list[DetectionResult]: + """Resolve confusion between similar encodings in the top results. + + Checks the top result against each candidate within a confidence band. + Always checks position 1 (preserving original top-2 behavior); for + positions 2+ only checks within the band. Uses bigram re-scoring + with category voting as fallback. + + :param data: The raw byte data to examine. + :param results: Detection results sorted by confidence descending. + :returns: A reordered list of :class:`DetectionResult` with the winner first. + """ + if len(results) < 2: + return results + + top = results[0] + if top.encoding is None: + return results + + maps = load_confusion_data() + top_conf = top.confidence + + for i in range(1, len(results)): + candidate = results[i] + if candidate.encoding is None: + continue + # Always check position 1 (original top-2 behavior). + # For positions 2+, only check within the confidence band. + if i > 1 and top_conf - candidate.confidence > _CONFUSION_BAND: + break + + pair_key = _find_pair_key(maps, top.encoding, candidate.encoding) + if pair_key is None: + continue + + diff_bytes, categories = maps[pair_key] + enc_a, enc_b = pair_key + + cat_winner = resolve_by_category_voting( + data, enc_a, enc_b, diff_bytes, categories + ) + bigram_winner = resolve_by_bigram_rescore(data, enc_a, enc_b, diff_bytes) + winner = bigram_winner if bigram_winner is not None else cat_winner + + if winner is not None and winner == candidate.encoding: + # Give the promoted candidate the top result's confidence so + # the promotion survives any downstream confidence-based sort. + promoted = DetectionResult( + candidate.encoding, + top.confidence, + candidate.language, + candidate.mime_type, + ) + rest = [r for j, r in enumerate(results) if j != i] + return [promoted, *rest] + + return results diff --git a/libs/chardet/pipeline/escape.py b/libs/chardet/pipeline/escape.py new file mode 100644 index 000000000..fe84288b3 --- /dev/null +++ b/libs/chardet/pipeline/escape.py @@ -0,0 +1,274 @@ +"""Early detection of escape-sequence-based encodings (ISO-2022, HZ-GB-2312, UTF-7). + +These encodings use ESC (0x1B), tilde (~), or plus (+) sequences to switch +character sets. They must be detected before binary detection (ESC is a control +byte) and before ASCII detection (HZ-GB-2312 and UTF-7 use only printable ASCII +bytes plus their respective shift markers). + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from chardet.pipeline import DETERMINISTIC_CONFIDENCE, DetectionResult + + +def _has_valid_hz_regions(data: bytes) -> bool: + """Check that at least one ~{...~} region contains valid GB2312 byte pairs. + + In HZ-GB-2312 GB mode, characters are encoded as pairs of bytes in the + 0x21-0x7E range. We require at least one region with a non-empty, even- + length run of such bytes. + """ + start = 0 + while True: + begin = data.find(b"~{", start) + if begin == -1: + return False + end = data.find(b"~}", begin + 2) + if end == -1: + return False + region = data[begin + 2 : end] + # Must be non-empty, even length, and all bytes in GB2312 range + if ( + len(region) >= 2 + and len(region) % 2 == 0 + and all(0x21 <= b <= 0x7E for b in region) + ): + return True + start = end + 2 + + +# Base64 alphabet used inside UTF-7 shifted sequences (+-) +_B64_CHARS: bytes = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +_UTF7_BASE64: frozenset[int] = frozenset(_B64_CHARS) + +# Lookup table mapping each Base64 byte to its 6-bit value (0-63). +_B64_DECODE: dict[int, int] = {c: i for i, c in enumerate(_B64_CHARS)} + + +def _is_valid_utf7_b64(b64_bytes: bytes) -> bool: + """Check if base64 bytes decode to valid UTF-16BE with correct padding. + + A valid UTF-7 shifted sequence must: + 1. Contain at least 3 Base64 characters (18 bits, enough for one 16-bit + UTF-16 code unit). + 2. Have zero-valued trailing padding bits (the unused low bits of the last + Base64 sextet after the last complete 16-bit code unit). + 3. Decode to valid UTF-16BE — no lone surrogates. + + This rejects accidental ``+-`` patterns found in URLs, MIME + boundaries, hex-encoded hashes (e.g. SHA-1 git refs), and other ASCII data. + + The caller (``_has_valid_utf7_sequences``) already checks ``b64_len >= 3`` + before calling this function, so *b64_bytes* is always at least 3 bytes. + """ + n = len(b64_bytes) + total_bits = n * 6 + # Check that padding bits (trailing bits after last complete code unit) + # are zero. + padding_bits = total_bits % 16 + if padding_bits > 0: + last_val = _B64_DECODE[b64_bytes[-1]] + # The low `padding_bits` of the last sextet must be zero + mask = (1 << padding_bits) - 1 + if last_val & mask: + return False + # Decode the base64 to raw bytes and validate as UTF-16BE. + # Lone surrogates (unpaired 0xD800-0xDFFF code units) are illegal in + # well-formed UTF-16 and cannot appear in real UTF-7 text. This catches + # hex-encoded hashes and other accidental base64-like sequences. + num_bytes = total_bits // 8 + raw = bytearray(num_bytes) + bit_buf = 0 + bit_count = 0 + out_idx = 0 + for c in b64_bytes: + bit_buf = (bit_buf << 6) | _B64_DECODE[c] + bit_count += 6 + if bit_count >= 8: + bit_count -= 8 + raw[out_idx] = (bit_buf >> bit_count) & 0xFF + out_idx += 1 + prev_high = False + for i in range(0, num_bytes - 1, 2): + code_unit = (raw[i] << 8) | raw[i + 1] + if 0xD800 <= code_unit <= 0xDBFF: # high surrogate + if prev_high: + return False # consecutive high surrogates + prev_high = True + elif 0xDC00 <= code_unit <= 0xDFFF: # low surrogate + if not prev_high: + return False # lone low surrogate + prev_high = False + else: + if prev_high: + return False # high surrogate not followed by low surrogate + prev_high = False + return not prev_high + + +def _is_embedded_in_base64(data: bytes, pos: int) -> bool: + """Return True if the ``+`` at *pos* is embedded in a base64 stream. + + Walks backward from *pos*, skipping CR/LF, and counts consecutive base64 + characters (including ``=`` for padding). If 4 or more are found, the + ``+`` is likely part of a PEM certificate, email attachment, or similar + base64 blob rather than a real UTF-7 shift character. + """ + b64_with_pad: frozenset[int] = _UTF7_BASE64 | frozenset(b"=") + count = 0 + i = pos - 1 + while i >= 0: + b = data[i] + if b in {0x0A, 0x0D}: # skip newlines + i -= 1 + continue + if b in b64_with_pad: + count += 1 + i -= 1 + else: + break + return count >= 4 + + +def _has_valid_utf7_sequences(data: bytes) -> bool: + """Check that *data* contains at least one valid UTF-7 shifted sequence. + + A valid shifted sequence is ``+`` terminated by either an + explicit ``-`` or any non-Base64 character (per RFC 2152). The base64 + portion must decode to valid UTF-16BE with correct zero-padding bits. + The sequence ``+-`` is a literal plus sign and is **not** counted. + """ + start = 0 + while True: + shift_pos = data.find(ord("+"), start) + if shift_pos == -1: + return False + pos = shift_pos + 1 # skip the '+' + # +- is a literal plus, not a shifted sequence + if pos < len(data) and data[pos] == ord("-"): + start = pos + 1 + continue + # Guard A: '+' as the first base64 character encodes PUA code points + # (U+F800-U+FBFC) which never appear in real text. This catches + # patterns like "C++20" and "++row". Skip past ALL consecutive '+' + # characters so the next '+' in a run like ``++`` or ``+++`` is not + # re-examined as a new shift character. + if pos < len(data) and data[pos] == ord("+"): + while pos < len(data) and data[pos] == ord("+"): + pos += 1 + start = pos + continue + # Guard B: if the '+' is embedded in a base64 stream (PEM, email + # attachment, etc.), it's not a real UTF-7 shift character. + if _is_embedded_in_base64(data, shift_pos): + start = pos + continue + # Collect consecutive Base64 characters + i = pos + while i < len(data) and data[i] in _UTF7_BASE64: + i += 1 + b64_len = i - pos + b64_data = data[pos:i] + # Guard C: reject base64 blocks with no uppercase letters. + # UTF-7 encodes UTF-16BE code points, and the high byte for virtually + # every script (Latin Extended, Cyrillic, Arabic, CJK, …) produces + # uppercase base64 characters. Sequences without any uppercase like + # "row", "foo", "pos" are almost always variable names or English + # words that accidentally follow a '+'. (bytes.islower() returns + # True when there are no uppercase letters, even if digits or '/' + # are present, which is the desired behavior here.) Out of 71,510 + # real UTF-7 base64 blocks in the test corpus, only 4 lack uppercase + # letters (0.006%). + if b64_len >= 3 and b64_data.islower(): + start = i + continue + # Accept if base64 content is valid UTF-16BE (padding bits check + # prevents false positives). Terminator can be '-', any non-Base64 + # byte, or end of data — all per RFC 2152. + if b64_len >= 3 and _is_valid_utf7_b64(b64_data): + return True + start = max(pos, i) + + +def detect_escape_encoding(data: bytes) -> DetectionResult | None: + """Detect ISO-2022, HZ-GB-2312, and UTF-7 from escape/tilde/plus sequences. + + :param data: The raw byte data to examine. + :returns: A :class:`DetectionResult` if an escape encoding is found, or ``None``. + """ + has_esc = b"\x1b" in data + has_tilde = b"~" in data + has_plus = b"+" in data + + if not has_esc and not has_tilde and not has_plus: + return None + + if has_esc: + # ISO-2022-JP-2004: JIS X 0213 designations are unique to this variant. + if b"\x1b$(O" in data or b"\x1b$(P" in data or b"\x1b$(Q" in data: + return DetectionResult( + encoding="iso2022_jp_2004", + confidence=DETERMINISTIC_CONFIDENCE, + language="ja", + ) + + # ISO-2022-JP-EXT: JIS X 0201 Kana designation is unique to this variant. + if b"\x1b(I" in data: + return DetectionResult( + encoding="iso2022_jp_ext", + confidence=DETERMINISTIC_CONFIDENCE, + language="ja", + ) + + # ISO-2022-JP base: JIS X 0208/0201/0212 designations. + if ( + b"\x1b$B" in data + or b"\x1b$@" in data + or b"\x1b(J" in data + or b"\x1b$(D" in data # JIS X 0212-1990 (JP-1/JP-2/JP-EXT) + ): + # SI/SO (0x0E / 0x0F) shift controls -> JP-EXT + if b"\x0e" in data and b"\x0f" in data: + return DetectionResult( + encoding="iso2022_jp_ext", + confidence=DETERMINISTIC_CONFIDENCE, + language="ja", + ) + # Default to JP-2: a strict superset of JP and JP-1 that + # decodes all base sequences correctly. + return DetectionResult( + encoding="iso2022_jp_2", + confidence=DETERMINISTIC_CONFIDENCE, + language="ja", + ) + + # ISO-2022-KR: ESC sequence for KS C 5601 + if b"\x1b$)C" in data: + return DetectionResult( + encoding="iso2022_kr", + confidence=DETERMINISTIC_CONFIDENCE, + language="ko", + ) + + # HZ-GB-2312: tilde escapes for GB2312 + # Require valid GB2312 byte pairs (0x21-0x7E range) between ~{ and ~} markers. + if has_tilde and b"~{" in data and b"~}" in data and _has_valid_hz_regions(data): + return DetectionResult( + encoding="hz", + confidence=DETERMINISTIC_CONFIDENCE, + language="zh", + ) + + # UTF-7: plus-sign shifts into Base64-encoded Unicode. + # UTF-7 is a 7-bit encoding (RFC 2152): every byte must be in 0x00-0x7F. + # Data with any byte > 0x7F cannot be UTF-7. + if has_plus and max(data) < 0x80 and _has_valid_utf7_sequences(data): + return DetectionResult( + encoding="utf-7", + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + ) + + return None diff --git a/libs/chardet/pipeline/magic.py b/libs/chardet/pipeline/magic.py new file mode 100644 index 000000000..8ce0d5df2 --- /dev/null +++ b/libs/chardet/pipeline/magic.py @@ -0,0 +1,277 @@ +"""Magic number detection for binary file types. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from chardet.pipeline import DetectionResult + +# (prefix_bytes, mime_type) — longest prefix first to avoid shorter prefixes +# shadowing longer ones. All entries match at offset 0. +# Formats with sub-type logic (ftyp, RIFF, FORM, ZIP) are handled separately. +_MAGIC_NUMBERS: tuple[tuple[bytes, str], ...] = ( + # Images + (b"\x89PNG\r\n\x1a\n", "image/png"), + (b"GIF87a", "image/gif"), + (b"GIF89a", "image/gif"), + (b"MM\x00\x2a", "image/tiff"), + (b"II\x2a\x00", "image/tiff"), + (b"8BPS", "image/vnd.adobe.photoshop"), + (b"qoif", "image/qoi"), + (b"BM", "image/bmp"), + (b"\xff\xd8\xff", "image/jpeg"), + # JPEG XL: 12-byte container signature (must precede the 2-byte codestream) + ( + b"\x00\x00\x00\x0c\x4a\x58\x4c\x20\x0d\x0a\x87\x0a", + "image/jxl", + ), + # JPEG XL: 2-byte codestream signature + (b"\xff\x0a", "image/jxl"), + ( + b"\x00\x00\x01\x00", + "image/vnd.microsoft.icon", + ), # ICO (not TTF — TTF is \x00\x01\x00\x00) + # Audio/Video + (b"ID3", "audio/mpeg"), + (b"MThd", "audio/midi"), + (b"OggS", "audio/ogg"), + (b"fLaC", "audio/flac"), + (b"\x1a\x45\xdf\xa3", "video/webm"), + # Archives (ZIP handled separately below for subtype detection) + (b"\x1f\x8b", "application/gzip"), + (b"BZh", "application/x-bzip2"), + (b"\xfd7zXZ\x00", "application/x-xz"), + (b"7z\xbc\xaf\x27\x1c", "application/x-7z-compressed"), + (b"Rar!\x1a\x07\x01\x00", "application/vnd.rar"), + (b"Rar!\x1a\x07\x00", "application/vnd.rar"), + (b"\x28\xb5\x2f\xfd", "application/zstd"), + # Documents / Data + (b"%PDF-", "application/pdf"), + (b"SQLite format 3\x00", "application/x-sqlite3"), + (b"ARROW1", "application/vnd.apache.arrow.file"), + (b"PAR1", "application/vnd.apache.parquet"), + (b"\x00asm", "application/wasm"), + # Executables / Bytecode (cafebabe handled separately — shared by Java + # class files and Mach-O fat binaries, disambiguated by bytes 4-7) + (b"dex\n", "application/vnd.android.dex"), + (b"\x7fELF", "application/x-elf"), + (b"\xfe\xed\xfa\xce", "application/x-mach-binary"), + (b"\xfe\xed\xfa\xcf", "application/x-mach-binary"), + (b"\xce\xfa\xed\xfe", "application/x-mach-binary"), + (b"\xcf\xfa\xed\xfe", "application/x-mach-binary"), + (b"MZ", "application/vnd.microsoft.portable-executable"), + # Fonts + (b"wOFF", "font/woff"), + (b"wOF2", "font/woff2"), + (b"OTTO", "font/otf"), + (b"\x00\x01\x00\x00", "font/ttf"), +) + +# TAR archives have "ustar" at offset 257 +_TAR_OFFSET = 257 +_TAR_SIGNATURES: tuple[bytes, ...] = (b"ustar\x00", b"ustar ") + +# RIFF container subtypes — determined by bytes 8-11 +_RIFF_SUBTYPES: dict[bytes, str] = { + b"WEBP": "image/webp", + b"WAVE": "audio/wav", + b"AVI ": "video/x-msvideo", +} + +# FORM container subtypes (same layout as RIFF: 4-byte tag, 4-byte size, 4-byte type) +_FORM_SUBTYPES: dict[bytes, str] = { + b"AIFF": "audio/aiff", + b"AIFC": "audio/aiff", +} + +# ZIP-based format detection — scan the first 4 KB for local file headers +# and classify based on entry filenames or content. Many ZIP generators +# set the data-descriptor flag on every entry, making sequential header +# walking impossible without decompression. Instead we search for +# PK\x03\x04 signatures and inspect the filename/content fields. +_ZIP_SIGNATURE = b"PK\x03\x04" +_ZIP_SCAN_LIMIT = 4096 + +# Filename prefix → MIME type (checked against each entry's filename) +_ZIP_FILENAME_PREFIXES: tuple[tuple[bytes, str], ...] = ( + # Office Open XML + (b"xl/", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + ( + b"word/", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ), + ( + b"ppt/", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ), + # Java + (b"META-INF/MANIFEST.MF", "application/java-archive"), + # Android + (b"AndroidManifest.xml", "application/vnd.android.package-archive"), + # EPUB + (b"META-INF/container.xml", "application/epub+zip"), +) + +# Filename suffix → MIME type (checked against each entry's filename) +_ZIP_FILENAME_SUFFIXES: tuple[tuple[bytes, str], ...] = ( + # Python wheels: entries like "package-1.0.dist-info/WHEEL" + (b".dist-info/", "application/x-wheel+zip"), +) + +# OpenDocument MIME types recognized in the "mimetype" entry content. +_OPENDOCUMENT_MIMES: frozenset[bytes] = frozenset( + { + b"application/vnd.oasis.opendocument.text", + b"application/vnd.oasis.opendocument.spreadsheet", + b"application/vnd.oasis.opendocument.presentation", + b"application/vnd.oasis.opendocument.graphics", + } +) + +# MP4/MOV/HEIC ftyp box — "ftyp" at offset 4 +_FTYP_MARKER = b"ftyp" +_FTYP_OFFSET = 4 +# Brand → MIME type for image ftyp brands +_FTYP_AVIF_BRANDS: frozenset[bytes] = frozenset({b"avif", b"avis"}) +_FTYP_HEIC_BRANDS: frozenset[bytes] = frozenset({b"heic", b"heix"}) +_FTYP_HEIF_BRANDS: frozenset[bytes] = frozenset({b"mif1", b"msf1"}) +_FTYP_AUDIO_BRANDS: frozenset[bytes] = frozenset({b"M4A ", b"M4B ", b"F4A "}) +_FTYP_QUICKTIME_BRANDS: frozenset[bytes] = frozenset({b"qt "}) + +# Java class file vs Mach-O fat binary — both start with \xca\xfe\xba\xbe. +# Bytes 4-7 disambiguate: Mach-O fat stores nfat_arch (big-endian uint32, +# typically 2-5), while Java class stores minor_version (uint16) + +# major_version (uint16, 45+ for Java 1.1 through modern Java). +_CAFEBABE = b"\xca\xfe\xba\xbe" +_CAFEBABE_MAX_FAT_ARCHES = 20 # no real fat binary exceeds this + + +def _classify_zip(data: bytes) -> str: + """Classify a ZIP file by scanning entry filenames and content. + + Scans for local file header signatures within the first + ``_ZIP_SCAN_LIMIT`` bytes. For each entry, checks the filename + against known prefixes/suffixes, and for ``mimetype`` entries reads + the uncompressed content to detect OpenDocument formats. + + **Limitation:** when an entry has the data-descriptor flag (bit 3) + set, the compressed size in the header is 0 and we cannot skip past + the entry content. The scan may find spurious PK local file header + signatures inside compressed data. In practice deflate output rarely produces + valid ZIP local file headers with recognizable filenames, so false + positives are unlikely. + """ + scan = data[:_ZIP_SCAN_LIMIT] + offset = 0 + while True: + idx = scan.find(_ZIP_SIGNATURE, offset) + if idx == -1 or len(scan) < idx + 30: + break + name_len = int.from_bytes(scan[idx + 26 : idx + 28], "little") + extra_len = int.from_bytes(scan[idx + 28 : idx + 30], "little") + name_start = idx + 30 + if len(scan) < name_start + name_len: + break + name = scan[name_start : name_start + name_len] + # Check filename prefixes + for prefix, mime in _ZIP_FILENAME_PREFIXES: + if name.startswith(prefix): + return mime + # Check filename suffixes + for suffix, mime in _ZIP_FILENAME_SUFFIXES: + if suffix in name: + return mime + # OpenDocument: "mimetype" entry with uncompressed content + if name == b"mimetype": + compression = int.from_bytes(scan[idx + 8 : idx + 10], "little") + if compression == 0: # stored (uncompressed) + content_start = name_start + name_len + extra_len + content_len = int.from_bytes(scan[idx + 22 : idx + 26], "little") + if len(scan) >= content_start + content_len: + content = scan[content_start : content_start + content_len] + if content in _OPENDOCUMENT_MIMES: + return content.decode("ascii") + # Advance past this entry's extra field and content to avoid + # matching PK\x03\x04 that appears inside file data. When the + # data-descriptor flag (bit 3) is set, compressed_size in the + # header is 0 — we still skip the extra field and hope the next + # real header is found by the scan. + flags = int.from_bytes(scan[idx + 6 : idx + 8], "little") + content_size = ( + 0 if flags & 0x0008 else int.from_bytes(scan[idx + 18 : idx + 22], "little") + ) + offset = name_start + name_len + extra_len + content_size + return "application/zip" + + +def _make_result(mime: str) -> DetectionResult: + return DetectionResult(encoding=None, confidence=1.0, language=None, mime_type=mime) + + +def detect_magic(data: bytes) -> DetectionResult | None: + """Check *data* for known binary file magic numbers. + + :param data: The raw byte data to examine. + :returns: A :class:`DetectionResult` with ``encoding=None`` and the + identified MIME type, or ``None`` if no magic number matches. + """ + if not data: + return None + + # Check ftyp box (MP4/MOV/HEIC/AVIF) — "ftyp" at offset 4. + # Bytes 0-3 are the box size (big-endian uint32). Valid ftyp boxes + # have size >= 8 and <= file length. The upper bound check prevents + # false positives on text (ASCII bytes 0-3 produce huge box sizes + # like 0x54686520 for "The "). + if len(data) >= 12 and data[_FTYP_OFFSET : _FTYP_OFFSET + 4] == _FTYP_MARKER: + box_size = int.from_bytes(data[:4], "big") + if 8 <= box_size <= len(data): + brand = data[8:12] + if brand in _FTYP_AVIF_BRANDS: + return _make_result("image/avif") + if brand in _FTYP_HEIC_BRANDS: + return _make_result("image/heic") + if brand in _FTYP_HEIF_BRANDS: + return _make_result("image/heif") + if brand in _FTYP_AUDIO_BRANDS: + return _make_result("audio/mp4") + if brand in _FTYP_QUICKTIME_BRANDS: + return _make_result("video/quicktime") + return _make_result("video/mp4") + + # RIFF container — check subtype at bytes 8-11 + if data[:4] == b"RIFF" and len(data) >= 12: + subtype = _RIFF_SUBTYPES.get(data[8:12]) + if subtype is not None: + return _make_result(subtype) + + # FORM container (AIFF) — same layout as RIFF + if data[:4] == b"FORM" and len(data) >= 12: + subtype = _FORM_SUBTYPES.get(data[8:12]) + if subtype is not None: + return _make_result(subtype) + + # ZIP-based format detection + if data.startswith(_ZIP_SIGNATURE): + return _make_result(_classify_zip(data)) + + # Java class file vs Mach-O fat binary (both \xca\xfe\xba\xbe) + if data[:4] == _CAFEBABE and len(data) >= 8: + nfat_arch = int.from_bytes(data[4:8], "big") + if nfat_arch <= _CAFEBABE_MAX_FAT_ARCHES: + return _make_result("application/x-mach-binary") + return _make_result("application/java-vm") + + # Fixed-offset magic numbers (all at offset 0) + for prefix, mime in _MAGIC_NUMBERS: + if data.startswith(prefix): + return _make_result(mime) + + # TAR archive — "ustar" at offset 257 + if len(data) >= _TAR_OFFSET + 6: + tar_sig = data[_TAR_OFFSET : _TAR_OFFSET + 6] + if tar_sig in _TAR_SIGNATURES: + return _make_result("application/x-tar") + + return None diff --git a/libs/chardet/pipeline/markup.py b/libs/chardet/pipeline/markup.py new file mode 100644 index 000000000..6588ba005 --- /dev/null +++ b/libs/chardet/pipeline/markup.py @@ -0,0 +1,107 @@ +"""Stage 1b: charset declaration extraction (HTML/XML/PEP 263).""" + +from __future__ import annotations + +import re + +from chardet.pipeline import DETERMINISTIC_CONFIDENCE, DetectionResult +from chardet.registry import lookup_encoding + +_SCAN_LIMIT = 4096 + +_XML_ENCODING_RE = re.compile( + rb"""<\?xml[^>]+encoding\s*=\s*['"]([^'"]+)['"]""", re.IGNORECASE +) +_HTML5_CHARSET_RE = re.compile( + rb"""]+charset\s*=\s*['"]?\s*([^\s'">;]+)""", re.IGNORECASE +) +_HTML4_CONTENT_TYPE_RE = re.compile( + rb"""]+content\s*=\s*['"][^'"]*charset=([^\s'">;]+)""", re.IGNORECASE +) + +# PEP 263: encoding declaration in the first two lines of a Python file. +# https://peps.python.org/pep-0263/ +_PEP263_RE = re.compile(rb"^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)", re.MULTILINE) + + +def _detect_pep263(data: bytes) -> DetectionResult | None: + """Check the first two lines of *data* for a PEP 263 encoding declaration. + + PEP 263 declarations (e.g. ``# -*- coding: utf-8 -*-``) are only valid + on line 1 or line 2 of a Python source file. + + :param data: The raw byte data to scan. + :returns: A :class:`DetectionResult` with confidence 0.95, or ``None``. + """ + # PEP 263 requires a '#' comment marker on line 1 or 2. + if b"#" not in data[:200]: + return None + # Extract first two lines only. + first_two_lines = b"\n".join(data.split(b"\n", 2)[:2]) + match = _PEP263_RE.search(first_two_lines) + if match: + try: + raw_name = match.group(1).decode("ascii").strip() + except (UnicodeDecodeError, ValueError): + return None + encoding = lookup_encoding(raw_name) + if encoding is not None and _validate_bytes(data, encoding): + return DetectionResult( + encoding=encoding, + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + mime_type="text/x-python", + ) + return None + + +def detect_markup_charset(data: bytes) -> DetectionResult | None: + """Scan the first bytes of *data* for a charset declaration. + + Checks for: + + 1. ```` + 2. ```` + 3. ```` + 4. PEP 263 ``# -*- coding: ... -*-`` (first two lines only) + + :param data: The raw byte data to scan. + :returns: A :class:`DetectionResult` with confidence 0.95, or ``None``. + """ + if not data: + return None + + head = data[:_SCAN_LIMIT] + + for pattern in (_XML_ENCODING_RE, _HTML5_CHARSET_RE, _HTML4_CONTENT_TYPE_RE): + match = pattern.search(head) + if match: + try: + raw_name = match.group(1).decode("ascii").strip() + except (UnicodeDecodeError, ValueError): + continue + encoding = lookup_encoding(raw_name) + if encoding is not None and _validate_bytes(data, encoding): + mime_type = "text/xml" if pattern is _XML_ENCODING_RE else "text/html" + return DetectionResult( + encoding=encoding, + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + mime_type=mime_type, + ) + + return _detect_pep263(data) + + +def _validate_bytes(data: bytes, encoding: str) -> bool: + """Check that *data* can be decoded under *encoding* without errors. + + Only validates the first ``_SCAN_LIMIT`` bytes to avoid decoding a + full 200 kB input just to verify a charset declaration found in the + header. + """ + try: + data[:_SCAN_LIMIT].decode(encoding) + except (UnicodeDecodeError, LookupError, ValueError): + return False + return True diff --git a/libs/chardet/pipeline/orchestrator.py b/libs/chardet/pipeline/orchestrator.py new file mode 100644 index 000000000..e4623169e --- /dev/null +++ b/libs/chardet/pipeline/orchestrator.py @@ -0,0 +1,786 @@ +"""Pipeline orchestrator — runs all detection stages in sequence. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +import warnings + +from chardet._utils import DEFAULT_MAX_BYTES +from chardet.enums import EncodingEra +from chardet.models import ( + BigramProfile, + has_model_variants, + infer_language, + score_best_language, +) +from chardet.pipeline import ( + _NONE_RESULT, + DETERMINISTIC_CONFIDENCE, + HIGH_BYTES, + DetectionResult, + PipelineContext, +) +from chardet.pipeline.ascii import detect_ascii +from chardet.pipeline.binary import is_binary +from chardet.pipeline.bom import detect_bom +from chardet.pipeline.confusion import resolve_confusion_groups +from chardet.pipeline.escape import detect_escape_encoding +from chardet.pipeline.magic import detect_magic +from chardet.pipeline.markup import detect_markup_charset +from chardet.pipeline.statistical import score_candidates +from chardet.pipeline.structural import ( + compute_lead_byte_diversity, + compute_multibyte_byte_coverage, + compute_structural_score, +) +from chardet.pipeline.utf8 import detect_utf8 +from chardet.pipeline.utf1632 import detect_utf1632_patterns +from chardet.pipeline.validity import filter_by_validity +from chardet.registry import REGISTRY, EncodingInfo, get_candidates + +_BINARY_RESULT = DetectionResult( + encoding=None, + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + mime_type="application/octet-stream", +) +# Threshold at which a CJK structural score is confident enough to trigger +# combined structural+statistical ranking rather than purely statistical. +_STRUCTURAL_CONFIDENCE_THRESHOLD = 0.85 + +# Maximum bytes used for statistical bigram scoring. Bigram models +# converge quickly — 16 KB is sufficient for discrimination across all +# language models (single-byte and multi-byte alike) while avoiding +# unnecessary work on large files. Experimentally verified: 0 real +# accuracy losses across 835 test files at this threshold. +_STAT_SCORE_MAX_BYTES = 16384 + +# Common Western Latin encodings that share the iso-8859-1 character +# repertoire for the byte values where iso-8859-10 is indistinguishable. +# Used as swap targets when demoting iso-8859-10 — we prefer these over +# iso-8859-10, but do not want to accidentally promote an unrelated encoding +# (e.g. windows-1254). +_COMMON_LATIN_ENCODINGS: frozenset[str] = frozenset( + { + "iso8859-1", + "iso8859-15", + "cp1252", + } +) + +# Bytes where iso-8859-10 decodes to a different character than iso-8859-1. +# Computed programmatically via: +# {b for b in range(0x80, 0x100) +# if bytes([b]).decode('iso-8859-10') != bytes([b]).decode('iso-8859-1')} +_ISO_8859_10_DISTINGUISHING: frozenset[int] = frozenset( + { + 0xA1, + 0xA2, + 0xA3, + 0xA4, + 0xA5, + 0xA6, + 0xA8, + 0xA9, + 0xAA, + 0xAB, + 0xAC, + 0xAE, + 0xAF, + 0xB1, + 0xB2, + 0xB3, + 0xB4, + 0xB5, + 0xB6, + 0xB8, + 0xB9, + 0xBA, + 0xBB, + 0xBC, + 0xBD, + 0xBE, + 0xBF, + 0xC0, + 0xC7, + 0xC8, + 0xCA, + 0xCC, + 0xD1, + 0xD2, + 0xD7, + 0xD9, + 0xE0, + 0xE7, + 0xE8, + 0xEA, + 0xEC, + 0xF1, + 0xF2, + 0xF7, + 0xF9, + 0xFF, + } +) + +# Bytes where iso-8859-14 decodes to a different character than iso-8859-1. +# Computed programmatically via: +# {b for b in range(0x80, 0x100) +# if bytes([b]).decode('iso-8859-14') != bytes([b]).decode('iso-8859-1')} +_ISO_8859_14_DISTINGUISHING: frozenset[int] = frozenset( + { + 0xA1, + 0xA2, + 0xA4, + 0xA5, + 0xA6, + 0xA8, + 0xAA, + 0xAB, + 0xAC, + 0xAF, + 0xB0, + 0xB1, + 0xB2, + 0xB3, + 0xB4, + 0xB5, + 0xB7, + 0xB8, + 0xB9, + 0xBA, + 0xBB, + 0xBC, + 0xBD, + 0xBE, + 0xBF, + 0xD0, + 0xD7, + 0xDE, + 0xF0, + 0xF7, + 0xFE, + } +) + +# Bytes where windows-1254 has Turkish-specific characters that differ from +# windows-1252. Windows-1254 differs from windows-1252 at 8 byte positions. +# Two (0x8E, 0x9E) are undefined in Windows-1254 but defined in Windows-1252; +# these are excluded here because undefined bytes are not useful for +# identifying Turkish text. The remaining six positions map to +# Turkish-specific letters and are the primary distinguishing signal. +_WINDOWS_1254_DISTINGUISHING: frozenset[int] = frozenset( + {0xD0, 0xDD, 0xDE, 0xF0, 0xFD, 0xFE} +) + +# Encodings that are often false positives when their distinguishing bytes +# are absent. Keyed by encoding name -> frozenset of byte values where +# that encoding differs from iso-8859-1 (or windows-1252 in the case of +# windows-1254). +# Bytes where HP-Roman8 maps to lowercase accented letters but ISO-8859-1 +# maps to uppercase letters. Real HP-Roman8 text (from HP-UX terminals) +# contains these bytes; data misdetected as HP-Roman8 typically does not. +# {b for b in range(0x80, 0x100) +# if (unicodedata.category(bytes([b]).decode('hp-roman8')) == 'Ll' +# and unicodedata.category(bytes([b]).decode('iso-8859-1')) == 'Lu')} +_HP_ROMAN8_DISTINGUISHING: frozenset[int] = frozenset( + { + 0xC0, + 0xC1, + 0xC2, + 0xC3, + 0xC4, + 0xC5, + 0xC6, + 0xC7, + 0xC8, + 0xC9, + 0xCA, + 0xCB, + 0xCC, + 0xCD, + 0xCE, + 0xCF, + 0xD1, + 0xD4, + 0xD5, + 0xD6, + 0xD9, + 0xDD, + 0xDE, + } +) + +_DEMOTION_CANDIDATES: dict[str, frozenset[int]] = { + "iso8859-10": _ISO_8859_10_DISTINGUISHING, + "iso8859-14": _ISO_8859_14_DISTINGUISHING, + "cp1254": _WINDOWS_1254_DISTINGUISHING, + "hp-roman8": _HP_ROMAN8_DISTINGUISHING, +} + +# Bytes where KOI8-T maps to Tajik-specific Cyrillic letters but KOI8-R +# maps to box-drawing characters. Presence of any of these bytes is strong +# evidence for KOI8-T over KOI8-R. +_KOI8_T_DISTINGUISHING: frozenset[int] = frozenset( + {0x80, 0x81, 0x83, 0x8A, 0x8C, 0x8D, 0x8E, 0x90, 0xA1, 0xA2, 0xA5, 0xB5} +) + + +# Markup charset declarations that commonly refer to a Windows superset +# encoding rather than the strict standard encoding. Japanese web content +# almost universally declares "Shift_JIS" but actually uses CP932 extensions; +# similarly, Korean web content declares "EUC-KR" but uses CP949/UHC. +# When the declared encoding resolves to the base (left), we check whether +# the superset (right) is a better structural match. +_MARKUP_SUPERSET_PROMOTIONS: dict[str, str] = { + "shift_jis_2004": "cp932", + "euc_kr": "cp949", +} + + +def _try_promote_markup_superset( + data: bytes, + markup_result: DetectionResult, + allowed: frozenset[str], +) -> DetectionResult: + """Promote a markup-declared encoding to its superset when structural evidence supports it. + + If the declared encoding has a known superset, the superset validates the + data, and the superset's structural score is materially better, return a + new result using the superset encoding. Otherwise return the original. + """ + if markup_result.encoding is None: + return markup_result + superset_name = _MARKUP_SUPERSET_PROMOTIONS.get(markup_result.encoding) + if superset_name is None or superset_name not in allowed: + return markup_result + superset_info = REGISTRY[superset_name] + # Validate: superset must be able to decode the data + try: + data.decode(superset_name, errors="strict") + except (UnicodeDecodeError, LookupError): + return markup_result + # Compare structural scores + ctx = PipelineContext() + base_score = compute_structural_score(data, REGISTRY[markup_result.encoding], ctx) + superset_score = compute_structural_score(data, superset_info, ctx) + if superset_score > base_score: + return DetectionResult( + superset_name, + markup_result.confidence, + markup_result.language, + markup_result.mime_type, + ) + return markup_result + + +def _make_fallback_or_none( + encoding: str, + allowed: frozenset[str], + param_name: str, +) -> list[DetectionResult]: + """Return a low-confidence result for *encoding*, or ``encoding=None`` if filtered out. + + ``stacklevel=5`` targets the public caller: + detect() -> run_pipeline() -> _run_pipeline_core() -> _make_fallback_or_none(). + """ + if encoding not in allowed: + warnings.warn( + f"{param_name} {encoding!r} is excluded by " + f"include_encodings/exclude_encodings; returning encoding=None", + UserWarning, + stacklevel=5, + ) + return [_NONE_RESULT] + return [DetectionResult(encoding=encoding, confidence=0.10, language=None)] + + +def _should_demote(encoding: str, data: bytes) -> bool: + """Return True if encoding is a demotion candidate with no distinguishing bytes. + + Checks whether any non-ASCII byte in *data* falls in the set of byte + values that decode differently under the given encoding vs iso-8859-1. + If none do, the data is equally valid under both encodings and there is + no byte-level evidence for preferring the candidate encoding. + """ + distinguishing = _DEMOTION_CANDIDATES.get(encoding) + if distinguishing is None: + return False + return not any(b in distinguishing for b in data if b > 0x7F) + + +# Minimum structural score (valid multi-byte sequences / lead bytes) required +# to keep a CJK multi-byte candidate. Below this threshold the encoding is +# eliminated as a false positive (e.g. Shift_JIS matching Latin data where +# scattered high bytes look like lead bytes but rarely form valid pairs). +_CJK_MIN_MB_RATIO = 0.05 +# Minimum number of non-ASCII bytes required for a CJK candidate to survive +# gating. Very short inputs are validated by the other gates (structural +# pair ratio, byte coverage) and by coverage-aware boosting in statistical +# scoring — so we keep this threshold low to let even 1-character CJK +# inputs compete. +_CJK_MIN_NON_ASCII = 2 +# Minimum ratio of non-ASCII bytes that must participate in valid multi-byte +# sequences for a CJK candidate to survive gating. Genuine CJK text has +# nearly all non-ASCII bytes in valid pairs (coverage >= 0.95); Latin text +# with scattered high bytes has many orphan bytes (coverage often < 0.5). +# The lowest true-positive coverage in the test suite is ~0.39 (a CP932 HTML +# file with many half-width katakana). +_CJK_MIN_BYTE_COVERAGE = 0.35 +# Minimum number of distinct lead byte values for a CJK candidate to +# survive gating. Genuine CJK text uses a wide range of lead bytes; +# European false positives cluster in a narrow band. Only applied when +# there are enough non-ASCII bytes to expect diversity (see +# _CJK_DIVERSITY_MIN_NON_ASCII). +_CJK_MIN_LEAD_DIVERSITY = 4 +# Minimum non-ASCII byte count before applying the lead diversity gate. +# Very small files (e.g. 8 non-ASCII bytes) may genuinely have low +# diversity even for real CJK text (e.g. repeated katakana). +_CJK_DIVERSITY_MIN_NON_ASCII = 16 + + +def _gate_cjk_candidates( + data: bytes, + valid_candidates: tuple[EncodingInfo, ...], + ctx: PipelineContext, +) -> tuple[EncodingInfo, ...]: + """Eliminate CJK multi-byte candidates that lack genuine multi-byte structure. + + Four checks are applied in order to each multi-byte candidate: + + 1. **Structural pair ratio** (valid_pairs / lead_bytes) must be + >= ``_CJK_MIN_MB_RATIO``. Catches files with many orphan lead bytes. + + 2. **Minimum non-ASCII byte count**: the data must contain at least + ``_CJK_MIN_NON_ASCII`` bytes > 0x7F. Tiny files with 1-5 high bytes + can accidentally form perfect pairs and score 1.0 structurally. + + 3. **Byte coverage** (non-ASCII bytes in valid multi-byte sequences / + total non-ASCII bytes) must be >= ``_CJK_MIN_BYTE_COVERAGE``. Latin + text has many high bytes that are NOT consumed by multi-byte pairs; + genuine CJK text has nearly all high bytes accounted for. + + 4. **Lead byte diversity**: the number of distinct lead byte values in + valid pairs must be >= ``_CJK_MIN_LEAD_DIVERSITY``. Genuine CJK text + draws from a wide repertoire of lead bytes; European false positives + cluster in a narrow band (e.g. 0xC0-0xDF for accented Latin). + + Returns the filtered candidate list. Structural scores are cached in + ``ctx.mb_scores`` for reuse in Stage 2b. + """ + gated: list[EncodingInfo] = [] + for enc in valid_candidates: + if enc.is_multibyte: + mb_score = compute_structural_score(data, enc, ctx) + ctx.mb_scores[enc.name] = mb_score + if mb_score < _CJK_MIN_MB_RATIO: + continue # No multi-byte structure -> eliminate + if ctx.non_ascii_count is None: + ctx.non_ascii_count = len(data) - len(data.translate(None, HIGH_BYTES)) + if ctx.non_ascii_count < _CJK_MIN_NON_ASCII: + continue # Too few high bytes to trust the score + byte_coverage = compute_multibyte_byte_coverage( + data, enc, ctx, non_ascii_count=ctx.non_ascii_count + ) + ctx.mb_coverage[enc.name] = byte_coverage + if byte_coverage < _CJK_MIN_BYTE_COVERAGE: + continue # Most high bytes are orphans -> not CJK + if ctx.non_ascii_count >= _CJK_DIVERSITY_MIN_NON_ASCII: + lead_diversity = compute_lead_byte_diversity(data, enc, ctx) + if lead_diversity < _CJK_MIN_LEAD_DIVERSITY: + continue # Too few distinct lead bytes -> not CJK + gated.append(enc) + return tuple(gated) + + +def _score_structural_candidates( + data: bytes, + structural_scores: list[tuple[str, float]], + valid_candidates: tuple[EncodingInfo, ...], + ctx: PipelineContext, +) -> list[DetectionResult]: + """Score structurally-valid CJK candidates using statistical bigrams. + + When multiple CJK encodings score equally high structurally, statistical + scoring differentiates them (e.g. euc-jp vs big5 for Japanese data). + Single-byte candidates are also scored and included so that the caller + can compare CJK vs single-byte confidence. + + Multi-byte candidates with high byte coverage (>= 0.95) receive a + confidence boost proportional to coverage. When nearly all non-ASCII + bytes form valid multi-byte pairs, the structural evidence is strong + and should increase the candidate's ranking relative to single-byte + alternatives whose bigram models may score higher on small samples. + + Note: boosted confidence values may exceed 1.0 and are used only for + relative ranking among candidates. ``run_pipeline`` clamps all + confidence values to [0.0, 1.0] before returning to callers. + """ + enc_lookup: dict[str, EncodingInfo] = { + e.name: e for e in valid_candidates if e.is_multibyte + } + valid_mb = tuple( + enc_lookup[name] for name, _sc in structural_scores if name in enc_lookup + ) + single_byte = tuple(e for e in valid_candidates if not e.is_multibyte) + results = list( + score_candidates(data[:_STAT_SCORE_MAX_BYTES], (*valid_mb, *single_byte)) + ) + + # Boost multi-byte candidates with high byte coverage. + boosted: list[DetectionResult] = [] + for r in results: + coverage = ctx.mb_coverage.get(r.encoding, 0.0) if r.encoding else 0.0 + if coverage >= 0.95: + boosted.append( + DetectionResult( + r.encoding, r.confidence * (1 + coverage), r.language, r.mime_type + ) + ) + else: + boosted.append(r) + boosted.sort(key=lambda x: x.confidence, reverse=True) + return boosted + + +def _demote_niche_latin( + data: bytes, + results: list[DetectionResult], +) -> list[DetectionResult]: + """Demote niche Latin encodings when no distinguishing bytes are present. + + Some bigram models (e.g. iso-8859-10, iso-8859-14, windows-1254) can win + on data that contains only bytes shared with common Western Latin + encodings. When there is no byte-level evidence for the winning + encoding, promote the first common Western Latin candidate to the top and + push the demoted encoding to last. + """ + if ( + len(results) > 1 + and results[0].encoding is not None + and _should_demote(results[0].encoding, data) + ): + demoted_encoding = results[0].encoding + top_conf = results[0].confidence + for r in results[1:]: + if r.encoding in _COMMON_LATIN_ENCODINGS: + promoted = DetectionResult( + r.encoding, top_conf, r.language, r.mime_type + ) + others = [ + x for x in results if x.encoding != demoted_encoding and x is not r + ] + demoted_entries = [x for x in results if x.encoding == demoted_encoding] + return [promoted, *others, *demoted_entries] + return results + + +def _promote_koi8t( + data: bytes, + results: list[DetectionResult], +) -> list[DetectionResult]: + """Promote KOI8-T over KOI8-R when Tajik-specific bytes are present. + + KOI8-T and KOI8-R share the entire 0xC0-0xFF Cyrillic letter block, + making statistical discrimination difficult. However, KOI8-T maps 12 + bytes in 0x80-0xBF to Tajik-specific Cyrillic letters where KOI8-R has + box-drawing characters. If any of these bytes appear, KOI8-T is the + better match. + """ + if not results or results[0].encoding != "koi8-r": + return results + # Check if KOI8-T is anywhere in the results + koi8t_idx = next((i for i, r in enumerate(results) if r.encoding == "koi8-t"), None) + if koi8t_idx is None: + return results + # Check for Tajik-specific bytes + if any(b in _KOI8_T_DISTINGUISHING for b in data if b > 0x7F): + koi8t_result = results[koi8t_idx] + top_conf = results[0].confidence + promoted = DetectionResult( + koi8t_result.encoding, + top_conf, + koi8t_result.language, + koi8t_result.mime_type, + ) + others = [r for i, r in enumerate(results) if i != koi8t_idx] + return [promoted, *others] + return results + + +# Maximum bytes of data used for language scoring in _fill_language. +# Language bigrams converge quickly — 2 KB is sufficient for discrimination +# across all language models while keeping Tier 3 (language-model scoring) fast. +_LANG_SCORE_MAX_BYTES = 2048 + + +def _to_utf8(data: bytes, encoding: str) -> bytes | None: + """Decode data from encoding and re-encode as UTF-8 for language scoring. + + Returns None if the encoding is unknown. For UTF-8, returns data as-is. + Uses ``errors="ignore"`` because the data already passed byte-validity + filtering for the detected encoding; any residual invalid bytes are + irrelevant for language scoring. + """ + if encoding == "utf-8": + return data + try: + return data.decode(encoding, errors="ignore").encode( + "utf-8", errors="surrogatepass" + ) + except (LookupError, TypeError, ValueError): + return None + + +def _fill_metadata( + data: bytes, results: list[DetectionResult] +) -> list[DetectionResult]: + """Fill in language and mime_type for results missing them. + + **Language** (only for text results where ``encoding is not None``): + + Tier 1: single-language encodings via hardcoded map (instant). + Tier 2: multi-language encodings via statistical bigram scoring (lazy). + Tier 3: decode to UTF-8, score against UTF-8 language models (universal fallback). + + **MIME type**: text results default to ``"text/plain"``, binary results + (``encoding is None``) default to ``"application/octet-stream"``. + """ + filled: list[DetectionResult] = [] + profile: BigramProfile | None = None + utf8_profile: BigramProfile | None = None + for result in results: + lang = result.language + if lang is None and result.encoding is not None: + # Tier 1: single-language encoding + lang = infer_language(result.encoding) + # Tier 2: statistical scoring for multi-language encodings + if lang is None and data and has_model_variants(result.encoding): + if profile is None: + profile = BigramProfile(data) + _, lang = score_best_language(data, result.encoding, profile=profile) + # Tier 3: decode to UTF-8, score against UTF-8 language models + if lang is None and data and has_model_variants("utf-8"): + utf8_data = _to_utf8(data, result.encoding) + if utf8_data: + if utf8_profile is None or result.encoding != "utf-8": + utf8_profile = BigramProfile(utf8_data) + _, lang = score_best_language( + utf8_data, "utf-8", profile=utf8_profile + ) + + mime = result.mime_type + if mime is None: + mime = ( + "text/plain" + if result.encoding is not None + else "application/octet-stream" + ) + + if lang != result.language or mime != result.mime_type: + filled.append( + DetectionResult(result.encoding, result.confidence, lang, mime) + ) + else: + filled.append(result) + return filled + + +def _postprocess_results( + data: bytes, + results: list[DetectionResult], +) -> list[DetectionResult]: + """Apply confusion resolution, niche Latin demotion, and KOI8-T promotion.""" + results = resolve_confusion_groups(data, results) + results = _demote_niche_latin(data, results) + return _promote_koi8t(data, results) + + +def _run_pipeline_core( # noqa: PLR0913 + data: bytes, + encoding_era: EncodingEra, + max_bytes: int = DEFAULT_MAX_BYTES, + *, + include_encodings: frozenset[str] | None = None, + exclude_encodings: frozenset[str] | None = None, + no_match_encoding: str = "cp1252", + empty_input_encoding: str = "utf-8", +) -> list[DetectionResult]: + """Core pipeline logic. Returns list of results sorted by confidence.""" + ctx = PipelineContext() + data = data[:max_bytes] + + # Build candidate set once — used for both early-exit gating and + # statistical scoring. The set incorporates encoding_era, include, and + # exclude filters so all pipeline stages are gated consistently. + candidates = get_candidates(encoding_era, include_encodings, exclude_encodings) + allowed: frozenset[str] = frozenset(enc.name for enc in candidates) + + if not data: + return _make_fallback_or_none( + empty_input_encoding, allowed, "empty_input_encoding" + ) + + # Stage 1a: BOM detection (runs first — BOMs are definitive and + # UTF-16/32 data looks binary due to null bytes) + bom_result = detect_bom(data) + if bom_result is not None and bom_result.encoding in allowed: + return [bom_result] + + # Stage 1a+: UTF-16/32 null-byte pattern detection (for files without + # BOMs — must run before binary detection since these encodings contain + # many null bytes that would trigger the binary check) + utf1632_result = detect_utf1632_patterns(data) + if utf1632_result is not None and utf1632_result.encoding in allowed: + return [utf1632_result] + + # Escape-sequence encodings (ISO-2022, HZ-GB-2312, UTF-7): must run + # before binary detection (ESC is a control byte) and before ASCII + # detection (HZ-GB-2312 uses only printable ASCII plus tildes). + escape_result = detect_escape_encoding(data) + if ( + escape_result is not None + and escape_result.encoding is not None + and escape_result.encoding in allowed + ): + return [escape_result] + + # Magic number detection for known binary formats — runs before + # UTF-8/ASCII prechecks to avoid unnecessary analysis on binary data. + magic_result = detect_magic(data) + if magic_result is not None: + return [magic_result] + + # Pre-check UTF-8 to prevent false binary classification. Valid UTF-8 + # with multi-byte sequences can contain control bytes (e.g. ESC for ANSI + # codes) that would otherwise exceed the binary threshold. We compute + # the result now but return it at the normal pipeline position (after + # markup) so that explicit charset declarations still take precedence. + utf8_precheck = detect_utf8(data) + + # Pre-check ASCII to prevent false binary classification. ASCII text + # with null byte separators (e.g. find -print0 output) would exceed the + # binary threshold due to the null bytes. Like the UTF-8 precheck, we + # compute the result now but return it at the normal position (after + # markup) so explicit charset declarations still take precedence. + ascii_precheck = detect_ascii(data) + + # Stage 0: Binary detection (skip when data is valid UTF-8 or ASCII) + # Binary detection (encoding=None) is NOT gated by filters. + if ( + utf8_precheck is None + and ascii_precheck is None + and is_binary(data, max_bytes=max_bytes) + ): + return [_BINARY_RESULT] + + # Stage 1b: Markup charset extraction (before ASCII/UTF-8 so explicit + # declarations like are honoured even + # when the bytes happen to be pure ASCII or valid UTF-8). + markup_result = detect_markup_charset(data) + if markup_result is not None and markup_result.encoding in allowed: + markup_result = _try_promote_markup_superset(data, markup_result, allowed) + return [markup_result] + + # Stage 1c: ASCII (use pre-computed result) + if ascii_precheck is not None and ascii_precheck.encoding in allowed: + return [ascii_precheck] + + # Stage 1d: UTF-8 structural validation (use pre-computed result) + if utf8_precheck is not None and utf8_precheck.encoding in allowed: + return [utf8_precheck] + + # Stage 2a: Byte validity filtering + valid_candidates = filter_by_validity(data, candidates) + + if not valid_candidates: + return _make_fallback_or_none(no_match_encoding, allowed, "no_match_encoding") + + # Gate: eliminate CJK multi-byte candidates that lack genuine + # multi-byte structure. Cache structural scores for Stage 2b. + valid_candidates = _gate_cjk_candidates(data, valid_candidates, ctx) + + if not valid_candidates: + return _make_fallback_or_none(no_match_encoding, allowed, "no_match_encoding") + + # Stage 2b: Structural probing for multi-byte encodings + # Reuse scores already computed during the CJK gate above. + structural_scores: list[tuple[str, float]] = [] + for enc in valid_candidates: + if enc.is_multibyte: + score = ctx.mb_scores.get(enc.name) + if score is None: # pragma: no cover - gate always populates cache + score = compute_structural_score(data, enc, ctx) + if score > 0.0: + structural_scores.append((enc.name, score)) + + # If a multi-byte encoding scored very high, score all candidates + # (CJK + single-byte) statistically. + if structural_scores: + structural_scores.sort(key=lambda x: x[1], reverse=True) + _, best_score = structural_scores[0] + if best_score >= _STRUCTURAL_CONFIDENCE_THRESHOLD: + results = _score_structural_candidates( + data, structural_scores, valid_candidates, ctx + ) + if results: + return _postprocess_results(data, results) + + # Stage 3: Statistical scoring for all remaining candidates. + # Bigram models converge quickly and don't benefit from scanning + # beyond 16 KB — cap the data to avoid unnecessary work on large files. + stat_data = data[:_STAT_SCORE_MAX_BYTES] + results = list(score_candidates(stat_data, tuple(valid_candidates))) + if not results: + return _make_fallback_or_none(no_match_encoding, allowed, "no_match_encoding") + + return _postprocess_results(data, results) + + +def run_pipeline( # noqa: PLR0913 + data: bytes, + encoding_era: EncodingEra, + max_bytes: int = DEFAULT_MAX_BYTES, + *, + include_encodings: frozenset[str] | None = None, + exclude_encodings: frozenset[str] | None = None, + no_match_encoding: str = "cp1252", + empty_input_encoding: str = "utf-8", +) -> list[DetectionResult]: + """Run the full detection pipeline. + + :param data: The raw byte data to analyze. + :param encoding_era: Filter candidates to a specific era of encodings. + :param max_bytes: Maximum number of bytes to process. + :param include_encodings: If not ``None``, only return these encodings. + :param exclude_encodings: If not ``None``, never return these encodings. + :param no_match_encoding: Encoding returned when no candidate survives. + :param empty_input_encoding: Encoding returned for empty input. + :returns: A list of :class:`DetectionResult` sorted by confidence descending. + """ + results = _run_pipeline_core( + data, + encoding_era, + max_bytes, + include_encodings=include_encodings, + exclude_encodings=exclude_encodings, + no_match_encoding=no_match_encoding, + empty_input_encoding=empty_input_encoding, + ) + # Language scoring uses only the first 2 KB — bigrams converge quickly + # and this keeps Tier 3 (language-model scoring) fast even on large inputs. + results = _fill_metadata(data[:_LANG_SCORE_MAX_BYTES], results) + if not results: # pragma: no cover + msg = "pipeline must always return at least one result" + raise RuntimeError(msg) + # Clamp confidence to [0.0, 1.0] at the public API boundary. Internal + # stages may boost confidence above 1.0 for ranking purposes (e.g. + # CJK byte-coverage boost), but callers expect a probability-like value. + return [ + DetectionResult(r.encoding, min(r.confidence, 1.0), r.language, r.mime_type) + if r.confidence > 1.0 + else r + for r in results + ] diff --git a/libs/chardet/pipeline/statistical.py b/libs/chardet/pipeline/statistical.py new file mode 100644 index 000000000..065611d54 --- /dev/null +++ b/libs/chardet/pipeline/statistical.py @@ -0,0 +1,37 @@ +"""Stage 3: Statistical bigram scoring. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from chardet.models import BigramProfile, score_best_language +from chardet.pipeline import DetectionResult +from chardet.registry import EncodingInfo + + +def score_candidates( + data: bytes, candidates: tuple[EncodingInfo, ...] +) -> list[DetectionResult]: + """Score all candidates and return results sorted by confidence descending. + + :param data: The raw byte data to score. + :param candidates: Encoding candidates to evaluate. + :returns: A list of :class:`DetectionResult` sorted by confidence. + """ + if not data or not candidates: + return [] + + profile = BigramProfile(data) + scores: list[tuple[str, float, str | None]] = [] + + for enc in candidates: + s, lang = score_best_language(data, enc.name, profile=profile) + if s > 0.0: + scores.append((enc.name, s, lang)) + + scores.sort(key=lambda x: x[1], reverse=True) + return [ + DetectionResult(encoding=name, confidence=s, language=lang) + for name, s, lang in scores + ] diff --git a/libs/chardet/pipeline/structural.py b/libs/chardet/pipeline/structural.py new file mode 100644 index 000000000..3c2609c8a --- /dev/null +++ b/libs/chardet/pipeline/structural.py @@ -0,0 +1,529 @@ +"""Stage 2b: Multi-byte structural probing. + +Computes how well byte patterns in the data match the expected multi-byte +structure for a given encoding. Used after byte-validity filtering (Stage 2a) +to further rank multi-byte encoding candidates. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from collections.abc import Callable + +from chardet.pipeline import HIGH_BYTES, PipelineContext +from chardet.registry import EncodingInfo + +# --------------------------------------------------------------------------- +# Per-encoding single-pass analyzers +# +# Each function walks the data once, computing three metrics simultaneously: +# - pair_ratio: valid multi-byte pairs / lead bytes (structural score) +# - mb_bytes: count of non-ASCII bytes in valid multi-byte sequences +# - lead_diversity: count of distinct lead byte values in valid pairs +# +# These are kept as separate functions (rather than a single parameterized +# analyzer) so that mypyc can inline the byte-range constants into each +# function's tight loop. +# --------------------------------------------------------------------------- + + +def _analyze_shift_jis( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass Shift_JIS structural analysis. + + Lead bytes: 0x81-0x9F, 0xE0-0xEF + Trail bytes: 0x40-0x7E, 0x80-0xFC + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if (0x81 <= b <= 0x9F) or (0xE0 <= b <= 0xEF): + lead_count += 1 + if i + 1 < length: + trail = data[i + 1] + if (0x40 <= trail <= 0x7E) or (0x80 <= trail <= 0xFC): + valid_count += 1 + leads.add(b) + # Lead is always > 0x7F; trail may or may not be + mb += 1 + if trail > 0x7F: + mb += 1 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_cp932( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass CP932 structural analysis. + + Lead bytes: 0x81-0x9F, 0xE0-0xFC + Trail bytes: 0x40-0x7E, 0x80-0xFC + + Extends Shift_JIS by raising the lead byte ceiling from 0xEF to 0xFC, + covering IBM vendor-defined characters (NEC-selected, IBM extensions). + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if (0x81 <= b <= 0x9F) or (0xE0 <= b <= 0xFC): + lead_count += 1 + if i + 1 < length: + trail = data[i + 1] + if (0x40 <= trail <= 0x7E) or (0x80 <= trail <= 0xFC): + valid_count += 1 + leads.add(b) + # Lead is always > 0x7F; trail may or may not be + mb += 1 + if trail > 0x7F: + mb += 1 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_euc_jp( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass EUC-JP structural analysis. + + Two-byte: Lead 0xA1-0xFE, Trail 0xA1-0xFE + SS2 (half-width katakana): 0x8E + 0xA1-0xDF + SS3 (JIS X 0212): 0x8F + 0xA1-0xFE + 0xA1-0xFE + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if b == 0x8E: + # SS2 sequence + lead_count += 1 + if i + 1 < length and 0xA1 <= data[i + 1] <= 0xDF: + valid_count += 1 + leads.add(b) + mb += 2 + i += 2 + continue + i += 1 + elif b == 0x8F: + # SS3 sequence + lead_count += 1 + if ( + i + 2 < length + and 0xA1 <= data[i + 1] <= 0xFE + and 0xA1 <= data[i + 2] <= 0xFE + ): + valid_count += 1 + leads.add(b) + mb += 3 + i += 3 + continue + i += 1 + elif 0xA1 <= b <= 0xFE: + lead_count += 1 + if i + 1 < length and 0xA1 <= data[i + 1] <= 0xFE: + valid_count += 1 + leads.add(b) + mb += 2 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_euc_kr( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass EUC-KR structural analysis. + + Lead 0xA1-0xFE; Trail 0xA1-0xFE + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if 0xA1 <= b <= 0xFE: + lead_count += 1 + if i + 1 < length and 0xA1 <= data[i + 1] <= 0xFE: + valid_count += 1 + leads.add(b) + mb += 2 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_cp949( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass CP949 (Unified Hangul Code) structural analysis. + + Lead bytes: 0x81-0xC8, 0xCA-0xFD + Trail bytes: 0x41-0x5A, 0x61-0x7A, 0x81-0xFE + + Extends EUC-KR by lowering the lead byte floor from 0xA1 to 0x81 and + adding ASCII letter trail ranges plus 0x81-0xA0. 0xC9 is not a valid + UHC lead byte. + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if (0x81 <= b <= 0xC8) or (0xCA <= b <= 0xFD): + lead_count += 1 + if i + 1 < length: + trail = data[i + 1] + if ( + (0x41 <= trail <= 0x5A) + or (0x61 <= trail <= 0x7A) + or (0x81 <= trail <= 0xFE) + ): + valid_count += 1 + leads.add(b) + # Lead is always > 0x7F; trail may or may not be + mb += 1 + if trail > 0x7F: + mb += 1 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_gb18030( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass GB18030 / GB2312 structural analysis. + + Only counts strict GB2312 2-byte pairs (lead 0xA1-0xF7, trail 0xA1-0xFE) + and GB18030 4-byte sequences. The broader GBK extension range + (lead 0x81-0xFE, trail 0x40-0x7E / 0x80-0xFE) is intentionally excluded + because it is so permissive that unrelated single-byte data (EBCDIC, DOS + codepages, etc.) can score 1.0, leading to false positives. + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if 0x81 <= b <= 0xFE: + lead_count += 1 + # Try 4-byte first (byte2 in 0x30-0x39 distinguishes from 2-byte) + if ( + i + 3 < length + and 0x30 <= data[i + 1] <= 0x39 + and 0x81 <= data[i + 2] <= 0xFE + and 0x30 <= data[i + 3] <= 0x39 + ): + valid_count += 1 + leads.add(b) + mb += 2 # bytes 0 and 2 are non-ASCII + i += 4 + continue + # 2-byte GB2312: Lead 0xA1-0xF7, Trail 0xA1-0xFE + if 0xA1 <= b <= 0xF7 and i + 1 < length and 0xA1 <= data[i + 1] <= 0xFE: + valid_count += 1 + leads.add(b) + mb += 2 # both bytes are > 0x7F + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_big5( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass Big5 structural analysis. + + Lead 0xA1-0xF9; Trail 0x40-0x7E, 0xA1-0xFE + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if 0xA1 <= b <= 0xF9: + lead_count += 1 + if i + 1 < length: + trail = data[i + 1] + if (0x40 <= trail <= 0x7E) or (0xA1 <= trail <= 0xFE): + valid_count += 1 + leads.add(b) + # Lead is always > 0x7F; trail may or may not be + mb += 1 + if trail > 0x7F: + mb += 1 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_big5hkscs( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass Big5-HKSCS structural analysis. + + Lead bytes: 0x87-0xFE + Trail bytes: 0x40-0x7E, 0xA1-0xFE + + Extends Big5 by lowering the lead byte floor from 0xA1 to 0x87 and + raising the ceiling from 0xF9 to 0xFE. 0x7F and 0x80-0xA0 are not + valid Big5/HKSCS trail bytes. + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if 0x87 <= b <= 0xFE: + lead_count += 1 + if i + 1 < length: + trail = data[i + 1] + if (0x40 <= trail <= 0x7E) or (0xA1 <= trail <= 0xFE): + valid_count += 1 + leads.add(b) + # Lead is always > 0x7F; trail may or may not be + mb += 1 + if trail > 0x7F: + mb += 1 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +def _analyze_johab( + data: bytes, +) -> tuple[float, int, int]: + """Single-pass Johab structural analysis. + + Lead: 0x84-0xD3, 0xD8-0xDE, 0xE0-0xF9 + Trail: 0x31-0x7E, 0x91-0xFE + + Returns (pair_ratio, mb_bytes, lead_diversity). + """ + lead_count = 0 + valid_count = 0 + mb = 0 + leads: set[int] = set() + i = 0 + length = len(data) + while i < length: + b = data[i] + if (0x84 <= b <= 0xD3) or (0xD8 <= b <= 0xDE) or (0xE0 <= b <= 0xF9): + lead_count += 1 + if i + 1 < length: + trail = data[i + 1] + if (0x31 <= trail <= 0x7E) or (0x91 <= trail <= 0xFE): + valid_count += 1 + leads.add(b) + if b > 0x7F: + mb += 1 + if trail > 0x7F: + mb += 1 + i += 2 + continue + i += 1 + else: + i += 1 + ratio = valid_count / lead_count if lead_count > 0 else 0.0 + return ratio, mb, len(leads) + + +# --------------------------------------------------------------------------- +# Dispatch table: encoding name -> analyzer function +# --------------------------------------------------------------------------- + +_ANALYZERS: dict[str, Callable[[bytes], tuple[float, int, int]]] = { + "shift_jis_2004": _analyze_shift_jis, + "cp932": _analyze_cp932, + "euc_jis_2004": _analyze_euc_jp, + "euc_kr": _analyze_euc_kr, + "cp949": _analyze_cp949, + "gb18030": _analyze_gb18030, + "big5hkscs": _analyze_big5hkscs, + "johab": _analyze_johab, +} + + +def _get_analysis( + data: bytes, name: str, ctx: PipelineContext +) -> tuple[float, int, int] | None: + """Return cached analysis or compute and cache it.""" + cached = ctx.analysis_cache.get(name) + if cached is not None: + return cached + analyzer = _ANALYZERS.get(name) + if analyzer is None: + return None + result = analyzer(data) + ctx.analysis_cache[name] = result + return result + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def compute_structural_score( + data: bytes, encoding_info: EncodingInfo, ctx: PipelineContext +) -> float: + """Return 0.0--1.0 indicating how well *data* matches the encoding's structure. + + For single-byte encodings, always returns 0.0. For empty data, always + returns 0.0. + + :param data: The raw byte data to analyze. + :param encoding_info: Metadata for the encoding to probe. + :param ctx: Pipeline context for caching analysis results. + :returns: A structural fit score between 0.0 and 1.0. + """ + if not data or not encoding_info.is_multibyte: + return 0.0 + + result = _get_analysis(data, encoding_info.name, ctx) + if result is None: + return 0.0 + + return result[0] # pair_ratio + + +def compute_multibyte_byte_coverage( + data: bytes, + encoding_info: EncodingInfo, + ctx: PipelineContext, + non_ascii_count: int | None = None, +) -> float: + """Ratio of non-ASCII bytes that participate in valid multi-byte sequences. + + Genuine CJK text has nearly all non-ASCII bytes paired into valid + multi-byte sequences (coverage close to 1.0), while Latin text with + scattered high bytes has many orphan bytes (coverage well below 1.0). + + :param data: The raw byte data to analyze. + :param encoding_info: Metadata for the encoding to probe. + :param ctx: Pipeline context for caching analysis results. + :param non_ascii_count: Pre-computed count of non-ASCII bytes, or ``None`` + to compute from *data*. + :returns: A coverage ratio between 0.0 and 1.0. + """ + if not data or not encoding_info.is_multibyte: + return 0.0 + + result = _get_analysis(data, encoding_info.name, ctx) + if result is None: + return 0.0 + + mb_bytes = result[1] + + non_ascii = ( + non_ascii_count + if non_ascii_count is not None + else len(data) - len(data.translate(None, HIGH_BYTES)) + ) + if non_ascii == 0: + return 0.0 + + return mb_bytes / non_ascii + + +def compute_lead_byte_diversity( + data: bytes, encoding_info: EncodingInfo, ctx: PipelineContext +) -> int: + """Count distinct lead byte values in valid multi-byte pairs. + + Genuine CJK text uses lead bytes from across the encoding's full + repertoire. European text falsely matching a CJK structural scorer + clusters lead bytes in a narrow band. + + :param data: The raw byte data to analyze. + :param encoding_info: Metadata for the encoding to probe. + :param ctx: Pipeline context for caching analysis results. + :returns: The number of distinct lead byte values found. + """ + if not data or not encoding_info.is_multibyte: + return 0 + result = _get_analysis(data, encoding_info.name, ctx) + if result is None: + return 256 # Unknown encoding -- don't gate + return result[2] # lead_diversity diff --git a/libs/chardet/pipeline/utf1632.py b/libs/chardet/pipeline/utf1632.py new file mode 100644 index 000000000..c28f0bd37 --- /dev/null +++ b/libs/chardet/pipeline/utf1632.py @@ -0,0 +1,302 @@ +"""Stage 1a+: UTF-16/UTF-32 detection for data without BOM. + +This stage runs after BOM detection but before binary detection. +UTF-16 and UTF-32 encoded text contains characteristic null-byte patterns +that would otherwise cause binary detection to reject the data. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +import unicodedata + +from chardet.pipeline import ASCII_TEXT_BYTES, DETERMINISTIC_CONFIDENCE, DetectionResult + +# How many bytes to sample for pattern analysis +_SAMPLE_SIZE = 4096 + +# Minimum bytes needed for reliable pattern detection +_MIN_BYTES_UTF32 = 16 # 4 full code units +_MIN_BYTES_UTF16 = 10 # 5 full code units + +# Minimum fraction of null bytes in the expected position for UTF-16. +# CJK-heavy UTF-16 text (Chinese, Japanese, Korean) can have as few as +# ~4.5% null bytes in the expected position, since CJK codepoints have +# non-zero high bytes. The validation step (decode + text quality check) +# prevents false positives from binary files at this lower threshold. +_UTF16_MIN_NULL_FRACTION = 0.03 + +# Minimum text-quality score to accept a UTF-16 candidate when both +# endiannesses show null-byte patterns. A score of 0.5 corresponds to +# roughly 50% letters with no ASCII bonus (or ~40% with whitespace +# present) — sufficient to distinguish real text from coincidental byte +# patterns. +_MIN_TEXT_QUALITY = 0.5 + +# Minimum fraction of printable characters for a decoded sample to be +# considered text rather than binary data. +_MIN_PRINTABLE_FRACTION = 0.7 + +# Maximum null fraction (in the candidate null-byte position) below which +# the data is checked for a null-separator pattern. If the null fraction +# is below this AND all non-null bytes are printable ASCII, the candidate +# is rejected as a null-separator false positive rather than real UTF-16. +# Real Latin UTF-16 has ~50% nulls; CJK UTF-16 has fewer but non-ASCII +# non-null bytes. 15% is generous — separator data is typically 1-5%. +_NULL_SEPARATOR_MAX_FRACTION = 0.15 + +# ASCII_TEXT_BYTES plus the null byte — used by the null-separator guard +# to check whether non-null bytes are all printable ASCII. +_NULL_SEPARATOR_ALLOWED: bytes = b"\x00" + ASCII_TEXT_BYTES + + +def _is_null_separator_pattern(data: bytes, null_frac: float) -> bool: + """Return True if the data looks like ASCII with null byte separators. + + :param data: The raw byte sample to examine. + :param null_frac: The positional null fraction for this UTF-16 candidate + (i.e. fraction of null bytes in even positions for BE, or odd positions + for LE) — not the total null fraction across all bytes. + + Checks two conditions: + 1. The positional null fraction is below ``_NULL_SEPARATOR_MAX_FRACTION`` + 2. Every non-null byte is printable ASCII or common whitespace + + When both conditions are met, the nulls are likely field separators + (e.g. ``find -print0``), not UTF-16 encoding artifacts. + """ + if null_frac >= _NULL_SEPARATOR_MAX_FRACTION: + return False + return not data.translate(None, _NULL_SEPARATOR_ALLOWED) + + +def detect_utf1632_patterns(data: bytes) -> DetectionResult | None: + """Detect UTF-32 or UTF-16 encoding from null-byte patterns. + + UTF-32 is checked before UTF-16 since UTF-32 patterns are more specific. + + :param data: The raw byte data to examine. + :returns: A :class:`DetectionResult` if a strong pattern is found, or ``None``. + """ + sample = data[:_SAMPLE_SIZE] + + if len(sample) < _MIN_BYTES_UTF16: + return None + + # Check UTF-32 first (more specific pattern) + result = _check_utf32(sample) + if result is not None: + return result + + # Then check UTF-16 + return _check_utf16(sample) + + +def _check_utf32(data: bytes) -> DetectionResult | None: + """Check for UTF-32 encoding based on 4-byte unit structure. + + For valid Unicode (U+0000 to U+10FFFF = 0x0010FFFF): + - UTF-32-BE: the first byte of each 4-byte unit is always 0x00 + - UTF-32-LE: the last byte of each 4-byte unit is always 0x00 + + For BMP characters (U+0000 to U+FFFF), additionally: + - UTF-32-BE: the second byte is also 0x00 + - UTF-32-LE: the third byte is also 0x00 + """ + # Trim to a multiple of 4 bytes (like _check_utf16 trims to even length) + trimmed_len = len(data) - (len(data) % 4) + if trimmed_len < _MIN_BYTES_UTF32: + return None + data = data[:trimmed_len] + + num_units = trimmed_len // 4 + + # UTF-32-BE: first byte of each 4-byte unit must be 0x00 + be_first_null = sum(1 for i in range(0, len(data), 4) if data[i] == 0) + # Second byte is 0x00 for BMP characters (the vast majority of text) + be_second_null = sum(1 for i in range(0, len(data), 4) if data[i + 1] == 0) + + if be_first_null == num_units and be_second_null / num_units > 0.5: + try: + text = data.decode("utf-32-be") + if _looks_like_text(text): + return DetectionResult( + encoding="utf-32-be", + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + ) + except UnicodeDecodeError: + pass + + # UTF-32-LE: last byte of each 4-byte unit must be 0x00 + le_last_null = sum(1 for i in range(3, len(data), 4) if data[i] == 0) + # Third byte is 0x00 for BMP characters + le_third_null = sum(1 for i in range(2, len(data), 4) if data[i] == 0) + + if le_last_null == num_units and le_third_null / num_units > 0.5: + try: + text = data.decode("utf-32-le") + if _looks_like_text(text): + return DetectionResult( + encoding="utf-32-le", + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + ) + except UnicodeDecodeError: + pass + + return None + + +def _check_utf16(data: bytes) -> DetectionResult | None: + """Check for UTF-16 via null-byte patterns in alternating positions. + + UTF-16 encodes each BMP character as two bytes. For characters whose + code-point high byte is 0x00 (Latin, digits, basic punctuation, many + control structures), one of the two bytes in each unit will be a null. + Even for non-Latin scripts (Arabic, CJK, Cyrillic, etc.) a significant + fraction of code units still contain at least one null byte. + + Non-UTF-16 single-byte encodings never contain null bytes, so even a + small null-byte fraction in alternating positions is a strong signal. + + When both endiannesses show null-byte patterns (e.g., Latin text where + every other byte is null), we disambiguate by decoding both ways and + comparing text-quality scores. + """ + sample_len = min(len(data), _SAMPLE_SIZE) + sample_len -= sample_len % 2 + if sample_len < _MIN_BYTES_UTF16: # pragma: no cover - caller checks length + return None + + num_units = sample_len // 2 + + # Count null bytes in even positions (UTF-16-BE high byte for ASCII) + be_null_count = sum(1 for i in range(0, sample_len, 2) if data[i] == 0) + # Count null bytes in odd positions (UTF-16-LE high byte for ASCII) + le_null_count = sum(1 for i in range(1, sample_len, 2) if data[i] == 0) + + be_frac = be_null_count / num_units + le_frac = le_null_count / num_units + + candidates: list[tuple[str, float]] = [] + if le_frac >= _UTF16_MIN_NULL_FRACTION and not _is_null_separator_pattern( + data[:sample_len], le_frac + ): + candidates.append(("utf-16-le", le_frac)) + if be_frac >= _UTF16_MIN_NULL_FRACTION and not _is_null_separator_pattern( + data[:sample_len], be_frac + ): + candidates.append(("utf-16-be", be_frac)) + + if not candidates: + return None + + # If only one candidate, validate and return + if len(candidates) == 1: + encoding = candidates[0][0] + try: + text = data[:sample_len].decode(encoding) + if _looks_like_text(text): + return DetectionResult( + encoding=encoding, + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + ) + except UnicodeDecodeError: + pass + return None + + # Both candidates matched (common for Latin-heavy text where every other + # byte is null). Decode both and pick the one with higher text quality. + best_encoding: str | None = None + best_quality = -1.0 + + for encoding, _ in candidates: + try: + text = data[:sample_len].decode(encoding) + except UnicodeDecodeError: + continue + quality = _text_quality(text) + if quality > best_quality: + best_quality = quality + best_encoding = encoding + + if best_encoding is not None and best_quality >= _MIN_TEXT_QUALITY: + return DetectionResult( + encoding=best_encoding, + confidence=DETERMINISTIC_CONFIDENCE, + language=None, + ) + + return None + + +def _looks_like_text(text: str) -> bool: + """Quick check: is decoded text mostly printable characters.""" + if not text: + return False + sample = text[:500] + printable = sum(1 for c in sample if c.isprintable() or c in "\n\r\t") + return printable / len(sample) > _MIN_PRINTABLE_FRACTION + + +def _text_quality(text: str, limit: int = 500) -> float: + """Score how much *text* looks like real human-readable content. + + Returns a score in the range [-1.0, ~1.6). Higher values indicate + more natural text. The practical maximum is 1.5 for all-ASCII-letter + input (1.6 approaches as sample size grows with all ASCII letters plus + whitespace). A score of -1.0 means the content is almost certainly not + valid text (too many control characters or combining marks). + + Scoring factors: + + * Base score: ratio of Unicode letters (category ``L*``) to sample length. + * ASCII bonus: additional 0.5x weight for ASCII letters. This is the + primary signal for disambiguating endianness — correct decoding of + Latin-heavy text produces ASCII letters, wrong decoding produces CJK. + * Space bonus: +0.1 when the sample contains at least one whitespace + character and is longer than 20 characters. + * Rejection: returns -1.0 if >10% control characters or >20% combining + marks (category ``M*``). + """ + sample = text[:limit] + n = len(sample) + if n == 0: # pragma: no cover - callers always pass non-empty text + return -1.0 + + letters = 0 + marks = 0 + spaces = 0 + controls = 0 + ascii_letters = 0 + + for c in sample: + cat = unicodedata.category(c) + if cat[0] == "L": + letters += 1 + if ord(c) < 128: + ascii_letters += 1 + elif cat[0] == "M": + marks += 1 + elif cat == "Zs" or c in "\n\r\t": + spaces += 1 + elif cat[0] == "C": + controls += 1 + + # Reject data with many control characters or combining marks + if controls / n > 0.1: + return -1.0 + if marks / n > 0.2: + return -1.0 + + score = letters / n + # ASCII letters strongly indicate correct endianness + score += (ascii_letters / n) * 0.5 + # Real text usually contains some whitespace + if n > 20 and spaces > 0: + score += 0.1 + + return score diff --git a/libs/chardet/pipeline/utf8.py b/libs/chardet/pipeline/utf8.py new file mode 100644 index 000000000..9233c81ba --- /dev/null +++ b/libs/chardet/pipeline/utf8.py @@ -0,0 +1,97 @@ +"""Stage 1d: UTF-8 structural validation. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from chardet.pipeline import DetectionResult + +# Confidence curve parameters for UTF-8 detection. +# Even a small fraction of valid multi-byte sequences is strong evidence. +_BASE_CONFIDENCE = 0.80 +_MAX_CONFIDENCE = 0.99 +# Scale factor for the multi-byte byte ratio: mb_ratio * 6 saturates the +# confidence ramp at ~17% multi-byte content. +_MB_RATIO_SCALE = 6 + + +def detect_utf8(data: bytes) -> DetectionResult | None: + """Validate UTF-8 byte structure. + + Returns a result only if multi-byte sequences are found (pure ASCII + is handled by the ASCII stage). + + :param data: The raw byte data to examine. + :returns: A :class:`DetectionResult` for UTF-8, or ``None``. + """ + if not data: + return None + + i = 0 + length = len(data) + multibyte_sequences = 0 + multibyte_bytes = 0 + + while i < length: + byte = data[i] + + if byte < 0x80: + i += 1 + continue + + # Determine expected sequence length from leading byte. + # 0xC0-0xC1 are overlong 2-byte encodings of ASCII, so we start at 0xC2. + if 0xC2 <= byte <= 0xDF: + seq_len = 2 + elif 0xE0 <= byte <= 0xEF: + seq_len = 3 + elif 0xF0 <= byte <= 0xF4: + seq_len = 4 + else: + # Invalid start byte (0x80-0xC1, 0xF5-0xFF) + return None + + # Truncated final sequence (e.g. from max_bytes slicing) — treat as + # valid since the bytes seen so far are structurally correct. + if i + seq_len > length: + break + + # Validate continuation bytes (must be 0x80-0xBF) + for j in range(1, seq_len): + if not (0x80 <= data[i + j] <= 0xBF): + return None + + # Reject overlong encodings and surrogates + if seq_len == 3: + # 0xE0: second byte must be >= 0xA0 (prevents overlong 3-byte) + if byte == 0xE0 and data[i + 1] < 0xA0: + return None + # 0xED: second byte must be <= 0x9F (prevents UTF-16 surrogates U+D800-U+DFFF) + if byte == 0xED and data[i + 1] > 0x9F: + return None + elif seq_len == 4: + # 0xF0: second byte must be >= 0x90 (prevents overlong 4-byte) + if byte == 0xF0 and data[i + 1] < 0x90: + return None + # 0xF4: second byte must be <= 0x8F (prevents codepoints above U+10FFFF) + if byte == 0xF4 and data[i + 1] > 0x8F: + return None + + multibyte_sequences += 1 + multibyte_bytes += seq_len + i += seq_len + + # Pure ASCII — let the ASCII detector handle it + if multibyte_sequences == 0: + return None + + # Confidence scales with the proportion of multi-byte bytes in the data. + # Even a small amount of valid multi-byte UTF-8 is strong evidence. + mb_ratio = multibyte_bytes / length + confidence_range = _MAX_CONFIDENCE - _BASE_CONFIDENCE + confidence = min( + _MAX_CONFIDENCE, + _BASE_CONFIDENCE + confidence_range * min(mb_ratio * _MB_RATIO_SCALE, 1.0), + ) + return DetectionResult(encoding="utf-8", confidence=confidence, language=None) diff --git a/libs/chardet/pipeline/validity.py b/libs/chardet/pipeline/validity.py new file mode 100644 index 000000000..5ba64a18e --- /dev/null +++ b/libs/chardet/pipeline/validity.py @@ -0,0 +1,30 @@ +"""Stage 2a: Byte sequence validity filtering. + +Note: ``from __future__ import annotations`` is intentionally omitted because +this module is compiled with mypyc, which does not support PEP 563 string +annotations. +""" + +from chardet.registry import EncodingInfo + + +def filter_by_validity( + data: bytes, candidates: tuple[EncodingInfo, ...] +) -> tuple[EncodingInfo, ...]: + """Filter candidates to only those where *data* decodes without errors. + + :param data: The raw byte data to test. + :param candidates: Encoding candidates to validate. + :returns: The subset of *candidates* that can decode *data*. + """ + if not data: + return candidates + + valid = [] + for enc in candidates: + try: + data.decode(enc.name, errors="strict") + valid.append(enc) + except (UnicodeDecodeError, LookupError): + continue + return tuple(valid) diff --git a/libs/chardet/registry.py b/libs/chardet/registry.py new file mode 100644 index 000000000..74a2cbcf7 --- /dev/null +++ b/libs/chardet/registry.py @@ -0,0 +1,1035 @@ +"""Encoding registry with metadata for all supported encodings.""" + +from __future__ import annotations + +import codecs +import dataclasses +import functools +from collections.abc import Iterable +from types import MappingProxyType +from typing import Literal + +from chardet.enums import EncodingEra + +EncodingName = Literal[ + "ascii", + "big5hkscs", + "cp1006", + "cp1026", + "cp1125", + "cp1140", + "cp1250", + "cp1251", + "cp1252", + "cp1253", + "cp1254", + "cp1255", + "cp1256", + "cp1257", + "cp1258", + "cp273", + "cp424", + "cp437", + "cp500", + "cp720", + "cp737", + "cp775", + "cp850", + "cp852", + "cp855", + "cp856", + "cp857", + "cp858", + "cp860", + "cp861", + "cp862", + "cp863", + "cp864", + "cp865", + "cp866", + "cp869", + "cp874", + "cp875", + "cp932", + "cp949", + "euc_jis_2004", + "euc_kr", + "gb18030", + "hp-roman8", + "hz", + "iso2022_jp_2", + "iso2022_jp_2004", + "iso2022_jp_ext", + "iso2022_kr", + "iso8859-1", + "iso8859-10", + "iso8859-13", + "iso8859-14", + "iso8859-15", + "iso8859-16", + "iso8859-2", + "iso8859-3", + "iso8859-4", + "iso8859-5", + "iso8859-6", + "iso8859-7", + "iso8859-8", + "iso8859-9", + "johab", + "koi8-r", + "koi8-t", + "koi8-u", + "kz1048", + "mac-cyrillic", + "mac-greek", + "mac-iceland", + "mac-latin2", + "mac-roman", + "mac-turkish", + "ptcp154", + "shift_jis_2004", + "tis-620", + "utf-16", + "utf-16-be", + "utf-16-le", + "utf-32", + "utf-32-be", + "utf-32-le", + "utf-7", + "utf-8", + "utf-8-sig", +] + +# Shared language tuples — used by multiple EncodingInfo entries below. +_WESTERN = ( + "br", + "cy", + "da", + "de", + "en", + "es", + "fi", + "fr", + "ga", + "id", + "is", + "it", + "ms", + "nl", + "no", + "pt", + "sv", +) +_WESTERN_TR = (*_WESTERN, "tr") +_CYRILLIC = ("ru", "bg", "uk", "sr", "mk", "be") +_CENTRAL_EU = ("pl", "cs", "hu", "hr", "ro", "sk", "sl") +_CENTRAL_EU_NO_RO = ("pl", "cs", "hu", "hr", "sk", "sl") +_BALTIC = ("et", "lt", "lv") +_ARABIC = ("ar", "fa") + + +@dataclasses.dataclass(frozen=True, slots=True) +class EncodingInfo: + """Metadata for a single encoding.""" + + name: EncodingName + aliases: tuple[str, ...] + era: EncodingEra + is_multibyte: bool + languages: tuple[str, ...] + + +@functools.lru_cache(maxsize=256) +def get_candidates( + era: EncodingEra, + include_encodings: frozenset[str] | None = None, + exclude_encodings: frozenset[str] | None = None, +) -> tuple[EncodingInfo, ...]: + """Return registry entries matching the given filters. + + Filters are applied in order: era, include, exclude. + + :param era: Bit flags specifying which encoding eras to include. + :param include_encodings: If not ``None``, only return encodings in this set. + :param exclude_encodings: If not ``None``, exclude encodings in this set. + :returns: A tuple of matching :class:`EncodingInfo` entries. + """ + candidates = (enc for enc in REGISTRY.values() if enc.era & era) + if include_encodings is not None: + candidates = (enc for enc in candidates if enc.name in include_encodings) + if exclude_encodings is not None: + candidates = (enc for enc in candidates if enc.name not in exclude_encodings) + return tuple(candidates) + + +# Era assignments match chardet 6.0.0's chardet/metadata/charsets.py + +_REGISTRY_ENTRIES = ( + # === MODERN_WEB === + EncodingInfo( + name="ascii", + aliases=("us-ascii",), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-8", + aliases=( + "utf-8", + "utf8", + "csutf8", + "unicode-1-1-utf-8", + "unicode11utf8", + "unicode20utf8", + "x-unicode20utf8", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-8-sig", + aliases=("UTF-8-SIG", "utf-8-bom"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-16", + aliases=("UTF-16", "utf16", "csutf16"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-16-be", + aliases=("UTF-16-BE", "utf-16be", "csutf16be"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-16-le", + aliases=("UTF-16-LE", "utf-16le", "csutf16le"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-32", + aliases=("UTF-32", "utf32", "csutf32"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-32-be", + aliases=("UTF-32-BE", "utf-32be", "csutf32be"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-32-le", + aliases=("UTF-32-LE", "utf-32le", "csutf32le"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(), + ), + EncodingInfo( + name="utf-7", + aliases=("UTF-7", "utf7", "csutf7"), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=(), + ), + # CJK - Modern Web + EncodingInfo( + name="big5hkscs", + aliases=( + "Big5-HKSCS", + "Big5HKSCS", + "big5", + "big5-tw", + "csbig5", + "cp950", + "cn-big5", + "x-x-big5", + "csbig5hkscs", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("zh",), + ), + EncodingInfo( + name="cp932", + aliases=( + "CP932", + "ms932", + "mskanji", + "ms-kanji", + "cswindows31j", + "windows-31j", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ja",), + ), + EncodingInfo( + # Note: "korean" is NOT an alias here. Python's codec table + # already resolves "korean" to ``euc_kr``, and WHATWG's primary + # name for the group is EUC-KR, so letting the default fall + # through is more spec-aligned than routing to cp949. + name="cp949", + aliases=( + "CP949", + "ms949", + "uhc", + "windows-949", + "csksc56011987", + "iso-ir-149", + "ks_c_5601-1987", + "ks_c_5601-1989", + "ksc5601", + "ksc_5601", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ko",), + ), + EncodingInfo( + name="euc_jis_2004", + aliases=( + "EUC-JIS-2004", + "euc-jp", + "eucjp", + "ujis", + "u-jis", + "euc-jisx0213", + "cseucpkdfmtjapanese", + "x-euc-jp", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ja",), + ), + EncodingInfo( + name="euc_kr", + aliases=("EUC-KR", "euckr", "cseuckr"), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ko",), + ), + EncodingInfo( + # Note: "chinese" is NOT listed here because the label is + # semantically ambiguous between Traditional (Big5) and Simplified + # (GB18030) Chinese. Python's codec table still resolves + # "chinese" -> "gb2312" -> (via the gb2312 alias below) gb18030, + # so the label continues to work for Simplified content via the + # codec fallback in lookup_encoding(). We simply decline to + # bless the ambiguity in our own table. + name="gb18030", + aliases=( + "GB18030", + "gb-18030", + "gb2312", + "gbk", + "csgb2312", + "gb_2312", + "gb_2312-80", + "x-gbk", + "csiso58gb231280", + "iso-ir-58", + "csgb18030", + "csgbk", + "cp936", + "ms936", + "windows-936", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("zh",), + ), + EncodingInfo( + name="hz", + aliases=("HZ-GB-2312", "hz"), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=True, + languages=("zh",), + ), + EncodingInfo( + name="iso2022_jp_2", + aliases=( + "ISO-2022-JP-2", + "iso-2022-jp", + "csiso2022jp", + "iso2022-jp-1", + "csiso2022jp2", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ja",), + ), + EncodingInfo( + name="iso2022_jp_2004", + aliases=("ISO-2022-JP-2004", "iso2022-jp-3"), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ja",), + ), + EncodingInfo( + name="iso2022_jp_ext", + aliases=("ISO-2022-JP-EXT",), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ja",), + ), + EncodingInfo( + name="iso2022_kr", + aliases=("ISO-2022-KR", "csiso2022kr"), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=True, + languages=("ko",), + ), + EncodingInfo( + name="shift_jis_2004", + aliases=( + "Shift-JIS-2004", + "Shift_JIS_2004", + "shift_jis", + "sjis", + "shiftjis", + "s_jis", + "shift-jisx0213", + "x-sjis", + "csshiftjis", + "ms_kanji", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=True, + languages=("ja",), + ), + # Windows code pages - Modern Web + EncodingInfo( + name="cp874", + aliases=("CP874", "windows-874", "dos-874"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("th",), + ), + EncodingInfo( + name="cp1250", + aliases=("Windows-1250", "cp1250", "x-cp1250", "cswindows1250"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=(*_CENTRAL_EU, "sr"), + ), + EncodingInfo( + name="cp1251", + aliases=("Windows-1251", "cp1251", "x-cp1251", "cswindows1251"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=_CYRILLIC, + ), + EncodingInfo( + name="cp1252", + aliases=("Windows-1252", "cp1252", "x-cp1252", "cswindows1252"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="cp1253", + aliases=("Windows-1253", "cp1253", "x-cp1253", "cswindows1253"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("el",), + ), + EncodingInfo( + name="cp1254", + aliases=("Windows-1254", "cp1254", "x-cp1254", "cswindows1254"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("tr",), + ), + EncodingInfo( + name="cp1255", + aliases=("Windows-1255", "cp1255", "x-cp1255", "cswindows1255"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("he",), + ), + EncodingInfo( + name="cp1256", + aliases=("Windows-1256", "cp1256", "x-cp1256", "cswindows1256"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=_ARABIC, + ), + EncodingInfo( + name="cp1257", + aliases=("Windows-1257", "cp1257", "x-cp1257", "cswindows1257"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=_BALTIC, + ), + EncodingInfo( + name="cp1258", + aliases=("Windows-1258", "cp1258", "x-cp1258", "cswindows1258"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("vi",), + ), + # KOI8 - Modern Web + EncodingInfo( + name="koi8-r", + aliases=("KOI8-R", "koi8r", "koi", "koi8", "cskoi8r"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("ru",), + ), + EncodingInfo( + name="koi8-u", + aliases=("KOI8-U", "koi8u", "koi8-ru", "cskoi8u"), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("uk",), + ), + # TIS-620 - Modern Web + EncodingInfo( + name="tis-620", + aliases=( + "TIS-620", + "tis620", + "iso-8859-11", + "iso8859-11", + "iso885911", + "cstis620", + ), + era=EncodingEra.MODERN_WEB, + is_multibyte=False, + languages=("th",), + ), + # === LEGACY_ISO === + EncodingInfo( + name="iso8859-1", + aliases=("ISO-8859-1", "latin-1", "latin1", "iso8859-1", "iso88591"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="iso8859-2", + aliases=("ISO-8859-2", "latin-2", "latin2", "iso8859-2", "iso88592"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_CENTRAL_EU, + ), + EncodingInfo( + name="iso8859-3", + aliases=("ISO-8859-3", "latin-3", "latin3", "iso8859-3", "iso88593"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("eo", "mt", "tr"), + ), + EncodingInfo( + name="iso8859-4", + aliases=("ISO-8859-4", "latin-4", "latin4", "iso8859-4", "iso88594"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_BALTIC, + ), + EncodingInfo( + name="iso8859-5", + aliases=("ISO-8859-5", "iso8859-5", "cyrillic", "iso88595"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_CYRILLIC, + ), + EncodingInfo( + # The -E ("explicit directionality") and -I ("implicit + # directionality") variants listed by IANA and WHATWG are + # higher-level bidi-ordering hints, not separate codecs -- Python + # stdlib has no distinct decoder for them, so all four resolve to + # the same ``iso8859-6`` canonical here. + name="iso8859-6", + aliases=( + "ISO-8859-6", + "iso8859-6", + "arabic", + "iso88596", + "iso-8859-6-e", + "iso-8859-6-i", + "csiso88596e", + "csiso88596i", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_ARABIC, + ), + EncodingInfo( + name="iso8859-7", + aliases=( + "ISO-8859-7", + "iso8859-7", + "greek", + "iso88597", + "sun_eu_greek", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("el",), + ), + EncodingInfo( + # WHATWG and IANA distinguish ISO-8859-8 ("visual", -E) from + # ISO-8859-8-I ("logical", -I) to signal Hebrew bidi ordering, but + # Python's stdlib has a single ``iso8859-8`` codec -- the bidi + # distinction is a higher-layer concern. All variants collapse + # onto this one canonical. + name="iso8859-8", + aliases=( + "ISO-8859-8", + "iso8859-8", + "hebrew", + "iso88598", + "iso-8859-8-e", + "iso-8859-8-i", + "csiso88598e", + "csiso88598i", + "visual", + "logical", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("he",), + ), + EncodingInfo( + name="iso8859-9", + aliases=("ISO-8859-9", "latin-5", "latin5", "iso8859-9", "iso88599"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("tr",), + ), + EncodingInfo( + name="iso8859-10", + aliases=("ISO-8859-10", "latin-6", "latin6", "iso8859-10", "iso885910"), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("is", "fi"), + ), + EncodingInfo( + name="iso8859-13", + aliases=( + "ISO-8859-13", + "latin-7", + "latin7", + "iso8859-13", + "iso885913", + "csiso885913", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_BALTIC, + ), + EncodingInfo( + # ``iso-celtic`` is a Python stdlib alias, not a WHATWG/IANA name. + name="iso8859-14", + aliases=( + "ISO-8859-14", + "latin-8", + "latin8", + "iso8859-14", + "iso885914", + "csiso885914", + "iso-ir-199", + "iso-celtic", + "l8", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("cy", "ga", "br", "gd"), + ), + EncodingInfo( + name="iso8859-15", + aliases=( + "ISO-8859-15", + "latin-9", + "latin9", + "iso8859-15", + "iso885915", + "csisolatin9", + "csiso885915", + "l9", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="iso8859-16", + aliases=( + "ISO-8859-16", + "latin-10", + "latin10", + "iso8859-16", + "iso885916", + "csiso885916", + "iso-ir-226", + "l10", + ), + era=EncodingEra.LEGACY_ISO, + is_multibyte=False, + languages=("ro", "pl", "hr", "hu", "sk", "sl"), + ), + # Johab - Legacy ISO per chardet 6.0.0 + EncodingInfo( + name="johab", + aliases=("Johab",), + era=EncodingEra.LEGACY_ISO, + is_multibyte=True, + languages=("ko",), + ), + # === LEGACY_MAC === + EncodingInfo( + name="mac-cyrillic", + aliases=( + "Mac-Cyrillic", + "MacCyrillic", + "maccyrillic", + "x-mac-cyrillic", + "x-mac-ukrainian", + ), + era=EncodingEra.LEGACY_MAC, + is_multibyte=False, + languages=_CYRILLIC, + ), + EncodingInfo( + name="mac-greek", + aliases=("Mac-Greek", "MacGreek", "macgreek"), + era=EncodingEra.LEGACY_MAC, + is_multibyte=False, + languages=("el",), + ), + EncodingInfo( + name="mac-iceland", + aliases=("Mac-Iceland", "MacIceland", "maciceland"), + era=EncodingEra.LEGACY_MAC, + is_multibyte=False, + languages=("is",), + ), + EncodingInfo( + name="mac-latin2", + aliases=("Mac-Latin2", "MacLatin2", "maclatin2", "maccentraleurope"), + era=EncodingEra.LEGACY_MAC, + is_multibyte=False, + languages=_CENTRAL_EU_NO_RO, + ), + EncodingInfo( + name="mac-roman", + aliases=( + "Mac-Roman", + "MacRoman", + "macroman", + "macintosh", + "csmacintosh", + "mac", + "x-mac-roman", + ), + era=EncodingEra.LEGACY_MAC, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="mac-turkish", + aliases=("Mac-Turkish", "MacTurkish", "macturkish"), + era=EncodingEra.LEGACY_MAC, + is_multibyte=False, + languages=("tr",), + ), + # === LEGACY_REGIONAL === + EncodingInfo( + name="cp720", + aliases=("CP720",), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=_ARABIC, + ), + EncodingInfo( + name="cp1006", + aliases=("CP1006",), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=("ur",), + ), + EncodingInfo( + name="cp1125", + aliases=("CP1125",), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=("uk",), + ), + EncodingInfo( + name="koi8-t", + aliases=("KOI8-T",), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=("tg",), + ), + EncodingInfo( + name="kz1048", + aliases=("KZ-1048", "kz1048", "strk1048-2002", "rk1048"), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=("kk",), + ), + EncodingInfo( + name="ptcp154", + aliases=("PTCP154", "pt154", "cp154"), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=("kk",), + ), + EncodingInfo( + name="hp-roman8", + aliases=("HP-Roman8", "roman8", "r8", "csHPRoman8"), + era=EncodingEra.LEGACY_REGIONAL, + is_multibyte=False, + languages=_WESTERN, + ), + # === DOS === + EncodingInfo( + name="cp437", + aliases=("CP437",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("en", "fr", "de", "es", "pt", "it", "nl", "da", "sv", "fi", "ga"), + ), + EncodingInfo( + name="cp737", + aliases=("CP737",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("el",), + ), + EncodingInfo( + name="cp775", + aliases=("CP775",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=_BALTIC, + ), + EncodingInfo( + name="cp850", + aliases=("CP850",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="cp852", + aliases=("CP852",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=_CENTRAL_EU, + ), + EncodingInfo( + name="cp855", + aliases=("CP855",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=_CYRILLIC, + ), + EncodingInfo( + name="cp856", + aliases=("CP856",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("he",), + ), + EncodingInfo( + name="cp857", + aliases=("CP857",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("tr",), + ), + EncodingInfo( + name="cp858", + aliases=("CP858",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="cp860", + aliases=("CP860",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("pt",), + ), + EncodingInfo( + name="cp861", + aliases=("CP861",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("is",), + ), + EncodingInfo( + name="cp862", + aliases=("CP862",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("he",), + ), + EncodingInfo( + name="cp863", + aliases=("CP863",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("fr",), + ), + EncodingInfo( + name="cp864", + aliases=("CP864",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("ar",), + ), + EncodingInfo( + name="cp865", + aliases=("CP865",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("da", "no"), + ), + EncodingInfo( + name="cp866", + aliases=("CP866",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=_CYRILLIC, + ), + EncodingInfo( + name="cp869", + aliases=("CP869",), + era=EncodingEra.DOS, + is_multibyte=False, + languages=("el",), + ), + # === MAINFRAME === + EncodingInfo( + name="cp1140", + aliases=( + "CP1140", + "cp037", + "cp01140", + "ibm01140", + "ibm1140", + "csibm01140", + ), + era=EncodingEra.MAINFRAME, + is_multibyte=False, + languages=_WESTERN_TR, + ), + EncodingInfo( + name="cp424", + aliases=("CP424",), + era=EncodingEra.MAINFRAME, + is_multibyte=False, + languages=("he",), + ), + EncodingInfo( + name="cp500", + aliases=("CP500",), + era=EncodingEra.MAINFRAME, + is_multibyte=False, + languages=_WESTERN, + ), + EncodingInfo( + name="cp875", + aliases=("CP875",), + era=EncodingEra.MAINFRAME, + is_multibyte=False, + languages=("el",), + ), + EncodingInfo( + name="cp1026", + aliases=("CP1026",), + era=EncodingEra.MAINFRAME, + is_multibyte=False, + languages=("tr",), + ), + EncodingInfo( + name="cp273", + aliases=("CP273",), + era=EncodingEra.MAINFRAME, + is_multibyte=False, + languages=("de",), + ), +) + +REGISTRY: MappingProxyType[str, EncodingInfo] = MappingProxyType( + {e.name: e for e in _REGISTRY_ENTRIES} +) + + +@functools.cache +def lookup_encoding(name: str) -> EncodingName | None: + """Convert an encoding name string to the canonical EncodingName. + + Handles arbitrary casing, aliases, and Python codec names. + + :param name: Any encoding name string. + :returns: The canonical :data:`EncodingName`, or ``None`` if unknown. + """ + lowered = name.lower() + for entry in REGISTRY.values(): + if entry.name == lowered: + return entry.name + for alias in entry.aliases: + if alias.lower() == lowered: + return entry.name + # Fallback: resolve through Python's codec registry + try: + codec_name = codecs.lookup(name).name + except (LookupError, ValueError): + return None + if codec_name != lowered: + return lookup_encoding(codec_name) + return None + + +def _validate_encoding(name: str, param_name: str) -> str: + """Validate and normalize a single encoding name. + + :param name: The encoding name to validate. + :param param_name: Parameter name for error messages. + :returns: The canonical encoding name. + :raises ValueError: If the encoding name is unknown. + """ + canonical = lookup_encoding(name) + if canonical is None: + msg = f"Unknown encoding {name!r} in {param_name}" + raise ValueError(msg) + return canonical + + +def normalize_encodings( + encodings: Iterable[str] | None, + param_name: str, +) -> frozenset[str] | None: + """Normalize an iterable of encoding names to canonical forms. + + :param encodings: Encoding names to normalize, or ``None``. + :param param_name: Parameter name for error messages. + :returns: A frozenset of canonical encoding names, or ``None``. + :raises ValueError: If any encoding name is unknown. + """ + if encodings is None: + return None + result = frozenset(_validate_encoding(name, param_name) for name in encodings) + if not result: + msg = f"{param_name} must not be empty; omit the argument or pass None to disable filtering" + raise ValueError(msg) + return result diff --git a/libs/chardet/resultdict.py b/libs/chardet/resultdict.py deleted file mode 100644 index 7d36e64c4..000000000 --- a/libs/chardet/resultdict.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -if TYPE_CHECKING: - # TypedDict was introduced in Python 3.8. - # - # TODO: Remove the else block and TYPE_CHECKING check when dropping support - # for Python 3.7. - from typing import TypedDict - - class ResultDict(TypedDict): - encoding: Optional[str] - confidence: float - language: Optional[str] - -else: - ResultDict = dict diff --git a/libs/chardet/sbcharsetprober.py b/libs/chardet/sbcharsetprober.py deleted file mode 100644 index 0ffbcdd2c..000000000 --- a/libs/chardet/sbcharsetprober.py +++ /dev/null @@ -1,162 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Dict, List, NamedTuple, Optional, Union - -from .charsetprober import CharSetProber -from .enums import CharacterCategory, ProbingState, SequenceLikelihood - - -class SingleByteCharSetModel(NamedTuple): - charset_name: str - language: str - char_to_order_map: Dict[int, int] - language_model: Dict[int, Dict[int, int]] - typical_positive_ratio: float - keep_ascii_letters: bool - alphabet: str - - -class SingleByteCharSetProber(CharSetProber): - SAMPLE_SIZE = 64 - SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2 - POSITIVE_SHORTCUT_THRESHOLD = 0.95 - NEGATIVE_SHORTCUT_THRESHOLD = 0.05 - - def __init__( - self, - model: SingleByteCharSetModel, - is_reversed: bool = False, - name_prober: Optional[CharSetProber] = None, - ) -> None: - super().__init__() - self._model = model - # TRUE if we need to reverse every pair in the model lookup - self._reversed = is_reversed - # Optional auxiliary prober for name decision - self._name_prober = name_prober - self._last_order = 255 - self._seq_counters: List[int] = [] - self._total_seqs = 0 - self._total_char = 0 - self._control_char = 0 - self._freq_char = 0 - self.reset() - - def reset(self) -> None: - super().reset() - # char order of last character - self._last_order = 255 - self._seq_counters = [0] * SequenceLikelihood.get_num_categories() - self._total_seqs = 0 - self._total_char = 0 - self._control_char = 0 - # characters that fall in our sampling range - self._freq_char = 0 - - @property - def charset_name(self) -> Optional[str]: - if self._name_prober: - return self._name_prober.charset_name - return self._model.charset_name - - @property - def language(self) -> Optional[str]: - if self._name_prober: - return self._name_prober.language - return self._model.language - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - # TODO: Make filter_international_words keep things in self.alphabet - if not self._model.keep_ascii_letters: - byte_str = self.filter_international_words(byte_str) - else: - byte_str = self.remove_xml_tags(byte_str) - if not byte_str: - return self.state - char_to_order_map = self._model.char_to_order_map - language_model = self._model.language_model - for char in byte_str: - order = char_to_order_map.get(char, CharacterCategory.UNDEFINED) - # XXX: This was SYMBOL_CAT_ORDER before, with a value of 250, but - # CharacterCategory.SYMBOL is actually 253, so we use CONTROL - # to make it closer to the original intent. The only difference - # is whether or not we count digits and control characters for - # _total_char purposes. - if order < CharacterCategory.CONTROL: - self._total_char += 1 - if order < self.SAMPLE_SIZE: - self._freq_char += 1 - if self._last_order < self.SAMPLE_SIZE: - self._total_seqs += 1 - if not self._reversed: - lm_cat = language_model[self._last_order][order] - else: - lm_cat = language_model[order][self._last_order] - self._seq_counters[lm_cat] += 1 - self._last_order = order - - charset_name = self._model.charset_name - if self.state == ProbingState.DETECTING: - if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD: - confidence = self.get_confidence() - if confidence > self.POSITIVE_SHORTCUT_THRESHOLD: - self.logger.debug( - "%s confidence = %s, we have a winner", charset_name, confidence - ) - self._state = ProbingState.FOUND_IT - elif confidence < self.NEGATIVE_SHORTCUT_THRESHOLD: - self.logger.debug( - "%s confidence = %s, below negative shortcut threshold %s", - charset_name, - confidence, - self.NEGATIVE_SHORTCUT_THRESHOLD, - ) - self._state = ProbingState.NOT_ME - - return self.state - - def get_confidence(self) -> float: - r = 0.01 - if self._total_seqs > 0: - r = ( - ( - self._seq_counters[SequenceLikelihood.POSITIVE] - + 0.25 * self._seq_counters[SequenceLikelihood.LIKELY] - ) - / self._total_seqs - / self._model.typical_positive_ratio - ) - # The more control characters (proportionnaly to the size - # of the text), the less confident we become in the current - # charset. - r = r * (self._total_char - self._control_char) / self._total_char - r = r * self._freq_char / self._total_char - if r >= 1.0: - r = 0.99 - return r diff --git a/libs/chardet/sbcsgroupprober.py b/libs/chardet/sbcsgroupprober.py deleted file mode 100644 index 890ae8465..000000000 --- a/libs/chardet/sbcsgroupprober.py +++ /dev/null @@ -1,88 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from .charsetgroupprober import CharSetGroupProber -from .hebrewprober import HebrewProber -from .langbulgarianmodel import ISO_8859_5_BULGARIAN_MODEL, WINDOWS_1251_BULGARIAN_MODEL -from .langgreekmodel import ISO_8859_7_GREEK_MODEL, WINDOWS_1253_GREEK_MODEL -from .langhebrewmodel import WINDOWS_1255_HEBREW_MODEL - -# from .langhungarianmodel import (ISO_8859_2_HUNGARIAN_MODEL, -# WINDOWS_1250_HUNGARIAN_MODEL) -from .langrussianmodel import ( - IBM855_RUSSIAN_MODEL, - IBM866_RUSSIAN_MODEL, - ISO_8859_5_RUSSIAN_MODEL, - KOI8_R_RUSSIAN_MODEL, - MACCYRILLIC_RUSSIAN_MODEL, - WINDOWS_1251_RUSSIAN_MODEL, -) -from .langthaimodel import TIS_620_THAI_MODEL -from .langturkishmodel import ISO_8859_9_TURKISH_MODEL -from .sbcharsetprober import SingleByteCharSetProber - - -class SBCSGroupProber(CharSetGroupProber): - def __init__(self) -> None: - super().__init__() - hebrew_prober = HebrewProber() - logical_hebrew_prober = SingleByteCharSetProber( - WINDOWS_1255_HEBREW_MODEL, is_reversed=False, name_prober=hebrew_prober - ) - # TODO: See if using ISO-8859-8 Hebrew model works better here, since - # it's actually the visual one - visual_hebrew_prober = SingleByteCharSetProber( - WINDOWS_1255_HEBREW_MODEL, is_reversed=True, name_prober=hebrew_prober - ) - hebrew_prober.set_model_probers(logical_hebrew_prober, visual_hebrew_prober) - # TODO: ORDER MATTERS HERE. I changed the order vs what was in master - # and several tests failed that did not before. Some thought - # should be put into the ordering, and we should consider making - # order not matter here, because that is very counter-intuitive. - self.probers = [ - SingleByteCharSetProber(WINDOWS_1251_RUSSIAN_MODEL), - SingleByteCharSetProber(KOI8_R_RUSSIAN_MODEL), - SingleByteCharSetProber(ISO_8859_5_RUSSIAN_MODEL), - SingleByteCharSetProber(MACCYRILLIC_RUSSIAN_MODEL), - SingleByteCharSetProber(IBM866_RUSSIAN_MODEL), - SingleByteCharSetProber(IBM855_RUSSIAN_MODEL), - SingleByteCharSetProber(ISO_8859_7_GREEK_MODEL), - SingleByteCharSetProber(WINDOWS_1253_GREEK_MODEL), - SingleByteCharSetProber(ISO_8859_5_BULGARIAN_MODEL), - SingleByteCharSetProber(WINDOWS_1251_BULGARIAN_MODEL), - # TODO: Restore Hungarian encodings (iso-8859-2 and windows-1250) - # after we retrain model. - # SingleByteCharSetProber(ISO_8859_2_HUNGARIAN_MODEL), - # SingleByteCharSetProber(WINDOWS_1250_HUNGARIAN_MODEL), - SingleByteCharSetProber(TIS_620_THAI_MODEL), - SingleByteCharSetProber(ISO_8859_9_TURKISH_MODEL), - hebrew_prober, - logical_hebrew_prober, - visual_hebrew_prober, - ] - self.reset() diff --git a/libs/chardet/sjisprober.py b/libs/chardet/sjisprober.py deleted file mode 100644 index 91df07796..000000000 --- a/libs/chardet/sjisprober.py +++ /dev/null @@ -1,105 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Union - -from .chardistribution import SJISDistributionAnalysis -from .codingstatemachine import CodingStateMachine -from .enums import MachineState, ProbingState -from .jpcntx import SJISContextAnalysis -from .mbcharsetprober import MultiByteCharSetProber -from .mbcssm import SJIS_SM_MODEL - - -class SJISProber(MultiByteCharSetProber): - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(SJIS_SM_MODEL) - self.distribution_analyzer = SJISDistributionAnalysis() - self.context_analyzer = SJISContextAnalysis() - self.reset() - - def reset(self) -> None: - super().reset() - self.context_analyzer.reset() - - @property - def charset_name(self) -> str: - return self.context_analyzer.charset_name - - @property - def language(self) -> str: - return "Japanese" - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - assert self.coding_sm is not None - assert self.distribution_analyzer is not None - - for i, byte in enumerate(byte_str): - coding_state = self.coding_sm.next_state(byte) - if coding_state == MachineState.ERROR: - self.logger.debug( - "%s %s prober hit error at byte %s", - self.charset_name, - self.language, - i, - ) - self._state = ProbingState.NOT_ME - break - if coding_state == MachineState.ITS_ME: - self._state = ProbingState.FOUND_IT - break - if coding_state == MachineState.START: - char_len = self.coding_sm.get_current_charlen() - if i == 0: - self._last_char[1] = byte - self.context_analyzer.feed( - self._last_char[2 - char_len :], char_len - ) - self.distribution_analyzer.feed(self._last_char, char_len) - else: - self.context_analyzer.feed( - byte_str[i + 1 - char_len : i + 3 - char_len], char_len - ) - self.distribution_analyzer.feed(byte_str[i - 1 : i + 1], char_len) - - self._last_char[0] = byte_str[-1] - - if self.state == ProbingState.DETECTING: - if self.context_analyzer.got_enough_data() and ( - self.get_confidence() > self.SHORTCUT_THRESHOLD - ): - self._state = ProbingState.FOUND_IT - - return self.state - - def get_confidence(self) -> float: - assert self.distribution_analyzer is not None - - context_conf = self.context_analyzer.get_confidence() - distrib_conf = self.distribution_analyzer.get_confidence() - return max(context_conf, distrib_conf) diff --git a/libs/chardet/universaldetector.py b/libs/chardet/universaldetector.py index 30c441dc2..9dd264a10 100644 --- a/libs/chardet/universaldetector.py +++ b/libs/chardet/universaldetector.py @@ -1,362 +1,25 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 2001 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### -""" -Module containing the UniversalDetector detector class, which is the primary -class a user of ``chardet`` should use. +"""Backward-compatibility stub for ``chardet.universaldetector``. -:author: Mark Pilgrim (initial port to Python) -:author: Shy Shalom (original C code) -:author: Dan Blanchard (major refactoring for 3.0) -:author: Ian Cordasco +chardet 6.x exposed :class:`UniversalDetector` via this module path. +In chardet 7+ the canonical location is :mod:`chardet.detector`, but this +stub keeps ``from chardet.universaldetector import UniversalDetector`` +working for existing callers. + +.. deprecated:: 7.0 + Import from :mod:`chardet` or :mod:`chardet.detector` instead. """ +from __future__ import annotations -import codecs -import logging -import re -from typing import List, Optional, Union +import warnings -from .charsetgroupprober import CharSetGroupProber -from .charsetprober import CharSetProber -from .enums import InputState, LanguageFilter, ProbingState -from .escprober import EscCharSetProber -from .latin1prober import Latin1Prober -from .macromanprober import MacRomanProber -from .mbcsgroupprober import MBCSGroupProber -from .resultdict import ResultDict -from .sbcsgroupprober import SBCSGroupProber -from .utf1632prober import UTF1632Prober +from chardet.detector import UniversalDetector +warnings.warn( + "chardet.universaldetector is deprecated, " + "import UniversalDetector from chardet or chardet.detector instead", + DeprecationWarning, + stacklevel=2, +) -class UniversalDetector: - """ - The ``UniversalDetector`` class underlies the ``chardet.detect`` function - and coordinates all of the different charset probers. - - To get a ``dict`` containing an encoding and its confidence, you can simply - run: - - .. code:: - - u = UniversalDetector() - u.feed(some_bytes) - u.close() - detected = u.result - - """ - - MINIMUM_THRESHOLD = 0.20 - HIGH_BYTE_DETECTOR = re.compile(b"[\x80-\xFF]") - ESC_DETECTOR = re.compile(b"(\033|~{)") - WIN_BYTE_DETECTOR = re.compile(b"[\x80-\x9F]") - ISO_WIN_MAP = { - "iso-8859-1": "Windows-1252", - "iso-8859-2": "Windows-1250", - "iso-8859-5": "Windows-1251", - "iso-8859-6": "Windows-1256", - "iso-8859-7": "Windows-1253", - "iso-8859-8": "Windows-1255", - "iso-8859-9": "Windows-1254", - "iso-8859-13": "Windows-1257", - } - # Based on https://encoding.spec.whatwg.org/#names-and-labels - # but altered to match Python names for encodings and remove mappings - # that break tests. - LEGACY_MAP = { - "ascii": "Windows-1252", - "iso-8859-1": "Windows-1252", - "tis-620": "ISO-8859-11", - "iso-8859-9": "Windows-1254", - "gb2312": "GB18030", - "euc-kr": "CP949", - "utf-16le": "UTF-16", - } - - def __init__( - self, - lang_filter: LanguageFilter = LanguageFilter.ALL, - should_rename_legacy: bool = False, - ) -> None: - self._esc_charset_prober: Optional[EscCharSetProber] = None - self._utf1632_prober: Optional[UTF1632Prober] = None - self._charset_probers: List[CharSetProber] = [] - self.result: ResultDict = { - "encoding": None, - "confidence": 0.0, - "language": None, - } - self.done = False - self._got_data = False - self._input_state = InputState.PURE_ASCII - self._last_char = b"" - self.lang_filter = lang_filter - self.logger = logging.getLogger(__name__) - self._has_win_bytes = False - self.should_rename_legacy = should_rename_legacy - self.reset() - - @property - def input_state(self) -> int: - return self._input_state - - @property - def has_win_bytes(self) -> bool: - return self._has_win_bytes - - @property - def charset_probers(self) -> List[CharSetProber]: - return self._charset_probers - - def reset(self) -> None: - """ - Reset the UniversalDetector and all of its probers back to their - initial states. This is called by ``__init__``, so you only need to - call this directly in between analyses of different documents. - """ - self.result = {"encoding": None, "confidence": 0.0, "language": None} - self.done = False - self._got_data = False - self._has_win_bytes = False - self._input_state = InputState.PURE_ASCII - self._last_char = b"" - if self._esc_charset_prober: - self._esc_charset_prober.reset() - if self._utf1632_prober: - self._utf1632_prober.reset() - for prober in self._charset_probers: - prober.reset() - - def feed(self, byte_str: Union[bytes, bytearray]) -> None: - """ - Takes a chunk of a document and feeds it through all of the relevant - charset probers. - - After calling ``feed``, you can check the value of the ``done`` - attribute to see if you need to continue feeding the - ``UniversalDetector`` more data, or if it has made a prediction - (in the ``result`` attribute). - - .. note:: - You should always call ``close`` when you're done feeding in your - document if ``done`` is not already ``True``. - """ - if self.done: - return - - if not byte_str: - return - - if not isinstance(byte_str, bytearray): - byte_str = bytearray(byte_str) - - # First check for known BOMs, since these are guaranteed to be correct - if not self._got_data: - # If the data starts with BOM, we know it is UTF - if byte_str.startswith(codecs.BOM_UTF8): - # EF BB BF UTF-8 with BOM - self.result = { - "encoding": "UTF-8-SIG", - "confidence": 1.0, - "language": "", - } - elif byte_str.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)): - # FF FE 00 00 UTF-32, little-endian BOM - # 00 00 FE FF UTF-32, big-endian BOM - self.result = {"encoding": "UTF-32", "confidence": 1.0, "language": ""} - elif byte_str.startswith(b"\xFE\xFF\x00\x00"): - # FE FF 00 00 UCS-4, unusual octet order BOM (3412) - self.result = { - # TODO: This encoding is not supported by Python. Should remove? - "encoding": "X-ISO-10646-UCS-4-3412", - "confidence": 1.0, - "language": "", - } - elif byte_str.startswith(b"\x00\x00\xFF\xFE"): - # 00 00 FF FE UCS-4, unusual octet order BOM (2143) - self.result = { - # TODO: This encoding is not supported by Python. Should remove? - "encoding": "X-ISO-10646-UCS-4-2143", - "confidence": 1.0, - "language": "", - } - elif byte_str.startswith((codecs.BOM_LE, codecs.BOM_BE)): - # FF FE UTF-16, little endian BOM - # FE FF UTF-16, big endian BOM - self.result = {"encoding": "UTF-16", "confidence": 1.0, "language": ""} - - self._got_data = True - if self.result["encoding"] is not None: - self.done = True - return - - # If none of those matched and we've only see ASCII so far, check - # for high bytes and escape sequences - if self._input_state == InputState.PURE_ASCII: - if self.HIGH_BYTE_DETECTOR.search(byte_str): - self._input_state = InputState.HIGH_BYTE - elif ( - self._input_state == InputState.PURE_ASCII - and self.ESC_DETECTOR.search(self._last_char + byte_str) - ): - self._input_state = InputState.ESC_ASCII - - self._last_char = byte_str[-1:] - - # next we will look to see if it is appears to be either a UTF-16 or - # UTF-32 encoding - if not self._utf1632_prober: - self._utf1632_prober = UTF1632Prober() - - if self._utf1632_prober.state == ProbingState.DETECTING: - if self._utf1632_prober.feed(byte_str) == ProbingState.FOUND_IT: - self.result = { - "encoding": self._utf1632_prober.charset_name, - "confidence": self._utf1632_prober.get_confidence(), - "language": "", - } - self.done = True - return - - # If we've seen escape sequences, use the EscCharSetProber, which - # uses a simple state machine to check for known escape sequences in - # HZ and ISO-2022 encodings, since those are the only encodings that - # use such sequences. - if self._input_state == InputState.ESC_ASCII: - if not self._esc_charset_prober: - self._esc_charset_prober = EscCharSetProber(self.lang_filter) - if self._esc_charset_prober.feed(byte_str) == ProbingState.FOUND_IT: - self.result = { - "encoding": self._esc_charset_prober.charset_name, - "confidence": self._esc_charset_prober.get_confidence(), - "language": self._esc_charset_prober.language, - } - self.done = True - # If we've seen high bytes (i.e., those with values greater than 127), - # we need to do more complicated checks using all our multi-byte and - # single-byte probers that are left. The single-byte probers - # use character bigram distributions to determine the encoding, whereas - # the multi-byte probers use a combination of character unigram and - # bigram distributions. - elif self._input_state == InputState.HIGH_BYTE: - if not self._charset_probers: - self._charset_probers = [MBCSGroupProber(self.lang_filter)] - # If we're checking non-CJK encodings, use single-byte prober - if self.lang_filter & LanguageFilter.NON_CJK: - self._charset_probers.append(SBCSGroupProber()) - self._charset_probers.append(Latin1Prober()) - self._charset_probers.append(MacRomanProber()) - for prober in self._charset_probers: - if prober.feed(byte_str) == ProbingState.FOUND_IT: - self.result = { - "encoding": prober.charset_name, - "confidence": prober.get_confidence(), - "language": prober.language, - } - self.done = True - break - if self.WIN_BYTE_DETECTOR.search(byte_str): - self._has_win_bytes = True - - def close(self) -> ResultDict: - """ - Stop analyzing the current document and come up with a final - prediction. - - :returns: The ``result`` attribute, a ``dict`` with the keys - `encoding`, `confidence`, and `language`. - """ - # Don't bother with checks if we're already done - if self.done: - return self.result - self.done = True - - if not self._got_data: - self.logger.debug("no data received!") - - # Default to ASCII if it is all we've seen so far - elif self._input_state == InputState.PURE_ASCII: - self.result = {"encoding": "ascii", "confidence": 1.0, "language": ""} - - # If we have seen non-ASCII, return the best that met MINIMUM_THRESHOLD - elif self._input_state == InputState.HIGH_BYTE: - prober_confidence = None - max_prober_confidence = 0.0 - max_prober = None - for prober in self._charset_probers: - if not prober: - continue - prober_confidence = prober.get_confidence() - if prober_confidence > max_prober_confidence: - max_prober_confidence = prober_confidence - max_prober = prober - if max_prober and (max_prober_confidence > self.MINIMUM_THRESHOLD): - charset_name = max_prober.charset_name - assert charset_name is not None - lower_charset_name = charset_name.lower() - confidence = max_prober.get_confidence() - # Use Windows encoding name instead of ISO-8859 if we saw any - # extra Windows-specific bytes - if lower_charset_name.startswith("iso-8859"): - if self._has_win_bytes: - charset_name = self.ISO_WIN_MAP.get( - lower_charset_name, charset_name - ) - # Rename legacy encodings with superset encodings if asked - if self.should_rename_legacy: - charset_name = self.LEGACY_MAP.get( - (charset_name or "").lower(), charset_name - ) - self.result = { - "encoding": charset_name, - "confidence": confidence, - "language": max_prober.language, - } - - # Log all prober confidences if none met MINIMUM_THRESHOLD - if self.logger.getEffectiveLevel() <= logging.DEBUG: - if self.result["encoding"] is None: - self.logger.debug("no probers hit minimum threshold") - for group_prober in self._charset_probers: - if not group_prober: - continue - if isinstance(group_prober, CharSetGroupProber): - for prober in group_prober.probers: - self.logger.debug( - "%s %s confidence = %s", - prober.charset_name, - prober.language, - prober.get_confidence(), - ) - else: - self.logger.debug( - "%s %s confidence = %s", - group_prober.charset_name, - group_prober.language, - group_prober.get_confidence(), - ) - return self.result +__all__ = ["UniversalDetector"] diff --git a/libs/chardet/utf1632prober.py b/libs/chardet/utf1632prober.py deleted file mode 100644 index 6bdec63d6..000000000 --- a/libs/chardet/utf1632prober.py +++ /dev/null @@ -1,225 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# -# Contributor(s): -# Jason Zavaglia -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### -from typing import List, Union - -from .charsetprober import CharSetProber -from .enums import ProbingState - - -class UTF1632Prober(CharSetProber): - """ - This class simply looks for occurrences of zero bytes, and infers - whether the file is UTF16 or UTF32 (low-endian or big-endian) - For instance, files looking like ( \0 \0 \0 [nonzero] )+ - have a good probability to be UTF32BE. Files looking like ( \0 [nonzero] )+ - may be guessed to be UTF16BE, and inversely for little-endian varieties. - """ - - # how many logical characters to scan before feeling confident of prediction - MIN_CHARS_FOR_DETECTION = 20 - # a fixed constant ratio of expected zeros or non-zeros in modulo-position. - EXPECTED_RATIO = 0.94 - - def __init__(self) -> None: - super().__init__() - self.position = 0 - self.zeros_at_mod = [0] * 4 - self.nonzeros_at_mod = [0] * 4 - self._state = ProbingState.DETECTING - self.quad = [0, 0, 0, 0] - self.invalid_utf16be = False - self.invalid_utf16le = False - self.invalid_utf32be = False - self.invalid_utf32le = False - self.first_half_surrogate_pair_detected_16be = False - self.first_half_surrogate_pair_detected_16le = False - self.reset() - - def reset(self) -> None: - super().reset() - self.position = 0 - self.zeros_at_mod = [0] * 4 - self.nonzeros_at_mod = [0] * 4 - self._state = ProbingState.DETECTING - self.invalid_utf16be = False - self.invalid_utf16le = False - self.invalid_utf32be = False - self.invalid_utf32le = False - self.first_half_surrogate_pair_detected_16be = False - self.first_half_surrogate_pair_detected_16le = False - self.quad = [0, 0, 0, 0] - - @property - def charset_name(self) -> str: - if self.is_likely_utf32be(): - return "utf-32be" - if self.is_likely_utf32le(): - return "utf-32le" - if self.is_likely_utf16be(): - return "utf-16be" - if self.is_likely_utf16le(): - return "utf-16le" - # default to something valid - return "utf-16" - - @property - def language(self) -> str: - return "" - - def approx_32bit_chars(self) -> float: - return max(1.0, self.position / 4.0) - - def approx_16bit_chars(self) -> float: - return max(1.0, self.position / 2.0) - - def is_likely_utf32be(self) -> bool: - approx_chars = self.approx_32bit_chars() - return approx_chars >= self.MIN_CHARS_FOR_DETECTION and ( - self.zeros_at_mod[0] / approx_chars > self.EXPECTED_RATIO - and self.zeros_at_mod[1] / approx_chars > self.EXPECTED_RATIO - and self.zeros_at_mod[2] / approx_chars > self.EXPECTED_RATIO - and self.nonzeros_at_mod[3] / approx_chars > self.EXPECTED_RATIO - and not self.invalid_utf32be - ) - - def is_likely_utf32le(self) -> bool: - approx_chars = self.approx_32bit_chars() - return approx_chars >= self.MIN_CHARS_FOR_DETECTION and ( - self.nonzeros_at_mod[0] / approx_chars > self.EXPECTED_RATIO - and self.zeros_at_mod[1] / approx_chars > self.EXPECTED_RATIO - and self.zeros_at_mod[2] / approx_chars > self.EXPECTED_RATIO - and self.zeros_at_mod[3] / approx_chars > self.EXPECTED_RATIO - and not self.invalid_utf32le - ) - - def is_likely_utf16be(self) -> bool: - approx_chars = self.approx_16bit_chars() - return approx_chars >= self.MIN_CHARS_FOR_DETECTION and ( - (self.nonzeros_at_mod[1] + self.nonzeros_at_mod[3]) / approx_chars - > self.EXPECTED_RATIO - and (self.zeros_at_mod[0] + self.zeros_at_mod[2]) / approx_chars - > self.EXPECTED_RATIO - and not self.invalid_utf16be - ) - - def is_likely_utf16le(self) -> bool: - approx_chars = self.approx_16bit_chars() - return approx_chars >= self.MIN_CHARS_FOR_DETECTION and ( - (self.nonzeros_at_mod[0] + self.nonzeros_at_mod[2]) / approx_chars - > self.EXPECTED_RATIO - and (self.zeros_at_mod[1] + self.zeros_at_mod[3]) / approx_chars - > self.EXPECTED_RATIO - and not self.invalid_utf16le - ) - - def validate_utf32_characters(self, quad: List[int]) -> None: - """ - Validate if the quad of bytes is valid UTF-32. - - UTF-32 is valid in the range 0x00000000 - 0x0010FFFF - excluding 0x0000D800 - 0x0000DFFF - - https://en.wikipedia.org/wiki/UTF-32 - """ - if ( - quad[0] != 0 - or quad[1] > 0x10 - or (quad[0] == 0 and quad[1] == 0 and 0xD8 <= quad[2] <= 0xDF) - ): - self.invalid_utf32be = True - if ( - quad[3] != 0 - or quad[2] > 0x10 - or (quad[3] == 0 and quad[2] == 0 and 0xD8 <= quad[1] <= 0xDF) - ): - self.invalid_utf32le = True - - def validate_utf16_characters(self, pair: List[int]) -> None: - """ - Validate if the pair of bytes is valid UTF-16. - - UTF-16 is valid in the range 0x0000 - 0xFFFF excluding 0xD800 - 0xFFFF - with an exception for surrogate pairs, which must be in the range - 0xD800-0xDBFF followed by 0xDC00-0xDFFF - - https://en.wikipedia.org/wiki/UTF-16 - """ - if not self.first_half_surrogate_pair_detected_16be: - if 0xD8 <= pair[0] <= 0xDB: - self.first_half_surrogate_pair_detected_16be = True - elif 0xDC <= pair[0] <= 0xDF: - self.invalid_utf16be = True - else: - if 0xDC <= pair[0] <= 0xDF: - self.first_half_surrogate_pair_detected_16be = False - else: - self.invalid_utf16be = True - - if not self.first_half_surrogate_pair_detected_16le: - if 0xD8 <= pair[1] <= 0xDB: - self.first_half_surrogate_pair_detected_16le = True - elif 0xDC <= pair[1] <= 0xDF: - self.invalid_utf16le = True - else: - if 0xDC <= pair[1] <= 0xDF: - self.first_half_surrogate_pair_detected_16le = False - else: - self.invalid_utf16le = True - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - for c in byte_str: - mod4 = self.position % 4 - self.quad[mod4] = c - if mod4 == 3: - self.validate_utf32_characters(self.quad) - self.validate_utf16_characters(self.quad[0:2]) - self.validate_utf16_characters(self.quad[2:4]) - if c == 0: - self.zeros_at_mod[mod4] += 1 - else: - self.nonzeros_at_mod[mod4] += 1 - self.position += 1 - return self.state - - @property - def state(self) -> ProbingState: - if self._state in {ProbingState.NOT_ME, ProbingState.FOUND_IT}: - # terminal, decided states - return self._state - if self.get_confidence() > 0.80: - self._state = ProbingState.FOUND_IT - elif self.position > 4 * 1024: - # if we get to 4kb into the file, and we can't conclude it's UTF, - # let's give up - self._state = ProbingState.NOT_ME - return self._state - - def get_confidence(self) -> float: - return ( - 0.85 - if ( - self.is_likely_utf16le() - or self.is_likely_utf16be() - or self.is_likely_utf32le() - or self.is_likely_utf32be() - ) - else 0.00 - ) diff --git a/libs/chardet/utf8prober.py b/libs/chardet/utf8prober.py deleted file mode 100644 index d96354d97..000000000 --- a/libs/chardet/utf8prober.py +++ /dev/null @@ -1,82 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -from typing import Union - -from .charsetprober import CharSetProber -from .codingstatemachine import CodingStateMachine -from .enums import MachineState, ProbingState -from .mbcssm import UTF8_SM_MODEL - - -class UTF8Prober(CharSetProber): - ONE_CHAR_PROB = 0.5 - - def __init__(self) -> None: - super().__init__() - self.coding_sm = CodingStateMachine(UTF8_SM_MODEL) - self._num_mb_chars = 0 - self.reset() - - def reset(self) -> None: - super().reset() - self.coding_sm.reset() - self._num_mb_chars = 0 - - @property - def charset_name(self) -> str: - return "utf-8" - - @property - def language(self) -> str: - return "" - - def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState: - for c in byte_str: - coding_state = self.coding_sm.next_state(c) - if coding_state == MachineState.ERROR: - self._state = ProbingState.NOT_ME - break - if coding_state == MachineState.ITS_ME: - self._state = ProbingState.FOUND_IT - break - if coding_state == MachineState.START: - if self.coding_sm.get_current_charlen() >= 2: - self._num_mb_chars += 1 - - if self.state == ProbingState.DETECTING: - if self.get_confidence() > self.SHORTCUT_THRESHOLD: - self._state = ProbingState.FOUND_IT - - return self.state - - def get_confidence(self) -> float: - unlike = 0.99 - if self._num_mb_chars < 6: - unlike *= self.ONE_CHAR_PROB**self._num_mb_chars - return 1.0 - unlike - return unlike diff --git a/libs/chardet/version.py b/libs/chardet/version.py deleted file mode 100644 index 19dd01e03..000000000 --- a/libs/chardet/version.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -This module exists only to simplify retrieving the version number of chardet -from within setuptools and from chardet subpackages. - -:author: Dan Blanchard (dan.blanchard@gmail.com) -""" - -__version__ = "5.2.0" -VERSION = __version__.split(".") diff --git a/libs/charset_normalizer-3.4.6.dist-info/INSTALLER b/libs/charset_normalizer-3.4.7.dist-info/INSTALLER similarity index 100% rename from libs/charset_normalizer-3.4.6.dist-info/INSTALLER rename to libs/charset_normalizer-3.4.7.dist-info/INSTALLER diff --git a/libs/charset_normalizer-3.4.6.dist-info/METADATA b/libs/charset_normalizer-3.4.7.dist-info/METADATA similarity index 99% rename from libs/charset_normalizer-3.4.6.dist-info/METADATA rename to libs/charset_normalizer-3.4.7.dist-info/METADATA index d2a40df91..6b5a36018 100644 --- a/libs/charset_normalizer-3.4.6.dist-info/METADATA +++ b/libs/charset_normalizer-3.4.7.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: charset-normalizer -Version: 3.4.6 +Version: 3.4.7 Summary: The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet. Author-email: "Ahmed R. TAHRI" Maintainer-email: "Ahmed R. TAHRI" @@ -26,6 +26,7 @@ Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient Classifier: Topic :: Text Processing :: Linguistic Classifier: Topic :: Utilities Classifier: Typing :: Typed @@ -303,6 +304,15 @@ tools. All notable changes to charset-normalizer will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.4.7](https://github.com/Ousret/charset_normalizer/compare/3.4.6...3.4.7) (2026-04-02) + +### Changed +- Pre-built optimized version using mypy[c] v1.20. +- Relax `setuptools` constraint to `setuptools>=68,<82.1`. + +### Fixed +- Correctly remove SIG remnant in utf-7 decoded string. (#718) (#716) + ## [3.4.6](https://github.com/Ousret/charset_normalizer/compare/3.4.5...3.4.6) (2026-03-15) ### Changed diff --git a/libs/charset_normalizer-3.4.6.dist-info/RECORD b/libs/charset_normalizer-3.4.7.dist-info/RECORD similarity index 55% rename from libs/charset_normalizer-3.4.6.dist-info/RECORD rename to libs/charset_normalizer-3.4.7.dist-info/RECORD index 9e596d239..1837e6ce3 100644 --- a/libs/charset_normalizer-3.4.6.dist-info/RECORD +++ b/libs/charset_normalizer-3.4.7.dist-info/RECORD @@ -1,22 +1,22 @@ ../../bin/normalizer,sha256=CK0fSuBOmHoIKSfCDVnx1HMTgra-d2jIh_2BbZMpjoU,209 -charset_normalizer-3.4.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -charset_normalizer-3.4.6.dist-info/METADATA,sha256=dk9sTA95N-j8mBeWuSnjw6zoAYbY9J7viW3Jukhd8S4,40556 -charset_normalizer-3.4.6.dist-info/RECORD,, -charset_normalizer-3.4.6.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -charset_normalizer-3.4.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -charset_normalizer-3.4.6.dist-info/entry_points.txt,sha256=ADSTKrkXZ3hhdOVFi6DcUEHQRS0xfxDIE_pEz4wLIXA,65 -charset_normalizer-3.4.6.dist-info/licenses/LICENSE,sha256=bQ1Bv-FwrGx9wkjJpj4lTQ-0WmDVCoJX0K-SxuJJuIc,1071 -charset_normalizer-3.4.6.dist-info/top_level.txt,sha256=7ASyzePr8_xuZWJsnqJjIBtyV8vhEo0wBCv1MPRRi3Q,19 +charset_normalizer-3.4.7.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +charset_normalizer-3.4.7.dist-info/METADATA,sha256=K8lK8L8LaZ1YmKvWLt3zEkpIxiCOC58xNhzFQrfQJxQ,40931 +charset_normalizer-3.4.7.dist-info/RECORD,, +charset_normalizer-3.4.7.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +charset_normalizer-3.4.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +charset_normalizer-3.4.7.dist-info/entry_points.txt,sha256=ADSTKrkXZ3hhdOVFi6DcUEHQRS0xfxDIE_pEz4wLIXA,65 +charset_normalizer-3.4.7.dist-info/licenses/LICENSE,sha256=bQ1Bv-FwrGx9wkjJpj4lTQ-0WmDVCoJX0K-SxuJJuIc,1071 +charset_normalizer-3.4.7.dist-info/top_level.txt,sha256=7ASyzePr8_xuZWJsnqJjIBtyV8vhEo0wBCv1MPRRi3Q,19 charset_normalizer/__init__.py,sha256=OKRxRv2Zhnqk00tqkN0c1BtJjm165fWXLydE52IKuHc,1590 charset_normalizer/__main__.py,sha256=yzYxMR-IhKRHYwcSlavEv8oGdwxsR89mr2X09qXGdps,109 -charset_normalizer/api.py,sha256=6kHyIpfgxrjR_o_s_IGZ_zweFIfiGRBf-tnKRPqwg_E,37960 +charset_normalizer/api.py,sha256=387F3n23MlMu-xfSbFULW2DLGsBmVrZVGhnkiGXeKBo,38844 charset_normalizer/cd.py,sha256=v0iPJweGsRegXywrM1LzUgqW9bJ1KFvIblQHP1jm5FQ,15174 charset_normalizer/cli/__init__.py,sha256=D8I86lFk2-py45JvqxniTirSj_sFyE6sjaY_0-G1shc,136 charset_normalizer/cli/__main__.py,sha256=E9FFSV1E2iOE_B2B1tJHQT9ExJqc60Ks_c-08sNawh8,11940 -charset_normalizer/constant.py,sha256=UMxXijQRKnoj27qP5vjNPJed2kJUB9rmCbRuLBjwIQ0,44431 +charset_normalizer/constant.py,sha256=yvLAWDrdSC743Cu4amhwHLIO-FGuRTOTZouCzZKGikc,44431 charset_normalizer/legacy.py,sha256=yBIFMNABNPE5JkdKOWyVo36fZtV9nm8bf37LrDWulz8,2661 charset_normalizer/md.py,sha256=AYCdfDX79FrgoId3zXqmbCuDcbGr1NRuGqgJN94Rx9Q,30441 -charset_normalizer/models.py,sha256=01sizRL-AUJ5FOzANwAtV6l2rQ0Dd5dZpJUfXv6HTxc,12360 +charset_normalizer/models.py,sha256=FbaQnI6ECmVmyHRSvVM5fHNeMAQ3KSGdwLjGcQqWDws,12821 charset_normalizer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 charset_normalizer/utils.py,sha256=9cpi-_0-vC9pGDfuoarhC6VlF_Jxwx5Jsa_8I4w2D8k,12282 -charset_normalizer/version.py,sha256=w4FVpMHIJ5sT9HC5JROzcW8kYPHryvjSgIDqUM5w1uw,115 +charset_normalizer/version.py,sha256=2LxFuGp3BBuIwt95cp64y7v8bCNHcMAi08IfXt_47Co,115 diff --git a/libs/charset_normalizer-3.4.6.dist-info/REQUESTED b/libs/charset_normalizer-3.4.7.dist-info/REQUESTED similarity index 100% rename from libs/charset_normalizer-3.4.6.dist-info/REQUESTED rename to libs/charset_normalizer-3.4.7.dist-info/REQUESTED diff --git a/libs/chardet-5.2.0.dist-info/WHEEL b/libs/charset_normalizer-3.4.7.dist-info/WHEEL similarity index 100% rename from libs/chardet-5.2.0.dist-info/WHEEL rename to libs/charset_normalizer-3.4.7.dist-info/WHEEL diff --git a/libs/charset_normalizer-3.4.6.dist-info/entry_points.txt b/libs/charset_normalizer-3.4.7.dist-info/entry_points.txt similarity index 100% rename from libs/charset_normalizer-3.4.6.dist-info/entry_points.txt rename to libs/charset_normalizer-3.4.7.dist-info/entry_points.txt diff --git a/libs/charset_normalizer-3.4.6.dist-info/licenses/LICENSE b/libs/charset_normalizer-3.4.7.dist-info/licenses/LICENSE similarity index 100% rename from libs/charset_normalizer-3.4.6.dist-info/licenses/LICENSE rename to libs/charset_normalizer-3.4.7.dist-info/licenses/LICENSE diff --git a/libs/charset_normalizer-3.4.6.dist-info/top_level.txt b/libs/charset_normalizer-3.4.7.dist-info/top_level.txt similarity index 100% rename from libs/charset_normalizer-3.4.6.dist-info/top_level.txt rename to libs/charset_normalizer-3.4.7.dist-info/top_level.txt diff --git a/libs/charset_normalizer/api.py b/libs/charset_normalizer/api.py index 1f32091f2..50cb95509 100644 --- a/libs/charset_normalizer/api.py +++ b/libs/charset_normalizer/api.py @@ -357,14 +357,28 @@ def from_bytes( encoding=encoding_iana, ) else: - decoded_payload = str( - ( - sequences - if strip_sig_or_bom is False - else sequences[len(sig_payload) :] - ), - encoding=encoding_iana, - ) + # UTF-7 BOM is encoded in modified Base64 whose byte boundary + # can overlap with the next character. Stripping raw SIG bytes + # before decoding may leave stray bytes that decode as garbage. + # Decode the full sequence and remove the leading BOM char instead. + # see https://github.com/jawah/charset_normalizer/issues/718 + # and https://github.com/jawah/charset_normalizer/issues/716 + if encoding_iana == "utf_7" and bom_or_sig_available: + decoded_payload = str( + sequences, + encoding=encoding_iana, + ) + if decoded_payload and decoded_payload[0] == "\ufeff": + decoded_payload = decoded_payload[1:] + else: + decoded_payload = str( + ( + sequences + if strip_sig_or_bom is False + else sequences[len(sig_payload) :] + ), + encoding=encoding_iana, + ) except (UnicodeDecodeError, LookupError) as e: if not isinstance(e, LookupError): logger.log( diff --git a/libs/charset_normalizer/constant.py b/libs/charset_normalizer/constant.py index 6ed779567..e1297d2d4 100644 --- a/libs/charset_normalizer/constant.py +++ b/libs/charset_normalizer/constant.py @@ -9,11 +9,11 @@ from re import compile as re_compile ENCODING_MARKS: dict[str, bytes | list[bytes]] = { "utf_8": BOM_UTF8, "utf_7": [ + b"\x2b\x2f\x76\x38\x2d", b"\x2b\x2f\x76\x38", b"\x2b\x2f\x76\x39", b"\x2b\x2f\x76\x2b", b"\x2b\x2f\x76\x2f", - b"\x2b\x2f\x76\x38\x2d", ], "gb18030": b"\x84\x31\x95\x33", "utf_32": [BOM_UTF32_BE, BOM_UTF32_LE], diff --git a/libs/charset_normalizer/models.py b/libs/charset_normalizer/models.py index 30e8a163e..382de159e 100644 --- a/libs/charset_normalizer/models.py +++ b/libs/charset_normalizer/models.py @@ -75,6 +75,16 @@ class CharsetMatch: # Lazy Str Loading if self._string is None: self._string = str(self._payload, self._encoding, "strict") + # UTF-7 BOM is encoded in modified Base64 whose byte boundary + # can overlap with the next character, so raw-byte stripping + # is unreliable. Strip the decoded BOM character instead. + if ( + self._has_sig_or_bom + and self._encoding == "utf_7" + and self._string + and self._string[0] == "\ufeff" + ): + self._string = self._string[1:] return self._string def __repr__(self) -> str: diff --git a/libs/charset_normalizer/version.py b/libs/charset_normalizer/version.py index a80346fc7..a93d3672d 100644 --- a/libs/charset_normalizer/version.py +++ b/libs/charset_normalizer/version.py @@ -4,5 +4,5 @@ Expose version from __future__ import annotations -__version__ = "3.4.6" +__version__ = "3.4.7" VERSION = __version__.split(".") diff --git a/libs/click-8.3.1.dist-info/RECORD b/libs/click-8.3.1.dist-info/RECORD deleted file mode 100644 index 0c86d4eb0..000000000 --- a/libs/click-8.3.1.dist-info/RECORD +++ /dev/null @@ -1,24 +0,0 @@ -click-8.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -click-8.3.1.dist-info/METADATA,sha256=XZeBrMAE0ghTE88SjfrSDuSyNCpBPplxJR1tbwD9oZg,2621 -click-8.3.1.dist-info/RECORD,, -click-8.3.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -click-8.3.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 -click-8.3.1.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 -click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473 -click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693 -click/_termui_impl.py,sha256=rgCb3On8X5A4200rA5L6i13u5iapmFer7sru57Jy6zA,27093 -click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400 -click/_utils.py,sha256=kZwtTf5gMuCilJJceS2iTCvRvCY-0aN5rJq8gKw7p8g,943 -click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465 -click/core.py,sha256=U6Bfxt8GkjNDqyJ0HqXvluJHtyZ4sY5USAvM1Cdq7mQ,132105 -click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461 -click/exceptions.py,sha256=8utf8w6V5hJXMnO_ic1FNrtbwuEn1NUu1aDwV8UqnG4,9954 -click/formatting.py,sha256=RVfwwr0rwWNpgGr8NaHodPzkIr7_tUyVh_nDdanLMNc,9730 -click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923 -click/parser.py,sha256=Q31pH0FlQZEq-UXE_ABRzlygEfvxPTuZbWNh4xfXmzw,19010 -click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -click/shell_completion.py,sha256=Cc4GQUFuWpfQBa9sF5qXeeYI7n3tI_1k6ZdSn4BZbT0,20994 -click/termui.py,sha256=hqCEjNndU-nzW08nRAkBaVgfZp_FdCA9KxfIWlKYaMc,31037 -click/testing.py,sha256=EERbzcl1br0mW0qBS9EqkknfNfXB9WQEW0ELIpkvuSs,19102 -click/types.py,sha256=ek54BNSFwPKsqtfT7jsqcc4WHui8AIFVMKM4oVZIXhc,39927 -click/utils.py,sha256=gCUoewdAhA-QLBUUHxrLh4uj6m7T1WjZZMNPvR0I7YA,20257 diff --git a/libs/click-8.3.1.dist-info/INSTALLER b/libs/click-8.4.1.dist-info/INSTALLER similarity index 100% rename from libs/click-8.3.1.dist-info/INSTALLER rename to libs/click-8.4.1.dist-info/INSTALLER diff --git a/libs/click-8.3.1.dist-info/METADATA b/libs/click-8.4.1.dist-info/METADATA similarity index 99% rename from libs/click-8.3.1.dist-info/METADATA rename to libs/click-8.4.1.dist-info/METADATA index 3f433afb9..21d7a0c3d 100644 --- a/libs/click-8.3.1.dist-info/METADATA +++ b/libs/click-8.4.1.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: click -Version: 8.3.1 +Version: 8.4.1 Summary: Composable command line interface toolkit Maintainer-email: Pallets Requires-Python: >=3.10 diff --git a/libs/click-8.4.1.dist-info/RECORD b/libs/click-8.4.1.dist-info/RECORD new file mode 100644 index 000000000..ee2a075f1 --- /dev/null +++ b/libs/click-8.4.1.dist-info/RECORD @@ -0,0 +1,24 @@ +click-8.4.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +click-8.4.1.dist-info/METADATA,sha256=uWALc7Y-Ixjj35al3B2qNsyXk-0tf578YgIsL1Vbn98,2621 +click-8.4.1.dist-info/RECORD,, +click-8.4.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click-8.4.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +click-8.4.1.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click/__init__.py,sha256=FId2fXCSJB3yeWD-e2uON-mBhFa2Yc9MvXGmHu8OXG0,4634 +click/_compat.py,sha256=XK1woqBSPRb1KNAwWwVsSMfEm8ZlfDwBCjceWzSGmEY,18910 +click/_termui_impl.py,sha256=VNyWY27pm0JUq12WK__pJNYxeUoDyfNwYEgE6vz_cnM,30411 +click/_textwrap.py,sha256=7Z0N7Vmn-66TNSTUwp6OXJbcUXRmYET9h9c2ucD8oQQ,6270 +click/_utils.py,sha256=eCZCtwJtsYD5QYkkNWJ8MY_8ABIjy8MczgMMyVY32rQ,996 +click/_winconsole.py,sha256=KSxfNbMlYRa6GOJuCLgsg2Pb3dVkgJNPqLJPae-Pa10,8543 +click/core.py,sha256=CQaJ0ALw4zyyVp6E7gP6a_G88eiuXE9mBzFnIJQ-igY,137917 +click/decorators.py,sha256=xgR8vZAClhWVkzitP22k6jETFRhKralh81zvA69Csd0,18467 +click/exceptions.py,sha256=FJitDd6MSZLj6CtKQWpkQzHnfsUw3UypxDU0t76bn0c,11294 +click/formatting.py,sha256=r6kGLpPyF0Cva6uSTwSZ_8ONRE7jp93I1C0JVRYQEqE,10370 +click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923 +click/parser.py,sha256=oJ-fU_3mvxugIuNtHaCATZ56lgEmHRggjJiSqEgYrjA,19052 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=QxeFb3RapNSP8ZwvK2BDVI2jrEKS1RbJaeuFMrHJAnU,21748 +click/termui.py,sha256=hflZ_tK7P4WUv_ef_RVVRL7n0nRPhTD0wN0pkHE7hDo,33000 +click/testing.py,sha256=0k_v5By_5ZO2McjBT7lTqQ57W8QJuW0_5Sx7d2ZLKec,25717 +click/types.py,sha256=UiVEx0yoQtrWO2wH22NgME4LfDxMNQi8j1qtJupYIpw,42783 +click/utils.py,sha256=YsTNrChEfwDXl39CTC_yWnQ-ZVkZxYjFESRCfEgClY4,20386 diff --git a/libs/click-8.3.1.dist-info/REQUESTED b/libs/click-8.4.1.dist-info/REQUESTED similarity index 100% rename from libs/click-8.3.1.dist-info/REQUESTED rename to libs/click-8.4.1.dist-info/REQUESTED diff --git a/libs/click-8.3.1.dist-info/WHEEL b/libs/click-8.4.1.dist-info/WHEEL similarity index 100% rename from libs/click-8.3.1.dist-info/WHEEL rename to libs/click-8.4.1.dist-info/WHEEL diff --git a/libs/click-8.3.1.dist-info/licenses/LICENSE.txt b/libs/click-8.4.1.dist-info/licenses/LICENSE.txt similarity index 100% rename from libs/click-8.3.1.dist-info/licenses/LICENSE.txt rename to libs/click-8.4.1.dist-info/licenses/LICENSE.txt diff --git a/libs/click/__init__.py b/libs/click/__init__.py index 1aa547c57..64be7e0c3 100644 --- a/libs/click/__init__.py +++ b/libs/click/__init__.py @@ -14,6 +14,7 @@ from .core import Context as Context from .core import Group as Group from .core import Option as Option from .core import Parameter as Parameter +from .core import ParameterSource as ParameterSource from .decorators import argument as argument from .decorators import command as command from .decorators import confirmation_option as confirmation_option @@ -32,6 +33,7 @@ from .exceptions import BadParameter as BadParameter from .exceptions import ClickException as ClickException from .exceptions import FileError as FileError from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchCommand as NoSuchCommand from .exceptions import NoSuchOption as NoSuchOption from .exceptions import UsageError as UsageError from .formatting import HelpFormatter as HelpFormatter @@ -41,6 +43,7 @@ from .termui import clear as clear from .termui import confirm as confirm from .termui import echo_via_pager as echo_via_pager from .termui import edit as edit +from .termui import get_pager_file as get_pager_file from .termui import getchar as getchar from .termui import launch as launch from .termui import pause as pause diff --git a/libs/click/_compat.py b/libs/click/_compat.py index f2726b93a..36c0e5397 100644 --- a/libs/click/_compat.py +++ b/libs/click/_compat.py @@ -12,6 +12,7 @@ from weakref import WeakKeyDictionary CYGWIN = sys.platform.startswith("cygwin") WIN = sys.platform.startswith("win") +MAC = sys.platform == "darwin" auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None _ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") @@ -502,6 +503,10 @@ def should_strip_ansi( if color is None: if stream is None: stream = sys.stdin + elif hasattr(stream, "color"): + # ._termui_impl.MaybeStripAnsi handles stripping ansi itself, + # so we don't need to strip it here + return False return not isatty(stream) and not _is_jupyter_kernel_output(stream) return not color diff --git a/libs/click/_termui_impl.py b/libs/click/_termui_impl.py index ee8225c4c..76113e9bb 100644 --- a/libs/click/_termui_impl.py +++ b/libs/click/_termui_impl.py @@ -8,6 +8,7 @@ from __future__ import annotations import collections.abc as cabc import contextlib +import io import math import os import shlex @@ -23,7 +24,6 @@ from ._compat import _default_text_stdout from ._compat import CYGWIN from ._compat import get_best_encoding from ._compat import isatty -from ._compat import open_stream from ._compat import strip_ansi from ._compat import term_len from ._compat import WIN @@ -32,6 +32,19 @@ from .utils import echo V = t.TypeVar("V") + +class _BufferedTextPagerStream(t.Protocol): + buffer: t.BinaryIO + + +def _has_binary_buffer( + stream: t.BinaryIO | t.TextIO, +) -> t.TypeGuard[_BufferedTextPagerStream]: + # TextIO is wider than TextIOWrapper; text-only streams such as StringIO + # are valid TextIO values but do not expose a binary buffer to wrap. + return getattr(stream, "buffer", None) is not None + + if os.name == "nt": BEFORE_BAR = "\r" AFTER_BAR = "\n" @@ -173,7 +186,13 @@ class ProgressBar(t.Generic[V]): hours = t % 24 t //= 24 if t > 0: - return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" + return "{d}{day_label} {h:02}:{m:02}:{s:02}".format( + d=t, + day_label=_("d"), + h=hours, + m=minutes, + s=seconds, + ) else: return f"{hours:02}:{minutes:02}:{seconds:02}" return "" @@ -366,7 +385,20 @@ class ProgressBar(t.Generic[V]): self.render_progress() -def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None: +class MaybeStripAnsi(io.TextIOWrapper): + def __init__(self, stream: t.IO[bytes], *, color: bool, **kwargs: t.Any): + super().__init__(stream, **kwargs) + self.color = color + + def write(self, text: str) -> int: + if not self.color: + text = strip_ansi(text) + return super().write(text) + + +def _pager_contextmanager( + color: bool | None = None, +) -> t.ContextManager[tuple[t.BinaryIO | t.TextIO, str, bool]]: """Decide what method to use for paging through text.""" stdout = _default_text_stdout() @@ -376,50 +408,69 @@ def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None: stdout = StringIO() if not isatty(sys.stdin) or not isatty(stdout): - return _nullpager(stdout, generator, color) + return _nullpager(stdout, color) - # Split and normalize the pager command into parts. - pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False) + # Split using POSIX mode (the default) so that quote characters are + # stripped from tokens and quoted Windows paths are preserved. + # Non-POSIX mode retains quotes in tokens, and wrapping tokens + # with shlex.quote re-introduces quoting issues on Windows. + pager_cmd_parts = shlex.split(os.environ.get("PAGER", "")) if pager_cmd_parts: if WIN: - if _tempfilepager(generator, pager_cmd_parts, color): - return - elif _pipepager(generator, pager_cmd_parts, color): - return + return _tempfilepager(pager_cmd_parts, color) + return _pipepager(pager_cmd_parts, color) if os.environ.get("TERM") in ("dumb", "emacs"): - return _nullpager(stdout, generator, color) - if (WIN or sys.platform.startswith("os2")) and _tempfilepager( - generator, ["more"], color - ): - return - if _pipepager(generator, ["less"], color): - return - - import tempfile - - fd, filename = tempfile.mkstemp() - os.close(fd) - try: - if _pipepager(generator, ["more"], color): - return - return _nullpager(stdout, generator, color) - finally: - os.unlink(filename) + return _nullpager(stdout, color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(["more"], color) + return _pipepager(["less"], color) +@contextlib.contextmanager +def get_pager_file(color: bool | None = None) -> t.Generator[t.TextIO, None, None]: + """Context manager. + Yields a writable file-like object which can be used as an output pager. + .. versionadded:: 8.4 + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + with _pager_contextmanager(color=color) as (stream, encoding, color): + # Split streams by capabilities rather than the abstract TextIO / + # BinaryIO annotations: buffered text streams can be unwrapped to bytes, + # while text-only streams are yielded as-is. + if _has_binary_buffer(stream): + # Text stream backed by a binary buffer. + stream = MaybeStripAnsi(stream.buffer, color=color, encoding=encoding) + elif isinstance(stream, t.BinaryIO): + # Binary stream + stream = MaybeStripAnsi(stream, color=color, encoding=encoding) + try: + yield stream + finally: + stream.flush() + + +@contextlib.contextmanager def _pipepager( - generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None -) -> bool: - """Page through text by feeding it to another program. Invoking a - pager through this might support colors. + cmd_parts: list[str], color: bool | None = None +) -> t.Iterator[tuple[t.BinaryIO | t.TextIO, str, bool]]: + """Page through text by feeding it to another program. - Returns `True` if the command was found, `False` otherwise and thus another - pager should be attempted. + Invokes the pager via :class:`subprocess.Popen` with an ``argv`` list + produced by :func:`shlex.split`. The command is resolved to an absolute + path with :func:`shutil.which` as recommended by the + :mod:`subprocess` docs for Windows compatibility. + + Invoking a pager through this might support colors: if piping to + ``less`` and the user hasn't decided on colors, ``LESS=-R`` is set + automatically. """ # Split the command into the invoked CLI and its parameters. if not cmd_parts: - return False + stdout = _default_text_stdout() or StringIO() + yield stdout, "utf-8", False + return import shutil @@ -428,7 +479,9 @@ def _pipepager( cmd_filepath = shutil.which(cmd) if not cmd_filepath: - return False + stdout = _default_text_stdout() or StringIO() + yield stdout, "utf-8", False + return # Produces a normalized absolute path string. # multi-call binaries such as busybox derive their identity from the symlink @@ -451,6 +504,9 @@ def _pipepager( elif "r" in less_flags or "R" in less_flags: color = True + if color is None: + color = False + c = subprocess.Popen( [str(cmd_path)] + cmd_params, shell=False, @@ -459,13 +515,10 @@ def _pipepager( errors="replace", text=True, ) - assert c.stdin is not None + stdin = t.cast(t.BinaryIO, c.stdin) + encoding = get_best_encoding(stdin) try: - for text in generator: - if not color: - text = strip_ansi(text) - - c.stdin.write(text) + yield stdin, encoding, color except BrokenPipeError: # In case the pager exited unexpectedly, ignore the broken pipe error. pass @@ -479,7 +532,7 @@ def _pipepager( finally: # We must close stdin and wait for the pager to exit before we continue try: - c.stdin.close() + stdin.close() # Close implies flush, so it might throw a BrokenPipeError if the pager # process exited already. except BrokenPipeError: @@ -501,64 +554,87 @@ def _pipepager( else: break - return True - +@contextlib.contextmanager def _tempfilepager( - generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None -) -> bool: + cmd_parts: list[str], color: bool | None = None +) -> t.Iterator[tuple[t.BinaryIO | t.TextIO, str, bool]]: """Page through text by invoking a program on a temporary file. - Returns `True` if the command was found, `False` otherwise and thus another - pager should be attempted. + Used as the primary pager strategy on Windows (where piping to + ``more`` adds spurious ``\\r\\n``), and as a fallback on other + platforms. The command is resolved to an absolute path with + :func:`shutil.which`. """ # Split the command into the invoked CLI and its parameters. if not cmd_parts: - return False + stdout = _default_text_stdout() or StringIO() + yield stdout, "utf-8", False + return import shutil + import subprocess cmd = cmd_parts[0] cmd_filepath = shutil.which(cmd) if not cmd_filepath: - return False + stdout = _default_text_stdout() or StringIO() + yield stdout, "utf-8", False + return + # Produces a normalized absolute path string. # multi-call binaries such as busybox derive their identity from the symlink # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox) cmd_path = Path(cmd_filepath).absolute() - import subprocess import tempfile - fd, filename = tempfile.mkstemp() - # TODO: This never terminates if the passed generator never terminates. - text = "".join(generator) - if not color: - text = strip_ansi(text) encoding = get_best_encoding(sys.stdout) - with open_stream(filename, "wb")[0] as f: - f.write(text.encode(encoding)) + if color is None: + color = False + # On Windows, NamedTemporaryFile cannot be opened by another process + # while Python still has it open, so we use delete=False and clean up manually + # rather than using a contextmanager here. + f = tempfile.NamedTemporaryFile(mode="wb", delete=False) try: - subprocess.call([str(cmd_path), filename]) - except OSError: - # Command not found - pass + yield t.cast(t.BinaryIO, f), encoding, color + f.flush() + f.close() + subprocess.call([str(cmd_path), f.name]) finally: - os.close(fd) - os.unlink(filename) - - return True + os.unlink(f.name) +class _SkipClose: + def __init__(self, stream: t.IO[t.Any]) -> None: + self.stream = stream + + def __getattr__(self, name: str) -> t.Any: + return getattr(self.stream, name) + + @property + def buffer(self) -> t.BinaryIO: + return _SkipClose(self.stream.buffer) # type: ignore[attr-defined, return-value] + + def close(self) -> None: + pass + + +@contextlib.contextmanager def _nullpager( - stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None -) -> None: - """Simply print unformatted text. This is the ultimate fallback.""" - for text in generator: - if not color: - text = strip_ansi(text) - stream.write(text) + stream: t.TextIO, color: bool | None = None +) -> t.Iterator[tuple[t.TextIO, str, bool]]: + """Simply print unformatted text. This is the ultimate fallback. Don't close the + output stream in this case, since it's coming from elsewhere rather than our + internal helpers. + """ + encoding = get_best_encoding(stream) + + if color is None: + color = False + + yield _SkipClose(stream), encoding, color # type: ignore[misc] class Editor: @@ -592,6 +668,8 @@ class Editor: return "vi" def edit_files(self, filenames: cabc.Iterable[str]) -> None: + """Open files in the user's editor.""" + import shlex import subprocess editor = self.get_editor() @@ -601,11 +679,13 @@ class Editor: environ = os.environ.copy() environ.update(self.env) - exc_filename = " ".join(f'"{filename}"' for filename in filenames) - try: + # Split in POSIX mode (the default) for the same reasons as + # in pager(): strips quotes from tokens and preserves quoted + # Windows paths. c = subprocess.Popen( - args=f"{editor} {exc_filename}", env=environ, shell=True + args=shlex.split(editor) + list(filenames), + env=environ, ) exit_code = c.wait() if exit_code != 0: @@ -699,18 +779,17 @@ def open_url(url: str, wait: bool = False, locate: bool = False) -> int: elif WIN: if locate: url = _unquote_file(url) - args = ["explorer", f"/select,{url}"] + args = ["explorer", "/select,", url] + try: + return subprocess.call(args) + except OSError: + return 127 else: - args = ["start"] - if wait: - args.append("/WAIT") - args.append("") - args.append(url) - try: - return subprocess.call(args) - except OSError: - # Command not found - return 127 + try: + os.startfile(url) # type: ignore[attr-defined] + except OSError: + return 127 + return 0 elif CYGWIN: if locate: url = _unquote_file(url) @@ -754,8 +833,6 @@ def _translate_ch_to_exc(ch: str) -> None: if ch == "\x1a" and WIN: # Windows, Ctrl+Z raise EOFError() - return None - if sys.platform == "win32": import msvcrt diff --git a/libs/click/_textwrap.py b/libs/click/_textwrap.py index 97fbee3dc..82840f2df 100644 --- a/libs/click/_textwrap.py +++ b/libs/click/_textwrap.py @@ -4,8 +4,47 @@ import collections.abc as cabc import textwrap from contextlib import contextmanager +from ._compat import _ansi_re +from ._compat import term_len + + +def _truncate_visible(text: str, n: int) -> str: + """Return the longest prefix of ``text`` containing at most ``n`` visible + characters. + + ANSI escape sequences inside the prefix are kept intact and do not count + toward the visible width. A cut is never placed inside an escape sequence. + """ + if n <= 0: + return "" + + visible = 0 + i = 0 + cut = 0 + end = len(text) + while i < end: + m = _ansi_re.match(text, i) + if m is not None: + i = m.end() + continue + visible += 1 + i += 1 + cut = i + if visible >= n: + break + return text[:cut] + class TextWrapper(textwrap.TextWrapper): + """``textwrap.TextWrapper`` variant that measures widths by visible + character count. + + ANSI escape sequences embedded in chunks, indents, or the placeholder are + excluded from the width budget. Without this, styled help text (a styled + ``Usage:`` prefix, a colorized option name, ...) would be wrapped earlier + than its visible length warrants and tokens would split mid-word. + """ + def _handle_long_word( self, reversed_chunks: list[str], @@ -17,13 +56,111 @@ class TextWrapper(textwrap.TextWrapper): if self.break_long_words: last = reversed_chunks[-1] - cut = last[:space_left] - res = last[space_left:] + cut = _truncate_visible(last, space_left) + res = last[len(cut) :] cur_line.append(cut) reversed_chunks[-1] = res elif not cur_line: cur_line.append(reversed_chunks.pop()) + def _wrap_chunks(self, chunks: list[str]) -> list[str]: + """Wrap chunks counting widths in visible characters. + + Mirrors the algorithm of :meth:`textwrap.TextWrapper._wrap_chunks` + with every width measurement routed through + :func:`click._compat.term_len` instead of :func:`len`, so ANSI escape + bytes in chunks, indents, or the placeholder do not inflate the count. + + .. seealso:: + :class:`textwrap.TextWrapper` in the Python standard library documentation: + https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper + + Reference implementation in CPython: + https://github.com/python/cpython/blob/main/Lib/textwrap.py + """ + lines: list[str] = [] + if self.width <= 0: + raise ValueError(f"invalid width {self.width!r} (must be > 0)") + if self.max_lines is not None: + if self.max_lines > 1: + indent = self.subsequent_indent + else: + indent = self.initial_indent + if term_len(indent) + term_len(self.placeholder.lstrip()) > self.width: + raise ValueError("placeholder too large for max width") + + chunks.reverse() + + while chunks: + cur_line: list[str] = [] + cur_len = 0 + + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + width = self.width - term_len(indent) + + if self.drop_whitespace and chunks[-1].strip() == "" and lines: + del chunks[-1] + + while chunks: + n = term_len(chunks[-1]) + + if cur_len + n <= width: + cur_line.append(chunks.pop()) + cur_len += n + + else: + break + + if chunks and term_len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + cur_len = sum(map(term_len, cur_line)) + + if self.drop_whitespace and cur_line and cur_line[-1].strip() == "": + cur_len -= term_len(cur_line[-1]) + del cur_line[-1] + + if cur_line: + if ( + self.max_lines is None + or len(lines) + 1 < self.max_lines + or ( + not chunks + or self.drop_whitespace + and len(chunks) == 1 + and not chunks[0].strip() + ) + and cur_len <= width + ): + lines.append(indent + "".join(cur_line)) + else: + while cur_line: + if ( + cur_line[-1].strip() + and cur_len + term_len(self.placeholder) <= width + ): + cur_line.append(self.placeholder) + lines.append(indent + "".join(cur_line)) + break + cur_len -= term_len(cur_line[-1]) + del cur_line[-1] + else: + if lines: + prev_line = lines[-1].rstrip() + if ( + term_len(prev_line) + term_len(self.placeholder) + <= self.width + ): + lines[-1] = prev_line + self.placeholder + break + lines.append(indent + self.placeholder.lstrip()) + break + + return lines + @contextmanager def extra_indent(self, indent: str) -> cabc.Iterator[None]: old_initial_indent = self.initial_indent diff --git a/libs/click/_utils.py b/libs/click/_utils.py index 09fb00855..05ee2e997 100644 --- a/libs/click/_utils.py +++ b/libs/click/_utils.py @@ -19,18 +19,18 @@ class Sentinel(enum.Enum): return f"{self.__class__.__name__}.{self.name}" -UNSET = Sentinel.UNSET +UNSET: t.Literal[Sentinel.UNSET] = Sentinel.UNSET """Sentinel used to indicate that a value is not set.""" -FLAG_NEEDS_VALUE = Sentinel.FLAG_NEEDS_VALUE +FLAG_NEEDS_VALUE: t.Literal[Sentinel.FLAG_NEEDS_VALUE] = Sentinel.FLAG_NEEDS_VALUE """Sentinel used to indicate an option was passed as a flag without a value but is not a flag option. ``Option.consume_value`` uses this to prompt or use the ``flag_value``. """ -T_UNSET = t.Literal[UNSET] # type: ignore[valid-type] +T_UNSET: t.TypeAlias = t.Literal[Sentinel.UNSET] """Type hint for the :data:`UNSET` sentinel value.""" -T_FLAG_NEEDS_VALUE = t.Literal[FLAG_NEEDS_VALUE] # type: ignore[valid-type] +T_FLAG_NEEDS_VALUE: t.TypeAlias = t.Literal[Sentinel.FLAG_NEEDS_VALUE] """Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value.""" diff --git a/libs/click/_winconsole.py b/libs/click/_winconsole.py index e56c7c6ae..d25178d66 100644 --- a/libs/click/_winconsole.py +++ b/libs/click/_winconsole.py @@ -28,6 +28,7 @@ from ctypes.wintypes import DWORD from ctypes.wintypes import HANDLE from ctypes.wintypes import LPCWSTR from ctypes.wintypes import LPWSTR +from gettext import gettext as _ from ._compat import _NonClosingTextIOWrapper @@ -152,7 +153,7 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase): # wait for KeyboardInterrupt time.sleep(0.1) if not rv: - raise OSError(f"Windows error: {GetLastError()}") + raise OSError(_("Windows error: {error}").format(error=GetLastError())) if buffer[0] == EOF: return 0 @@ -169,7 +170,7 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): return "ERROR_SUCCESS" elif errno == ERROR_NOT_ENOUGH_MEMORY: return "ERROR_NOT_ENOUGH_MEMORY" - return f"Windows error {errno}" + return _("Windows error: {error}").format(error=errno) def write(self, b: Buffer) -> int: bytes_to_be_written = len(b) diff --git a/libs/click/core.py b/libs/click/core.py index 57f549c79..bc04f6426 100644 --- a/libs/click/core.py +++ b/libs/click/core.py @@ -7,6 +7,8 @@ import inspect import os import sys import typing as t +from abc import ABC +from abc import abstractmethod from collections import abc from collections import Counter from contextlib import AbstractContextManager @@ -27,6 +29,7 @@ from .exceptions import ClickException from .exceptions import Exit from .exceptions import MissingParameter from .exceptions import NoArgsIsHelpError +from .exceptions import NoSuchCommand from .exceptions import UsageError from .formatting import HelpFormatter from .formatting import join_options @@ -90,6 +93,23 @@ def _check_nested_chain( raise RuntimeError(message) +def _format_deprecated_label(deprecated: bool | str) -> str: + """Return the parenthesized deprecation label shown in help text.""" + label = _("deprecated").upper() + if isinstance(deprecated, str): + return f"({label}: {deprecated})" + return f"({label})" + + +def _format_deprecated_suffix(deprecated: bool | str) -> str: + """Return the trailing reason for a ``DeprecationWarning`` message, + prefixed with a space, or an empty string when no reason was given. + """ + if isinstance(deprecated, str): + return f" {deprecated}" + return "" + + def batch(iterable: cabc.Iterable[V], batch_size: int) -> list[tuple[V, ...]]: return list(zip(*repeat(iter(iterable), batch_size), strict=False)) @@ -140,13 +160,26 @@ def iter_params_for_processing( return sorted(declaration_order, key=sort_key) -class ParameterSource(enum.Enum): - """This is an :class:`~enum.Enum` that indicates the source of a +class ParameterSource(enum.IntEnum): + """This is an :class:`~enum.IntEnum` that indicates the source of a parameter's value. Use :meth:`click.Context.get_parameter_source` to get the source for a parameter by name. + Members are ordered from most explicit to least explicit source. + This allows comparison to check if a value was explicitly provided: + + .. code-block:: python + + source = ctx.get_parameter_source("port") + if source < click.ParameterSource.DEFAULT_MAP: + ... # value was explicitly set + + .. versionchanged:: 8.3.3 + Use :class:`~enum.IntEnum` and reorder members from most to + least explicit. Supports comparison operators. + .. versionchanged:: 8.0 Use :class:`~enum.Enum` and drop the ``validate`` method. @@ -154,16 +187,16 @@ class ParameterSource(enum.Enum): Added the ``PROMPT`` value. """ + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" COMMANDLINE = enum.auto() """The value was provided by the command line args.""" ENVIRONMENT = enum.auto() """The value was provided with an environment variable.""" - DEFAULT = enum.auto() - """Used the default specified by the parameter.""" DEFAULT_MAP = enum.auto() """Used a default provided by :attr:`Context.default_map`.""" - PROMPT = enum.auto() - """Used a prompt to confirm a default or provide a value.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" class Context: @@ -183,7 +216,7 @@ class Context: :param info_name: the info name for this invocation. Generally this is the most descriptive name for the script or command. For the toplevel script it is usually - the name of the script, for commands below it it's + the name of the script, for commands below that it's the name of the script. :param obj: an arbitrary object of user data. :param auto_envvar_prefix: the prefix to use for automatic environment @@ -438,6 +471,12 @@ class Context: self._close_callbacks: list[t.Callable[[], t.Any]] = [] self._depth = 0 self._parameter_source: dict[str, ParameterSource] = {} + # Tracks whether the option that currently owns each parameter slot in + # :attr:`params` had its ``default`` set explicitly by the user. Used + # to tie-break feature-switch groups where multiple options share a + # parameter name and both fall back to their default value. + # Refs: https://github.com/pallets/click/issues/3403 + self._param_default_explicit: dict[str, bool] = {} self._exit_stack = ExitStack() @property @@ -685,6 +724,20 @@ class Context: self.obj = rv = object_type() return rv + def _default_map_has(self, name: str | None) -> bool: + """Check if :attr:`default_map` contains a real value for ``name``. + + Returns ``False`` when the key is absent, the map is ``None``, + ``name`` is ``None``, or the stored value is the internal + :data:`UNSET` sentinel. + """ + return ( + name is not None + and self.default_map is not None + and name in self.default_map + and self.default_map[name] is not UNSET + ) + @t.overload def lookup_default( self, name: str, call: t.Literal[True] = True @@ -705,15 +758,17 @@ class Context: .. versionchanged:: 8.0 Added the ``call`` parameter. """ - if self.default_map is not None: - value = self.default_map.get(name, UNSET) + if not self._default_map_has(name): + return None - if call and callable(value): - return value() + # Assert to make the type checker happy. + assert self.default_map is not None + value = self.default_map[name] - return value + if call and callable(value): + return value() - return UNSET + return value def fail(self, message: str) -> t.NoReturn: """Aborts the execution of the program with a specific error @@ -809,9 +864,7 @@ class Context: # https://github.com/pallets/click/pull/3068 if default_value is UNSET: default_value = None - kwargs[param.name] = param.type_cast_value( # type: ignore - ctx, default_value - ) + kwargs[param.name] = param.type_cast_value(ctx, default_value) # Track all kwargs as params, so that forward() will pass # them on in subsequent calls. @@ -1066,7 +1119,7 @@ class Command: # Cache the help option object in private _help_option attribute to # avoid creating it multiple times. Not doing this will break the - # callback odering by iter_params_for_processing(), which relies on + # callback ordering by iter_params_for_processing(), which relies on # object comparison. if self._help_option is None: # Avoid circular import. @@ -1106,14 +1159,7 @@ class Command: text = "" if self.deprecated: - deprecated_message = ( - f"(DEPRECATED: {self.deprecated})" - if isinstance(self.deprecated, str) - else "(DEPRECATED)" - ) - text = _("{text} {deprecated_message}").format( - text=text, deprecated_message=deprecated_message - ) + text = f"{_(text)} {_format_deprecated_label(self.deprecated)}" return text.strip() @@ -1143,14 +1189,7 @@ class Command: text = "" if self.deprecated: - deprecated_message = ( - f"(DEPRECATED: {self.deprecated})" - if isinstance(self.deprecated, str) - else "(DEPRECATED)" - ) - text = _("{text} {deprecated_message}").format( - text=text, deprecated_message=deprecated_message - ) + text = f"{_(text)} {_format_deprecated_label(self.deprecated)}" if text: formatter.write_paragraph() @@ -1257,12 +1296,12 @@ class Command: in the right way. """ if self.deprecated: - extra_message = ( - f" {self.deprecated}" if isinstance(self.deprecated, str) else "" - ) message = _( "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" - ).format(name=self.name, extra_message=extra_message) + ).format( + name=self.name, + extra_message=_format_deprecated_suffix(self.deprecated), + ) echo(style(message, fg="red"), err=True) if self.callback is not None: @@ -1291,7 +1330,7 @@ class Command: or param.hidden or ( not param.multiple - and ctx.get_parameter_source(param.name) # type: ignore + and ctx.get_parameter_source(param.name) is ParameterSource.COMMANDLINE ) ): @@ -1908,7 +1947,6 @@ class Group(Command): self, ctx: Context, args: list[str] ) -> tuple[str | None, Command | None, list[str]]: cmd_name = make_str(args[0]) - original_cmd_name = cmd_name # Get the command cmd = self.get_command(ctx, cmd_name) @@ -1928,7 +1966,7 @@ class Group(Command): if cmd is None and not ctx.resilient_parsing: if _split_opt(cmd_name)[0]: self.parse_args(ctx, args) - ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) + raise NoSuchCommand(cmd_name, possibilities=self.commands, ctx=ctx) return cmd_name if cmd else None, cmd, args[1:] def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: @@ -2024,7 +2062,7 @@ def _check_iter(value: t.Any) -> cabc.Iterator[t.Any]: return iter(value) -class Parameter: +class Parameter(ABC): r"""A parameter to a command comes in two versions: they are either :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently not supported by design as some of the internals for parsing are @@ -2120,7 +2158,7 @@ class Parameter: def __init__( self, param_decls: cabc.Sequence[str] | None = None, - type: types.ParamType | t.Any | None = None, + type: types.ParamType[t.Any] | t.Any | None = None, required: bool = False, # XXX The default historically embed two concepts: # - the declaration of a Parameter object carrying the default (handy to @@ -2146,13 +2184,13 @@ class Parameter: | None = None, deprecated: bool | str = False, ) -> None: - self.name: str | None + self.name: str self.opts: list[str] self.secondary_opts: list[str] self.name, self.opts, self.secondary_opts = self._parse_decls( param_decls or (), expose_value ) - self.type: types.ParamType = types.convert_type(type, default) + self.type: types.ParamType[t.Any] = types.convert_type(type, default) # Default nargs to what the type tells us if we have that # information available. @@ -2168,6 +2206,12 @@ class Parameter: self.multiple = multiple self.expose_value = expose_value self.default: t.Any | t.Callable[[], t.Any] | None = default + # Whether the user passed ``default`` explicitly to the constructor. + # Captured before any auto-derived default (like ``False`` for boolean + # flags in :class:`Option`) replaces the :data:`UNSET` sentinel, so it + # remains ``False`` when the default was inferred rather than chosen. + # Refs: https://github.com/pallets/click/issues/3403 + self._default_explicit: bool = default is not UNSET self.is_eager = is_eager self.metavar = metavar self.envvar = envvar @@ -2219,17 +2263,17 @@ class Parameter: def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.name}>" + @abstractmethod def _parse_decls( self, decls: cabc.Sequence[str], expose_value: bool - ) -> tuple[str | None, list[str], list[str]]: - raise NotImplementedError() + ) -> tuple[str, list[str], list[str]]: ... @property def human_readable_name(self) -> str: """Returns the human readable name of this parameter. This is the same as the name for options, but the metavar for arguments. """ - return self.name # type: ignore + return self.name def make_metavar(self, ctx: Context) -> str: if self.metavar is not None: @@ -2278,9 +2322,9 @@ class Parameter: .. versionchanged:: 8.0 Added the ``call`` parameter. """ - value = ctx.lookup_default(self.name, call=False) # type: ignore + value = ctx.lookup_default(self.name, call=False) - if value is UNSET: + if value is None and not ctx._default_map_has(self.name): value = self.default if call and callable(value): @@ -2288,8 +2332,8 @@ class Parameter: return value - def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: - raise NotImplementedError() + @abstractmethod + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: ... def consume_value( self, ctx: Context, opts: cabc.Mapping[str, t.Any] @@ -2305,7 +2349,7 @@ class Parameter: :meta private: """ # Collect from the parse the value passed by the user to the CLI. - value = opts.get(self.name, UNSET) # type: ignore + value = opts.get(self.name, UNSET) # If the value is set, it means it was sourced from the command line by the # parser, otherwise it left unset by default. source = ( @@ -2321,11 +2365,16 @@ class Parameter: source = ParameterSource.ENVIRONMENT if value is UNSET: - default_map_value = ctx.lookup_default(self.name) # type: ignore - if default_map_value is not UNSET: + default_map_value = ctx.lookup_default(self.name) + if default_map_value is not None or ctx._default_map_has(self.name): value = default_map_value source = ParameterSource.DEFAULT_MAP + # A string from default_map must be split for multi-value + # parameters, matching value_from_envvar behavior. + if isinstance(value, str) and self.nargs != 1: + value = self.type.split_envvar_value(value) + if value is UNSET: default_value = self.get_default(ctx) if default_value is not UNSET: @@ -2549,27 +2598,35 @@ class Parameter: :meta private: """ + # Capture the slot's existing state before we mutate + # ``_parameter_source`` so the write decision below can compare our + # incoming source against the source of the option that already wrote + # the slot (if any). + existing_value = ctx.params.get(self.name, UNSET) + existing_source = ctx.get_parameter_source(self.name) + existing_default_explicit = ctx._param_default_explicit.get(self.name, False) + with augment_usage_errors(ctx, param=self): value, source = self.consume_value(ctx, opts) - ctx.set_parameter_source(self.name, source) # type: ignore + # Record the source before processing so eager callbacks and type + # conversion can inspect it. Restored after arbitration if this + # option loses a feature-switch group. + ctx.set_parameter_source(self.name, source) # Display a deprecation warning if necessary. if ( self.deprecated and value is not UNSET - and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + and source < ParameterSource.DEFAULT_MAP ): - extra_message = ( - f" {self.deprecated}" if isinstance(self.deprecated, str) else "" - ) message = _( "DeprecationWarning: The {param_type} {name!r} is deprecated." "{extra_message}" ).format( param_type=self.param_type_name, name=self.human_readable_name, - extra_message=extra_message, + extra_message=_format_deprecated_suffix(self.deprecated), ) echo(style(message, fg="red"), err=True) @@ -2584,32 +2641,49 @@ class Parameter: # to UNSET, which will be interpreted as a missing value. value = UNSET - # Add parameter's value to the context. - if ( - self.expose_value - # We skip adding the value if it was previously set by another parameter - # targeting the same variable name. This prevents parameters competing for - # the same name to override each other. - and (self.name not in ctx.params or ctx.params[self.name] is UNSET) - ): - # Click is logically enforcing that the name is None if the parameter is - # not to be exposed. We still assert it here to please the type checker. - assert self.name is not None, ( - f"{self!r} parameter's name should not be None when exposing value." - ) - ctx.params[self.name] = value + # Arbitrate the slot when several parameters target the same variable + # name (feature-switch groups). See: https://github.com/pallets/click/issues/3403 + slot_empty = existing_value is UNSET + more_explicit = existing_source is not None and source < existing_source + same_source = existing_source is not None and source == existing_source + auto_would_downgrade_explicit = ( + same_source + and source == ParameterSource.DEFAULT + and existing_default_explicit + and not self._default_explicit + ) + is_winner = ( + slot_empty + or more_explicit + or (same_source and not auto_would_downgrade_explicit) + ) + + if is_winner: + if self.expose_value: + ctx.params[self.name] = value + ctx._param_default_explicit[self.name] = self._default_explicit + elif existing_source is not None: + # Lost arbitration; restore the winning option's source. + ctx.set_parameter_source(self.name, existing_source) + # else: ctx.params[self.name] was populated by code that bypassed + # handle_parse_result (from another option's callback for example). Keep + # the provisional source recorded before process_value so downstream + # lookups don't return ``None``. return value, args def get_help_record(self, ctx: Context) -> tuple[str, str] | None: - pass + return None def get_usage_pieces(self, ctx: Context) -> list[str]: return [] - def get_error_hint(self, ctx: Context) -> str: + def get_error_hint(self, ctx: Context | None) -> str: """Get a stringified version of the param for use in error messages to indicate which param caused the error. + + .. versionchanged:: 8.4.0 + ``ctx`` can be ``None``. """ hint_list = self.opts or [self.human_readable_name] return " / ".join(f"'{x}'" for x in hint_list) @@ -2618,7 +2692,7 @@ class Parameter: """Return a list of completions for the incomplete value. If a ``shell_complete`` function was given during init, it is used. Otherwise, the :attr:`type` - :meth:`~click.types.ParamType.shell_complete` function is used. + :meth:`~click.types.ParamType[t.Any].shell_complete` function is used. :param ctx: Invocation context for this command. :param incomplete: Value being completed. May be empty. @@ -2684,6 +2758,11 @@ class Option(Parameter): :param hidden: hide this option from help outputs. :param attrs: Other command arguments described in :class:`Parameter`. + .. versionchanged:: 8.4.0 + Non-basic ``flag_value`` types (not ``str``, ``int``, ``float``, or + ``bool``) are passed through unchanged instead of being stringified. + Previously, ``type=click.UNPROCESSED`` was required to preserve them. + .. versionchanged:: 8.2 ``envvar`` used with ``flag_value`` will always use the ``flag_value``, previously it would use the value of the environment variable. @@ -2701,7 +2780,8 @@ class Option(Parameter): default value is ``False``. .. versionchanged:: 8.0.1 - ``type`` is detected from ``flag_value`` if given. + ``type`` is detected from ``flag_value`` if given, for basic Python + types (``str``, ``int``, ``float``, ``bool``). """ param_type_name = "option" @@ -2719,7 +2799,7 @@ class Option(Parameter): multiple: bool = False, count: bool = False, allow_from_autoenv: bool = True, - type: types.ParamType | t.Any | None = None, + type: types.ParamType[t.Any] | t.Any | None = None, help: str | None = None, hidden: bool = False, show_choices: bool = True, @@ -2735,7 +2815,7 @@ class Option(Parameter): ) if prompt is True: - if self.name is None: + if not self.name: raise TypeError("'name' is required with 'prompt=True'.") prompt_text: str | None = self.name.replace("_", " ").capitalize() @@ -2745,12 +2825,8 @@ class Option(Parameter): prompt_text = prompt if deprecated: - deprecated_message = ( - f"(DEPRECATED: {deprecated})" - if isinstance(deprecated, str) - else "(DEPRECATED)" - ) - help = help + deprecated_message if help is not None else deprecated_message + label = _format_deprecated_label(deprecated) + help = f"{help} {label}" if help is not None else label self.prompt = prompt_text self.confirmation_prompt = confirmation_prompt @@ -2777,10 +2853,13 @@ class Option(Parameter): # Implicitly a flag because secondary options names were given. elif self.secondary_opts: is_flag = True - # The option is explicitly not a flag. But we do not know yet if it needs a - # value or not. So we look at the default value to determine it. + + # The option is explicitly not a flag, but to determine whether or not it needs + # value, we need to check if `flag_value` or `default` was set. Either one is + # sufficient. + # Ref: https://github.com/pallets/click/issues/3084 elif is_flag is False and not self._flag_needs_value: - self._flag_needs_value = self.default is UNSET + self._flag_needs_value = flag_value is not UNSET or self.default is UNSET if is_flag: # Set missing default for flags if not explicitly required or prompted. @@ -2792,13 +2871,26 @@ class Option(Parameter): if type is None: # A flag without a flag_value is a boolean flag. if flag_value is UNSET: - self.type: types.ParamType = types.BoolParamType() + self.type: types.ParamType[t.Any] = types.BoolParamType() # If the flag value is a boolean, use BoolParamType. elif isinstance(flag_value, bool): self.type = types.BoolParamType() # Otherwise, guess the type from the flag value. else: - self.type = types.convert_type(None, flag_value) + guessed = types.convert_type(None, flag_value) + if ( + isinstance(guessed, types.StringParamType) + and not isinstance(flag_value, str) + and flag_value is not None + ): + # The flag_value type couldn't be auto-detected + # (not str, int, float, or bool). Since flag_value + # is a programmer-provided Python object, not CLI + # input, pass it through unchanged instead of + # stringifying it. + self.type = types.UNPROCESSED + else: + self.type = guessed self.is_flag: bool = bool(is_flag) self.is_bool_flag: bool = bool( @@ -2811,14 +2903,12 @@ class Option(Parameter): if self.default is UNSET and not self.required: self.default = False - # Support the special case of aligning the default value with the flag_value - # for flags whose default is explicitly set to True. Note that as long as we - # have this condition, there is no way a flag can have a default set to True, - # and a flag_value set to something else. Refs: + # The alignment of default to the flag_value is resolved lazily in + # get_default() to prevent callable flag_values (like classes) from + # being instantiated. Refs: + # https://github.com/pallets/click/issues/3121 # https://github.com/pallets/click/issues/3024#issuecomment-3146199461 # https://github.com/pallets/click/pull/3030/commits/06847da - if self.default is True and self.flag_value is not UNSET: - self.default = self.flag_value # Set the default flag_value if it is not set. if self.flag_value is UNSET: @@ -2882,7 +2972,40 @@ class Option(Parameter): ) return info_dict - def get_error_hint(self, ctx: Context) -> str: + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Any | t.Callable[[], t.Any] | None: + """Return the default value for this option. + + For non-boolean flag options, ``default=True`` is treated as a sentinel + meaning "activate this flag by default" and is resolved to + :attr:`flag_value`. For example, with ``--upper/--lower`` feature + switches where ``flag_value="upper"`` and ``default=True``, the default + resolves to ``"upper"``. + + .. caution:: + This substitution only applies to non-boolean flags + (:attr:`is_bool_flag` is ``False``). For boolean flags, ``True`` is + a legitimate Python value and ``default=True`` is returned as-is. + + .. versionchanged:: 8.3.3 + ``default=True`` is no longer substituted with ``flag_value`` for + boolean flags, fixing negative boolean flags like + ``flag_value=False, default=True``. + """ + value = super().get_default(ctx, call=False) + + # Resolve default=True to flag_value lazily (here instead of + # __init__) to prevent callable flag_values (like classes) from + # being instantiated by the callable check below. + if value is True and self.is_flag and not self.is_bool_flag: + value = self.flag_value + elif call and callable(value): + value = value() + + return value + + def get_error_hint(self, ctx: Context | None) -> str: result = super().get_error_hint(ctx) if self.show_envvar and self.envvar is not None: result += f" (env var: '{self.envvar}')" @@ -2890,7 +3013,7 @@ class Option(Parameter): def _parse_decls( self, decls: cabc.Sequence[str], expose_value: bool - ) -> tuple[str | None, list[str], list[str]]: + ) -> tuple[str, list[str], list[str]]: opts = [] secondary_opts = [] name = None @@ -2899,7 +3022,7 @@ class Option(Parameter): for decl in decls: if decl.isidentifier(): if name is not None: - raise TypeError(f"Name '{name}' defined twice") + raise TypeError(_("Name '{name}' defined twice").format(name=name)) name = decl else: split_char = ";" if decl[:1] == "/" else "/" @@ -2914,8 +3037,10 @@ class Option(Parameter): secondary_opts.append(second.lstrip()) if first == second: raise ValueError( - f"Boolean option {decl!r} cannot use the" - " same flag for true/false." + _( + "Boolean option {decl!r} cannot use the" + " same flag for true/false." + ).format(decl=decl) ) else: possible_names.append(_split_opt(decl)) @@ -2929,16 +3054,20 @@ class Option(Parameter): if name is None: if not expose_value: - return None, opts, secondary_opts + return "", opts, secondary_opts raise TypeError( - f"Could not determine name for option with declarations {decls!r}" + _( + "Could not determine name for option with declarations {decls!r}" + ).format(decls=decls) ) if not opts and not secondary_opts: raise TypeError( - f"No options defined but a name was passed ({name})." - " Did you mean to declare an argument instead? Did" - f" you mean to pass '--{name}'?" + _( + "No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + " you mean to pass '--{name}'?" + ).format(name=name) ) return name, opts, secondary_opts @@ -3037,7 +3166,7 @@ class Option(Parameter): if ( self.allow_from_autoenv and ctx.auto_envvar_prefix is not None - and self.name is not None + and self.name ): envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" @@ -3088,7 +3217,7 @@ class Option(Parameter): )[1] elif self.is_bool_flag and not self.secondary_opts and not default_value: default_string = "" - elif default_value == "": + elif isinstance(default_value, str) and default_value == "": default_string = '""' else: default_string = str(default_value) @@ -3142,10 +3271,10 @@ class Option(Parameter): default = bool(default) return confirm(self.prompt, default) - # If show_default is set to True/False, provide this to `prompt` as well. For - # non-bool values of `show_default`, we use `prompt`'s default behavior + # If show_default is given, provide this to `prompt` as well, + # otherwise we use `prompt`'s default behavior prompt_kwargs: t.Any = {} - if isinstance(self.show_default, bool): + if self.show_default is not None: prompt_kwargs["show_default"] = self.show_default return prompt( @@ -3176,11 +3305,7 @@ class Option(Parameter): if rv is not None: return rv - if ( - self.allow_from_autoenv - and ctx.auto_envvar_prefix is not None - and self.name is not None - ): + if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None and self.name: envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" rv = os.environ.get(envvar) @@ -3263,7 +3388,7 @@ class Option(Parameter): self.is_flag and value is True and not self.is_bool_flag - and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + and source < ParameterSource.DEFAULT_MAP ): value = self.flag_value @@ -3273,7 +3398,8 @@ class Option(Parameter): elif ( self.multiple and value is not UNSET - and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + and isinstance(value, cabc.Iterable) + and source < ParameterSource.DEFAULT_MAP and any(v is FLAG_NEEDS_VALUE for v in value) ): value = [self.flag_value if v is FLAG_NEEDS_VALUE else v for v in value] @@ -3282,10 +3408,7 @@ class Option(Parameter): # The value wasn't set, or used the param's default, prompt for one to the user # if prompting is enabled. elif ( - ( - value is UNSET - or source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) - ) + (value is UNSET or source >= ParameterSource.DEFAULT_MAP) and self.prompt is not None and (self.required or self.prompt_required) and not ctx.resilient_parsing @@ -3348,14 +3471,14 @@ class Argument(Parameter): def human_readable_name(self) -> str: if self.metavar is not None: return self.metavar - return self.name.upper() # type: ignore + return self.name.upper() def make_metavar(self, ctx: Context) -> str: if self.metavar is not None: return self.metavar var = self.type.get_metavar(param=self, ctx=ctx) if not var: - var = self.name.upper() # type: ignore + var = self.name.upper() if self.deprecated: var += "!" if not self.required: @@ -3366,26 +3489,30 @@ class Argument(Parameter): def _parse_decls( self, decls: cabc.Sequence[str], expose_value: bool - ) -> tuple[str | None, list[str], list[str]]: + ) -> tuple[str, list[str], list[str]]: if not decls: if not expose_value: - return None, [], [] + return "", [], [] raise TypeError("Argument is marked as exposed, but does not have a name.") if len(decls) == 1: name = arg = decls[0] name = name.replace("-", "_").lower() else: raise TypeError( - "Arguments take exactly one parameter declaration, got" - f" {len(decls)}: {decls}." + _( + "Arguments take exactly one parameter declaration, got" + " {length}: {decls}." + ).format(length=len(decls), decls=decls) ) return name, [arg], [] def get_usage_pieces(self, ctx: Context) -> list[str]: return [self.make_metavar(ctx)] - def get_error_hint(self, ctx: Context) -> str: - return f"'{self.make_metavar(ctx)}'" + def get_error_hint(self, ctx: Context | None) -> str: + if ctx is not None: + return f"'{self.make_metavar(ctx)}'" + return f"'{self.human_readable_name}'" def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/libs/click/decorators.py b/libs/click/decorators.py index 21f4c3422..14aee42ea 100644 --- a/libs/click/decorators.py +++ b/libs/click/decorators.py @@ -396,8 +396,8 @@ def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], kwargs.setdefault("is_flag", True) kwargs.setdefault("callback", callback) kwargs.setdefault("expose_value", False) - kwargs.setdefault("prompt", "Do you want to continue?") - kwargs.setdefault("help", "Confirm the action without prompting.") + kwargs.setdefault("prompt", _("Do you want to continue?")) + kwargs.setdefault("help", _("Confirm the action without prompting.")) return option(*param_decls, **kwargs) diff --git a/libs/click/exceptions.py b/libs/click/exceptions.py index 4d782ee36..68c280603 100644 --- a/libs/click/exceptions.py +++ b/libs/click/exceptions.py @@ -23,6 +23,15 @@ def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None return param_hint +def _format_possibilities(possibilities: list[str]) -> str: + possibility_str = ", ".join(repr(p) for p in sorted(possibilities)) + return ngettext( + "Did you mean {possibility}?", + "(Did you mean one of: {possibilities}?)", + len(possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + + class ClickException(Exception): """An exception that Click can handle and show to the user.""" @@ -78,8 +87,12 @@ class UsageError(ClickException): self.ctx is not None and self.ctx.command.get_help_option(self.ctx) is not None ): + help_names = self.ctx.command.get_help_option_names(self.ctx) + # Pick the longest name (like ``--help`` over ``-h``) for + # readability in error messages. hint = _("Try '{command} {option}' for help.").format( - command=self.ctx.command_path, option=self.ctx.help_option_names[0] + command=self.ctx.command_path, + option=max(help_names, key=len), ) hint = f"{hint}\n" if self.ctx is not None: @@ -125,7 +138,7 @@ class BadParameter(UsageError): if self.param_hint is not None: param_hint = self.param_hint elif self.param is not None: - param_hint = self.param.get_error_hint(self.ctx) # type: ignore + param_hint = self.param.get_error_hint(self.ctx) else: return _("Invalid value: {message}").format(message=self.message) @@ -161,7 +174,7 @@ class MissingParameter(BadParameter): if self.param_hint is not None: param_hint: cabc.Sequence[str] | str | None = self.param_hint elif self.param is not None: - param_hint = self.param.get_error_hint(self.ctx) # type: ignore + param_hint = self.param.get_error_hint(self.ctx) else: param_hint = None @@ -206,8 +219,7 @@ class MissingParameter(BadParameter): class NoSuchOption(UsageError): - """Raised if click attempted to handle an option that does not - exist. + """Raised if Click attempted to handle an option that does not exist. .. versionadded:: 4.0 """ @@ -216,27 +228,54 @@ class NoSuchOption(UsageError): self, option_name: str, message: str | None = None, - possibilities: cabc.Sequence[str] | None = None, + possibilities: cabc.Iterable[str] | None = None, ctx: Context | None = None, ) -> None: if message is None: - message = _("No such option: {name}").format(name=option_name) + message = _("No such option {name!r}.").format(name=option_name) super().__init__(message, ctx) self.option_name = option_name - self.possibilities = possibilities + self.possibilities: list[str] | None = None + if possibilities: + from difflib import get_close_matches + + self.possibilities = get_close_matches(option_name, possibilities) def format_message(self) -> str: if not self.possibilities: return self.message + return f"{self.message} {_format_possibilities(self.possibilities)}" - possibility_str = ", ".join(sorted(self.possibilities)) - suggest = ngettext( - "Did you mean {possibility}?", - "(Possible options: {possibilities})", - len(self.possibilities), - ).format(possibility=possibility_str, possibilities=possibility_str) - return f"{self.message} {suggest}" + +class NoSuchCommand(UsageError): + """Raised if Click attempted to handle a command that does not exist. + + .. versionadded:: 8.4.0 + """ + + def __init__( + self, + command_name: str, + message: str | None = None, + possibilities: cabc.Iterable[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such command {name!r}.").format(name=command_name) + + super().__init__(message, ctx) + self.command_name = command_name + self.possibilities: list[str] | None = None + if possibilities: + from difflib import get_close_matches + + self.possibilities = get_close_matches(command_name, possibilities) + + def format_message(self) -> str: + if not self.possibilities: + return self.message + return f"{self.message} {_format_possibilities(self.possibilities)}" class BadOptionUsage(UsageError): diff --git a/libs/click/formatting.py b/libs/click/formatting.py index 0b64f831b..886a4ada7 100644 --- a/libs/click/formatting.py +++ b/libs/click/formatting.py @@ -52,6 +52,12 @@ def wrap_text( each consecutive line. :param preserve_paragraphs: if this flag is set then the wrapping will intelligently handle paragraphs. + + .. versionchanged:: 8.4.0 + Width is measured in visible characters. ANSI escape sequences in + ``text``, ``initial_indent``, or ``subsequent_indent`` no longer + count toward the width budget, so styled input wraps based on what + the user sees instead of raw byte length. """ from ._textwrap import TextWrapper @@ -153,11 +159,19 @@ class HelpFormatter: ``"Usage: "``. """ if prefix is None: - prefix = f"{_('Usage:')} " + prefix = "{usage} ".format(usage=_("Usage:")) usage_prefix = f"{prefix:>{self.current_indent}}{prog} " text_width = self.width - self.current_indent + if not args: + # Without args, the prefix's trailing space and the wrap_text + # call that would normally place args on the line are both + # unnecessary. Emit just the prefix line. + self.write(usage_prefix.rstrip(" ")) + self.write("\n") + return + if text_width >= (term_len(usage_prefix) + 20): # The arguments will fit to the right of the prefix. indent = " " * term_len(usage_prefix) diff --git a/libs/click/parser.py b/libs/click/parser.py index 1ea1f7166..4fcbf7caa 100644 --- a/libs/click/parser.py +++ b/libs/click/parser.py @@ -50,7 +50,7 @@ V = t.TypeVar("V") def _unpack_args( args: cabc.Sequence[str], nargs_spec: cabc.Sequence[int] -) -> tuple[cabc.Sequence[str | cabc.Sequence[str | None] | None], list[str]]: +) -> tuple[cabc.Sequence[str | cabc.Sequence[str | T_UNSET] | T_UNSET], list[str]]: """Given an iterable of arguments and an iterable of nargs specifications, it returns a tuple with all the unpacked arguments at the first index and all remaining arguments as the second. @@ -65,7 +65,7 @@ def _unpack_args( rv: list[str | tuple[str | T_UNSET, ...] | T_UNSET] = [] spos: int | None = None - def _fetch(c: deque[V]) -> V | T_UNSET: + def _fetch(c: deque[str]) -> str | T_UNSET: try: if spos is None: return c.popleft() @@ -75,15 +75,15 @@ def _unpack_args( return UNSET while nargs_spec: - nargs = _fetch(nargs_spec) - - if nargs is None: - continue + if spos is None: + nargs = nargs_spec.popleft() + else: + nargs = nargs_spec.pop() if nargs == 1: - rv.append(_fetch(args)) # type: ignore[arg-type] + rv.append(_fetch(args)) elif nargs > 1: - x = [_fetch(args) for _ in range(nargs)] + x: list[str | T_UNSET] = [_fetch(args) for _ in range(nargs)] # If we're reversed, we're pulling in the arguments in reverse, # so we need to turn them around. @@ -141,7 +141,11 @@ class _Option: for opt in opts: prefix, value = _split_opt(opt) if not prefix: - raise ValueError(f"Invalid start character for option ({opt})") + raise ValueError( + _("Invalid start character for option ({option})").format( + option=opt + ) + ) self.prefixes.add(prefix[0]) if len(prefix) == 1 and len(value) == 1: self._short_opts.append(opt) @@ -186,12 +190,12 @@ class _Argument: def process( self, - value: str | cabc.Sequence[str | None] | None | T_UNSET, + value: str | cabc.Sequence[str | T_UNSET] | T_UNSET, state: _ParsingState, ) -> None: if self.nargs > 1: assert isinstance(value, cabc.Sequence) - holes = sum(1 for x in value if x is UNSET) + holes = sum(x is UNSET for x in value) if holes == len(value): value = UNSET elif holes != 0: @@ -360,10 +364,7 @@ class _OptionParser: self, opt: str, explicit_value: str | None, state: _ParsingState ) -> None: if opt not in self._long_opt: - from difflib import get_close_matches - - possibilities = get_close_matches(opt, self._long_opt) - raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + raise NoSuchOption(opt, possibilities=self._long_opt, ctx=self.ctx) option = self._long_opt[opt] if option.takes_value: @@ -421,17 +422,17 @@ class _OptionParser: # If we got any unknown options we recombine the string of the # remaining options and re-attach the prefix, then report that - # to the state as new larg. This way there is basic combinatorics + # to the state as new large. This way there is basic combinatorics # that can be achieved while still ignoring unknown arguments. if self.ignore_unknown_options and unknown_options: state.largs.append(f"{prefix}{''.join(unknown_options)}") def _get_value_from_state( self, option_name: str, option: _Option, state: _ParsingState - ) -> str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE: + ) -> str | cabc.Sequence[str] | T_UNSET | T_FLAG_NEEDS_VALUE: nargs = option.nargs - value: str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE + value: str | cabc.Sequence[str] | T_UNSET | T_FLAG_NEEDS_VALUE if len(state.rargs) < nargs: if option.obj._flag_needs_value: diff --git a/libs/click/shell_completion.py b/libs/click/shell_completion.py index 8f1564c49..b01e290a9 100644 --- a/libs/click/shell_completion.py +++ b/libs/click/shell_completion.py @@ -43,12 +43,13 @@ def shell_complete( comp = comp_cls(cli, ctx_args, prog_name, complete_var) + # Write bytes, otherwise Windows text stdout translates LF to CRLF and breaks. if instruction == "source": - echo(comp.source()) + echo(comp.source().encode(), nl=False) return 0 if instruction == "complete": - echo(comp.complete()) + echo(comp.complete().encode()) return 0 return 1 @@ -180,14 +181,18 @@ function %(complete_func)s; COMP_CWORD=(commandline -t) %(prog_name)s); for completion in $response; - set -l metadata (string split "," $completion); + set -l metadata (string split \n $completion); if test $metadata[1] = "dir"; __fish_complete_directories $metadata[2]; else if test $metadata[1] = "file"; __fish_complete_path $metadata[2]; else if test $metadata[1] = "plain"; - echo $metadata[2]; + if test $metadata[3] != "_"; + echo $metadata[2]\t$metadata[3]; + else; + echo $metadata[2]; + end; end; end; end; @@ -417,10 +422,20 @@ class FishComplete(ShellComplete): return args, incomplete def format_completion(self, item: CompletionItem) -> str: - if item.help: - return f"{item.type},{item.value}\t{item.help}" - - return f"{item.type},{item.value}" + """ + .. versionchanged:: 8.4.0 + Escape newlines in value and help to fix completion errors with + multi-line help strings. + """ + # The fish completion script splits each response line on literal + # newlines, so any newline in the value or help would corrupt the + # frame. Replace them with the two-character escape "\n" so the text + # round-trips through fish without breaking the format. The "_" + # sentinel for missing help mirrors :class:`ZshComplete`. + help_ = item.help or "_" + value = item.value.replace("\n", r"\n") + help_escaped = help_.replace("\n", r"\n") + return f"{item.type}\n{value}\n{help_escaped}" ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]") @@ -511,8 +526,6 @@ def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: if not isinstance(param, Argument): return False - assert param.name is not None - # Will be None if expose_value is False. value = ctx.params.get(param.name) return ( param.nargs == -1 diff --git a/libs/click/termui.py b/libs/click/termui.py index 2e98a0771..91ef6e19b 100644 --- a/libs/click/termui.py +++ b/libs/click/termui.py @@ -4,13 +4,16 @@ import collections.abc as cabc import inspect import io import itertools +import re import sys import typing as t from contextlib import AbstractContextManager +from contextlib import redirect_stdout from gettext import gettext as _ from ._compat import isatty from ._compat import strip_ansi +from ._compat import WIN from .exceptions import Abort from .exceptions import UsageError from .globals import resolve_color_default @@ -51,23 +54,66 @@ _ansi_colors = { _ansi_reset_all = "\033[0m" +_HIDDEN_INPUT_MASK = "'***'" + + +def _mask_hidden_input(message: str, value: str) -> str: + """Replace occurrences of ``value`` in ``message`` with a fixed mask. + + Both ``repr(value)`` (the form built-in :class:`ParamType` errors use + via ``{value!r}``) and the raw value are masked. The raw-value pass + uses word-boundary lookarounds so a substring like ``"1"`` does not + match inside ``"10"``, and ``"ent"`` does not match inside + ``"Authentication"``. The empty string is skipped to avoid matching + at every boundary. + """ + message = message.replace(repr(value), _HIDDEN_INPUT_MASK) + if value: + message = re.sub( + rf"(? str: import getpass return getpass.getpass(prompt) +def _readline_prompt(func: t.Callable[[str], str], text: str, err: bool) -> str: + """Call a prompt function, passing the full prompt on non-Windows so + readline can handle line editing and cursor positioning correctly. + + On Windows the prompt is written separately via :func:`echo` for + colorama support, with only the last character passed to *func*. + """ + if WIN: + # Write the prompt separately so that we get nice coloring + # through colorama on Windows. + echo(text[:-1], nl=False, err=err) + # Echo the last character to stdout to work around an issue + # where readline causes backspace to clear the whole line. + return func(text[-1:]) + if err: + with redirect_stdout(sys.stderr): + return func(text) + return func(text) + + def _build_prompt( text: str, suffix: str, - show_default: bool = False, + show_default: bool | str = False, default: t.Any | None = None, show_choices: bool = True, - type: ParamType | None = None, + type: ParamType[t.Any] | None = None, ) -> str: prompt = text if type is not None and show_choices and isinstance(type, Choice): prompt += f" ({', '.join(map(str, type.choices))})" + if isinstance(show_default, str): + default = f"({show_default})" if default is not None and show_default: prompt = f"{prompt} [{_format_default(default)}]" return f"{prompt}{suffix}" @@ -85,10 +131,10 @@ def prompt( default: t.Any | None = None, hide_input: bool = False, confirmation_prompt: bool | str = False, - type: ParamType | t.Any | None = None, + type: ParamType[t.Any] | t.Any | None = None, value_proc: t.Callable[[str], t.Any] | None = None, prompt_suffix: str = ": ", - show_default: bool = True, + show_default: bool | str = True, err: bool = False, show_choices: bool = True, ) -> t.Any: @@ -112,6 +158,8 @@ def prompt( convert a value. :param prompt_suffix: a suffix that should be added to the prompt. :param show_default: shows or hides the default value in the prompt. + If this value is a string, it shows that string + in parentheses instead of the actual value. :param err: if set to true the file defaults to ``stderr`` instead of ``stdout``, the same as with echo. :param show_choices: Show or hide choices if the passed type is a Choice. @@ -119,6 +167,10 @@ def prompt( show_choices is true and text is "Group by" then the prompt will be "Group by (day, week): ". + .. versionchanged:: 8.3.3 + ``show_default`` can be a string to show a custom value instead + of the actual default, matching the help text behavior. + .. versionchanged:: 8.3.1 A space is no longer appended to the prompt. @@ -139,12 +191,7 @@ def prompt( def prompt_func(text: str) -> str: f = hidden_prompt_func if hide_input else visible_prompt_func try: - # Write the prompt separately so that we get nice - # coloring through colorama on Windows - echo(text[:-1], nl=False, err=err) - # Echo the last character to stdout to work around an issue where - # readline causes backspace to clear the whole line. - return f(text[-1:]) + return _readline_prompt(f, text, err) except (KeyboardInterrupt, EOFError): # getpass doesn't print a newline if the user aborts input with ^C. # Allegedly this behavior is inherited from getpass(3). @@ -177,10 +224,8 @@ def prompt( try: result = value_proc(value) except UsageError as e: - if hide_input: - echo(_("Error: The value you entered was invalid."), err=err) - else: - echo(_("Error: {e.message}").format(e=e), err=err) + message = _mask_hidden_input(e.message, value) if hide_input else e.message + echo(_("Error: {message}").format(message=message), err=err) continue if not confirmation_prompt: return result @@ -235,12 +280,7 @@ def confirm( while True: try: - # Write the prompt separately so that we get nice - # coloring through colorama on Windows - echo(prompt[:-1], nl=False, err=err) - # Echo the last character to stdout to work around an issue where - # readline causes backspace to clear the whole line. - value = visible_prompt_func(prompt[-1:]).lower().strip() + value = _readline_prompt(visible_prompt_func, prompt, err).lower().strip() except (KeyboardInterrupt, EOFError): raise Abort() from None if value in ("y", "yes"): @@ -258,6 +298,25 @@ def confirm( return rv +def get_pager_file( + color: bool | None = None, +) -> t.ContextManager[t.TextIO]: + """Context manager. + + Yields a writable file-like object which can be used as an output pager. + + .. versionadded:: 8.4.0 + + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + from ._termui_impl import get_pager_file + + color = resolve_color_default(color) + + return get_pager_file(color=color) + + def echo_via_pager( text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str, color: bool | None = None, @@ -273,7 +332,6 @@ def echo_via_pager( :param color: controls if the pager supports ANSI colors or not. The default is autodetection. """ - color = resolve_color_default(color) if inspect.isgeneratorfunction(text_or_generator): i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)() @@ -285,9 +343,9 @@ def echo_via_pager( # convert every element of i to a text type if necessary text_generator = (el if isinstance(el, str) else str(el) for el in i) - from ._termui_impl import pager - - return pager(itertools.chain(text_generator, "\n"), color) + with get_pager_file(color=color) as pager: + for text in itertools.chain(text_generator, "\n"): + pager.write(text) @t.overload @@ -614,13 +672,13 @@ def style( try: bits.append(f"\033[{_interpret_color(fg)}m") except KeyError: - raise TypeError(f"Unknown color {fg!r}") from None + raise TypeError(_("Unknown color {colour!r}").format(colour=fg)) from None if bg: try: bits.append(f"\033[{_interpret_color(bg, 10)}m") except KeyError: - raise TypeError(f"Unknown color {bg!r}") from None + raise TypeError(_("Unknown color {colour!r}").format(colour=bg)) from None if bold is not None: bits.append(f"\033[{1 if bold else 22}m") diff --git a/libs/click/testing.py b/libs/click/testing.py index f6f60b809..1eb81d29e 100644 --- a/libs/click/testing.py +++ b/libs/click/testing.py @@ -4,6 +4,7 @@ import collections.abc as cabc import contextlib import io import os +import pdb import shlex import sys import tempfile @@ -21,6 +22,8 @@ if t.TYPE_CHECKING: from .core import Command +CaptureMode = t.Literal["sys", "fd"] + class EchoingStdin: def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: @@ -66,6 +69,39 @@ def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]: stream._paused = False +class _FDCapture: + """Redirect a file descriptor to a temporary file for capture. + + Saves the current target of *targetfd* via :func:`os.dup`, then + redirects it to a temporary file via :func:`os.dup2`. On + :meth:`stop`, restores the original ``fd`` and returns the captured + bytes. Inspired by Pytest's ``FDCapture``. + + .. versionadded:: 8.4.0 + """ + + def __init__(self, targetfd: int) -> None: + self._targetfd = targetfd + self.saved_fd: int = -1 + self._tmpfile: t.BinaryIO | None = None + + def start(self) -> None: + self.saved_fd = os.dup(self._targetfd) + self._tmpfile = tempfile.TemporaryFile(buffering=0) + os.dup2(self._tmpfile.fileno(), self._targetfd) + + def stop(self) -> bytes: + assert self._tmpfile is not None, "_FDCapture.start() was not called" + os.dup2(self.saved_fd, self._targetfd) + os.close(self.saved_fd) + self.saved_fd = -1 + self._tmpfile.seek(0) + data = self._tmpfile.read() + self._tmpfile.close() + self._tmpfile = None + return data + + class BytesIOCopy(io.BytesIO): """Patch ``io.BytesIO`` to let the written stream be copied to another. @@ -98,26 +134,48 @@ class StreamMixer: self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output) self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output) - def __del__(self) -> None: - """ - Guarantee that embedded file-like objects are closed in a - predictable order, protecting against races between - self.output being closed and other streams being flushed on close - - .. versionadded:: 8.2.2 - """ - self.stderr.close() - self.stdout.close() - self.output.close() - class _NamedTextIOWrapper(io.TextIOWrapper): + """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode`` + that does not close its underlying buffer. + + When ``CliRunner`` runs in ``fd`` mode, ``_original_fd`` is patched to + point at the saved (pre-redirection) ``fd``, so C-level consumers that call + :meth:`fileno` (like ``faulthandler`` or ``subprocess``) keep working. In + the default ``sys`` mode ``_original_fd`` stays at ``-1`` and + :meth:`fileno` raises :exc:`io.UnsupportedOperation`, matching the + pre-``8.3.3`` behavior. + """ + def __init__( - self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any + self, + buffer: t.BinaryIO, + name: str, + mode: str, + **kwargs: t.Any, ) -> None: super().__init__(buffer, **kwargs) self._name = name self._mode = mode + self._original_fd: int = -1 + + def close(self) -> None: + """The buffer this object contains belongs to some other object, + so prevent the default ``__del__`` implementation from closing + that buffer. + + .. versionadded:: 8.3.2 + """ + + def fileno(self) -> int: + """Return the file descriptor of the saved original stream when + ``CliRunner`` runs in ``fd`` mode. Otherwise delegate to + :class:`~io.TextIOWrapper`, which raises + :exc:`io.UnsupportedOperation` for a ``BytesIO``-backed buffer. + """ + if self._original_fd >= 0: + return self._original_fd + return super().fileno() @property def name(self) -> str: @@ -240,6 +298,21 @@ class CliRunner: will automatically echo the input. :param catch_exceptions: Whether to catch any exceptions other than ``SystemExit`` when running :meth:`~CliRunner.invoke`. + :param capture: Selects the output capture strategy. ``sys`` (default) + captures Python-level writes only and leaves + :meth:`sys.stdout.fileno` raising :exc:`io.UnsupportedOperation`, so + user code that calls :func:`os.dup2` on ``sys.stdout.fileno()`` cannot + clobber the host runner's stdout. ``fd`` redirects file descriptors + ``1`` and ``2`` via :func:`os.dup2` to a temporary file, also catching + output from stale stream references, C extensions, and subprocesses. + ``fd`` is not supported on Windows. + + .. versionchanged:: 8.4.0 + Added the ``capture`` parameter. The default ``sys`` mode no longer + exposes the original fd through :meth:`fileno`, reverting the change + introduced in ``8.3.3`` that broke Pytest's ``fd``-level capture + teardown. Use ``capture="fd"`` to restore that behavior with proper + isolation. :issue:`3384` .. versionchanged:: 8.2 Added the ``catch_exceptions`` parameter. @@ -254,11 +327,21 @@ class CliRunner: env: cabc.Mapping[str, str | None] | None = None, echo_stdin: bool = False, catch_exceptions: bool = True, + capture: CaptureMode = "sys", ) -> None: + if capture not in {"sys", "fd"}: + raise ValueError( + f"capture={capture!r} is not valid. Choose from 'sys' or 'fd'." + ) + if capture == "fd" and sys.platform == "win32": + raise ValueError( + f"capture={capture!r} is not supported on Windows. Use 'sys'." + ) self.charset = charset self.env: cabc.Mapping[str, str | None] = env or {} self.echo_stdin = echo_stdin self.catch_exceptions = catch_exceptions + self.capture: CaptureMode = capture def get_default_prog_name(self, cli: Command) -> str: """Given a command object it will return the default program name @@ -338,7 +421,10 @@ class CliRunner: text_input._CHUNK_SIZE = 1 # type: ignore sys.stdout = _NamedTextIOWrapper( - stream_mixer.stdout, encoding=self.charset, name="", mode="w" + stream_mixer.stdout, + encoding=self.charset, + name="", + mode="w", ) sys.stderr = _NamedTextIOWrapper( @@ -393,12 +479,52 @@ class CliRunner: old__getchar_func = termui._getchar old_should_strip_ansi = utils.should_strip_ansi # type: ignore old__compat_should_strip_ansi = _compat.should_strip_ansi + old_pdb_init = pdb.Pdb.__init__ termui.visible_prompt_func = visible_input termui.hidden_prompt_func = hidden_input termui._getchar = _getchar utils.should_strip_ansi = should_strip_ansi # type: ignore _compat.should_strip_ansi = should_strip_ansi + def _patched_pdb_init( + self: pdb.Pdb, + completekey: str = "tab", + stdin: t.IO[str] | None = None, + stdout: t.IO[str] | None = None, + **kwargs: t.Any, + ) -> None: + """Default ``pdb.Pdb`` to real terminal streams during + ``CliRunner`` isolation. + + Without this patch, ``pdb.Pdb.__init__`` inherits from + ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout`` + when no explicit streams are provided. During isolation + those are ``BytesIO``-backed wrappers, so the debugger + reads from an empty buffer and writes to captured output, + making interactive debugging impossible. + + By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the + original terminal streams Python preserves regardless of + redirection), debuggers can interact with the user while + ``click.echo`` output is still captured normally. + + This covers ``pdb.set_trace()``, ``breakpoint()``, + ``pdb.post_mortem()``, and debuggers that subclass + ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout`` + arguments are honored and not overridden. Debuggers that + do not subclass ``pdb.Pdb`` (pudb, debugpy) are not + covered. + """ + if stdin is None: + stdin = sys.__stdin__ + if stdout is None: + stdout = sys.__stdout__ + old_pdb_init( + self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs + ) + + pdb.Pdb.__init__ = _patched_pdb_init # type: ignore[assignment] + old_env = {} try: for key, value in env.items(): @@ -429,6 +555,7 @@ class CliRunner: utils.should_strip_ansi = old_should_strip_ansi # type: ignore _compat.should_strip_ansi = old__compat_should_strip_ansi formatting.FORCED_WIDTH = old_forced_width + pdb.Pdb.__init__ = old_pdb_init # type: ignore[method-assign] def invoke( self, @@ -487,7 +614,27 @@ class CliRunner: if catch_exceptions is None: catch_exceptions = self.catch_exceptions + # Set up fd capture before isolation replaces sys.stdout and sys.stderr. + cap_out: _FDCapture | None = None + cap_err: _FDCapture | None = None + + if self.capture == "fd": + cap_out = _FDCapture(1) + cap_err = _FDCapture(2) + try: + cap_out.start() + cap_err.start() + except OSError: + cap_out = cap_err = None + with self.isolation(input=input, env=env, color=color) as outstreams: + # Point the captured streams' fileno() at the saved (original) + # fd so that C-level consumers like faulthandler keep working + # while fd 1/2 are redirected to the capture tmpfile. + if cap_out is not None and cap_err is not None: + sys.stdout._original_fd = cap_out.saved_fd # type: ignore[union-attr] + sys.stderr._original_fd = cap_err.saved_fd # type: ignore[union-attr] + return_value = None exception: BaseException | None = None exit_code = 0 @@ -528,6 +675,18 @@ class CliRunner: finally: sys.stdout.flush() sys.stderr.flush() + + # Stop fd capture and merge the captured bytes into + # the stdout/stderr BytesIO streams. BytesIOCopy mirrors + # those writes into outstreams[2] automatically. + if cap_out is not None and cap_err is not None: + fd_out = cap_out.stop() + fd_err = cap_err.stop() + if fd_out: + outstreams[0].write(fd_out) + if fd_err: + outstreams[1].write(fd_err) + stdout = outstreams[0].getvalue() stderr = outstreams[1].getvalue() output = outstreams[2].getvalue() diff --git a/libs/click/types.py b/libs/click/types.py index e71c1c21e..98c2d616b 100644 --- a/libs/click/types.py +++ b/libs/click/types.py @@ -1,11 +1,13 @@ from __future__ import annotations +import abc import collections.abc as cabc import enum import os import stat import sys import typing as t +import uuid from datetime import datetime from gettext import gettext as _ from gettext import ngettext @@ -27,7 +29,12 @@ if t.TYPE_CHECKING: ParamTypeValue = t.TypeVar("ParamTypeValue") -class ParamType: +class ParamTypeInfoDict(t.TypedDict): + param_type: str + name: str + + +class ParamType(t.Generic[ParamTypeValue], abc.ABC): """Represents the type of a parameter. Validates and converts values from the command line or Python into the correct type. @@ -43,6 +50,12 @@ class ParamType: - It must be able to convert a value if the ``ctx`` and ``param`` arguments are ``None``. This can occur when converting prompt input. + + .. versionchanged:: 8.4.0 + Now a generic abstract base class. Parameterize with the + converted value type (``ParamType[int]`` for an integer-returning + type) so that :meth:`convert` and downstream consumers carry the + narrowed return type. """ is_composite: t.ClassVar[bool] = False @@ -59,7 +72,7 @@ class ParamType: #: Windows). envvar_list_splitter: t.ClassVar[str | None] = None - def to_info_dict(self) -> dict[str, t.Any]: + def to_info_dict(self) -> ParamTypeInfoDict: """Gather information that could be useful for a tool generating user-facing documentation. @@ -85,9 +98,10 @@ class ParamType: value: t.Any, param: Parameter | None = None, ctx: Context | None = None, - ) -> t.Any: + ) -> ParamTypeValue | None: if value is not None: return self.convert(value, param, ctx) + return None def get_metavar(self, param: Parameter, ctx: Context) -> str | None: """Returns the metavar default for this param if it provides one.""" @@ -101,7 +115,7 @@ class ParamType: def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> ParamTypeValue: """Convert the value to the correct type. This is not called if the value is ``None`` (the missing value). @@ -121,7 +135,9 @@ class ParamType: :param ctx: The current context that arrived at this value. May be ``None``. """ - return value + # The default returns the value as-is so subclasses that only customize + # metadata are not forced to redeclare ``convert``. + return t.cast("ParamTypeValue", value) def split_envvar_value(self, rv: str) -> cabc.Sequence[str]: """Given a value from an environment variable this splits it up @@ -160,39 +176,44 @@ class ParamType: return [] -class CompositeParamType(ParamType): +class CompositeParamType(ParamType[ParamTypeValue]): is_composite = True @property - def arity(self) -> int: # type: ignore - raise NotImplementedError() + @abc.abstractmethod + def arity(self) -> int: ... # type: ignore[override] -class FuncParamType(ParamType): - def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: +class FuncParamTypeInfoDict(ParamTypeInfoDict): + func: t.Callable[[t.Any], t.Any] + + +class FuncParamType(ParamType[ParamTypeValue]): + def __init__(self, func: t.Callable[[t.Any], ParamTypeValue]) -> None: self.name: str = func.__name__ self.func = func - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["func"] = self.func - return info_dict + def to_info_dict(self) -> FuncParamTypeInfoDict: + return {"func": self.func, **super().to_info_dict()} def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> ParamTypeValue: try: return self.func(value) - except ValueError: - try: - value = str(value) - except UnicodeError: - value = value.decode("utf-8", "replace") + except ValueError as exc: + message = str(exc) - self.fail(value, param, ctx) + if not message: + try: + message = str(value) + except UnicodeError: + message = value.decode("utf-8", "replace") + + self.fail(message, param, ctx) -class UnprocessedParamType(ParamType): +class UnprocessedParamType(ParamType[t.Any]): name = "text" def convert( @@ -204,12 +225,12 @@ class UnprocessedParamType(ParamType): return "UNPROCESSED" -class StringParamType(ParamType): +class StringParamType(ParamType[str]): name = "text" def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> str: if isinstance(value, bytes): enc = _get_argv_encoding() try: @@ -223,14 +244,19 @@ class StringParamType(ParamType): value = value.decode("utf-8", "replace") else: value = value.decode("utf-8", "replace") - return value + return value # type: ignore[no-any-return] return str(value) def __repr__(self) -> str: return "STRING" -class Choice(ParamType, t.Generic[ParamTypeValue]): +class ChoiceInfoDict(ParamTypeInfoDict): + choices: cabc.Sequence[t.Any] + case_sensitive: bool + + +class Choice(ParamType[ParamTypeValue], t.Generic[ParamTypeValue]): """The choice type allows a value to be checked against a fixed set of supported values. @@ -244,6 +270,11 @@ class Choice(ParamType, t.Generic[ParamTypeValue]): :param case_sensitive: Set to false to make choices case insensitive. Defaults to true. + .. versionchanged:: 8.4.0 + Now generic in the choice value type. Parameterize with the type of + the choice values (``Choice[HashType]`` for an enum, ``Choice[str]`` + for plain strings) to enable type-checked consumers. + .. versionchanged:: 8.2.0 Non-``str`` ``choices`` are now supported. It can additionally be any iterable. Before you were not recommended to pass anything but a list or @@ -261,11 +292,12 @@ class Choice(ParamType, t.Generic[ParamTypeValue]): self.choices: cabc.Sequence[ParamTypeValue] = tuple(choices) self.case_sensitive = case_sensitive - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["choices"] = self.choices - info_dict["case_sensitive"] = self.case_sensitive - return info_dict + def to_info_dict(self) -> ChoiceInfoDict: + return { + "choices": self.choices, + "case_sensitive": self.case_sensitive, + **super().to_info_dict(), + } def _normalized_mapping( self, ctx: Context | None = None @@ -372,7 +404,7 @@ class Choice(ParamType, t.Generic[ParamTypeValue]): ).format(value=value, choice=choices_str, choices=choices_str) def __repr__(self) -> str: - return f"Choice({list(self.choices)})" + return _("Choice({choices})").format(choices=list(self.choices)) def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -387,8 +419,7 @@ class Choice(ParamType, t.Generic[ParamTypeValue]): """ from click.shell_completion import CompletionItem - str_choices = map(str, self.choices) - + str_choices = [self.normalize_choice(choice, ctx) for choice in self.choices] if self.case_sensitive: matched = (c for c in str_choices if c.startswith(incomplete)) else: @@ -398,7 +429,11 @@ class Choice(ParamType, t.Generic[ParamTypeValue]): return [CompletionItem(c) for c in matched] -class DateTime(ParamType): +class DateTimeInfoDict(ParamTypeInfoDict): + formats: cabc.Sequence[str] + + +class DateTime(ParamType[datetime]): """The DateTime type converts date strings into `datetime` objects. The format strings which are checked are configurable, but default to some @@ -428,10 +463,8 @@ class DateTime(ParamType): "%Y-%m-%d %H:%M:%S", ] - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["formats"] = self.formats - return info_dict + def to_info_dict(self) -> DateTimeInfoDict: + return {"formats": self.formats, **super().to_info_dict()} def get_metavar(self, param: Parameter, ctx: Context) -> str | None: return f"[{'|'.join(self.formats)}]" @@ -444,7 +477,7 @@ class DateTime(ParamType): def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> datetime: if isinstance(value, datetime): return value @@ -469,12 +502,12 @@ class DateTime(ParamType): return "DateTime" -class _NumberParamTypeBase(ParamType): - _number_class: t.ClassVar[type[t.Any]] +class _NumberParamTypeBase(ParamType[ParamTypeValue]): + _number_class: t.Callable[[t.Any], ParamTypeValue] def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> ParamTypeValue: try: return self._number_class(value) except ValueError: @@ -487,7 +520,15 @@ class _NumberParamTypeBase(ParamType): ) -class _NumberRangeBase(_NumberParamTypeBase): +class NumberRangeInfoDict(ParamTypeInfoDict): + min: float | None + max: float | None + min_open: bool + max_open: bool + clamp: bool + + +class _NumberRangeBase(_NumberParamTypeBase[ParamTypeValue]): def __init__( self, min: float | None = None, @@ -502,36 +543,37 @@ class _NumberRangeBase(_NumberParamTypeBase): self.max_open = max_open self.clamp = clamp - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict.update( - min=self.min, - max=self.max, - min_open=self.min_open, - max_open=self.max_open, - clamp=self.clamp, - ) - return info_dict + def to_info_dict(self) -> NumberRangeInfoDict: + return { + "min": self.min, + "max": self.max, + "min_open": self.min_open, + "max_open": self.max_open, + "clamp": self.clamp, + **super().to_info_dict(), + } def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> ParamTypeValue: import operator rv = super().convert(value, param, ctx) - lt_min: bool = self.min is not None and ( + min = self.min + max = self.max + lt_min: bool = min is not None and ( operator.le if self.min_open else operator.lt - )(rv, self.min) - gt_max: bool = self.max is not None and ( + )(rv, min) # type: ignore[arg-type] + gt_max: bool = max is not None and ( operator.ge if self.max_open else operator.gt - )(rv, self.max) + )(rv, max) # type: ignore[arg-type] if self.clamp: - if lt_min: - return self._clamp(self.min, 1, self.min_open) # type: ignore + if min is not None and lt_min: + return self._clamp(min, 1, self.min_open) # type: ignore[arg-type] - if gt_max: - return self._clamp(self.max, -1, self.max_open) # type: ignore + if max is not None and gt_max: + return self._clamp(max, -1, self.max_open) # type: ignore[arg-type] if lt_min or gt_max: self.fail( @@ -544,7 +586,10 @@ class _NumberRangeBase(_NumberParamTypeBase): return rv - def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float: + @abc.abstractmethod + def _clamp( + self, bound: ParamTypeValue, dir: t.Literal[1, -1], open: bool + ) -> ParamTypeValue: """Find the valid value to clamp to bound in the given direction. @@ -552,7 +597,7 @@ class _NumberRangeBase(_NumberParamTypeBase): :param dir: 1 or -1 indicating the direction to move. :param open: If true, the range does not include the bound. """ - raise NotImplementedError + ... def _describe_range(self) -> str: """Describe the range for use in help text.""" @@ -573,7 +618,7 @@ class _NumberRangeBase(_NumberParamTypeBase): return f"<{type(self).__name__} {self._describe_range()}{clamp}>" -class IntParamType(_NumberParamTypeBase): +class IntParamType(_NumberParamTypeBase[int]): name = "integer" _number_class = int @@ -581,7 +626,7 @@ class IntParamType(_NumberParamTypeBase): return "INT" -class IntRange(_NumberRangeBase, IntParamType): +class IntRange(_NumberRangeBase[int], IntParamType): """Restrict an :data:`click.INT` value to a range of accepted values. See :ref:`ranges`. @@ -598,16 +643,14 @@ class IntRange(_NumberRangeBase, IntParamType): name = "integer range" - def _clamp( # type: ignore - self, bound: int, dir: t.Literal[1, -1], open: bool - ) -> int: + def _clamp(self, bound: int, dir: t.Literal[1, -1], open: bool) -> int: if not open: return bound return bound + dir -class FloatParamType(_NumberParamTypeBase): +class FloatParamType(_NumberParamTypeBase[float]): name = "float" _number_class = float @@ -615,7 +658,7 @@ class FloatParamType(_NumberParamTypeBase): return "FLOAT" -class FloatRange(_NumberRangeBase, FloatParamType): +class FloatRange(_NumberRangeBase[float], FloatParamType): """Restrict a :data:`click.FLOAT` value to a range of accepted values. See :ref:`ranges`. @@ -658,7 +701,7 @@ class FloatRange(_NumberRangeBase, FloatParamType): raise RuntimeError("Clamping is not supported for open bounds.") -class BoolParamType(ParamType): +class BoolParamType(ParamType[bool]): name = "boolean" bool_states: dict[str, bool] = { @@ -727,14 +770,12 @@ class BoolParamType(ParamType): return "BOOL" -class UUIDParameterType(ParamType): +class UUIDParameterType(ParamType[uuid.UUID]): name = "uuid" def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: - import uuid - + ) -> uuid.UUID: if isinstance(value, uuid.UUID): return value @@ -751,7 +792,12 @@ class UUIDParameterType(ParamType): return "UUID" -class File(ParamType): +class FileInfoDict(ParamTypeInfoDict): + mode: str + encoding: str | None + + +class File(ParamType[t.IO[t.Any]]): """Declares a parameter to be a file for reading or writing. The file is automatically closed once the context tears down (after the command finished working). @@ -798,10 +844,12 @@ class File(ParamType): self.lazy = lazy self.atomic = atomic - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict.update(mode=self.mode, encoding=self.encoding) - return info_dict + def to_info_dict(self) -> FileInfoDict: + return { + "mode": self.mode, + "encoding": self.encoding, + **super().to_info_dict(), + } def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: if self.lazy is not None: @@ -853,7 +901,11 @@ class File(ParamType): return f except OSError as e: - self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) + self.fail( + f"'{format_filename(value)}': {e.strerror}", + param, + ctx, + ) def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -876,7 +928,16 @@ def _is_file_like(value: t.Any) -> te.TypeGuard[t.IO[t.Any]]: return hasattr(value, "read") or hasattr(value, "write") -class Path(ParamType): +class PathInfoDict(ParamTypeInfoDict): + exists: bool + file_okay: bool + dir_okay: bool + writable: bool + readable: bool + allow_dash: bool + + +class Path(ParamType[str | bytes | os.PathLike[str]]): """The ``Path`` type is similar to the :class:`File` type, but returns the filename instead of an open file. Various checks can be enabled to validate the type of file and permissions. @@ -940,17 +1001,16 @@ class Path(ParamType): else: self.name = _("path") - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict.update( - exists=self.exists, - file_okay=self.file_okay, - dir_okay=self.dir_okay, - writable=self.writable, - readable=self.readable, - allow_dash=self.allow_dash, - ) - return info_dict + def to_info_dict(self) -> PathInfoDict: + return { + "exists": self.exists, + "file_okay": self.file_okay, + "dir_okay": self.dir_okay, + "writable": self.writable, + "readable": self.readable, + "allow_dash": self.allow_dash, + **super().to_info_dict(), + } def coerce_path_result( self, value: str | os.PathLike[str] @@ -1057,7 +1117,11 @@ class Path(ParamType): return [CompletionItem(incomplete, type=type)] -class Tuple(CompositeParamType): +class TupleInfoDict(ParamTypeInfoDict): + types: cabc.Sequence[ParamTypeInfoDict] + + +class Tuple(CompositeParamType[tuple[t.Any, ...]]): """The default behavior of Click is to apply a type on a value directly. This works well in most cases, except for when `nargs` is set to a fixed count and different types should be used for different items. In this @@ -1071,25 +1135,26 @@ class Tuple(CompositeParamType): :param types: a list of types that should be used for the tuple items. """ - def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType]) -> None: - self.types: cabc.Sequence[ParamType] = [convert_type(ty) for ty in types] + def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType[t.Any]]) -> None: + self.types: cabc.Sequence[ParamType[t.Any]] = [convert_type(ty) for ty in types] - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["types"] = [t.to_info_dict() for t in self.types] - return info_dict + def to_info_dict(self) -> TupleInfoDict: + return { + "types": [ty.to_info_dict() for ty in self.types], + **super().to_info_dict(), + } @property - def name(self) -> str: # type: ignore + def name(self) -> str: # type: ignore[override] return f"<{' '.join(ty.name for ty in self.types)}>" @property - def arity(self) -> int: # type: ignore + def arity(self) -> int: # type: ignore[override] return len(self.types) def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: + ) -> tuple[t.Any, ...]: len_type = len(self.types) len_value = len(value) @@ -1109,64 +1174,99 @@ class Tuple(CompositeParamType): ) -def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: +def _guess_type( + ty: type[t.Any] | ParamType[t.Any] | None, + default: t.Any | None, +) -> type[t.Any] | tuple[type[t.Any], ...] | ParamType[t.Any] | None: + """Infer a type from *ty* or *default*. + + Returns *ty* unchanged when it is not ``None``. Otherwise inspects + *default* to produce a ``type``, a ``tuple`` of types (for tuple + defaults), or ``None``. + """ + if ty is not None: + return ty + + if default is None: + return None + + if not isinstance(default, (tuple, list)): + return type(default) + + # If the default is empty, return None so convert_type falls + # through to STRING. + if not default: + return None + + item = default[0] + + # A sequence of iterables needs to detect the inner types. + # Can't call convert_type recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + return tuple(map(type, item)) + + return type(item) + + +@t.overload +def convert_type(ty: None, default: None = None) -> StringParamType: ... + + +@t.overload +def convert_type( + ty: type[t.Any] | ParamType[t.Any], default: t.Any | None = None +) -> ParamType[t.Any]: ... + + +@t.overload +def convert_type( + ty: t.Any | None, default: t.Any | None = None +) -> ParamType[t.Any]: ... + + +def convert_type( + ty: t.Any | None = None, default: t.Any | None = None +) -> ParamType[t.Any]: """Find the most appropriate :class:`ParamType` for the given Python type. If the type isn't provided, it can be inferred from a default value. """ - guessed_type = False + guessed = _guess_type(ty, default) + is_guessed = guessed is not ty - if ty is None and default is not None: - if isinstance(default, (tuple, list)): - # If the default is empty, ty will remain None and will - # return STRING. - if default: - item = default[0] + if isinstance(guessed, tuple): + return Tuple(guessed) - # A tuple of tuples needs to detect the inner types. - # Can't call convert recursively because that would - # incorrectly unwind the tuple to a single type. - if isinstance(item, (tuple, list)): - ty = tuple(map(type, item)) - else: - ty = type(item) - else: - ty = type(default) + if isinstance(guessed, ParamType): + return guessed - guessed_type = True - - if isinstance(ty, tuple): - return Tuple(ty) - - if isinstance(ty, ParamType): - return ty - - if ty is str or ty is None: + if guessed is str or guessed is None: return STRING - if ty is int: + if guessed is int: return INT - if ty is float: + if guessed is float: return FLOAT - if ty is bool: + if guessed is bool: return BOOL - if guessed_type: + if is_guessed: return STRING if __debug__: try: - if issubclass(ty, ParamType): + if issubclass(guessed, ParamType): raise AssertionError( - f"Attempted to use an uninstantiated parameter type ({ty})." + f"Attempted to use an uninstantiated parameter type ({guessed})." ) except TypeError: - # ty is an instance (correct), so issubclass fails. + # guessed is an instance (correct), so issubclass fails. pass - return FuncParamType(ty) + return FuncParamType(guessed) #: A dummy parameter type that just does nothing. From a user's @@ -1177,7 +1277,7 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: #: #: For path related uses the :class:`Path` type is a better choice but #: there are situations where an unprocessed type is useful which is why -#: it is is provided. +#: it is provided. #: #: .. versionadded:: 4.0 UNPROCESSED = UnprocessedParamType() diff --git a/libs/click/utils.py b/libs/click/utils.py index beae26f76..67922b894 100644 --- a/libs/click/utils.py +++ b/libs/click/utils.py @@ -6,6 +6,7 @@ import re import sys import typing as t from functools import update_wrapper +from gettext import gettext as _ from types import ModuleType from types import TracebackType @@ -57,7 +58,10 @@ def make_str(value: t.Any) -> str: def make_default_short_help(help: str, max_length: int = 45) -> str: - """Returns a condensed version of help string.""" + """Returns a condensed version of help string. + + :meta private: + """ # Consider only the first paragraph. paragraph_end = help.find("\n\n") @@ -113,6 +117,14 @@ class LazyFile: files for writing. """ + name: str + mode: str + encoding: str | None + errors: str | None + atomic: bool + _f: t.IO[t.Any] | None + should_close: bool + def __init__( self, filename: str | os.PathLike[str], @@ -120,14 +132,12 @@ class LazyFile: encoding: str | None = None, errors: str | None = "strict", atomic: bool = False, - ): - self.name: str = os.fspath(filename) + ) -> None: + self.name = os.fspath(filename) self.mode = mode self.encoding = encoding self.errors = errors self.atomic = atomic - self._f: t.IO[t.Any] | None - self.should_close: bool if self.name == "-": self._f, self.should_close = open_stream(filename, mode, encoding, errors) @@ -195,8 +205,10 @@ class LazyFile: class KeepOpenFile: + _file: t.IO[t.Any] + def __init__(self, file: t.IO[t.Any]) -> None: - self._file: t.IO[t.Any] = file + self._file = file def __getattr__(self, name: str) -> t.Any: return getattr(self._file, name) @@ -220,7 +232,7 @@ class KeepOpenFile: def echo( - message: t.Any | None = None, + message: object = None, file: t.IO[t.Any] | None = None, nl: bool = True, err: bool = False, @@ -275,14 +287,15 @@ def echo( if file is None: return - # Convert non bytes/text into the native string type. - if message is not None and not isinstance(message, (str, bytes, bytearray)): - out: str | bytes | bytearray | None = str(message) - else: - out = message + match message: + case str() | bytes() | bytearray(): + out = message + case None: + out = "" + case _: + out = str(message) if nl: - out = out or "" if isinstance(out, str): out += "\n" else: @@ -298,7 +311,6 @@ def echo( # would expect. Eg: you can write to StringIO for other cases. if isinstance(out, (bytes, bytearray)): binary_file = _find_binary_writer(file) - if binary_file is not None: file.flush() binary_file.write(out) @@ -330,7 +342,7 @@ def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryI """ opener = binary_streams.get(name) if opener is None: - raise TypeError(f"Unknown standard stream '{name}'") + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) return opener() @@ -351,7 +363,7 @@ def get_text_stream( """ opener = text_streams.get(name) if opener is None: - raise TypeError(f"Unknown standard stream '{name}'") + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) return opener(encoding, errors) @@ -504,6 +516,8 @@ class PacifyFlushWrapper: pipe, all calls and attributes are proxied. """ + wrapped: t.IO[t.Any] + def __init__(self, wrapped: t.IO[t.Any]) -> None: self.wrapped = wrapped diff --git a/libs/commonmark-0.9.1.dist-info/INSTALLER b/libs/commonmark-0.9.2.dist-info/INSTALLER similarity index 100% rename from libs/commonmark-0.9.1.dist-info/INSTALLER rename to libs/commonmark-0.9.2.dist-info/INSTALLER diff --git a/libs/commonmark-0.9.1.dist-info/METADATA b/libs/commonmark-0.9.2.dist-info/METADATA similarity index 90% rename from libs/commonmark-0.9.1.dist-info/METADATA rename to libs/commonmark-0.9.2.dist-info/METADATA index c077daf32..d592177b9 100644 --- a/libs/commonmark-0.9.1.dist-info/METADATA +++ b/libs/commonmark-0.9.2.dist-info/METADATA @@ -1,12 +1,10 @@ Metadata-Version: 2.4 Name: commonmark -Version: 0.9.1 +Version: 0.9.2 Summary: Python parser for the CommonMark Markdown spec Home-page: https://github.com/rtfd/commonmark.py Author: Bibek Kafle , Roland Shoemaker Author-email: rolandshoemaker@gmail.com -Maintainer: Nikolas Nyby -Maintainer-email: nikolas@gnu.org License: BSD-3-Clause Keywords: markup,markdown,commonmark Classifier: Programming Language :: Python @@ -28,8 +26,8 @@ Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: future>=0.14.0; python_version < "3" Provides-Extra: test -Requires-Dist: flake8==3.7.8; extra == "test" -Requires-Dist: hypothesis==3.55.3; extra == "test" +Requires-Dist: flake8==3.9.2; extra == "test" +Requires-Dist: hypothesis==4.24.4; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier @@ -39,8 +37,6 @@ Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file -Dynamic: maintainer -Dynamic: maintainer-email Dynamic: provides-extra Dynamic: requires-dist Dynamic: summary @@ -48,6 +44,20 @@ Dynamic: summary commonmark.py ============= +|No Maintenance Intended| + +**Warning:** commonmark.py is now deprecated. We recommend using `markdown-it-py`_ +for a commonmark parser going forward. See `this issue`_ for background +and discussion. + +-- + +.. _markdown-it-py: https://github.com/executablebooks/markdown-it-py +.. _this issue: https://github.com/readthedocs/commonmark.py/issues/308 + +.. |No Maintenance Intended| image:: http://unmaintained.tech/badge.svg + :target: http://unmaintained.tech/ + commonmark.py is a pure Python port of `jgm `__'s `commonmark.js `__, a Markdown parser and renderer for the @@ -57,9 +67,9 @@ stable we will release the first ``1.0`` version and attempt to keep up to date with changes in ``commonmark.js``. commonmark.py is tested against the CommonMark spec with Python versions -2.7, 3.4, 3.5, 3.6, and 3.7. +2.7, 3.5, 3.6, 3.7, and 3.8. -**Current version:** 0.9.1 +**Current version:** 0.9.2 |Pypi Link| |Build Status| |Doc Link| @@ -171,7 +181,6 @@ Authors - `Bibek Kafle `__ - `Roland Shoemaker `__ -- `Nikolas Nyby `__ .. |Pypi Link| image:: https://img.shields.io/pypi/v/commonmark.svg :target: https://pypi.org/project/commonmark/ diff --git a/libs/commonmark-0.9.1.dist-info/RECORD b/libs/commonmark-0.9.2.dist-info/RECORD similarity index 62% rename from libs/commonmark-0.9.1.dist-info/RECORD rename to libs/commonmark-0.9.2.dist-info/RECORD index 51c548f78..7b112c974 100644 --- a/libs/commonmark-0.9.1.dist-info/RECORD +++ b/libs/commonmark-0.9.2.dist-info/RECORD @@ -1,27 +1,27 @@ ../../bin/cmark,sha256=g6uoxffQUa9UYL2yLCpSGbNBnjlhDSQcQAD2n3KX_1Q,191 -commonmark-0.9.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -commonmark-0.9.1.dist-info/METADATA,sha256=GPMY5sZ6IQiKHDgWfQA4wqQA2uI31Szl12fZDUTn2rQ,6018 -commonmark-0.9.1.dist-info/RECORD,, -commonmark-0.9.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -commonmark-0.9.1.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 -commonmark-0.9.1.dist-info/entry_points.txt,sha256=TKUdr3xoGWriPzVk97_5NN8G-BPB_9_lr-EaMXOw8ng,48 -commonmark-0.9.1.dist-info/licenses/LICENSE,sha256=2gzYPRg8HXgZy_BFQcKl0fX2g47YKBCpFDqJ3cvCt94,1645 -commonmark-0.9.1.dist-info/top_level.txt,sha256=HeslGuf22YTnjJce2B9Q2yrMRAr_Gia2EDi47Dye_Ig,11 +commonmark-0.9.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +commonmark-0.9.2.dist-info/METADATA,sha256=VSmSrIJYzPnMyWdzoSL0dkM9SLLbwCl9_873iRLWo2w,6321 +commonmark-0.9.2.dist-info/RECORD,, +commonmark-0.9.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +commonmark-0.9.2.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 +commonmark-0.9.2.dist-info/entry_points.txt,sha256=TKUdr3xoGWriPzVk97_5NN8G-BPB_9_lr-EaMXOw8ng,48 +commonmark-0.9.2.dist-info/licenses/LICENSE,sha256=2gzYPRg8HXgZy_BFQcKl0fX2g47YKBCpFDqJ3cvCt94,1645 +commonmark-0.9.2.dist-info/top_level.txt,sha256=HeslGuf22YTnjJce2B9Q2yrMRAr_Gia2EDi47Dye_Ig,11 commonmark/__init__.py,sha256=RqqRUq_nHkT5wIsiQEdILfl6hFSiokLSWpfSAvgFEa0,302 commonmark/blocks.py,sha256=GGPw34diVDEnEeO99OEu2ORmgKC1x8yvYin3uDdYQQg,30411 commonmark/cmark.py,sha256=ssCG5zCsFipenNmwjc8FriRpVjn7UbnDMUc7WGWcGH4,1430 -commonmark/common.py,sha256=PE8oRuraJH8IZovPUkywUi_8ix4yNMruEJsQfhZzep0,3309 +commonmark/common.py,sha256=vUDRJZswOf_i9tLwtZaae3gsVy4sPQUOobDWFsORAnA,3310 commonmark/dump.py,sha256=VGsnONW_Gtmwp57qfj4TZw3tyDv3sjEtW3GqboWeZJU,3644 commonmark/entitytrans.py,sha256=6CQTSe7RIOvCFTOAC-V8f9UHWElVo-WobpdVBsQsHS8,61553 -commonmark/inlines.py,sha256=myBArP34yOcV3luwTFoET1Wxr6YrZ_2N4yGUY_GKfzA,30985 +commonmark/inlines.py,sha256=4VWa94a8yyCR-s4anTeDQrng9ehISx_RUnthIB73evs,30898 commonmark/main.py,sha256=WNp0rNa7pFO8aDyG7PxtYuhQMJGPc7teR3nPmJPQkgQ,1268 commonmark/node.py,sha256=eVZuPU1eDN-gyEQkXtECDUAUmgC7iRynnBk3GuoXG04,4614 commonmark/normalize_reference.py,sha256=dec-gL7R-hZywxy4gF0b60P6FksEU7AvXAzYZsP0UFc,6388 commonmark/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 commonmark/render/html.py,sha256=eLOM4kGWCBS0dOMzp0RluXL2bDTacHB6hI1Iax-PmBY,6585 commonmark/render/renderer.py,sha256=alZSDpipMFnMT69G1XmmapRrdDQU7F4J9e0F_V3aahw,1121 -commonmark/render/rst.py,sha256=OTWTYc29QRFdC0p6LNL2wDdw_CNJKjOeMrVMfNQJiLo,3879 +commonmark/render/rst.py,sha256=8eEfbafD3q2iClK75nr-9WDJNMechRssK-8ilFMw5Kw,3969 commonmark/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -commonmark/tests/rst_tests.py,sha256=GtW3aE26zeAbP5QP-PE2sjA_fpVhE2WIuRWs1ndMAq8,3540 +commonmark/tests/rst_tests.py,sha256=gwrdnjcJXZ168b--perkRYBajw_8AeN-WM7lx8u6_xg,3979 commonmark/tests/run_spec_tests.py,sha256=AMc_CzFWFoorSgr0VlFOPUWHKUgXrPmFQNi5K_pUykc,7622 commonmark/tests/unit_tests.py,sha256=f5F7gEs9ccbJb4hYAfdVHEuy7Gg6v3ZorOl-0jmqARc,4495 diff --git a/libs/commonmark-0.9.1.dist-info/REQUESTED b/libs/commonmark-0.9.2.dist-info/REQUESTED similarity index 100% rename from libs/commonmark-0.9.1.dist-info/REQUESTED rename to libs/commonmark-0.9.2.dist-info/REQUESTED diff --git a/libs/commonmark-0.9.1.dist-info/WHEEL b/libs/commonmark-0.9.2.dist-info/WHEEL similarity index 100% rename from libs/commonmark-0.9.1.dist-info/WHEEL rename to libs/commonmark-0.9.2.dist-info/WHEEL diff --git a/libs/commonmark-0.9.1.dist-info/entry_points.txt b/libs/commonmark-0.9.2.dist-info/entry_points.txt similarity index 100% rename from libs/commonmark-0.9.1.dist-info/entry_points.txt rename to libs/commonmark-0.9.2.dist-info/entry_points.txt diff --git a/libs/commonmark-0.9.1.dist-info/licenses/LICENSE b/libs/commonmark-0.9.2.dist-info/licenses/LICENSE similarity index 100% rename from libs/commonmark-0.9.1.dist-info/licenses/LICENSE rename to libs/commonmark-0.9.2.dist-info/licenses/LICENSE diff --git a/libs/commonmark-0.9.1.dist-info/top_level.txt b/libs/commonmark-0.9.2.dist-info/top_level.txt similarity index 100% rename from libs/commonmark-0.9.1.dist-info/top_level.txt rename to libs/commonmark-0.9.2.dist-info/top_level.txt diff --git a/libs/commonmark/common.py b/libs/commonmark/common.py index b15a8b604..05b52a131 100644 --- a/libs/commonmark/common.py +++ b/libs/commonmark/common.py @@ -67,7 +67,7 @@ def unescape_string(s): def normalize_uri(uri): try: - return quote(uri.encode('utf-8'), safe=str('/@:+?=&()%#*,')) + return quote(uri.encode('utf-8'), safe=str(';/@:+?=&()%#*,')) except UnicodeDecodeError: # Python 2 also throws a UnicodeDecodeError, complaining about # the width of the "safe" string. Removing this parameter diff --git a/libs/commonmark/inlines.py b/libs/commonmark/inlines.py index 88a84cf45..0aa50d3a6 100644 --- a/libs/commonmark/inlines.py +++ b/libs/commonmark/inlines.py @@ -875,8 +875,6 @@ class InlineParser(object): self.brackets = None while (self.parseInline(block)): pass - # allow raw string to be garbage collected - block.string_content = None self.processEmphasis(None) parse = parseInlines diff --git a/libs/commonmark/render/rst.py b/libs/commonmark/render/rst.py index a18f7b233..45adca32b 100644 --- a/libs/commonmark/render/rst.py +++ b/libs/commonmark/render/rst.py @@ -119,11 +119,13 @@ class ReStructuredTextRenderer(Renderer): self.cr() def item(self, node, entering): - tagname = '*' if node.list_data['type'] == 'bullet' else '#.' + tagname = '* ' if node.list_data['type'] == 'bullet' else '#. ' if entering: - self.out(tagname + ' ') + self.out(tagname) + self.indent_length += len(tagname) else: + self.indent_length -= len(tagname) self.cr() def block_quote(self, node, entering): diff --git a/libs/commonmark/tests/rst_tests.py b/libs/commonmark/tests/rst_tests.py index b8fa89009..22aef0852 100644 --- a/libs/commonmark/tests/rst_tests.py +++ b/libs/commonmark/tests/rst_tests.py @@ -97,6 +97,24 @@ This is a ordered list: #. One #. Two #. Three +""" + self.assertEqualRender(src_markdown, expected_rst) + + def test_ordered_list_with_multi_line_items(self): + src_markdown = """ +This is an ordered list with multi-line items: +1. First item, +with lazy indentation. +2. Second item, + with normal indentation. +""" + expected_rst = """ +This is an ordered list with multi-line items: + +#. First item, + with lazy indentation. +#. Second item, + with normal indentation. """ self.assertEqualRender(src_markdown, expected_rst) diff --git a/libs/dateutil/_version.py b/libs/dateutil/_version.py index 7fb347016..ddda98098 100644 --- a/libs/dateutil/_version.py +++ b/libs/dateutil/_version.py @@ -1,34 +1,4 @@ -# file generated by setuptools-scm +# file generated by setuptools_scm # don't change, don't track in version control - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] - COMMIT_ID = Union[str, None] -else: - VERSION_TUPLE = object - COMMIT_ID = object - -version: str -__version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE -commit_id: COMMIT_ID -__commit_id__: COMMIT_ID - -__version__ = version = '2.9.0' +__version__ = version = '2.9.0.post0' __version_tuple__ = version_tuple = (2, 9, 0) - -__commit_id__ = commit_id = None diff --git a/libs/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/libs/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz index c2c4c1a04..1461f8c86 100644 Binary files a/libs/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz and b/libs/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz differ diff --git a/libs/decorator-5.2.1.dist-info/RECORD b/libs/decorator-5.2.1.dist-info/RECORD deleted file mode 100644 index 2bad5b16a..000000000 --- a/libs/decorator-5.2.1.dist-info/RECORD +++ /dev/null @@ -1,8 +0,0 @@ -decorator-5.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -decorator-5.2.1.dist-info/METADATA,sha256=U46D37X6OWGXA6Tt-WtK5yasg95VceqHFqVWn-OSENY,3956 -decorator-5.2.1.dist-info/RECORD,, -decorator-5.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -decorator-5.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -decorator-5.2.1.dist-info/licenses/LICENSE.txt,sha256=kU7m7Xil78FzvaaY8nCERMqdFA_MQIDGDQUD1A2znaY,1308 -decorator-5.2.1.dist-info/top_level.txt,sha256=Kn6eQjo83ctWxXVyBMOYt0_YpjRjBznKYVuNyuC_DSI,10 -decorator.py,sha256=cWnO6cKKohQrXNEG1-Fauiw8djwE3f2PA2gJGofwgXA,17067 diff --git a/libs/decorator-5.2.1.dist-info/INSTALLER b/libs/decorator-5.3.1.dist-info/INSTALLER similarity index 100% rename from libs/decorator-5.2.1.dist-info/INSTALLER rename to libs/decorator-5.3.1.dist-info/INSTALLER diff --git a/libs/decorator-5.2.1.dist-info/METADATA b/libs/decorator-5.3.1.dist-info/METADATA similarity index 95% rename from libs/decorator-5.2.1.dist-info/METADATA rename to libs/decorator-5.3.1.dist-info/METADATA index 8a94dcbdb..2383f6676 100644 --- a/libs/decorator-5.2.1.dist-info/METADATA +++ b/libs/decorator-5.3.1.dist-info/METADATA @@ -1,13 +1,12 @@ Metadata-Version: 2.4 Name: decorator -Version: 5.2.1 +Version: 5.3.1 Summary: Decorators for Humans Author-email: Michele Simionato License: BSD-2-Clause Keywords: decorators Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python @@ -17,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities @@ -59,11 +59,7 @@ Testing If you have the source code installation you can run the tests with - `$ python src/tests/test.py -v` - -or (if you have setuptools installed) - - `$ python setup.py test` + `$ python tests/test.py -v` Notice that you may run into trouble if in your system there is an older version of the decorator module; in such a case remove the diff --git a/libs/decorator-5.3.1.dist-info/RECORD b/libs/decorator-5.3.1.dist-info/RECORD new file mode 100644 index 000000000..1f4875c89 --- /dev/null +++ b/libs/decorator-5.3.1.dist-info/RECORD @@ -0,0 +1,10 @@ +decorator-5.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +decorator-5.3.1.dist-info/METADATA,sha256=ITN5JIVAO71znh21lgIovtvN5tAMCswFKyfznbrltcQ,3886 +decorator-5.3.1.dist-info/RECORD,, +decorator-5.3.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +decorator-5.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +decorator-5.3.1.dist-info/licenses/LICENSE.txt,sha256=vRLMUUBcEIckl8EMohYB8dSrxQSWWVzInomrXa0k-2I,1308 +decorator-5.3.1.dist-info/top_level.txt,sha256=Kn6eQjo83ctWxXVyBMOYt0_YpjRjBznKYVuNyuC_DSI,10 +decorator/__init__.py,sha256=R8iHB8ZYy4XTd0VPqSVMOLRB7kduUwgzKkaeLtJDmYM,17366 +decorator/__init__.pyi,sha256=WDnEsxXTGvpTKmsAoJaVSLbnWRTexXeALL5mniWi7O8,2896 +decorator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/libs/decorator-5.2.1.dist-info/REQUESTED b/libs/decorator-5.3.1.dist-info/REQUESTED similarity index 100% rename from libs/decorator-5.2.1.dist-info/REQUESTED rename to libs/decorator-5.3.1.dist-info/REQUESTED diff --git a/libs/charset_normalizer-3.4.6.dist-info/WHEEL b/libs/decorator-5.3.1.dist-info/WHEEL similarity index 100% rename from libs/charset_normalizer-3.4.6.dist-info/WHEEL rename to libs/decorator-5.3.1.dist-info/WHEEL diff --git a/libs/decorator-5.2.1.dist-info/licenses/LICENSE.txt b/libs/decorator-5.3.1.dist-info/licenses/LICENSE.txt similarity index 96% rename from libs/decorator-5.2.1.dist-info/licenses/LICENSE.txt rename to libs/decorator-5.3.1.dist-info/licenses/LICENSE.txt index 3e01d05b1..0263356df 100644 --- a/libs/decorator-5.2.1.dist-info/licenses/LICENSE.txt +++ b/libs/decorator-5.3.1.dist-info/licenses/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2005-2025, Michele Simionato +Copyright (c) 2005-2026, Michele Simionato All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/libs/decorator-5.2.1.dist-info/top_level.txt b/libs/decorator-5.3.1.dist-info/top_level.txt similarity index 100% rename from libs/decorator-5.2.1.dist-info/top_level.txt rename to libs/decorator-5.3.1.dist-info/top_level.txt diff --git a/libs/decorator.py b/libs/decorator/__init__.py similarity index 94% rename from libs/decorator.py rename to libs/decorator/__init__.py index 40a39f2f1..5f0879024 100644 --- a/libs/decorator.py +++ b/libs/decorator/__init__.py @@ -1,6 +1,6 @@ # ######################### LICENSE ############################ # -# Copyright (c) 2005-2025, Michele Simionato +# Copyright (c) 2005-2026, Michele Simionato # All rights reserved. # Redistribution and use in source and binary forms, with or without @@ -40,8 +40,18 @@ import itertools import functools from contextlib import _GeneratorContextManager from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction +from typing import Any, Dict, List, Optional +try: + import annotationlib # in Python 3.14+ -__version__ = '5.2.1' + def inspect_sig(func): + return inspect.signature( + func, annotation_format=annotationlib.Format.FORWARDREF) + +except ImportError: + inspect_sig = inspect.signature + +__version__ = '5.3.1' DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(') POS = inspect.Parameter.POSITIONAL_OR_KEYWORD @@ -49,7 +59,7 @@ EMPTY = inspect.Parameter.empty # this is not used anymore in the core, but kept for backward compatibility -class FunctionMaker(object): +class FunctionMaker: """ An object with the ability to create functions with a given signature. It has attributes name, doc, module, signature, defaults, dict and @@ -60,7 +70,10 @@ class FunctionMaker(object): _compile_count = itertools.count() # make pylint happy - args = varargs = varkw = defaults = kwonlyargs = kwonlydefaults = () + args: List[str] = [] + varargs = varkw = defaults = None + kwonlyargs: List[str] = [] + kwonlydefaults: Optional[Dict[str, Any]] = None def __init__(self, func=None, name=None, signature=None, defaults=None, doc=None, module=None, funcdict=None): @@ -89,7 +102,7 @@ class FunctionMaker(object): allargs.append('*') # single star syntax for a in self.kwonlyargs: allargs.append('%s=None' % a) - allshortargs.append('%s=%s' % (a, a)) + allshortargs.append('{}={}'.format(a, a)) if self.varkw: allargs.append('**' + self.varkw) allshortargs.append('**' + self.varkw) @@ -147,7 +160,7 @@ class FunctionMaker(object): self.shortsignature.split(',')]) for n in names: if n in ('_func_', '_call_'): - raise NameError('%s is overridden in\n%s' % (n, src)) + raise NameError('{} is overridden in\n{}'.format(n, src)) if not src.endswith('\n'): # add a newline for old Pythons src += '\n' @@ -190,8 +203,8 @@ class FunctionMaker(object): ibody = '\n'.join(' ' + line for line in body.splitlines()) caller = evaldict.get('_call_') # when called from `decorate` if caller and iscoroutinefunction(caller): - body = ('async def %(name)s(%(signature)s):\n' + ibody).replace( - 'return', 'return await') + body = ('async def %(name)s(%(signature)s):\n' + ibody) + body = re.sub(r'\breturn\b', 'return await', body) else: body = 'def %(name)s(%(signature)s):\n' + ibody return self.make(body, evaldict, addsource, **attrs) @@ -214,7 +227,7 @@ def decorate(func, caller, extras=(), kwsyntax=False): even if such argument are positional, similarly to what functools.wraps does. By default kwsyntax is False and the the arguments are untouched. """ - sig = inspect.signature(func) + sig = inspect_sig(func) if isinstance(func, functools.partial): func = functools.update_wrapper(func, func.func) if iscoroutinefunction(caller): @@ -226,15 +239,13 @@ def decorate(func, caller, extras=(), kwsyntax=False): def fun(*args, **kw): if not kwsyntax: args, kw = fix(args, kw, sig) - for res in caller(func, *(extras + args), **kw): - yield res + yield from caller(func, *(extras + args), **kw) else: def fun(*args, **kw): if not kwsyntax: args, kw = fix(args, kw, sig) return caller(func, *(extras + args), **kw) - fun.__name__ = func.__name__ fun.__doc__ = func.__doc__ fun.__wrapped__ = func fun.__signature__ = sig @@ -290,7 +301,7 @@ def decorator(caller, _func=None, kwsyntax=False): # this is obsolete behavior; you should use decorate instead return decorate(_func, caller, (), kwsyntax) # else return a decorator function - sig = inspect.signature(caller) + sig = inspect_sig(caller) dec_params = [p for p in sig.parameters.values() if p.kind is POS] def dec(func=None, *args, **kw): @@ -400,7 +411,7 @@ def dispatch_on(*dispatch_args): n_vas = len(vas) if n_vas > 1: raise RuntimeError( - 'Ambiguous dispatch for %s: %s' % (t, vas)) + 'Ambiguous dispatch for {}: {}'.format(t, vas)) elif n_vas == 1: va, = vas mro = type('t', (t, va), {}).mro()[1:] diff --git a/libs/decorator/__init__.pyi b/libs/decorator/__init__.pyi new file mode 100644 index 000000000..d0a6f08a1 --- /dev/null +++ b/libs/decorator/__init__.pyi @@ -0,0 +1,79 @@ +import inspect +import annotationlib +from builtins import dict as _dict # alias to avoid conflicts with attribute name +from collections.abc import Callable, Generator, Iterator +from contextlib import _GeneratorContextManager +from inspect import Signature, getfullargspec as getfullargspec, iscoroutinefunction as iscoroutinefunction +from re import Pattern +from typing import Any, Final, Literal, TypeVar, Tuple +from typing_extensions import ParamSpec + +_C = TypeVar("_C", bound=Callable[..., Any]) +_Func = TypeVar("_Func", bound=Callable[..., Any]) +_T = TypeVar("_T") +_P = ParamSpec("_P") + +DEF: Final[Pattern[str]] +POS: Final[Literal[inspect._ParameterKind.POSITIONAL_OR_KEYWORD]] +EMPTY: Final[type[inspect._empty]] + +def inspect_sig( + func: _Func +) -> Tuple: ... + + +class FunctionMaker: + args: list[str] + varargs: str | None + varkw: str | None + defaults: tuple[Any, ...] | None + kwonlyargs: list[str] + kwonlydefaults: dict[str, Any] | None + shortsignature: str | None + name: str + doc: str | None + module: str | None + annotations: _dict[str, Any] + signature: str + dict: _dict[str, Any] + def __init__( + self, + func: Callable[..., Any] | None = ..., + name: str | None = ..., + signature: str | None = ..., + defaults: tuple[Any, ...] | None = ..., + doc: str | None = ..., + module: str | None = ..., + funcdict: _dict[str, Any] | None = ..., + ) -> None: ... + def update(self, func: Any, **kw: Any) -> None: ... + def make( + self, src_templ: str, evaldict: _dict[str, Any] | None = ..., addsource: bool = ..., **attrs: Any + ) -> Callable[..., Any]: ... + @classmethod + def create( + cls, + obj: Any, + body: str, + evaldict: _dict[str, Any], + defaults: tuple[Any, ...] | None = ..., + doc: str | None = ..., + module: str | None = ..., + addsource: bool = ..., + **attrs: Any, + ) -> Callable[..., Any]: ... + +def fix(args: tuple[Any, ...], kwargs: dict[str, Any], sig: Signature) -> tuple[tuple[Any, ...], dict[str, Any]]: ... +def decorate(func: _Func, caller: Callable[..., Any], extras: tuple[Any, ...] = ..., kwsyntax: bool = False) -> _Func: ... +def decoratorx(caller: Callable[..., Any]) -> Callable[..., Any]: ... +def decorator( + caller: Callable[..., Any], _func: Callable[..., Any] | None = None, kwsyntax: bool = False +) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ... + +class ContextManager(_GeneratorContextManager[_T]): + def __init__(self, g: Callable[..., Generator[_T]], *a: Any, **k: Any) -> None: ... + def __call__(self, func: _C) -> _C: ... + +def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, ContextManager[_T]]: ... +def append(a: type, vancestors: list[type]) -> None: ... +def dispatch_on(*dispatch_args: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ... diff --git a/libs/chardet/cli/__init__.py b/libs/decorator/py.typed similarity index 100% rename from libs/chardet/cli/__init__.py rename to libs/decorator/py.typed diff --git a/libs/dynaconf-3.2.12.dist-info/INSTALLER b/libs/dynaconf-3.2.13.dist-info/INSTALLER similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/INSTALLER rename to libs/dynaconf-3.2.13.dist-info/INSTALLER diff --git a/libs/dynaconf-3.2.12.dist-info/METADATA b/libs/dynaconf-3.2.13.dist-info/METADATA similarity index 99% rename from libs/dynaconf-3.2.12.dist-info/METADATA rename to libs/dynaconf-3.2.13.dist-info/METADATA index f2dd83d86..4d4269354 100644 --- a/libs/dynaconf-3.2.12.dist-info/METADATA +++ b/libs/dynaconf-3.2.13.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: dynaconf -Version: 3.2.12 +Version: 3.2.13 Summary: The dynamic configurator for your Python Project Home-page: https://github.com/dynaconf/dynaconf Author: Bruno Rocha diff --git a/libs/dynaconf-3.2.12.dist-info/RECORD b/libs/dynaconf-3.2.13.dist-info/RECORD similarity index 88% rename from libs/dynaconf-3.2.12.dist-info/RECORD rename to libs/dynaconf-3.2.13.dist-info/RECORD index 3469fd15a..e89574f5f 100644 --- a/libs/dynaconf-3.2.12.dist-info/RECORD +++ b/libs/dynaconf-3.2.13.dist-info/RECORD @@ -1,24 +1,24 @@ ../../bin/dynaconf,sha256=-FSvOwTY9houenFaB1kKxmC7_yfhsPPKTAUuSoRkBAs,187 -dynaconf-3.2.12.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -dynaconf-3.2.12.dist-info/METADATA,sha256=OjXoZS_xFMmNCGCwapQFBCtr9aHNIQjLVbPtTi7PI4U,9417 -dynaconf-3.2.12.dist-info/RECORD,, -dynaconf-3.2.12.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -dynaconf-3.2.12.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 -dynaconf-3.2.12.dist-info/entry_points.txt,sha256=olhFwb2rJswbRmjY97fTrkNfqPQhMv1Pxjez4mSxMSg,47 -dynaconf-3.2.12.dist-info/licenses/LICENSE,sha256=VXHf05Hz7uwTdLkQEFy4s7L4cb3mJ6Jc6yvoRPCg65c,1078 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/box-LICENSE.txt,sha256=OA9QtZRAYGpaFevuHWrHckHw0fQT4yhurfH56sJYDqw,1076 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/click-LICENSE.rst,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/licenses.sh,sha256=9eID6RHwv82Obub4D4G--lIbRZya65Fa9EwgYNWXvFU,769 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/python-dotenv-LICENSE.txt,sha256=0nIJqz0WJ4Ko-OOHK5s1PEngksmqRnpkUiiDQH2NEDA,4600 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/ruamel.yaml-LICENSE.txt,sha256=QYxdb3JocDuWY6zvk3rVKcVO47J-9miiolkf4w6hH_8,1121 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/toml-LICENSE.txt,sha256=QitPoBx3Vs5N5go556B0Zf3Mz4YGxWeUiRYa9C1EGMg,1253 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/tomli-LICENSE.txt,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072 -dynaconf-3.2.12.dist-info/licenses/vendor_licenses/vendor_versions.txt,sha256=QD2pB7JBhr6oIGaPCXFVhhFfs2uQibsJXBT9cUYlgHQ,100 -dynaconf-3.2.12.dist-info/top_level.txt,sha256=y9G5RO-Ts5h0yOx6qVr8r_TBCSFA8wBxEWcvMZJq66c,9 -dynaconf/VERSION,sha256=AuldHXhefHbKIRCWuFMfRmLBkXSPxKYcrh6Ug_vsoRc,7 +dynaconf-3.2.13.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +dynaconf-3.2.13.dist-info/METADATA,sha256=jUtkMPb03BQZN86mb7GGRWbqVcfnlMvSTI4PYj64UGY,9417 +dynaconf-3.2.13.dist-info/RECORD,, +dynaconf-3.2.13.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +dynaconf-3.2.13.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 +dynaconf-3.2.13.dist-info/entry_points.txt,sha256=olhFwb2rJswbRmjY97fTrkNfqPQhMv1Pxjez4mSxMSg,47 +dynaconf-3.2.13.dist-info/licenses/LICENSE,sha256=VXHf05Hz7uwTdLkQEFy4s7L4cb3mJ6Jc6yvoRPCg65c,1078 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/box-LICENSE.txt,sha256=OA9QtZRAYGpaFevuHWrHckHw0fQT4yhurfH56sJYDqw,1076 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/click-LICENSE.rst,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/licenses.sh,sha256=9eID6RHwv82Obub4D4G--lIbRZya65Fa9EwgYNWXvFU,769 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/python-dotenv-LICENSE.txt,sha256=0nIJqz0WJ4Ko-OOHK5s1PEngksmqRnpkUiiDQH2NEDA,4600 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/ruamel.yaml-LICENSE.txt,sha256=QYxdb3JocDuWY6zvk3rVKcVO47J-9miiolkf4w6hH_8,1121 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/toml-LICENSE.txt,sha256=QitPoBx3Vs5N5go556B0Zf3Mz4YGxWeUiRYa9C1EGMg,1253 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/tomli-LICENSE.txt,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072 +dynaconf-3.2.13.dist-info/licenses/vendor_licenses/vendor_versions.txt,sha256=QD2pB7JBhr6oIGaPCXFVhhFfs2uQibsJXBT9cUYlgHQ,100 +dynaconf-3.2.13.dist-info/top_level.txt,sha256=y9G5RO-Ts5h0yOx6qVr8r_TBCSFA8wBxEWcvMZJq66c,9 +dynaconf/VERSION,sha256=zxZsIJTPWxa6OWGC01YZXA55VcgqC-0nCP5FJbJXaL8,7 dynaconf/__init__.py,sha256=XRctNsCYmSFUvmZHU3Cz4BLV3YbxSiXvwq9MB1M41pI,1277 dynaconf/__main__.py,sha256=mHeEO4Ae_9Ams2xunnHknpHKOUoHpUuRNv2jQCKnojA,105 -dynaconf/base.py,sha256=oOkTOzO2bG3F0IDpNwJRN3y6d9FUyePee4g1pqnV8MM,53310 +dynaconf/base.py,sha256=PkUfKqFAJ7BvUxdJx1bpWPda7XTA_ImDw0sQFHIVssw,54364 dynaconf/cli.py,sha256=_zJlB3fM2V76HvaRzYlivSDiPsDQto-wAaTNifgEzM0,28447 dynaconf/constants.py,sha256=DKUWa2XfcyqA-TpbfLBnpkwMF9rjAr86P_GrB8PbR1Y,1259 dynaconf/contrib/__init__.py,sha256=kyYhXVPeWZL-zeMFGqbal4f3U0-B6ktLK7-IEOetgP8,216 @@ -44,7 +44,7 @@ dynaconf/utils/boxing.py,sha256=dFz25RuJxJ0As9CMIpZN6_5zYsGTU-tSF6Hv-rdvYiw,3052 dynaconf/utils/files.py,sha256=b7VfD9uln2zTnP6hGtXIFfQnxI1YdP0yggGbD5Bf7Sg,4211 dynaconf/utils/functional.py,sha256=Umiy8J3DO_kFvWtz1Xz5ZFJMdG6RwvutXH656NRJvVA,4582 dynaconf/utils/inspect.py,sha256=veV3bLbzz_Hjth6LaAnA5iO7iyWIofF_-MQTkJ-VycQ,14854 -dynaconf/utils/parse_conf.py,sha256=9idL7Dyu6NQbksctq--RPAy46o7QhusqeXBd64wPIMY,17519 +dynaconf/utils/parse_conf.py,sha256=7y5Y-aN7YPsCagPP6c_8GWn3jBzHs5y0U8fzP-d-BiM,18842 dynaconf/validator.py,sha256=nPkV6DnQdtCcNM-HkRw0ogr_snMpKS8Xves8G67TQ7M,18496 dynaconf/validator_conditions.py,sha256=d3hKJJ6nBKxMWmyghtGsiRArdRwBqhpNt21WKoh81bM,1527 dynaconf/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 @@ -88,7 +88,7 @@ dynaconf/vendor/ruamel/yaml/comments.py,sha256=PLUOxpv3FiJ0zn_vnnX044lmAvTqhHYEt dynaconf/vendor/ruamel/yaml/compat.py,sha256=mj74dbRSkO5A2vPvbek7zZwSII3PCOGjm3FkW2EqXrA,4459 dynaconf/vendor/ruamel/yaml/composer.py,sha256=fBSqryTtbxk8mY2B92Yxo5QIbKv3bi5dQG6XeF491Tw,4048 dynaconf/vendor/ruamel/yaml/configobjwalker.py,sha256=iNBnAbQVLYCJrynvB303Ytq78K8A9m0bcKiy7-ZT8Sc,253 -dynaconf/vendor/ruamel/yaml/constructor.py,sha256=qP49T9ehcN0MSPJ7Pt35xGCFLYsDqNOVWtCKSqCXiB0,40748 +dynaconf/vendor/ruamel/yaml/constructor.py,sha256=SCbysFxsGijI9Ej9QITVfnPOkJNKdbI1DutRj5f3Zgc,40744 dynaconf/vendor/ruamel/yaml/cyaml.py,sha256=YzDqNt5xkJK3vc_VdDQk14p7SYKyuNc61W1b-CRUo5Q,3257 dynaconf/vendor/ruamel/yaml/dumper.py,sha256=TLwBtv5hHHbZ2ZDMeJSenSpWjEccMpZjLr5ug_VmVc4,3512 dynaconf/vendor/ruamel/yaml/emitter.py,sha256=dtDkT03LM4cQtR7lYwelxJmBd_9YvwbXkyG0e_vlqbs,35831 diff --git a/libs/dynaconf-3.2.12.dist-info/REQUESTED b/libs/dynaconf-3.2.13.dist-info/REQUESTED similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/REQUESTED rename to libs/dynaconf-3.2.13.dist-info/REQUESTED diff --git a/libs/dynaconf-3.2.12.dist-info/WHEEL b/libs/dynaconf-3.2.13.dist-info/WHEEL similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/WHEEL rename to libs/dynaconf-3.2.13.dist-info/WHEEL diff --git a/libs/dynaconf-3.2.12.dist-info/entry_points.txt b/libs/dynaconf-3.2.13.dist-info/entry_points.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/entry_points.txt rename to libs/dynaconf-3.2.13.dist-info/entry_points.txt diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/LICENSE b/libs/dynaconf-3.2.13.dist-info/licenses/LICENSE similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/LICENSE rename to libs/dynaconf-3.2.13.dist-info/licenses/LICENSE diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/box-LICENSE.txt b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/box-LICENSE.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/box-LICENSE.txt rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/box-LICENSE.txt diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/click-LICENSE.rst b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/click-LICENSE.rst similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/click-LICENSE.rst rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/click-LICENSE.rst diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/licenses.sh b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/licenses.sh similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/licenses.sh rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/licenses.sh diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/python-dotenv-LICENSE.txt b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/python-dotenv-LICENSE.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/python-dotenv-LICENSE.txt rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/python-dotenv-LICENSE.txt diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/ruamel.yaml-LICENSE.txt b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/ruamel.yaml-LICENSE.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/ruamel.yaml-LICENSE.txt rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/ruamel.yaml-LICENSE.txt diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/toml-LICENSE.txt b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/toml-LICENSE.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/toml-LICENSE.txt rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/toml-LICENSE.txt diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/tomli-LICENSE.txt b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/tomli-LICENSE.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/tomli-LICENSE.txt rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/tomli-LICENSE.txt diff --git a/libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/vendor_versions.txt b/libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/vendor_versions.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/licenses/vendor_licenses/vendor_versions.txt rename to libs/dynaconf-3.2.13.dist-info/licenses/vendor_licenses/vendor_versions.txt diff --git a/libs/dynaconf-3.2.12.dist-info/top_level.txt b/libs/dynaconf-3.2.13.dist-info/top_level.txt similarity index 100% rename from libs/dynaconf-3.2.12.dist-info/top_level.txt rename to libs/dynaconf-3.2.13.dist-info/top_level.txt diff --git a/libs/dynaconf/base.py b/libs/dynaconf/base.py index 78a82e79b..c403739a2 100644 --- a/libs/dynaconf/base.py +++ b/libs/dynaconf/base.py @@ -121,7 +121,7 @@ class LazySettings(LazyObject): self._setup() if name in self._wrapped._deleted: # noqa raise AttributeError( - f"Attribute {name} was deleted, " "or belongs to different env" + f"Attribute {name} was deleted, or belongs to different env" ) if name not in RESERVED_ATTRS: @@ -298,6 +298,13 @@ class Settings: def __contains__(self, item): """Respond to `item in settings`""" + nested_sep = getattr(self, "NESTED_SEPARATOR_FOR_DYNACONF", None) + + if isinstance(item, str) and nested_sep and nested_sep in item: + item = item.replace(nested_sep, ".") + if isinstance(item, str) and "." in item: + return self.get(item, default=missing) is not missing + return item.upper() in self.store or item.lower() in self.store def __getattribute__(self, name): @@ -429,12 +436,6 @@ class Settings: :param default: In case of not found it will be returned :param parent: Is there a pre-loaded parent in a nested data? """ - # if parent is not traverseable raise error - if parent and not hasattr(parent, "get"): - raise AttributeError( - f"cannot lookup {dotted_key!r} from {type(parent).__name__!r}" - ) - split_key = dotted_key.split(".") name, keys = split_key[0], split_key[1:] result = self.get(name, default=default, parent=parent, **kwargs) @@ -447,6 +448,13 @@ class Settings: return parse_conf_data(result, tomlfy=True, box_settings=self) return result + # Still keys left, but current result/parent is not a data container + if keys and not isinstance(result, (dict, list)): + result_type = type(result).__name__ + raise AttributeError( + f"Invalid dotted lookup in {dotted_key}. {name} is a {result_type}" + ) + # If we've still got key elements to traverse, let's do that. return self._dotted_get( ".".join(keys), default=default, parent=result, cast=cast, **kwargs @@ -526,7 +534,7 @@ class Settings: self.unset(key) self.execute_loaders(key=key) - data = (parent or self.store).get(key, default) + data = _get_with_default(parent or self.store, key, default) if cast: data = apply_converter(cast, data, box_settings=self) return data @@ -1527,3 +1535,25 @@ RESERVED_ATTRS = ( "_REGISTERED_HOOKS", ] ) + +# These are special fields defined by Dynaconf, but users can access it +_PUBLIC_PROPERTIES = [ + name + for name, _ in inspect.getmembers( + Settings, lambda x: isinstance(x, property) + ) +] + + +def _get_with_default(data: dict | list, key: str, default): + if isinstance(data, dict): + return data.get(key, default) + elif isinstance(data, list): + if not key.isdigit(): + raise ValueError(f"Expected integer, got: {key}") + try: + return data[int(key)] + except KeyError: + return default + else: + raise AttributeError(f"Unknown data container type: {type(data)}") diff --git a/libs/dynaconf/utils/parse_conf.py b/libs/dynaconf/utils/parse_conf.py index f44310556..341d743d8 100644 --- a/libs/dynaconf/utils/parse_conf.py +++ b/libs/dynaconf/utils/parse_conf.py @@ -3,6 +3,7 @@ from __future__ import annotations import json import os import re +import string import warnings from functools import wraps @@ -16,9 +17,10 @@ from dynaconf.vendor import toml from dynaconf.vendor import tomllib try: - from jinja2 import Environment + import jinja2 + from jinja2.sandbox import SandboxedEnvironment - jinja_env = Environment() + jinja_env = SandboxedEnvironment() for p_method in ("abspath", "realpath", "relpath", "dirname", "basename"): jinja_env.filters[p_method] = getattr(os.path, p_method) except ImportError: # pragma: no cover @@ -211,12 +213,16 @@ class BaseFormatter: return str(self.token) -def _jinja_formatter(value, **context): +def _jinja_formatter(value: str, **context) -> str: if jinja_env is None: # pragma: no cover raise ImportError( "jinja2 must be installed to enable '@jinja' settings in dynaconf" ) - return jinja_env.from_string(value).render(**context) + try: + return jinja_env.from_string(value).render(**context) + except jinja2.exceptions.SecurityError: + warnings.warn(f"Unsafe access attempt to: {value}") + return "" def _get_formatter(value, **context): @@ -236,27 +242,59 @@ def _get_formatter(value, **context): cast group will match anything provided after @ the default group will match anything between single or double quotes """ - pattern = re.compile( - r"(?P\w+(?:\.\w+)?)\s*" - r"(?:(?P@\w+)\s*)?" - r'(?P["\']?)' - r'\s*(?P[^"\']*)\s*(?P=quote)?' - ) - if match := pattern.match(value.strip()): - data = match.groupdict() - return context["this"].get( - key=data["key"], - default=data["default"], - cast=data["cast"], - ) - else: + tokens = value.strip().split() + if not tokens or len(tokens) > 3: raise DynaconfFormatError(f"Error parsing {value} malformed syntax.") + params = { + "key": tokens[0], + "cast": None, + "default": None, + } + for token in tokens[1:]: + if token.startswith("@"): + params["cast"] = token + else: + params["default"] = token + + if not params["default"] and params["key"] not in context["this"]: + raise DynaconfParseError( + f"Key {params['key']} not found in settings and no default value provided." + ) + + return context["this"].get(**params) + + +class SafeFormatter(string.Formatter): + def get_field(self, field_name, args, context): + self._validate_key_exists(field_name, context) + return super().get_field(field_name, args, context) + + def _validate_key_exists(self, field_name: str, context): + if not field_name.lower().startswith("this"): + return + from dynaconf.base import _PUBLIC_PROPERTIES + + field_name = field_name.replace("[", ".") + field_name = field_name.replace("]", "") + context_name, _, key = field_name.partition(".") + # these are accessible by the user, but are not considered setting keys + # e.g, settings.current_env + if key in _PUBLIC_PROPERTIES: + return + # allow only existing setting keys + if key not in context[context_name]: + raise AttributeError(key) + + +def _format_formatter(input: str, **context) -> str: + return SafeFormatter().format(input, **context) + class Formatters: """Dynaconf builtin formatters""" - python_formatter = BaseFormatter(str.format, "format") + python_formatter = BaseFormatter(_format_formatter, "format") jinja_formatter = BaseFormatter(_jinja_formatter, "jinja") get_formatter = BaseFormatter(_get_formatter, "get") diff --git a/libs/dynaconf/vendor/ruamel/yaml/constructor.py b/libs/dynaconf/vendor/ruamel/yaml/constructor.py index 3dd3771f5..44151b211 100644 --- a/libs/dynaconf/vendor/ruamel/yaml/constructor.py +++ b/libs/dynaconf/vendor/ruamel/yaml/constructor.py @@ -230,7 +230,7 @@ class SafeConstructor(BaseConstructor): bool_values={'yes':_C,'no':_B,'y':_C,'n':_B,'true':_C,'false':_B,'on':_C,'off':_B} def construct_yaml_bool(self,node):value=self.construct_scalar(node);return self.bool_values[value.lower()] def construct_yaml_int(self,node): - value_s=to_str(self.construct_scalar(node));value_s=value_s.replace(_D,'');sign=+1 + value_s=to_str(self.construct_scalar(node));value_s=value_s.replace(_D,'');sign=1 if value_s[0]=='-':sign=-1 if value_s[0]in _L:value_s=value_s[1:] if value_s==_F:return 0 @@ -247,7 +247,7 @@ class SafeConstructor(BaseConstructor): while inf_value!=inf_value*inf_value:inf_value*=inf_value nan_value=-inf_value/inf_value def construct_yaml_float(self,node): - value_so=to_str(self.construct_scalar(node));value_s=value_so.replace(_D,'').lower();sign=+1 + value_so=to_str(self.construct_scalar(node));value_s=value_so.replace(_D,'').lower();sign=1 if value_s[0]=='-':sign=-1 if value_s[0]in _L:value_s=value_s[1:] if value_s=='.inf':return sign*self.inf_value @@ -459,7 +459,7 @@ class RoundTripConstructor(SafeConstructor): try:sx=value_su.rstrip(_D);underscore=[len(sx)-sx.rindex(_D)-1,_B,_B] except ValueError:underscore=_A except IndexError:underscore=_A - value_s=value_su.replace(_D,'');sign=+1 + value_s=value_su.replace(_D,'');sign=1 if value_s[0]=='-':sign=-1 if value_s[0]in _L:value_s=value_s[1:] if value_s==_F:return 0 @@ -497,7 +497,7 @@ class RoundTripConstructor(SafeConstructor): if v[idx]==_F:lead0+=1 idx+=1 return lead0 - m_sign=_B;value_so=to_str(self.construct_scalar(node));value_s=value_so.replace(_D,'').lower();sign=+1 + m_sign=_B;value_so=to_str(self.construct_scalar(node));value_s=value_so.replace(_D,'').lower();sign=1 if value_s[0]=='-':sign=-1 if value_s[0]in _L:m_sign=value_s[0];value_s=value_s[1:] if value_s=='.inf':return sign*self.inf_value diff --git a/libs/engineio/async_drivers/aiohttp.py b/libs/engineio/async_drivers/aiohttp.py index 41131d830..72dab6476 100644 --- a/libs/engineio/async_drivers/aiohttp.py +++ b/libs/engineio/async_drivers/aiohttp.py @@ -79,11 +79,13 @@ class WebSocket: # pragma: no cover """ def __init__(self, handler, server): self.handler = handler + self.server = server self._sock = None async def __call__(self, environ): request = environ['aiohttp.request'] - self._sock = WebSocketResponse(max_msg_size=0) + self._sock = WebSocketResponse( + max_msg_size=self.server.max_http_buffer_size) await self._sock.prepare(request) self.environ = environ diff --git a/libs/engineio/async_drivers/asgi.py b/libs/engineio/async_drivers/asgi.py index c3581c643..9889047ea 100644 --- a/libs/engineio/async_drivers/asgi.py +++ b/libs/engineio/async_drivers/asgi.py @@ -136,10 +136,18 @@ class ASGIApp: async def translate_request(scope, receive, send): class AwaitablePayload: # pragma: no cover - def __init__(self, payload): - self.payload = payload or b'' + def __init__(self, event): + self.event = event + self.payload = None async def read(self, length=None): + if self.payload is None and event['type'] == 'http.request': + # read payload from http request + self.payload = self.event.get('body') or b'' + while self.event.get('more_body'): + self.event = await receive() + if self.event['type'] == 'http.request': + self.payload += self.event.get('body') or b'' if length is None: r = self.payload self.payload = b'' @@ -149,16 +157,7 @@ async def translate_request(scope, receive, send): return r event = await receive() - payload = b'' - if event['type'] == 'http.request': - payload += event.get('body') or b'' - while event.get('more_body'): - event = await receive() - if event['type'] == 'http.request': - payload += event.get('body') or b'' - elif event['type'] == 'websocket.connect': - pass - else: + if event['type'] not in ['http.request', 'websocket.connect']: return {} raw_uri = scope['path'] @@ -171,7 +170,7 @@ async def translate_request(scope, receive, send): else: raw_uri += '?' + query_string environ = { - 'wsgi.input': AwaitablePayload(payload), + 'wsgi.input': AwaitablePayload(event), 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, diff --git a/libs/engineio/async_server.py b/libs/engineio/async_server.py index e0e252e0a..91339c3f4 100644 --- a/libs/engineio/async_server.py +++ b/libs/engineio/async_server.py @@ -462,7 +462,6 @@ class AsyncServer(base_server.BaseServer): 'maxPayload': self.max_http_buffer_size, }) await s.send(pkt) - s.schedule_ping() ret = await self._trigger_event('connect', sid, environ, run_async=False) @@ -471,6 +470,8 @@ class AsyncServer(base_server.BaseServer): self.logger.warning('Application rejected connection') return self._unauthorized(ret or None) + s.schedule_ping() + if transport == 'websocket': ret = await s.handle_get_request(environ) if s.closed and sid in self.sockets: diff --git a/libs/engineio/async_socket.py b/libs/engineio/async_socket.py index cfdbe1a2d..c1948eefc 100644 --- a/libs/engineio/async_socket.py +++ b/libs/engineio/async_socket.py @@ -128,10 +128,12 @@ class AsyncSocket(base_socket.BaseSocket): await self.queue.join() def schedule_ping(self): - self.server.start_background_task(self._send_ping) + # only schedule a new ping if the previous ping wait cycle completed + if self.last_ping: + self.last_ping = None + self.server.start_background_task(self._send_ping) async def _send_ping(self): - self.last_ping = None await asyncio.sleep(self.server.ping_interval) if not self.closing and not self.closed: self.last_ping = time.time() diff --git a/libs/engineio/base_socket.py b/libs/engineio/base_socket.py index 6b5d7dc8a..9db39c65c 100644 --- a/libs/engineio/base_socket.py +++ b/libs/engineio/base_socket.py @@ -1,3 +1,6 @@ +import time + + class BaseSocket: upgrade_protocols = ['websocket'] @@ -5,7 +8,7 @@ class BaseSocket: self.server = server self.sid = sid self.queue = self.server.create_queue() - self.last_ping = None + self.last_ping = time.time() self.connected = False self.upgrading = False self.upgraded = False diff --git a/libs/engineio/server.py b/libs/engineio/server.py index 917c53f2c..8117d6011 100644 --- a/libs/engineio/server.py +++ b/libs/engineio/server.py @@ -401,7 +401,6 @@ class Server(base_server.BaseServer): 'maxPayload': self.max_http_buffer_size, }) s.send(pkt) - s.schedule_ping() # NOTE: some sections below are marked as "no cover" to workaround # what seems to be a bug in the coverage package. All the lines below @@ -413,6 +412,8 @@ class Server(base_server.BaseServer): self.logger.warning('Application rejected connection') return self._unauthorized(ret or None) + s.schedule_ping() + if transport == 'websocket': # pragma: no cover ret = s.handle_get_request(environ, start_response) if s.closed and sid in self.sockets: diff --git a/libs/engineio/socket.py b/libs/engineio/socket.py index 26bb94b51..1ddd31d28 100644 --- a/libs/engineio/socket.py +++ b/libs/engineio/socket.py @@ -130,10 +130,12 @@ class Socket(base_socket.BaseSocket): self.queue.join() def schedule_ping(self): - self.server.start_background_task(self._send_ping) + # only schedule a new ping if the previous ping wait cycle completed + if self.last_ping: + self.last_ping = None + self.server.start_background_task(self._send_ping) def _send_ping(self): - self.last_ping = None self.server.sleep(self.server.ping_interval) if not self.closing and not self.closed: self.last_ping = time.time() diff --git a/libs/ffsubsync-0.4.31.dist-info/RECORD b/libs/ffsubsync-0.4.31.dist-info/RECORD deleted file mode 100644 index f0083e096..000000000 --- a/libs/ffsubsync-0.4.31.dist-info/RECORD +++ /dev/null @@ -1,26 +0,0 @@ -../../bin/ffs,sha256=32XxXbBZUX4ykfqdmPzCJi1nyuY8eNklysCZ6vXDNro,184 -../../bin/ffsubsync,sha256=32XxXbBZUX4ykfqdmPzCJi1nyuY8eNklysCZ6vXDNro,184 -../../bin/subsync,sha256=32XxXbBZUX4ykfqdmPzCJi1nyuY8eNklysCZ6vXDNro,184 -ffsubsync-0.4.31.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -ffsubsync-0.4.31.dist-info/METADATA,sha256=atAt88_KOHZSJeQeOnok6axknkSv-lb8rw3qdxHFRwo,10336 -ffsubsync-0.4.31.dist-info/RECORD,, -ffsubsync-0.4.31.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -ffsubsync-0.4.31.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109 -ffsubsync-0.4.31.dist-info/entry_points.txt,sha256=x5tYAy_9mGKvB6HtyypAE7bMClVIVPxcxcTozISatsg,91 -ffsubsync-0.4.31.dist-info/licenses/LICENSE,sha256=DojZETNKKMCxh_kvQYZwyPFvuoM9pkllyQDqVdT9Ja4,1053 -ffsubsync-0.4.31.dist-info/top_level.txt,sha256=rD4Pc_QfEQZsZ-i6QXAdnbhVWanzT9edNbD7htPRjWs,10 -ffsubsync/__init__.py,sha256=ZvmoQa78CTLOAugFvZ2Ui4CpqSIbvcAEbDSNUjLNwt0,601 -ffsubsync/_version.py,sha256=L_crtYFeaUjpj1PjY9398V2NobkBGQ79wehWJuimw1w,498 -ffsubsync/aligners.py,sha256=uaWmoCax0t8C6njy9paPkhgG4luPk8VIDm97WrZMzWU,5738 -ffsubsync/constants.py,sha256=SYqge3UW2ZQsGk_bi_wgg010SJCBhu6akR9R9fatv_0,1324 -ffsubsync/ffmpeg_utils.py,sha256=QeWpxoZ4Lm1IyLptl-pc1DZjn2zgLocZ0wyie86Popc,3161 -ffsubsync/ffsubsync.py,sha256=8b0bj4z25AdPIglVtoshWwPI90RV9F3H_YYcFgo9hng,26017 -ffsubsync/ffsubsync_gui.py,sha256=0cC9shk5q6KShAMzpo9ERbh2xVTyJNY7T4IOPqrE5p8,3617 -ffsubsync/file_utils.py,sha256=lKFNzGEVObAaCNsGt8drPSRFP_MLRGVWw3pi_ltlEUo,790 -ffsubsync/generic_subtitles.py,sha256=ikm_x09aU4aDQUfqLHPmuaxbvWTTH3Q4hla75IuEUk4,6036 -ffsubsync/golden_section_search.py,sha256=hJjjORLTzN01Yaz2H95_3RFHxmPoJGAg3yPSdg_7IKI,1831 -ffsubsync/sklearn_shim.py,sha256=KcnfAfCUMUDEWYiDooTZrAvWZSYsrRZhBZZn787NrrU,14569 -ffsubsync/speech_transformers.py,sha256=msv1V5DIbkF3NSli2as0pJuSZQMgg1t2aWk867IS-pg,19267 -ffsubsync/subtitle_parser.py,sha256=Edy5OYTyGxNgluFlz09qKLp96p2PvY9nlXoHqgdZNiU,6483 -ffsubsync/subtitle_transformers.py,sha256=qxD-FDRLDHUdXBXtr25Y8eXVLlV_XPv8tt5UQUMekGE,4547 -ffsubsync/version.py,sha256=GCDvVUjTaRo3LJ6PsWPopYN5eY6rjRFl3MdlxeH2nsI,1232 diff --git a/libs/ffsubsync-0.4.31.dist-info/INSTALLER b/libs/ffsubsync-0.5.0.dist-info/INSTALLER similarity index 100% rename from libs/ffsubsync-0.4.31.dist-info/INSTALLER rename to libs/ffsubsync-0.5.0.dist-info/INSTALLER diff --git a/libs/ffsubsync-0.4.31.dist-info/METADATA b/libs/ffsubsync-0.5.0.dist-info/METADATA similarity index 63% rename from libs/ffsubsync-0.4.31.dist-info/METADATA rename to libs/ffsubsync-0.5.0.dist-info/METADATA index 44e846251..620b318d2 100644 --- a/libs/ffsubsync-0.4.31.dist-info/METADATA +++ b/libs/ffsubsync-0.5.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ffsubsync -Version: 0.4.31 +Version: 0.5.0 Home-page: https://github.com/smacke/ffsubsync Author: Stephen Macke Author-email: stephen.macke@gmail.com @@ -31,12 +31,10 @@ Requires-Dist: numpy>=1.12.0 Requires-Dist: pysubs2; python_version < "3.7" Requires-Dist: pysubs2>=1.2.0; python_version >= "3.7" Requires-Dist: rich -Requires-Dist: setuptools Requires-Dist: srt>=3.0.0 Requires-Dist: tqdm Requires-Dist: typing_extensions -Requires-Dist: webrtcvad; platform_system != "Windows" -Requires-Dist: webrtcvad-wheels; platform_system == "Windows" +Requires-Dist: webrtcvad-wheels Dynamic: author Dynamic: author-email Dynamic: classifier @@ -114,6 +112,107 @@ ffsubsync reference.srt -i unsynchronized.srt -o synchronized.srt `ffsubsync` uses the file extension to decide whether to perform voice activity detection on the audio or to directly extract speech from an srt file. +If you omit `-i`, `ffsubsync` auto-detects input subtitles sitting next to the +reference that share its name, so you can simply run: +~~~ +ffs video.mp4 +~~~ +This picks up files like `video.srt` and `video.en.srt` from the reference's +directory and writes the synced result for each to a `.synced.srt` +alongside it (e.g. `video.synced.srt`), leaving the originals untouched. +Previously-synced `*.synced.srt` outputs are skipped, so re-running is safe. Add +`--overwrite-input` to overwrite the detected file(s) in place instead. Auto- +detection is skipped when subtitles are piped in on stdin. + +The reference can also be a remote URL instead of a local file. Anything ffmpeg +can read directly works as a video / audio reference, and remote subtitle files +work as a reference too: +~~~ +ffs "https://example.com/video.mp4" -i unsynchronized.srt -o synchronized.srt +ffs "https://example.com/reference.srt" -i unsynchronized.srt -o synchronized.srt +~~~ +Supported protocols are `http(s)://`, `rtmp://`, `rtsp://`, and `ftp://`. +Processing streams the reference over the network, so it depends on connection +stability; for large or flaky sources, downloading first is more reliable. +Sibling-subtitle auto-detection (the no-`-i` form above) is local-only and is +skipped for remote references. + +To speed up long references, `--max-duration-seconds N` processes only the +first `N` seconds (measured from `--start-seconds`). This is especially helpful +for remote references, since ffmpeg stops reading—and therefore downloading—once +that duration is reached: +~~~ +ffs "https://example.com/video.mp4" -i unsynchronized.srt -o synchronized.srt --max-duration-seconds 600 +~~~ +On flaky connections, `--extract-audio-first` can be more reliable: instead of +holding a network stream open throughout speech detection, it first copies the +remote audio track to a local temp file (no re-encode) and runs detection on +that. It is ignored for local references and composes with +`--max-duration-seconds`: +~~~ +ffs "https://example.com/video.mp4" -i unsynchronized.srt -o synchronized.srt --extract-audio-first +~~~ + +For long references where `--max-duration-seconds` would miss desync that only +shows up later, `--multi-segment-sync` instead samples several short segments +spread across the whole reference and runs speech detection on just those: +~~~ +ffs "https://example.com/video.mp4" -i unsynchronized.srt -o synchronized.srt --multi-segment-sync +~~~ +Only the sampled audio is extracted (and, for remote references, downloaded), +but because each segment keeps its true position on the timeline, the usual +framerate-ratio and offset search is unchanged—so a framerate mismatch is still +detected and corrected. Tune it with `--segment-count N` (default 8), +`--skip-intro-outro` (skip the first 30s / last 60s, which often lack dialogue), +and `--parallel-workers N` (overlap segment downloads, default 4). It applies to +video / audio references only. + +Docker +------ +Prebuilt images are published to the GitHub Container Registry. Pull the +latest release with: +~~~ +docker pull ghcr.io/smacke/ffsubsync:latest +~~~ +Run it by mounting the directory with your video and subtitles into `/video`: +~~~ +docker run --rm -v "$PWD":/video ghcr.io/smacke/ffsubsync:latest \ + video.mp4 -i unsynchronized.srt -o synchronized.srt +~~~ +You can also build the image yourself. The multi-stage Dockerfile defaults to +installing from the current working tree: +~~~ +docker build -t ffsubsync . +~~~ +To install a specific version from PyPI instead, set `FFSUBSYNC_VERSION`: +~~~ +docker build -t ffsubsync --build-arg FFSUBSYNC_VERSION=0.4.31 . +~~~ + +Using as a Library +------------------ +`ffsubsync` can be driven programmatically via `ffsubsync.run`, which accepts an +`argparse.Namespace` (build one with `make_parser`). To surface progress in your +own UI, pass a `progress_handler` callback; it is invoked repeatedly while the +reference's audio is being decoded with a `ProgressInfo` describing how far along +the extraction is: +~~~python +import ffsubsync +from ffsubsync.ffsubsync import make_parser + +def on_progress(info: ffsubsync.ProgressInfo) -> None: + # info.processed_seconds / info.total_seconds (total may be None); + # info.fraction is a 0.0-1.0 ratio (None if the total is unknown). + if info.fraction is not None: + print(f"{info.fraction:.0%}") + +args = make_parser().parse_args(["ref.mkv", "-i", "in.srt", "-o", "out.srt"]) +result = ffsubsync.run(args, progress_handler=on_progress) +~~~ +The handler is called only for the video / audio reference path (the dominant +cost of a sync). Exceptions it raises are logged and swallowed, so a buggy +handler can never abort syncing. + Sync Issues ----------- If the sync fails, the following recourses are available: @@ -130,6 +229,22 @@ If the sync fails, the following recourses are available: Auditok does not specifically detect voice, but instead detects all audio; this property can yield suboptimal syncing behavior when a proper VAD can work well, but can be effective in some cases. +- Try `--vad=fused`, which combines WebRTC with the neural + [silero](https://github.com/snakers4/silero-vad) VAD and can be more robust on + noisy audio. The strategy can be tuned: `--vad=fused:intersection` + (conservative -- speech only where both agree), `--vad=fused:union` + (aggressive -- speech where either fires), or `--vad=fused:weighted` (the + default). These options require the optional silero dependency, which itself + requires PyTorch; install both with `pip install ffsubsync[torch]` (or just + `pip install torch`). torch is not installed with `ffsubsync` by default. + +When syncing in bulk, a bad sync can be worse than none. Passing +`--skip-sync-on-low-quality` leaves the subtitles unmodified when the alignment +looks untrustworthy—an anti-correlated score (`--min-score`, default 0.0) or an +implausibly large offset (`--quality-max-offset-seconds`, default 30). There is +also a `--max-framerate-deviation` check (default 0.1, which permits every +framerate correction `ffsubsync` makes); tighten it only when you know the +framerate should not change. If the sync still fails, consider trying one of the following similar tools: - [sc0ty/subsync](https://github.com/sc0ty/subsync): does speech-to-text and looks for matching word morphemes diff --git a/libs/ffsubsync-0.5.0.dist-info/RECORD b/libs/ffsubsync-0.5.0.dist-info/RECORD new file mode 100644 index 000000000..855496ac1 --- /dev/null +++ b/libs/ffsubsync-0.5.0.dist-info/RECORD @@ -0,0 +1,27 @@ +../../bin/ffs,sha256=32XxXbBZUX4ykfqdmPzCJi1nyuY8eNklysCZ6vXDNro,184 +../../bin/ffsubsync,sha256=32XxXbBZUX4ykfqdmPzCJi1nyuY8eNklysCZ6vXDNro,184 +../../bin/subsync,sha256=32XxXbBZUX4ykfqdmPzCJi1nyuY8eNklysCZ6vXDNro,184 +ffsubsync-0.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +ffsubsync-0.5.0.dist-info/METADATA,sha256=2DwXmV1C__kI7RrLawcx4RhxwQdglg1TRt-dZR5VNpo,15955 +ffsubsync-0.5.0.dist-info/RECORD,, +ffsubsync-0.5.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ffsubsync-0.5.0.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109 +ffsubsync-0.5.0.dist-info/entry_points.txt,sha256=x5tYAy_9mGKvB6HtyypAE7bMClVIVPxcxcTozISatsg,91 +ffsubsync-0.5.0.dist-info/licenses/LICENSE,sha256=DojZETNKKMCxh_kvQYZwyPFvuoM9pkllyQDqVdT9Ja4,1053 +ffsubsync-0.5.0.dist-info/top_level.txt,sha256=rD4Pc_QfEQZsZ-i6QXAdnbhVWanzT9edNbD7htPRjWs,10 +ffsubsync/__init__.py,sha256=j02sS907bLQyVLAlFlIV11xWe5tMYH7k-tTDskbiUng,660 +ffsubsync/_version.py,sha256=pbmwfjRIWmXVrlnlTcT6_n9ygQ349jdyv22zuCyx5Qk,497 +ffsubsync/aligners.py,sha256=04PhgaXpKY0lfOMNupLXJcEesH1F-4JiEnYG9ZoYj9Q,6262 +ffsubsync/constants.py,sha256=WtlD9n_n1wQ0urjfmALs9KwvY9035ZGl6vDQZYvUCzY,2393 +ffsubsync/ffmpeg_utils.py,sha256=QeWpxoZ4Lm1IyLptl-pc1DZjn2zgLocZ0wyie86Popc,3161 +ffsubsync/ffsubsync.py,sha256=J7JXb6wbN3MF-1TDOQ7hDYkFnzwdpdhmq3twrLPpTVw,40075 +ffsubsync/ffsubsync_gui.py,sha256=0cC9shk5q6KShAMzpo9ERbh2xVTyJNY7T4IOPqrE5p8,3617 +ffsubsync/file_utils.py,sha256=5Cx4YWMKP3w5vDPtSrHlyWSty9pFNRayqAIbHjwH0-U,1110 +ffsubsync/generic_subtitles.py,sha256=ikm_x09aU4aDQUfqLHPmuaxbvWTTH3Q4hla75IuEUk4,6036 +ffsubsync/golden_section_search.py,sha256=hJjjORLTzN01Yaz2H95_3RFHxmPoJGAg3yPSdg_7IKI,1831 +ffsubsync/sklearn_shim.py,sha256=KcnfAfCUMUDEWYiDooTZrAvWZSYsrRZhBZZn787NrrU,14569 +ffsubsync/speech_transformers.py,sha256=x5oVi3DrXckTCrRctsgGYyFpqMHLsex6Q9XWXa6lLgs,47632 +ffsubsync/subtitle_parser.py,sha256=Edy5OYTyGxNgluFlz09qKLp96p2PvY9nlXoHqgdZNiU,6483 +ffsubsync/subtitle_preprocessor.py,sha256=vv5VGv7jR4JoPJoogM1zjHA7FUUmjm_7CY-wGqHkk7Y,5590 +ffsubsync/subtitle_transformers.py,sha256=qxD-FDRLDHUdXBXtr25Y8eXVLlV_XPv8tt5UQUMekGE,4547 +ffsubsync/version.py,sha256=M5YQCdJkUGzPj1stEbbGl9YwHyW3wItVwDHMMso7XQI,1617 diff --git a/libs/ffsubsync-0.4.31.dist-info/REQUESTED b/libs/ffsubsync-0.5.0.dist-info/REQUESTED similarity index 100% rename from libs/ffsubsync-0.4.31.dist-info/REQUESTED rename to libs/ffsubsync-0.5.0.dist-info/REQUESTED diff --git a/libs/ffsubsync-0.4.31.dist-info/WHEEL b/libs/ffsubsync-0.5.0.dist-info/WHEEL similarity index 71% rename from libs/ffsubsync-0.4.31.dist-info/WHEEL rename to libs/ffsubsync-0.5.0.dist-info/WHEEL index 5f133dbb5..b9b700f66 100644 --- a/libs/ffsubsync-0.4.31.dist-info/WHEEL +++ b/libs/ffsubsync-0.5.0.dist-info/WHEEL @@ -1,5 +1,5 @@ Wheel-Version: 1.0 -Generator: setuptools (80.9.0) +Generator: setuptools (82.0.1) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any diff --git a/libs/ffsubsync-0.4.31.dist-info/entry_points.txt b/libs/ffsubsync-0.5.0.dist-info/entry_points.txt similarity index 100% rename from libs/ffsubsync-0.4.31.dist-info/entry_points.txt rename to libs/ffsubsync-0.5.0.dist-info/entry_points.txt diff --git a/libs/ffsubsync-0.4.31.dist-info/licenses/LICENSE b/libs/ffsubsync-0.5.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/ffsubsync-0.4.31.dist-info/licenses/LICENSE rename to libs/ffsubsync-0.5.0.dist-info/licenses/LICENSE diff --git a/libs/ffsubsync-0.4.31.dist-info/top_level.txt b/libs/ffsubsync-0.5.0.dist-info/top_level.txt similarity index 100% rename from libs/ffsubsync-0.4.31.dist-info/top_level.txt rename to libs/ffsubsync-0.5.0.dist-info/top_level.txt diff --git a/libs/ffsubsync/__init__.py b/libs/ffsubsync/__init__.py index a97907205..ee52bd39a 100644 --- a/libs/ffsubsync/__init__.py +++ b/libs/ffsubsync/__init__.py @@ -18,4 +18,5 @@ except: # noqa: E722 logging.basicConfig(stream=sys.stderr, level=logging.INFO) from .version import __version__ # noqa -from .ffsubsync import main # noqa +from .ffsubsync import main, run # noqa +from .speech_transformers import ProgressInfo # noqa diff --git a/libs/ffsubsync/_version.py b/libs/ffsubsync/_version.py index 0517bde13..76f335264 100644 --- a/libs/ffsubsync/_version.py +++ b/libs/ffsubsync/_version.py @@ -8,11 +8,11 @@ import json version_json = ''' { - "date": "2025-11-23T21:22:43-0800", + "date": "2026-06-17T00:11:28-0700", "dirty": false, "error": null, - "full-revisionid": "73968e5c43bee79a363eb7e30c3fab87f99ca7f8", - "version": "0.4.31" + "full-revisionid": "d69d3f98aa0dfbca5d1f7932143c220c6aac278c", + "version": "0.5.0" } ''' # END VERSION_JSON diff --git a/libs/ffsubsync/aligners.py b/libs/ffsubsync/aligners.py index 28b7bcf9d..22f2390d1 100644 --- a/libs/ffsubsync/aligners.py +++ b/libs/ffsubsync/aligners.py @@ -55,6 +55,15 @@ class FFTAligner(TransformerMixin): refstring, substring = map( lambda s: 2 * np.array(s).astype(float) - 1, [refstring, substring] ) + if len(refstring) == 0 or len(substring) == 0: + # empty speech on either side has no meaningful alignment, and would + # otherwise yield a garbage offset (or crash math.log when both empty) + raise FailedToFindAlignmentException( + "cannot align empty speech data " + "(reference length=%d, subtitle length=%d); " + "the reference or subtitles may contain no detectable speech" + % (len(refstring), len(substring)) + ) total_bits = math.log(len(substring) + len(refstring), 2) total_length = int(2 ** math.ceil(total_bits)) extra_zeros = total_length - len(substring) - len(refstring) diff --git a/libs/ffsubsync/constants.py b/libs/ffsubsync/constants.py index b8df034fe..ffb7dfb02 100644 --- a/libs/ffsubsync/constants.py +++ b/libs/ffsubsync/constants.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from typing import List, Tuple +from typing import List, Optional, Tuple SUBSYNC_RESOURCES_ENV_MAGIC: str = "ffsubsync_resources_xj48gjdkl340" @@ -18,8 +18,36 @@ DEFAULT_VAD: str = "subs_then_webrtc" DEFAULT_MAX_OFFSET_SECONDS: int = 60 DEFAULT_APPLY_OFFSET_SECONDS: int = 0 +# Quality gating (--skip-sync-on-low-quality). The score's sign is meaningful even +# though its magnitude is not, so 0.0 rejects only anti-correlated alignments. The +# framerate-deviation default clears every real correction (discrete ratios reach +# ~0.0427 and a typical --gss result stays well within +/-0.1), so it does not +# reject a legitimate correction -- it is a knob to tighten when you know the +# framerate should not change. +DEFAULT_MIN_SCORE: float = 0.0 +DEFAULT_QUALITY_MAX_OFFSET_SECONDS: float = 30.0 +DEFAULT_MAX_FRAMERATE_DEVIATION: float = 0.1 + SUBTITLE_EXTENSIONS: Tuple[str, ...] = ("srt", "ass", "ssa", "sub") +# Remote URL protocols that ffmpeg can read directly as an input (`-i `), +# so a reference of this form needs no local download or read-permission check. +REMOTE_URL_PROTOCOLS: Tuple[str, ...] = ( + "http://", + "https://", + "rtmp://", + "rtsp://", + "ftp://", +) + + +def is_remote_url(path: Optional[str]) -> bool: + """Return True if *path* is a remote URL ffmpeg can stream directly.""" + if path is None: + return False + return path.startswith(REMOTE_URL_PROTOCOLS) + + GITHUB_DEV_USER: str = "smacke" PROJECT_NAME: str = "FFsubsync" PROJECT_LICENSE: str = "MIT" diff --git a/libs/ffsubsync/ffsubsync.py b/libs/ffsubsync/ffsubsync.py index b0114cc89..37776ae10 100644 --- a/libs/ffsubsync/ffsubsync.py +++ b/libs/ffsubsync/ffsubsync.py @@ -24,12 +24,19 @@ from ffsubsync.constants import ( FRAMERATE_RATIOS, SAMPLE_RATE, SUBTITLE_EXTENSIONS, + DEFAULT_MIN_SCORE, + DEFAULT_QUALITY_MAX_OFFSET_SECONDS, + DEFAULT_MAX_FRAMERATE_DEVIATION, + is_remote_url, ) from ffsubsync.ffmpeg_utils import ffmpeg_bin_path from ffsubsync.sklearn_shim import Pipeline, TransformerMixin from ffsubsync.speech_transformers import ( VideoSpeechTransformer, + MultiSegmentVideoSpeechTransformer, DeserializeSpeechTransformer, + PGSSpeechTransformer, + ProgressInfo, make_subtitle_speech_pipeline, ) from ffsubsync.subtitle_parser import make_subtitle_parser @@ -94,6 +101,20 @@ def make_test_case( return 0 +def _resolve_srtout(args: argparse.Namespace, srtin: Optional[str]) -> Optional[str]: + """Decide where the synced subtitles for ``srtin`` should be written. + + Overwriting the input takes precedence; otherwise, when subtitles were + auto-detected from the reference, write a `.synced.srt` next to each + input; otherwise fall back to the explicit (possibly ``None``) output. + """ + if args.overwrite_input: + return srtin + if getattr(args, "auto_srtout", False) and srtin is not None: + return "{}.synced.srt".format(os.path.splitext(srtin)[0]) + return args.srtout + + def get_srt_pipe_maker( args: argparse.Namespace, srtin: Optional[str] ) -> Callable[[Optional[float]], Union[Pipeline, Callable[[float], Pipeline]]]: @@ -121,6 +142,38 @@ def get_framerate_ratios_to_try(args: argparse.Namespace) -> List[Optional[float return framerate_ratios +def assess_alignment_quality( + best_score: float, + offset_seconds: float, + scale_factor: float, + *, + min_score: float, + max_offset_seconds: float, + max_framerate_deviation: float, +) -> List[str]: + """Return reasons an alignment looks too low-quality to trust (empty = trust it). + + Used by --skip-sync-on-low-quality to decide whether to leave the subtitles + unmodified rather than apply a probably-wrong shift. A negative score means the + best alignment is anti-correlated; an implausibly large offset or framerate + scale suggests a spurious match. + """ + reasons: List[str] = [] + if best_score < min_score: + reasons.append("score %.1f < %.1f" % (best_score, min_score)) + if abs(offset_seconds) > max_offset_seconds: + reasons.append( + "|offset| %.1fs > %.1fs" % (abs(offset_seconds), max_offset_seconds) + ) + framerate_deviation = abs(scale_factor - 1.0) + if framerate_deviation > max_framerate_deviation: + reasons.append( + "framerate deviation %.3f > %.3f" + % (framerate_deviation, max_framerate_deviation) + ) + return reasons + + def try_sync( args: argparse.Namespace, reference_pipe: Optional[Pipeline], result: Dict[str, Any] ) -> bool: @@ -138,7 +191,7 @@ def try_sync( skip_infer_framerate_ratio = ( args.skip_infer_framerate_ratio or reference_pipe is None ) - srtout = srtin if args.overwrite_input else args.srtout + srtout = _resolve_srtout(args, srtin) srt_pipe_maker = get_srt_pipe_maker(args, srtin) framerate_ratios = get_framerate_ratios_to_try(args) srt_pipes = [srt_pipe_maker(1.0)] + [ @@ -149,8 +202,10 @@ def try_sync( continue else: srt_pipe.fit(srtin) - if not skip_infer_framerate_ratio and hasattr( - reference_pipe[-1], "num_frames" + if ( + not skip_infer_framerate_ratio + and hasattr(reference_pipe[-1], "num_frames") + and reference_pipe[-1].num_frames is not None ): inferred_framerate_ratio_from_length = ( float(reference_pipe[-1].num_frames) @@ -188,6 +243,32 @@ def try_sync( logger.info("score: %.3f", best_score) logger.info("offset seconds: %.3f", offset_seconds) logger.info("framerate scale factor: %.3f", scale_step.scale_factor) + low_quality_reasons: List[str] = [] + if getattr(args, "skip_sync_on_low_quality", False): + low_quality_reasons = assess_alignment_quality( + best_score, + offset_seconds, + scale_step.scale_factor, + min_score=args.min_score, + max_offset_seconds=args.quality_max_offset_seconds, + max_framerate_deviation=args.max_framerate_deviation, + ) + if low_quality_reasons: + logger.warning( + "low-quality alignment (%s); leaving subtitles unmodified", + "; ".join(low_quality_reasons), + ) + sync_was_successful = False + # write the original (unscaled, unshifted) subtitles unchanged + original_subs = best_srt_pipe.named_steps["parse"].subs_ + out_subs = original_subs.clone_props_for_subs(list(original_subs)) + if args.output_encoding != "same": + out_subs = out_subs.set_encoding(args.output_encoding) + logger.info( + "writing original (unsynced) output to {}".format(srtout or "stdout") + ) + out_subs.write_file(srtout) + continue output_steps: List[Tuple[str, TransformerMixin]] = [ ("shift", SubtitleShifter(offset_seconds)) ] @@ -219,7 +300,30 @@ def try_sync( return sync_was_successful -def make_reference_pipe(args: argparse.Namespace) -> Pipeline: +def make_reference_pipe( + args: argparse.Namespace, + progress_handler: Optional[Callable[[ProgressInfo], None]] = None, +) -> Pipeline: + pgs_stream = getattr(args, "pgs_ref_stream", None) + if pgs_stream is not None: + # "auto" (bare --pgs-ref-stream flag) → let PGSSpeechTransformer auto-detect + resolved_stream: Optional[str] = None if pgs_stream == "auto" else pgs_stream + if resolved_stream is not None and not resolved_stream.startswith("0:"): + resolved_stream = "0:" + resolved_stream + return Pipeline( + [ + ( + "speech_extract", + PGSSpeechTransformer( + sample_rate=SAMPLE_RATE, + start_seconds=args.start_seconds, + ffmpeg_path=args.ffmpeg_path, + ref_stream=resolved_stream, + gui_mode=args.gui_mode, + ), + ), + ] + ) ref_format = _ref_format(args.reference) if ref_format in SUBTITLE_EXTENSIONS: if args.vad is not None: @@ -246,6 +350,29 @@ def make_reference_pipe(args: argparse.Namespace) -> Pipeline: ref_stream = args.reference_stream if ref_stream is not None and not ref_stream.startswith("0:"): ref_stream = "0:" + ref_stream + if getattr(args, "multi_segment_sync", False): + return Pipeline( + [ + ( + "speech_extract", + MultiSegmentVideoSpeechTransformer( + vad=vad, + sample_rate=SAMPLE_RATE, + frame_rate=args.frame_rate, + non_speech_label=args.non_speech_label, + segment_count=getattr(args, "segment_count", 8), + skip_intro_outro=getattr( + args, "skip_intro_outro", False + ), + parallel_workers=getattr(args, "parallel_workers", 4), + ffmpeg_path=args.ffmpeg_path, + ref_stream=ref_stream, + vlc_mode=args.vlc_mode, + gui_mode=args.gui_mode, + ), + ), + ] + ) return Pipeline( [ ( @@ -260,6 +387,13 @@ def make_reference_pipe(args: argparse.Namespace) -> Pipeline: ref_stream=ref_stream, vlc_mode=args.vlc_mode, gui_mode=args.gui_mode, + max_duration_seconds=getattr( + args, "max_duration_seconds", None + ), + extract_audio_first=getattr( + args, "extract_audio_first", False + ), + progress_handler=progress_handler, ), ), ] @@ -312,6 +446,34 @@ def extract_subtitles_from_reference(args: argparse.Namespace) -> int: return retcode +def _detect_srtin_from_reference(reference: str) -> List[str]: + """Find subtitle files that sit next to the reference and share its name. + + Matches `.srt` and `..srt` (e.g. + `movie.srt`, `movie.en.srt` for a `movie.mkv` reference) in the reference's + own directory, so detection works regardless of the process's current + working directory. The reference itself is never returned, so this is safe + even when the reference is already a subtitle file. + """ + reference_dir = os.path.dirname(reference) or "." + reference_stem = os.path.splitext(os.path.basename(reference))[0] + reference_abspath = os.path.abspath(reference) + matches = [] + for name in sorted(os.listdir(reference_dir)): + stem, ext = os.path.splitext(name) + if ext.lower() != ".srt": + continue + if name.endswith(".synced.srt"): + continue # skip our own previously-synced outputs so re-runs are idempotent + if stem != reference_stem and not stem.startswith(reference_stem + "."): + continue + path = os.path.join(reference_dir, name) + if os.path.abspath(path) == reference_abspath: + continue + matches.append(path) + return matches + + def validate_args(args: argparse.Namespace) -> None: if args.vlc_mode: logger.setLevel(logging.CRITICAL) @@ -336,6 +498,36 @@ def validate_args(args: argparse.Namespace) -> None: raise ValueError("cannot specify multiple input srt files for test cases") if len(args.srtin) > 1 and args.gui_mode: raise ValueError("cannot specify multiple input srt files in GUI mode") + elif ( + args.reference is not None + and not is_remote_url(args.reference) # can't list a remote dir for sibling srts + and args.extract_subs_from_stream is None + and not args.gui_mode + and not args.make_test_case + and sys.stdin.isatty() # don't hijack subtitles piped in on stdin + ): + # No input subtitles were given (and nothing is piped in), so look for + # subtitle files alongside the reference that share its name. + logger.info("no input srt specified; detecting input srt from reference") + detected = _detect_srtin_from_reference(args.reference) + if detected: + for path in detected: + logger.info("detected input srt: %s", path) + args.srtin = detected + if len(detected) > 1 and args.srtout is not None: + raise ValueError( + "detected multiple input srt files but an output file was " + "specified; re-run with --overwrite-input or a single input" + ) + if args.srtout is None and not args.overwrite_input: + args.auto_srtout = True + logger.info( + "writing synced output alongside each input as " + ".synced.srt; pass --overwrite-input to overwrite the " + "input file(s) in place instead" + ) + else: + logger.info("no input srt detected from reference") if ( args.make_test_case and not args.gui_mode ): # this validation not necessary for gui mode @@ -373,7 +565,11 @@ def validate_file_permissions(args: argparse.Namespace) -> None: "unable to {action} {file}; " "try ensuring file exists and has correct permissions" ) - if args.reference is not None and not os.access(args.reference, os.R_OK): + if ( + args.reference is not None + and not is_remote_url(args.reference) # ffmpeg streams URLs directly; no local file to check + and not os.access(args.reference, os.R_OK) + ): raise ValueError( error_string_template.format(action="read reference", file=args.reference) ) @@ -423,7 +619,11 @@ def _npy_savename(args: argparse.Namespace) -> str: return os.path.splitext(args.reference)[0] + ".npz" -def _run_impl(args: argparse.Namespace, result: Dict[str, Any]) -> bool: +def _run_impl( + args: argparse.Namespace, + result: Dict[str, Any], + progress_handler: Optional[Callable[[ProgressInfo], None]] = None, +) -> bool: if args.extract_subs_from_stream is not None: result["retval"] = extract_subtitles_from_reference(args) return True @@ -432,7 +632,7 @@ def _run_impl(args: argparse.Namespace, result: Dict[str, Any]) -> bool: or (len(args.srtin) == 1 and args.srtin[0] == args.reference) ): return try_sync(args, None, result) - reference_pipe = make_reference_pipe(args) + reference_pipe = make_reference_pipe(args, progress_handler=progress_handler) logger.info("extracting speech segments from reference '%s'...", args.reference) reference_pipe.fit(args.reference) logger.info("...done") @@ -451,7 +651,7 @@ def _run_impl(args: argparse.Namespace, result: Dict[str, Any]) -> bool: def validate_and_transform_args( - parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace] + parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace], ) -> Optional[argparse.Namespace]: if isinstance(parser_or_args, argparse.Namespace): parser = None @@ -484,8 +684,18 @@ def validate_and_transform_args( def run( - parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace] + parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace], + progress_handler: Optional[Callable[[ProgressInfo], None]] = None, ) -> Dict[str, Any]: + """Synchronize subtitles. + + ``progress_handler``, if given, is called repeatedly during reference speech + extraction with a :class:`~ffsubsync.speech_transformers.ProgressInfo` + describing how much of the reference audio has been processed. This lets a + host application (e.g. a web UI) display sync progress. The handler is only + invoked for the video-reference path; exceptions it raises are logged and + swallowed so a buggy handler cannot abort syncing. + """ sync_was_successful = False result = { "retval": 0, @@ -498,7 +708,9 @@ def run( return result log_path, log_handler = _setup_logging(args) try: - sync_was_successful = _run_impl(args, result) + sync_was_successful = _run_impl( + args, result, progress_handler=progress_handler + ) result["sync_was_successful"] = sync_was_successful return result finally: @@ -523,7 +735,16 @@ def add_main_args_for_cli(parser: argparse.ArgumentParser) -> None: ), ) parser.add_argument( - "-i", "--srtin", nargs="*", help="Input subtitles file (default=stdin)." + "-i", + "--srtin", + nargs="*", + help=( + "Input subtitles file (default=stdin). If omitted (and nothing is " + "piped in), subtitles sharing the reference's name in its directory " + "are auto-detected (e.g. `movie.srt`, `movie.en.srt` for `movie.mkv`) " + "and each is synced to a `.synced.srt` next to it; pass " + "--overwrite-input to overwrite the detected file(s) in place." + ), ) parser.add_argument( "-o", "--srtout", help="Output subtitles file (default=stdout)." @@ -556,6 +777,21 @@ def add_main_args_for_cli(parser: argparse.ArgumentParser) -> None: "Example: `ffs ref.mkv -i in.srt -o out.srt --reference-stream s:2`" ), ) + parser.add_argument( + "--pgs-ref-stream", + "--pgsstream", + nargs="?", + const="auto", + default=None, + help=( + "Use a PGS (Presentation Graphic Stream) image-based subtitle track from " + "the reference MKV as the sync reference instead of audio VAD. " + "Optionally specify the stream (leading `0:` is optional, e.g. `s:0` or `3`). " + "Omit the value to auto-detect the first hdmv_pgs_subtitle track. " + "Example: `ffs ref.mkv -i in.srt -o out.srt --pgs-ref-stream` (auto) " + "or `ffs ref.mkv -i in.srt -o out.srt --pgs-ref-stream s:2` (explicit)." + ), + ) def add_cli_only_args(parser: argparse.ArgumentParser) -> None: @@ -564,7 +800,7 @@ def add_cli_only_args(parser: argparse.ArgumentParser) -> None: "--version", action="version", version="{package} {version}".format( - package=__package__, version=get_version() + package=__package__ or "ffsubsync", version=get_version() ), ) parser.add_argument( @@ -602,6 +838,50 @@ def add_cli_only_args(parser: argparse.ArgumentParser) -> None: help="The max allowed offset seconds for any subtitle segment " "(default=%d seconds)." % DEFAULT_MAX_OFFSET_SECONDS, ) + parser.add_argument( + "--max-duration-seconds", + type=float, + default=None, + help="If specified, only process the first this-many seconds of the " + "reference (measured from --start-seconds). Useful for speeding up " + "long or remote references, since ffmpeg stops reading/downloading " + "once this duration is reached.", + ) + parser.add_argument( + "--extract-audio-first", + action="store_true", + help="For remote URL references, first copy the audio track to a local " + "temp file (no re-encode) and run speech detection on that, instead of " + "streaming the full container over the network during detection. Can be " + "more stable on flaky connections; ignored for local references.", + ) + parser.add_argument( + "--multi-segment-sync", + action="store_true", + help="Sample a few short segments spread across the reference and run " + "speech detection only on those, instead of the whole reference. Speeds " + "up long or remote references; the usual framerate and offset search is " + "unchanged. Only applies to video / audio references.", + ) + parser.add_argument( + "--segment-count", + type=int, + default=8, + help="Number of segments to sample for --multi-segment-sync (default=8).", + ) + parser.add_argument( + "--skip-intro-outro", + action="store_true", + help="With --multi-segment-sync, skip the first 30s and last 60s of the " + "reference when placing segments (intros/credits often lack dialogue).", + ) + parser.add_argument( + "--parallel-workers", + type=int, + default=4, + help="How many segments to extract in parallel for --multi-segment-sync " + "(default=4); useful for overlapping downloads of remote references.", + ) parser.add_argument( "--apply-offset-seconds", type=float, @@ -609,6 +889,40 @@ def add_cli_only_args(parser: argparse.ArgumentParser) -> None: help="Apply a predefined offset in seconds to all subtitle segments " "(default=%d seconds)." % DEFAULT_APPLY_OFFSET_SECONDS, ) + parser.add_argument( + "--skip-sync-on-low-quality", + action="store_true", + help="If the alignment looks untrustworthy (see the thresholds below), " + "leave the subtitles unmodified instead of applying a probably-wrong " + "sync. Useful for batch jobs where a bad sync is worse than none.", + ) + parser.add_argument( + "--min-score", + type=float, + default=DEFAULT_MIN_SCORE, + help="With --skip-sync-on-low-quality, reject alignments scoring below " + "this. The score's magnitude is not normalized, but its sign is " + "meaningful, so the default of %.1f rejects only anti-correlated " + "(clearly wrong) alignments." % DEFAULT_MIN_SCORE, + ) + parser.add_argument( + "--quality-max-offset-seconds", + type=float, + default=DEFAULT_QUALITY_MAX_OFFSET_SECONDS, + help="With --skip-sync-on-low-quality, reject alignments whose offset " + "exceeds this many seconds (default=%.1f)." + % DEFAULT_QUALITY_MAX_OFFSET_SECONDS, + ) + parser.add_argument( + "--max-framerate-deviation", + type=float, + default=DEFAULT_MAX_FRAMERATE_DEVIATION, + help="With --skip-sync-on-low-quality, reject alignments whose framerate " + "scale deviates from 1.0 by more than this. The default of %.2f permits " + "every framerate correction ffsubsync can make (so it never rejects a " + "legitimate one); tighten it only when you know the framerate should not " + "change." % DEFAULT_MAX_FRAMERATE_DEVIATION, + ) parser.add_argument( "--frame-rate", type=int, @@ -648,10 +962,16 @@ def add_cli_only_args(parser: argparse.ArgumentParser) -> None: "auditok", "subs_then_silero", "silero", + "fused", + "fused:weighted", + "fused:intersection", + "fused:union", ], default=None, help="Which voice activity detector to use for speech extraction " - "(if using video / audio as a reference, default={}).".format(DEFAULT_VAD), + "(if using video / audio as a reference, default={}). The `fused` " + "options combine webrtc and silero and require the optional silero " + "dependency (torch).".format(DEFAULT_VAD), ) parser.add_argument( "--no-fix-framerate", diff --git a/libs/ffsubsync/file_utils.py b/libs/ffsubsync/file_utils.py index 9fce7bc96..c090b58a8 100644 --- a/libs/ffsubsync/file_utils.py +++ b/libs/ffsubsync/file_utils.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import sys +from ffsubsync.constants import is_remote_url + class open_file: """ @@ -13,6 +15,12 @@ class open_file: if filename is None: stream = sys.stdout if "w" in args else sys.stdin self.fh = open(stream.fileno(), *args, **kwargs) + elif isinstance(filename, str) and is_remote_url(filename): + import urllib.request + + req = urllib.request.Request(filename, headers={"User-Agent": "ffsubsync"}) + self.fh = urllib.request.urlopen(req) + self.closing = True elif isinstance(filename, str): self.fh = open(filename, *args, **kwargs) self.closing = True diff --git a/libs/ffsubsync/speech_transformers.py b/libs/ffsubsync/speech_transformers.py index 38883d14b..adc48f785 100644 --- a/libs/ffsubsync/speech_transformers.py +++ b/libs/ffsubsync/speech_transformers.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- import os +from concurrent.futures import as_completed, ThreadPoolExecutor from contextlib import contextmanager import logging import io +import re import subprocess import sys +import tempfile from datetime import timedelta -from typing import cast, Callable, Dict, List, Optional, Union +from typing import cast, Callable, Dict, List, NamedTuple, Optional, Tuple, Union import ffmpeg import numpy as np @@ -18,6 +21,7 @@ from ffsubsync.constants import ( DEFAULT_SCALE_FACTOR, DEFAULT_START_SECONDS, SAMPLE_RATE, + is_remote_url, ) from ffsubsync.ffmpeg_utils import ffmpeg_bin_path, subprocess_args from ffsubsync.generic_subtitles import GenericSubtitle @@ -31,6 +35,24 @@ logging.basicConfig(level=logging.INFO) logger: logging.Logger = logging.getLogger(__name__) +class ProgressInfo(NamedTuple): + """Progress emitted to a ``progress_handler`` during speech extraction. + + ``processed_seconds`` is the amount of reference audio decoded so far and + ``total_seconds`` is the reference's total duration (``None`` when ffprobe + could not determine it). Use :attr:`fraction` for a 0.0-1.0 ratio. + """ + + processed_seconds: float + total_seconds: Optional[float] + + @property + def fraction(self) -> Optional[float]: + if not self.total_seconds: + return None + return min(1.0, self.processed_seconds / self.total_seconds) + + def make_subtitle_speech_pipeline( fmt: str = "srt", encoding: str = DEFAULT_ENCODING, @@ -164,7 +186,17 @@ def _make_webrtcvad_detector( def _make_silero_detector( sample_rate: int, frame_rate: int, non_speech_label: float ) -> Callable[[bytes], np.ndarray]: - import torch + try: + import torch + except ImportError as e: + logger.error( + "Error: the silero VAD requires PyTorch, which is not installed!\n" + " Install it with `pip install ffsubsync[torch]`, or install\n" + " torch directly with `pip install torch` (see https://pytorch.org\n" + " for platform-specific instructions). torch is an optional\n" + " dependency and is not installed with ffsubsync by default." + ) + raise e window_duration = 1.0 / sample_rate # duration in seconds frames_per_window = int(window_duration * frame_rate + 0.5) @@ -204,6 +236,66 @@ def _make_silero_detector( return _detect +# bitmap (image-based) subtitle codecs cannot be muxed to SRT; mapping one +# into an SRT extraction aborts the whole ffmpeg invocation, so they are +# skipped when enumerating embedded subtitle streams +_BITMAP_SUBTITLE_CODECS: frozenset = frozenset( + { + "hdmv_pgs_subtitle", + "dvd_subtitle", + "dvb_subtitle", + "dvb_teletext", + "xsub", + } +) + + +_FUSION_STRATEGIES: Tuple[str, ...] = ("weighted", "intersection", "union") + + +def _make_fused_detector( + sample_rate: int, + frame_rate: int, + non_speech_label: float, + fusion_strategy: str = "weighted", +) -> Callable[[bytes], np.ndarray]: + """Combine the webrtc and silero VADs into a single detector. + + Requires the optional silero dependency (torch); a clear error is raised if + it is missing. ``fusion_strategy`` controls how the two are combined: + ``intersection`` (speech only where both agree -- conservative), + ``union`` (speech where either fires -- aggressive), or ``weighted`` + (default; ``0.6 * silero + 0.4 * webrtc``). + """ + if fusion_strategy not in _FUSION_STRATEGIES: + raise ValueError( + "unknown fused VAD strategy %r; choose one of %s" + % (fusion_strategy, ", ".join(_FUSION_STRATEGIES)) + ) + webrtc_detector = _make_webrtcvad_detector( + sample_rate, frame_rate, non_speech_label + ) + silero_detector = _make_silero_detector( + sample_rate, frame_rate, non_speech_label + ) + + def _detect(asegment) -> np.ndarray: + webrtc_result = webrtc_detector(asegment) + silero_result = silero_detector(asegment) + # the two detectors can disagree by a frame at the tail; clip to common length + min_len = min(len(webrtc_result), len(silero_result)) + webrtc_result = webrtc_result[:min_len] + silero_result = silero_result[:min_len] + if fusion_strategy == "intersection": + return np.minimum(webrtc_result, silero_result) + elif fusion_strategy == "union": + return np.maximum(webrtc_result, silero_result) + else: # weighted + return 0.6 * silero_result + 0.4 * webrtc_result + + return _detect + + class ComputeSpeechFrameBoundariesMixin: def __init__(self) -> None: self.start_frame_: Optional[int] = None @@ -237,6 +329,9 @@ class VideoSpeechTransformer(TransformerMixin): ref_stream: Optional[str] = None, vlc_mode: bool = False, gui_mode: bool = False, + max_duration_seconds: Optional[float] = None, + extract_audio_first: bool = False, + progress_handler: Optional[Callable[["ProgressInfo"], None]] = None, ) -> None: super(VideoSpeechTransformer, self).__init__() self.vad: str = vad @@ -248,46 +343,172 @@ class VideoSpeechTransformer(TransformerMixin): self.ref_stream: Optional[str] = ref_stream self.vlc_mode: bool = vlc_mode self.gui_mode: bool = gui_mode + self.max_duration_seconds: Optional[float] = max_duration_seconds + self.extract_audio_first: bool = extract_audio_first + self.progress_handler: Optional[ + Callable[["ProgressInfo"], None] + ] = progress_handler self.video_speech_results_: Optional[np.ndarray] = None - def try_fit_using_embedded_subs(self, fname: str) -> None: - embedded_subs = [] - embedded_subs_times = [] - if self.ref_stream is None: - # check first 5; should cover 99% of movies - streams_to_try: List[str] = list(map("0:s:{}".format, range(5))) - else: - streams_to_try = [self.ref_stream] - for stream in streams_to_try: + def _probe_embedded_subtitle_streams(self, fname: str) -> Optional[List[str]]: + """Enumerate text-based subtitle streams in ``fname`` via ffprobe. + + Returns a list of ffmpeg ``-map`` specifiers (e.g. ``["0:2", "0:3"]``) + so that every subtitle stream can be extracted in a single ffmpeg pass. + Bitmap subtitle codecs (PGS, VobSub, DVB, ...) are skipped because they + cannot be muxed to SRT and would otherwise abort the whole extraction. + + Returns ``None`` when ffprobe is unavailable or fails, signaling the + caller to fall back to extracting streams one at a time. + """ + ffprobe_args = [ + ffmpeg_bin_path( + "ffprobe", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path + ), + "-loglevel", + "fatal", + "-select_streams", + "s", + "-show_entries", + "stream=index,codec_name", + "-of", + "csv=p=0", + fname, + ] + try: + process = subprocess.Popen( + ffprobe_args, **subprocess_args(include_stdout=True) + ) + output = process.communicate()[0] + except OSError as e: + logger.warning("ffprobe unavailable while enumerating subtitles: %s", e) + return None + if process.returncode != 0: + return None + streams: List[str] = [] + for line in output.decode("utf-8", errors="replace").splitlines(): + parts = line.strip().split(",") + if not parts or not parts[0].isdigit(): + continue + index = parts[0] + codec_name = parts[1].strip().lower() if len(parts) > 1 else "" + if codec_name in _BITMAP_SUBTITLE_CODECS: + continue + streams.append("0:{}".format(index)) + return streams or None + + def _extract_embedded_subs_single_pass( + self, fname: str, streams: List[str] + ) -> Optional[List[io.BytesIO]]: + """Extract several subtitle streams in one ffmpeg invocation. + + ffmpeg can only send a single output to stdout, so each stream is + written to a temporary file (in the system temp dir -- never next to + the source media), read back into memory, and then deleted along with + the temp dir. Returns one in-memory buffer per stream that produced + data, or ``None`` if the ffmpeg invocation failed wholesale (the caller + then falls back to per-stream extraction). + """ + with tempfile.TemporaryDirectory(prefix="ffsubsync_subs_") as tmpdir: ffmpeg_args = [ ffmpeg_bin_path( "ffmpeg", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path - ) + ), + "-loglevel", + "fatal", + "-nostdin", + "-i", + fname, ] - ffmpeg_args.extend( - [ - "-loglevel", - "fatal", - "-nostdin", - "-i", - fname, - "-map", - "{}".format(stream), - "-f", - "srt", - "-", - ] - ) + out_paths: List[str] = [] + for i, stream in enumerate(streams): + out_path = os.path.join(tmpdir, "embedded.{}.srt".format(i)) + out_paths.append(out_path) + ffmpeg_args.extend( + ["-map", "{}".format(stream), "-f", "srt", out_path] + ) process = subprocess.Popen( ffmpeg_args, **subprocess_args(include_stdout=True) ) - output = io.BytesIO(process.communicate()[0]) + process.communicate() + if process.returncode != 0: + return None + buffers: List[io.BytesIO] = [] + for out_path in out_paths: + if not os.path.exists(out_path) or os.path.getsize(out_path) == 0: + continue + with open(out_path, "rb") as f: + buffers.append(io.BytesIO(f.read())) + return buffers + + def _extract_embedded_subs_per_stream( + self, fname: str, streams: List[str] + ) -> List[io.BytesIO]: + """Extract subtitle streams one ffmpeg invocation at a time (to stdout). + + This preserves the original extraction behavior and is used as a + fallback whenever single-pass extraction is unavailable (no ffprobe) or + fails. Stops at the first stream ffmpeg cannot extract. + """ + buffers: List[io.BytesIO] = [] + for stream in streams: + ffmpeg_args = [ + ffmpeg_bin_path( + "ffmpeg", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path + ), + "-loglevel", + "fatal", + "-nostdin", + "-i", + fname, + "-map", + "{}".format(stream), + "-f", + "srt", + "-", + ] + process = subprocess.Popen( + ffmpeg_args, **subprocess_args(include_stdout=True) + ) + output = process.communicate()[0] if process.returncode != 0: break + buffers.append(io.BytesIO(output)) + return buffers + + def try_fit_using_embedded_subs(self, fname: str) -> None: + if self.ref_stream is not None: + # a specific stream was requested; extract just that one + subtitle_buffers = self._extract_embedded_subs_per_stream( + fname, [self.ref_stream] + ) + else: + # enumerate the subtitle streams so they can all be extracted in a + # single ffmpeg pass (~5x faster than one invocation per stream) + streams = self._probe_embedded_subtitle_streams(fname) + if streams: + subtitle_buffers = self._extract_embedded_subs_single_pass( + fname, streams + ) + if subtitle_buffers is None: + # single pass failed; degrade to per-stream over the same + # (known-present) streams + subtitle_buffers = self._extract_embedded_subs_per_stream( + fname, streams + ) + else: + # ffprobe unavailable/failed: fall back to probing the first 5 + # streams individually (should cover 99% of movies) + subtitle_buffers = self._extract_embedded_subs_per_stream( + fname, list(map("0:s:{}".format, range(5))) + ) + embedded_subs = [] + embedded_subs_times = [] + for buffer in subtitle_buffers: pipe = cast( Pipeline, make_subtitle_speech_pipeline(start_seconds=self.start_seconds), - ).fit(output) + ).fit(buffer) speech_step = pipe.steps[-1][1] embedded_subs.append(speech_step) embedded_subs_times.append(speech_step.max_time_) @@ -301,49 +522,7 @@ class VideoSpeechTransformer(TransformerMixin): subs_to_use = embedded_subs[int(np.argmax(embedded_subs_times))] self.video_speech_results_ = subs_to_use.subtitle_speech_results_ - def fit(self, fname: str, *_) -> "VideoSpeechTransformer": - if "subs" in self.vad and ( - self.ref_stream is None or self.ref_stream.startswith("0:s:") - ): - try: - logger.info("Checking video for subtitles stream...") - self.try_fit_using_embedded_subs(fname) - logger.info("...success!") - return self - except Exception as e: - logger.info(e) - try: - total_duration = ( - float( - ffmpeg.probe( - fname, - cmd=ffmpeg_bin_path( - "ffprobe", - self.gui_mode, - ffmpeg_resources_path=self.ffmpeg_path, - ), - )["format"]["duration"] - ) - - self.start_seconds - ) - except Exception as e: - logger.warning(e) - total_duration = None - if "webrtc" in self.vad: - detector = _make_webrtcvad_detector( - self.sample_rate, self.frame_rate, self._non_speech_label - ) - elif "auditok" in self.vad: - detector = _make_auditok_detector( - self.sample_rate, self.frame_rate, self._non_speech_label - ) - elif "silero" in self.vad: - detector = _make_silero_detector( - self.sample_rate, self.frame_rate, self._non_speech_label - ) - else: - raise ValueError("unknown vad: %s" % self.vad) - media_bstring: List[np.ndarray] = [] + def _build_ffmpeg_args(self, fname: str) -> List[str]: ffmpeg_args = [ ffmpeg_bin_path( "ffmpeg", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path @@ -356,6 +535,12 @@ class VideoSpeechTransformer(TransformerMixin): str(timedelta(seconds=self.start_seconds)), ] ) + if self.max_duration_seconds is not None: + # input-side -t: stop reading (and, for remote URLs, downloading) + # after this many seconds past the seek point + ffmpeg_args.extend( + ["-t", str(timedelta(seconds=self.max_duration_seconds))] + ) ffmpeg_args.extend(["-loglevel", "fatal", "-nostdin", "-i", fname]) if self.ref_stream is not None and self.ref_stream.startswith("0:a:"): ffmpeg_args.extend(["-map", self.ref_stream]) @@ -374,6 +559,126 @@ class VideoSpeechTransformer(TransformerMixin): "-", ] ) + return ffmpeg_args + + def _extract_audio_to_temp(self, url: str) -> Optional[str]: + """Copy the reference's audio to a local temp file (no re-encode). + + Returns the temp path, or None if extraction fails (the caller then + falls back to streaming the URL directly). Uses a Matroska (.mka) + container, which accepts essentially any audio codec, so ``-acodec + copy`` is safe regardless of the source's codec. + """ + fd, temp_path = tempfile.mkstemp(suffix=".mka") + os.close(fd) + ffmpeg_args = [ + ffmpeg_bin_path( + "ffmpeg", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path + ), + "-loglevel", + "fatal", + "-nostdin", + "-y", + "-i", + url, + "-vn", + "-acodec", + "copy", + ] + if self.max_duration_seconds is not None: + # extract from t=0 up to start+max so the main pass can still seek + # --start-seconds accurately within the local file + limit = self.start_seconds + self.max_duration_seconds + ffmpeg_args.extend(["-t", str(timedelta(seconds=limit))]) + ffmpeg_args.append(temp_path) + logger.info("extracting audio from remote reference to %s...", temp_path) + retcode = subprocess.call(ffmpeg_args, **subprocess_args(include_stdout=False)) + if retcode != 0 or not os.path.getsize(temp_path): + logger.warning( + "audio extraction failed (ffmpeg returned %d); " + "falling back to streaming the reference directly", + retcode, + ) + try: + os.remove(temp_path) + except OSError: + pass + return None + return temp_path + + def fit(self, fname: str, *_) -> "VideoSpeechTransformer": + if "subs" in self.vad and ( + self.ref_stream is None or self.ref_stream.startswith("0:s:") + ): + try: + logger.info("Checking video for subtitles stream...") + self.try_fit_using_embedded_subs(fname) + logger.info("...success!") + return self + except Exception as e: + logger.info(e) + temp_audio = None + if self.extract_audio_first and is_remote_url(fname): + temp_audio = self._extract_audio_to_temp(fname) + if temp_audio is not None: + fname = temp_audio + try: + self._fit_using_audio(fname) + finally: + if temp_audio is not None and os.path.exists(temp_audio): + try: + os.remove(temp_audio) + except OSError: + logger.warning("failed to remove temp audio file %s", temp_audio) + return self + + def _fit_using_audio(self, fname: str) -> None: + try: + total_duration = ( + float( + ffmpeg.probe( + fname, + cmd=ffmpeg_bin_path( + "ffprobe", + self.gui_mode, + ffmpeg_resources_path=self.ffmpeg_path, + ), + )["format"]["duration"] + ) + - self.start_seconds + ) + except Exception as e: + logger.warning(e) + total_duration = None + if self.max_duration_seconds is not None and total_duration is not None: + total_duration = min(total_duration, self.max_duration_seconds) + if "fused" in self.vad: + # e.g. "fused" or "fused:intersection"; default strategy is weighted + fusion_strategy = ( + self.vad.split(":", 1)[1] if ":" in self.vad else "weighted" + ) + detector = _make_fused_detector( + self.sample_rate, + self.frame_rate, + self._non_speech_label, + fusion_strategy, + ) + elif "webrtc" in self.vad: + detector = _make_webrtcvad_detector( + self.sample_rate, self.frame_rate, self._non_speech_label + ) + elif "auditok" in self.vad: + detector = _make_auditok_detector( + self.sample_rate, self.frame_rate, self._non_speech_label + ) + elif "silero" in self.vad: + detector = _make_silero_detector( + self.sample_rate, self.frame_rate, self._non_speech_label + ) + else: + raise ValueError("unknown vad: %s" % self.vad) + media_bstring: List[np.ndarray] = [] + ffmpeg_args = self._build_ffmpeg_args(fname) process = subprocess.Popen(ffmpeg_args, **subprocess_args(include_stdout=True)) bytes_per_frame = 2 frames_per_window = bytes_per_frame * self.frame_rate // self.sample_rate @@ -416,6 +721,17 @@ class VideoSpeechTransformer(TransformerMixin): newstuff = total_duration - simple_progress simple_progress += newstuff pbar.update(newstuff) + if self.progress_handler is not None: + try: + self.progress_handler( + ProgressInfo( + processed_seconds=simple_progress, + total_seconds=total_duration, + ) + ) + except Exception as e: + # a host-supplied callback must never break syncing + logger.warning("progress_handler raised: %s", e) if self.vlc_mode and total_duration is not None: print("%d" % int(simple_progress * 100.0 / total_duration)) sys.stdout.flush() @@ -423,7 +739,9 @@ class VideoSpeechTransformer(TransformerMixin): assert self.gui_mode # no need to flush since we pass -u to do unbuffered output for gui mode print(pbar_output.read()) - if "silero" not in self.vad: + # silero (and fused, which wraps it) needs the raw s16le + # bytes for its own int16 reinterpretation + if "silero" not in self.vad and "fused" not in self.vad: in_bytes = np.frombuffer(in_bytes, np.uint8) media_bstring.append(detector(in_bytes)) process.wait() @@ -434,6 +752,151 @@ class VideoSpeechTransformer(TransformerMixin): ) self.video_speech_results_ = np.concatenate(media_bstring) logger.info("total of speech segments: %s", np.sum(self.video_speech_results_)) + + def transform(self, *_) -> np.ndarray: + return self.video_speech_results_ + + +class MultiSegmentVideoSpeechTransformer(TransformerMixin): + """Produce a reference speech signal by sampling only a few short segments. + + Instead of extracting and running VAD over the entire reference, this samples + ``segment_count`` short windows spread across it, runs VAD on each, and places + the results into a full-length array that is zero everywhere else. That sparse + signal can be fed straight into the normal alignment path: because it preserves + each segment's true position on the reference timeline, the existing + framerate-ratio search and offset cross-correlation recover the same answer + they would from the full signal -- but only the sampled audio has to be + extracted (and, for remote URLs, downloaded). Useful for long / remote + references; for typical local files the plain VideoSpeechTransformer is fine. + """ + + # margins skipped at the very start/end when skip_intro_outro is set, since + # intros/credits often lack dialogue + START_MARGIN_SECONDS: int = 30 + END_MARGIN_SECONDS: int = 60 + + def __init__( + self, + vad: str, + sample_rate: int, + frame_rate: int, + non_speech_label: float, + segment_count: int = 8, + segment_duration: int = 60, + skip_intro_outro: bool = False, + parallel_workers: int = 4, + ffmpeg_path: Optional[str] = None, + ref_stream: Optional[str] = None, + vlc_mode: bool = False, + gui_mode: bool = False, + ) -> None: + super(MultiSegmentVideoSpeechTransformer, self).__init__() + # sampling is audio-only, so drop any "subs_then_" prefix (embedded-subtitle + # extraction ignores the per-segment time window) + self.vad: str = vad.split("subs_then_")[-1] + self.sample_rate: int = sample_rate + self.frame_rate: int = frame_rate + self._non_speech_label: float = non_speech_label + self.segment_count: int = segment_count + self.segment_duration: int = segment_duration + self.skip_intro_outro: bool = skip_intro_outro + self.parallel_workers: int = parallel_workers + self.ffmpeg_path: Optional[str] = ffmpeg_path + self.ref_stream: Optional[str] = ref_stream + self.vlc_mode: bool = vlc_mode + self.gui_mode: bool = gui_mode + self.video_speech_results_: Optional[np.ndarray] = None + + def _segment_starts(self, total_duration: float) -> List[int]: + """Evenly-spaced segment start times (seconds) across the reference.""" + duration = self.segment_duration + if total_duration <= duration: + return [0] + start_margin = self.START_MARGIN_SECONDS if self.skip_intro_outro else 0 + end_margin = self.END_MARGIN_SECONDS if self.skip_intro_outro else 0 + lo = float(start_margin) + hi = total_duration - end_margin + if hi - lo < duration: # margins too large for this reference; ignore them + lo, hi = 0.0, total_duration + usable = hi - lo - duration + n = max(1, self.segment_count) + if usable <= 0 or n == 1: + return [int(max(0.0, min(lo, total_duration - duration)))] + step = usable / (n - 1) + starts = [int(round(lo + i * step)) for i in range(n)] + # clamp into range and drop duplicates that clamping may create + starts = [max(0, min(s, int(total_duration) - duration)) for s in starts] + return sorted(set(starts)) + + def _extract_segment_speech(self, fname: str, start: int) -> Tuple[int, np.ndarray]: + """Run VAD over a single window, returning (start_seconds, speech array).""" + segment = VideoSpeechTransformer( + vad=self.vad, + sample_rate=self.sample_rate, + frame_rate=self.frame_rate, + non_speech_label=self._non_speech_label, + start_seconds=start, + ffmpeg_path=self.ffmpeg_path, + ref_stream=self.ref_stream, + vlc_mode=self.vlc_mode, + gui_mode=self.gui_mode, + max_duration_seconds=self.segment_duration, + ) + segment.fit(fname) + return start, segment.transform() + + def fit(self, fname: str, *_) -> "MultiSegmentVideoSpeechTransformer": + try: + total_duration = float( + ffmpeg.probe( + fname, + cmd=ffmpeg_bin_path( + "ffprobe", self.gui_mode, ffmpeg_resources_path=self.ffmpeg_path + ), + )["format"]["duration"] + ) + except Exception as e: + raise ValueError( + "multi-segment sync needs the reference duration, but probing " + "'%s' failed: %s" % (fname, e) + ) + starts = self._segment_starts(total_duration) + logger.info( + "multi-segment sync: sampling %d segment(s) of up to %ds at %s", + len(starts), + self.segment_duration, + [int(s) for s in starts], + ) + sparse = np.zeros(int(total_duration * self.sample_rate) + 2, dtype=float) + workers = max(1, min(self.parallel_workers, len(starts))) + with ThreadPoolExecutor(max_workers=workers) as executor: + future_to_start = { + executor.submit(self._extract_segment_speech, fname, start): start + for start in starts + } + for future in as_completed(future_to_start): + start = future_to_start[future] + try: + start, seg_speech = future.result() + except Exception as e: + # a single flaky segment shouldn't sink the whole sync; the + # remaining segments still localize the offset + logger.warning("failed to extract segment at %ds: %s", start, e) + continue + begin = int(start * self.sample_rate) + end = min(begin + len(seg_speech), len(sparse)) + if end > begin: + sparse[begin:end] = seg_speech[: end - begin] + if not np.any(sparse > 0): + raise ValueError( + "Unable to detect speech in any sampled segment. " + "Perhaps try specifying a different stream / track, or a different vad." + ) + self.video_speech_results_ = sparse + logger.info( + "total of speech segments: %s", np.sum(self.video_speech_results_) + ) return self def transform(self, *_) -> np.ndarray: @@ -444,14 +907,24 @@ _PAIRED_NESTER: Dict[str, str] = { "(": ")", "{": "}", "[": "]", - # FIXME: False positive sometimes when there are html tags, e.g. Hello? - # '<': '>', + "(": ")", # full-width / CJK brackets, common in non-English subtitles + "【": "】", + "「": "」", } +# Markup tags (e.g. , , ) carry no speech. Stripping them +# before classifying a line lets us recognize a wrapped cue like "[music]" +# as non-dialogue while still treating "Hello?" as dialogue -- which is +# why '<' is intentionally not a paired nester above. +_MARKUP_TAG: "re.Pattern[str]" = re.compile(r"<[^>]+>") + +# Symbols that, on their own, denote a musical / non-speech cue. +_NON_DIALOGUE_SYMBOLS: frozenset = frozenset("♪♫♬♩🎵🎶") + # TODO: need way better metadata detector def _is_metadata(content: str, is_beginning_or_end: bool) -> bool: - content = content.strip() + content = _MARKUP_TAG.sub("", content).strip() if len(content) == 0: return True if ( @@ -459,6 +932,9 @@ def _is_metadata(content: str, is_beginning_or_end: bool) -> bool: and content[-1] == _PAIRED_NESTER[content[0]] ): return True + # lines consisting only of music notes / sound symbols are cues, not speech + if all(ch.isspace() or ch in _NON_DIALOGUE_SYMBOLS for ch in content): + return True if is_beginning_or_end: if "english" in content.lower(): return True @@ -531,3 +1007,202 @@ class DeserializeSpeechTransformer(TransformerMixin): def transform(self, *_) -> np.ndarray: assert self.deserialized_speech_results_ is not None return self.deserialized_speech_results_ + + +def find_pgs_stream( + fname: str, + ffmpeg_path: Optional[str] = None, + gui_mode: bool = False, +) -> Optional[str]: + """Return the ffmpeg stream specifier for the first PGS subtitle track in *fname*. + + Uses ``ffprobe`` to inspect the file. Returns a string like ``"0:s:0"`` on + success, or ``None`` if the file has no ``hdmv_pgs_subtitle`` streams. + """ + try: + probe = ffmpeg.probe( + fname, + cmd=ffmpeg_bin_path("ffprobe", gui_mode, ffmpeg_resources_path=ffmpeg_path), + ) + except Exception as e: + logger.warning("ffprobe failed while searching for PGS streams: %s", e) + return None + + sub_index = 0 + for stream in probe.get("streams", []): + if stream.get("codec_type") == "subtitle": + if stream.get("codec_name") == "hdmv_pgs_subtitle": + specifier = "0:s:{}".format(sub_index) + logger.info( + "auto-detected PGS stream: %s (ffmpeg stream index %s)", + specifier, + stream.get("index"), + ) + return specifier + sub_index += 1 + + return None + + +def _get_pgs_timings_via_ffprobe( + fname: str, + stream: str, + ffmpeg_path: Optional[str] = None, + gui_mode: bool = False, +) -> Optional[List[Tuple[float, float]]]: + """Read PGS timings from container metadata using ffprobe. + + MKV stores per-packet PTS and duration for subtitle streams, so we can + get start/end timestamps without extracting or parsing the raw SUP binary. + Show events are large packets with a numeric ``duration_time``; clear events + are tiny (~30-byte) packets with ``duration_time=N/A``. + + Returns a list of ``(start_seconds, end_seconds)`` tuples, or ``None`` if + ffprobe fails or returns no usable durations. + """ + ffprobe_cmd = ffmpeg_bin_path( + "ffprobe", gui_mode, ffmpeg_resources_path=ffmpeg_path + ) + # ffprobe -select_streams does not accept the "0:" input-index prefix; + # strip it so "0:s:0" → "s:0" and "0:3" → "3". + probe_stream = stream[2:] if stream.startswith("0:") else stream + try: + probe_data = ffmpeg.probe( + fname, + cmd=ffprobe_cmd, + show_packets=None, + select_streams=probe_stream, + show_entries="packet=pts_time,duration_time,size", + ) + except Exception: + return None + + results: List[Tuple[float, float]] = [] + for packet in probe_data.get("packets", []): + pts_time_str = packet.get("pts_time") + duration_time_str = packet.get("duration_time") + size_str = packet.get("size") + if pts_time_str is None or duration_time_str is None or size_str is None: + continue + if duration_time_str == "N/A": + continue + try: + pts_time = float(pts_time_str) + duration_time = float(duration_time_str) + size = int(size_str) + except ValueError: + continue + if size > 50: # skip clear events (~30 bytes) + results.append((pts_time, pts_time + duration_time)) + + if not results: + return None + return results + + +class PGSSpeechTransformer(TransformerMixin, ComputeSpeechFrameBoundariesMixin): + """Use PGS (Presentation Graphic Stream) subtitle timings as a sync reference. + + PGS subtitles are bitmap-based (e.g. Blu-ray / HDMV) and cannot be converted + to text by ffmpeg, so they can't be fed through the normal subtitle pipeline. + However, when muxed into an MKV the container still stores a presentation + timestamp (``pts_time``) and ``duration_time`` for every subtitle packet, so + we can recover *when* each caption is on screen without decoding the bitmaps + or parsing the raw SUP/PCS binary at all. + + This transformer reads those per-packet timings via ``ffprobe`` (see + :func:`_get_pgs_timings_via_ffprobe`), filtering out the tiny "clear" packets + that carry no image, and builds the same kind of sparse binary speech signal + that :class:`SubtitleSpeechTransformer` produces for text subtitles: 1.0 while + a caption is displayed, 0.0 otherwise. That signal can then be aligned against + the input subtitle file by the normal ffsubsync pipeline. + + The reference stream may be given explicitly via ``ref_stream`` (with or + without a leading ``0:``), or left as ``None`` to auto-detect the first + ``hdmv_pgs_subtitle`` track in the file. + """ + + # PGS is already in the MKV timebase so its duration cannot be compared + # against the SRT to infer a framerate ratio. Returning None here prevents + # the duration-based framerate inference in try_sync from running. + @property + def num_frames(self) -> None: + return None + + def __init__( + self, + sample_rate: int, + start_seconds: int = 0, + ffmpeg_path: Optional[str] = None, + ref_stream: Optional[str] = None, + gui_mode: bool = False, + ) -> None: + super(PGSSpeechTransformer, self).__init__() + self.sample_rate: int = sample_rate + self.start_seconds: int = start_seconds + self.ffmpeg_path: Optional[str] = ffmpeg_path + self.ref_stream: Optional[str] = ref_stream + self.gui_mode: bool = gui_mode + self.pgs_speech_results_: Optional[np.ndarray] = None + + def fit(self, fname: str, *_) -> "PGSSpeechTransformer": + if self.ref_stream is None: + stream = find_pgs_stream(fname, self.ffmpeg_path, self.gui_mode) + if stream is None: + raise ValueError( + "No hdmv_pgs_subtitle stream found in {}. " + "Specify one explicitly with --pgs-ref-stream.".format(fname) + ) + else: + stream = self.ref_stream + if not stream.startswith("0:"): + stream = "0:" + stream + + logger.info("reading PGS timings for stream %s from %s...", stream, fname) + timings = _get_pgs_timings_via_ffprobe( + fname, stream, self.ffmpeg_path, self.gui_mode + ) + if timings is None: + raise ValueError( + "Failed to get PGS timings via ffprobe for stream {} from {}. " + "Make sure the stream exists and is an hdmv_pgs_subtitle track " + "(check with: ffprobe -show_streams {}).".format(stream, fname, fname) + ) + + if not timings: + raise ValueError( + "No subtitle timings found in PGS stream {}.".format(stream) + ) + + logger.info("found %d PGS subtitle segments", len(timings)) + for i, (s, e) in enumerate(timings[:8]): + logger.debug( + " PGS[%d]: %s --> %s (%.3fs)", + i, + str(timedelta(seconds=s)), + str(timedelta(seconds=e)), + e - s, + ) + + max_time = max(end for _, end in timings) + num_samples = int(max_time * self.sample_rate) + 2 + samples = np.zeros(num_samples, dtype=float) + + for start, end in timings: + start_sample = int(round((start - self.start_seconds) * self.sample_rate)) + end_sample = int(round((end - self.start_seconds) * self.sample_rate)) + start_sample = max(start_sample, 0) + end_sample = min(end_sample, num_samples) + if start_sample < end_sample: + samples[start_sample:end_sample] = 1.0 + + self.pgs_speech_results_ = samples + self.fit_boundaries(self.pgs_speech_results_) + logger.info( + "total PGS subtitle frames: %d", int(np.sum(self.pgs_speech_results_)) + ) + return self + + def transform(self, *_) -> np.ndarray: + assert self.pgs_speech_results_ is not None + return self.pgs_speech_results_ diff --git a/libs/ffsubsync/subtitle_preprocessor.py b/libs/ffsubsync/subtitle_preprocessor.py new file mode 100644 index 000000000..2fd5c8b4c --- /dev/null +++ b/libs/ffsubsync/subtitle_preprocessor.py @@ -0,0 +1,172 @@ +"""Subtitle preprocessing for improved alignment accuracy.""" + +import re +import logging +from typing import List, Optional +from ffsubsync.generic_subtitles import GenericSubtitle + +logger = logging.getLogger(__name__) + +# Patterns for non-dialogue content +NON_DIALOGUE_PATTERNS = [ + # Music symbols + r'^[♪♫🎵🎶\s]+$', + # Bracketed content (sound effects, descriptions) + r'^\s*[\[\((【][^\]\))】]*[\]\))】]\s*$', + # Italic/styled sound effects like [music] + r'^\s*<[^>]+>\s*[\[\((【][^\]\))】]*[\]\))】]\s*]+>\s*$', + # Pure ellipsis or dashes + r'^[\.\-\s…—]+$', + # Single repeated characters + r'^(.)\1{2,}$', +] + +# Compiled patterns for efficiency +_COMPILED_PATTERNS = [re.compile(p, re.IGNORECASE | re.UNICODE) for p in NON_DIALOGUE_PATTERNS] + +# Keywords that indicate non-dialogue (case-insensitive) +NON_DIALOGUE_KEYWORDS = [ + '♪', '♫', '🎵', '🎶', + '[music]', '[singing]', '[song]', + '[applause]', '[laughter]', '[cheering]', + '[gunshot]', '[explosion]', '[thunder]', + '[silence]', '[inaudible]', + '(music)', '(singing)', '(song)', + '(applause)', '(laughter)', '(cheering)', + # Chinese equivalents + '[音乐]', '[掌声]', '[笑声]', '[枪声]', '[爆炸声]', + '(音乐)', '(掌声)', '(笑声)', +] + + +def is_non_dialogue(content: str) -> bool: + """Check if subtitle content is non-dialogue (sound effect, music, etc.).""" + content = content.strip() + + if not content: + return True + + # Check patterns + for pattern in _COMPILED_PATTERNS: + if pattern.match(content): + return True + + # Check keywords + content_lower = content.lower() + for keyword in NON_DIALOGUE_KEYWORDS: + if keyword.lower() in content_lower: + # Only if the keyword is a significant part of the content + if len(keyword) > len(content) * 0.5: + return True + # Or if content is mostly the keyword + if content_lower.strip('[]()()【】<>/').strip() == keyword.lower().strip('[]()()【】'): + return True + + return False + + +def merge_short_subtitles( + subs: List[GenericSubtitle], + min_duration: float = 0.3, + max_gap: float = 0.3 +) -> List[GenericSubtitle]: + """Merge subtitles that are too short or have small gaps. + + Args: + subs: List of subtitles + min_duration: Minimum duration in seconds for a subtitle + max_gap: Maximum gap in seconds between subtitles to merge + + Returns: + List of merged subtitles + """ + if not subs: + return subs + + merged = [] + current = None + + for sub in subs: + duration = sub.end.total_seconds() - sub.start.total_seconds() + + if current is None: + current = sub + continue + + gap = sub.start.total_seconds() - current.end.total_seconds() + current_duration = current.end.total_seconds() - current.start.total_seconds() + + # Merge if gap is small and either subtitle is short + if gap <= max_gap and (current_duration < min_duration or duration < min_duration): + # Extend current subtitle + from datetime import timedelta + new_end = sub.end + new_content = current.content + " " + sub.content + # Create new subtitle with extended range + current = GenericSubtitle( + current.index, + current.start, + new_end, + new_content + ) + else: + merged.append(current) + current = sub + + if current is not None: + merged.append(current) + + return merged + + +def preprocess_subtitles( + subs: List[GenericSubtitle], + filter_non_dialogue: bool = True, + merge_short: bool = True, + min_duration: float = 0.3, + max_gap: float = 0.3, + min_keep_ratio: float = 0.3 +) -> List[GenericSubtitle]: + """Preprocess subtitles for improved alignment. + + Args: + subs: List of subtitles + filter_non_dialogue: Whether to filter out non-dialogue content + merge_short: Whether to merge short subtitles + min_duration: Minimum duration for short subtitle merging + max_gap: Maximum gap for short subtitle merging + min_keep_ratio: Minimum ratio of subtitles to keep (safety check) + + Returns: + Preprocessed list of subtitles + """ + if not subs: + return subs + + original_count = len(subs) + processed = list(subs) + + # Step 1: Filter non-dialogue content + if filter_non_dialogue: + processed = [s for s in processed if not is_non_dialogue(s.content)] + filtered_count = original_count - len(processed) + if filtered_count > 0: + logger.info("Filtered %d non-dialogue subtitles", filtered_count) + + # Safety check: if too many filtered, use original + if len(processed) < original_count * min_keep_ratio: + logger.warning( + "Too many subtitles filtered (%d/%d), using original subtitles", + original_count - len(processed), original_count + ) + return list(subs) + + # Step 2: Merge short subtitles + if merge_short and len(processed) > 1: + before_merge = len(processed) + processed = merge_short_subtitles(processed, min_duration, max_gap) + merged_count = before_merge - len(processed) + if merged_count > 0: + logger.info("Merged %d short subtitle segments", merged_count) + + return processed diff --git a/libs/ffsubsync/version.py b/libs/ffsubsync/version.py index 936e12209..3c6d4b316 100644 --- a/libs/ffsubsync/version.py +++ b/libs/ffsubsync/version.py @@ -9,6 +9,15 @@ del get_versions def get_version(): if "unknown" in __version__.lower(): + # When frozen into a standalone binary (e.g. via PyInstaller) there is + # no git metadata or resource file to read the version from, so the CI + # build writes the tag into this module ahead of time. + try: + from ffsubsync._frozen_version import FFSUBSYNC_VERSION + + return FFSUBSYNC_VERSION + except ImportError: + pass with open( os.path.join(os.environ[SUBSYNC_RESOURCES_ENV_MAGIC], "__version__") ) as f: diff --git a/libs/flask_cors-6.0.2.dist-info/RECORD b/libs/flask_cors-6.0.2.dist-info/RECORD deleted file mode 100644 index 11229e132..000000000 --- a/libs/flask_cors-6.0.2.dist-info/RECORD +++ /dev/null @@ -1,11 +0,0 @@ -flask_cors-6.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -flask_cors-6.0.2.dist-info/METADATA,sha256=xYYixYpXPBqR21WrRmV6PMSlRh7OQ5J3A4G9WD6k_-M,5327 -flask_cors-6.0.2.dist-info/RECORD,, -flask_cors-6.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -flask_cors-6.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -flask_cors-6.0.2.dist-info/top_level.txt,sha256=aWye_0QNZPp_QtPF4ZluLHqnyVLT9CPJsfiGhwqkWuo,11 -flask_cors/__init__.py,sha256=Hnwpy4nxEz4tlcB3enWNXUBctcjgxRnUqa5B5zDabQ4,522 -flask_cors/core.py,sha256=S8Wv7dz9e0FcR386TkBT-T8_VRsjFQGhNiR0jIpUYiY,14416 -flask_cors/decorator.py,sha256=YnPIDyWAfz60t2yxLgoOZLzgWWjZEvxQAPTTV4qfsss,4706 -flask_cors/extension.py,sha256=ONk6eNJiYT7HPEtmmySG8KgjzvAs9xtcJHNYvWLAS9s,8268 -flask_cors/version.py,sha256=lze97Y7ToUnpENKGAhOoSKx1zlZQe6ZfCSr8M50_K1g,22 diff --git a/libs/flask_cors-6.0.2.dist-info/INSTALLER b/libs/flask_cors-6.0.5.dist-info/INSTALLER similarity index 100% rename from libs/flask_cors-6.0.2.dist-info/INSTALLER rename to libs/flask_cors-6.0.5.dist-info/INSTALLER diff --git a/libs/flask_cors-6.0.2.dist-info/METADATA b/libs/flask_cors-6.0.5.dist-info/METADATA similarity index 98% rename from libs/flask_cors-6.0.2.dist-info/METADATA rename to libs/flask_cors-6.0.5.dist-info/METADATA index 44bd26425..39686d45a 100644 --- a/libs/flask_cors-6.0.2.dist-info/METADATA +++ b/libs/flask_cors-6.0.5.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: flask-cors -Version: 6.0.2 +Version: 6.0.5 Summary: A Flask extension simplifying CORS support Author-email: Cory Dolphin License-Expression: MIT @@ -21,6 +21,7 @@ Requires-Python: <4.0,>=3.9 Description-Content-Type: text/x-rst Requires-Dist: flask>=0.9 Requires-Dist: Werkzeug>=0.7 +Requires-Dist: typing_extensions>=4.6.0; python_version < "3.11" Flask-CORS ========== diff --git a/libs/flask_cors-6.0.5.dist-info/RECORD b/libs/flask_cors-6.0.5.dist-info/RECORD new file mode 100644 index 000000000..aa6207827 --- /dev/null +++ b/libs/flask_cors-6.0.5.dist-info/RECORD @@ -0,0 +1,11 @@ +flask_cors-6.0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask_cors-6.0.5.dist-info/METADATA,sha256=VnSfhXDOUAIUQl8vlHh741-tY3bhlDy7U9cy_GERI64,5392 +flask_cors-6.0.5.dist-info/RECORD,, +flask_cors-6.0.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_cors-6.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +flask_cors-6.0.5.dist-info/top_level.txt,sha256=aWye_0QNZPp_QtPF4ZluLHqnyVLT9CPJsfiGhwqkWuo,11 +flask_cors/__init__.py,sha256=tnaGlwkJFxwAO6F-7RWrwDyCdvZVatVDE-Gk8o10ftY,701 +flask_cors/core.py,sha256=Uz4JGJXK0aVe9l-pDpuXctTs2JuPWYkiaAew1S70iwM,22655 +flask_cors/decorator.py,sha256=IBCL6ldVfaK1bD5fDfzAmXyX0kKaCVCtH7APdGia9D0,5349 +flask_cors/extension.py,sha256=2kV_-MX8jhAmlGwFf-xTXScjuh3GtBwyRjfSSTb3mRo,9577 +flask_cors/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/libs/flask_cors-6.0.2.dist-info/REQUESTED b/libs/flask_cors-6.0.5.dist-info/REQUESTED similarity index 100% rename from libs/flask_cors-6.0.2.dist-info/REQUESTED rename to libs/flask_cors-6.0.5.dist-info/REQUESTED diff --git a/libs/decorator-5.2.1.dist-info/WHEEL b/libs/flask_cors-6.0.5.dist-info/WHEEL similarity index 100% rename from libs/decorator-5.2.1.dist-info/WHEEL rename to libs/flask_cors-6.0.5.dist-info/WHEEL diff --git a/libs/flask_cors-6.0.2.dist-info/top_level.txt b/libs/flask_cors-6.0.5.dist-info/top_level.txt similarity index 100% rename from libs/flask_cors-6.0.2.dist-info/top_level.txt rename to libs/flask_cors-6.0.5.dist-info/top_level.txt diff --git a/libs/flask_cors/__init__.py b/libs/flask_cors/__init__.py index 732d401fe..ba0472df1 100644 --- a/libs/flask_cors/__init__.py +++ b/libs/flask_cors/__init__.py @@ -1,6 +1,12 @@ +from importlib.metadata import PackageNotFoundError, version + from .decorator import cross_origin from .extension import CORS -from .version import __version__ + +try: + __version__ = version("flask-cors") +except PackageNotFoundError: # pragma: no cover - package is not installed + __version__ = "unknown" __all__ = ["CORS", "__version__", "cross_origin"] diff --git a/libs/flask_cors/core.py b/libs/flask_cors/core.py index 8df343c93..1eeb20a83 100644 --- a/libs/flask_cors/core.py +++ b/libs/flask_cors/core.py @@ -1,13 +1,106 @@ +from __future__ import annotations + import logging import re -from collections.abc import Iterable +from collections.abc import Iterable, Mapping +from dataclasses import dataclass from datetime import timedelta +from typing import Any, TypedDict, Union, cast -from flask import current_app, request +from flask import Blueprint, Flask, Response, current_app, request from werkzeug.datastructures import Headers, MultiDict LOG = logging.getLogger(__name__) +# A resource/origin/header pattern may either be a literal string or a +# pre-compiled regular expression. +ResourcePattern = Union[str, "re.Pattern[str]"] + +# What a user may pass as ``resources``: a single pattern, a list of patterns, +# or a mapping of pattern -> per-resource (raw) options. +ResourceSpec = Union[ + str, + "re.Pattern[str]", + "list[ResourcePattern]", + "dict[ResourcePattern, dict[str, Any]]", +] + +# Input shapes accepted from users for individual options, before they are +# normalized by :func:`serialize_options`. These are deliberately broad: a +# scalar or an iterable of scalars are both accepted in most cases. +OriginsInput = Union[ResourcePattern, "Iterable[ResourcePattern]"] +HeadersInput = Union[ResourcePattern, "Iterable[ResourcePattern]"] +MethodsInput = Union[str, "Iterable[str]"] +ExposeHeadersInput = Union[str, "Iterable[str]", None] +MaxAgeInput = Union[timedelta, int, str, None] + + +@dataclass(frozen=True) +class _ComputedCorsOptions: + """The fully-resolved CORS options for a single route. + + Private internal representation, built only by :func:`serialize_options`. + Configure CORS via :class:`~flask_cors.CORS` / :func:`cross_origin` keyword + arguments, not by constructing this directly. + """ + + # ``origins``/``allow_headers`` are normalized to a list of patterns: each + # entry is either a compiled ``re.Pattern`` (regex) or a literal ``str``, + # so request-time matching never has to guess which it is. + origins: list[ResourcePattern] + allow_headers: list[ResourcePattern] + # Whether ``origins`` includes the catch-all ``.*`` wildcard. Computed once + # here because the wildcard is otherwise indistinguishable from a regex + # after the origins have been compiled. + allow_all_origins: bool + # ``methods``/``expose_headers`` join to a comma-separated string, or are + # ``None`` when not configured. + methods: str | None + expose_headers: str | None + # ``max_age`` is left as-is unless a timedelta is given, then stringified. + max_age: str | int | None + supports_credentials: bool + send_wildcard: bool + automatic_options: bool + vary_header: bool + intercept_exceptions: bool + always_send: bool + allow_private_network: bool + + +class CrossOriginOptionsInput(TypedDict, total=False): + """The keyword options accepted by :func:`cross_origin`. + + Every key is optional (``total=False``): only the options a caller + actually passes are forwarded, so app-level ``CORS_*`` configuration and + the defaults still apply to anything left out. Used with PEP 692 + ``Unpack`` to type ``**kwargs`` precisely. + """ + + origins: OriginsInput + methods: MethodsInput + expose_headers: ExposeHeadersInput + allow_headers: HeadersInput + supports_credentials: bool + max_age: MaxAgeInput + send_wildcard: bool + vary_header: bool + automatic_options: bool + always_send: bool + allow_private_network: bool + + +class CorsOptionsInput(CrossOriginOptionsInput, total=False): + """The keyword options accepted by :class:`CORS` / :meth:`CORS.init_app`. + + Extends :class:`CrossOriginOptionsInput` with the application-level options that + only make sense for the extension. + """ + + resources: ResourceSpec + intercept_exceptions: bool + + # Response Headers ACL_ORIGIN = "Access-Control-Allow-Origin" ACL_METHODS = "Access-Control-Allow-Methods" @@ -43,77 +136,97 @@ CONFIG_OPTIONS = [ # to a view. FLASK_CORS_EVALUATED = "_FLASK_CORS_EVALUATED" -# Strange, but this gets the type of a compiled regex, which is otherwise not -# exposed in a public API. -RegexObject = type(re.compile("")) -DEFAULT_OPTIONS = dict( - origins="*", - methods=ALL_METHODS, - allow_headers="*", - expose_headers=None, - supports_credentials=False, - max_age=None, - send_wildcard=False, - automatic_options=True, - vary_header=True, - resources=r"/*", - intercept_exceptions=True, - always_send=True, - allow_private_network=False, -) +# The type of a compiled regular expression. Exposed publicly as +# ``re.Pattern`` since Python 3.8. +RegexObject = re.Pattern + +# Characters commonly found in regular expressions. Their presence is used as +# a heuristic for whether a string is a regex rather than a literal value. +_REGEX_HINT_CHARS = frozenset("*\\]?$^[]()") + +# The set of recognized option names, derived from the input schema (which +# lists exactly the keys a user may pass; the resolved dataclass additionally +# carries computed fields such as ``allow_all_origins``). +_KNOWN_OPTIONS = frozenset(CorsOptionsInput.__required_keys__) | frozenset(CorsOptionsInput.__optional_keys__) + +# The raw, un-normalized option defaults. These are merged with app config and +# user kwargs before being handed to :func:`serialize_options`, which produces +# the strongly-typed :class:`_ComputedCorsOptions`. Typing this as ``CorsOptionsInput`` +# lets mypy verify the defaults match the documented input schema. +DEFAULT_OPTIONS: CorsOptionsInput = { + "origins": "*", + "methods": ALL_METHODS, + "allow_headers": "*", + "expose_headers": None, + "supports_credentials": False, + "max_age": None, + "send_wildcard": False, + "automatic_options": True, + "vary_header": True, + "resources": r"/*", + "intercept_exceptions": True, + "always_send": True, + "allow_private_network": False, +} -def parse_resources(resources): +def parse_resources(resources: ResourceSpec) -> list[tuple[ResourcePattern, dict[str, Any]]]: if isinstance(resources, dict): # To make the API more consistent with the decorator, allow a # resource of '*', which is not actually a valid regexp. - resources = [(re_fix(k), v) for k, v in resources.items()] + resource_pairs = [(re_fix(k), v) for k, v in resources.items()] # Sort patterns with static (literal) paths first, then by regex specificity - def sort_key(pair): + def sort_key(pair: tuple[ResourcePattern, Any]) -> tuple[int, int, int, int]: pattern, _ = pair - if isinstance(pattern, RegexObject): + if isinstance(pattern, re.Pattern): return (1, 0, -pattern.pattern.count("/"), -len(pattern.pattern)) elif probably_regex(pattern): return (1, 1, -pattern.count("/"), -len(pattern)) else: return (0, 0, -pattern.count("/"), -len(pattern)) - return sorted(resources, key=sort_key) + resource_pairs.sort(key=sort_key) elif isinstance(resources, str): - return [(re_fix(resources), {})] + resource_pairs = [(re_fix(resources), {})] elif isinstance(resources, Iterable): - return [(re_fix(r), {}) for r in resources] + resource_pairs = [(re_fix(r), {}) for r in resources] # Type of compiled regex is not part of the public API. Test for this # at runtime. - elif isinstance(resources, RegexObject): - return [(re_fix(resources), {})] + elif isinstance(resources, re.Pattern): + resource_pairs = [(re_fix(resources), {})] else: - raise ValueError("Unexpected value for resources argument.") + # ValueError (rather than the arguably more correct TypeError) is the + # historical public contract here; callers may catch it. + raise ValueError("Unexpected value for resources argument.") # noqa: TRY004 + + # Resolve each path pattern once (paths are matched case-sensitively), so + # request handling never re-runs the regex heuristic. Patterns are sorted + # above, before compilation, so specificity ordering is preserved. + return [(_resolve_pattern(pattern, ignore_case=False), opts) for (pattern, opts) in resource_pairs] -def get_regexp_pattern(regexp): +def get_regexp_pattern(regexp: ResourcePattern) -> str: """ Helper that returns regexp pattern from given value. :param regexp: regular expression to stringify - :type regexp: _sre.SRE_Pattern or str + :type regexp: re.Pattern or str :returns: string representation of given regexp pattern :rtype: str """ - try: + if isinstance(regexp, re.Pattern): return regexp.pattern - except AttributeError: - return str(regexp) + return str(regexp) -def get_cors_origins(options, request_origin): - origins = options.get("origins") - wildcard = r".*" in origins +def get_cors_origins(options: _ComputedCorsOptions, request_origin: str | None) -> list[str] | None: + origins = options.origins + wildcard = options.allow_all_origins # If the Origin header is not present terminate this set of steps. # The request is outside the scope of this specification.-- W3Spec @@ -121,7 +234,7 @@ def get_cors_origins(options, request_origin): LOG.debug("CORS request received with 'Origin' %s", request_origin) # If the allowed origins is an asterisk or 'wildcard', always match - if wildcard and options.get("send_wildcard"): + if wildcard and options.send_wildcard: LOG.debug("Allowed origins are set to '*'. Sending wildcard CORS header.") return ["*"] # If the value of the Origin header is a case-insensitive match @@ -139,7 +252,7 @@ def get_cors_origins(options, request_origin): LOG.debug("The request's Origin header does not match any of allowed origins.") return None - elif options.get("always_send"): + elif options.always_send: if wildcard: # If wildcard is in the origins, even if 'send_wildcard' is False, # simply send the wildcard. Unless supports_credentials is True, @@ -147,13 +260,14 @@ def get_cors_origins(options, request_origin): # It is the most-likely to be correct thing to do (the only other # option is to return nothing, which almost certainly not what # the developer wants if the '*' origin was specified. - if options.get("supports_credentials"): + if options.supports_credentials: return None else: return ["*"] else: - # Return all origins that are not regexes. - return sorted([o for o in origins if not probably_regex(o)]) + # Return all literal (non-regex) origins. After resolution a plain + # ``str`` entry is always a literal; regexes are compiled patterns. + return sorted(o for o in origins if isinstance(o, str)) # Terminate these steps, return the original request untouched. else: @@ -163,38 +277,46 @@ def get_cors_origins(options, request_origin): return None -def get_allow_headers(options, acl_request_headers): +def get_allow_headers(options: _ComputedCorsOptions, acl_request_headers: str | None) -> str | None: if acl_request_headers: + allow_headers = options.allow_headers request_headers = [h.strip() for h in acl_request_headers.split(",")] # any header that matches in the allow_headers - matching_headers = filter(lambda h: try_match_any_pattern(h, options.get("allow_headers"), caseSensitive=False), request_headers) + matching_headers = filter( + lambda h: try_match_any_pattern(h, allow_headers, caseSensitive=False), + request_headers, + ) return ", ".join(sorted(matching_headers)) return None -def get_cors_headers(options, request_headers, request_method): +def get_cors_headers( + options: _ComputedCorsOptions, request_headers: Headers, request_method: str +) -> MultiDict[str, str | int | None]: origins_to_set = get_cors_origins(options, request_headers.get("Origin")) - headers = MultiDict() + # Values are header values (str), with ``max_age`` allowing int and a few + # options allowing None; the trailing comprehension drops the None/empty + # entries before the result is returned. + headers: MultiDict[str, str | int | None] = MultiDict() if not origins_to_set: # CORS is not enabled for this route return headers - for origin in origins_to_set: - headers.add(ACL_ORIGIN, origin) + headers.setlist(ACL_ORIGIN, origins_to_set) - headers[ACL_EXPOSE_HEADERS] = options.get("expose_headers") + headers[ACL_EXPOSE_HEADERS] = options.expose_headers - if options.get("supports_credentials"): + if options.supports_credentials: headers[ACL_CREDENTIALS] = "true" # case sensitive if ( ACL_REQUEST_HEADER_PRIVATE_NETWORK in request_headers and request_headers.get(ACL_REQUEST_HEADER_PRIVATE_NETWORK) == "true" ): - allow_private_network = "true" if options.get("allow_private_network") else "false" + allow_private_network = "true" if options.allow_private_network else "false" headers[ACL_RESPONSE_PRIVATE_NETWORK] = allow_private_network # This is a preflight request @@ -204,36 +326,35 @@ def get_cors_headers(options, request_headers, request_method): # If there is no Access-Control-Request-Method header or if parsing # failed, do not set any additional headers - if acl_request_method and acl_request_method in options.get("methods"): + methods = options.methods + if acl_request_method and methods and acl_request_method in methods: # If method is not a case-sensitive match for any of the values in # list of methods do not set any additional headers and terminate # this set of steps. headers[ACL_ALLOW_HEADERS] = get_allow_headers(options, request_headers.get(ACL_REQUEST_HEADERS)) - headers[ACL_MAX_AGE] = options.get("max_age") - headers[ACL_METHODS] = options.get("methods") + headers[ACL_MAX_AGE] = options.max_age + headers[ACL_METHODS] = methods else: LOG.info( "The request's Access-Control-Request-Method header does not match allowed methods. CORS headers will not be applied." ) # http://www.w3.org/TR/cors/#resource-implementation - if options.get("vary_header"): - # Only set header if the origin returned will vary dynamically, - # i.e. if we are not returning an asterisk, and there are multiple - # origins that can be matched. - if headers[ACL_ORIGIN] == "*": - pass - elif ( - len(options.get("origins")) > 1 - or len(origins_to_set) > 1 - or any(map(probably_regex, options.get("origins"))) + if options.vary_header: + # Only set the header if the resolved origin can vary dynamically: + # i.e. we are not returning a wildcard, and more than one origin (or a + # regex) could have matched. + origins = options.origins + returns_wildcard = headers[ACL_ORIGIN] == "*" + if not returns_wildcard and ( + len(origins) > 1 or len(origins_to_set) > 1 or any(isinstance(o, re.Pattern) for o in origins) ): headers.add("Vary", "Origin") return MultiDict((k, v) for k, v in headers.items() if v) -def set_cors_headers(resp, options): +def set_cors_headers(resp: Response, options: _ComputedCorsOptions) -> Response: """ Performs the actual evaluation of Flask-CORS options and actually modifies the response object. @@ -251,6 +372,8 @@ def set_cors_headers(resp, options): # objects (Werkzeug Headers work as well). This is a problem because # headers allow repeated values. if not isinstance(resp.headers, Headers) and not isinstance(resp.headers, MultiDict): + # The non-standard header container is replaced with a MultiDict so + # repeated headers behave consistently. resp.headers = MultiDict(resp.headers) headers_to_set = get_cors_headers(options, request.headers, request.method) @@ -263,17 +386,16 @@ def set_cors_headers(resp, options): return resp -def probably_regex(maybe_regex): - if isinstance(maybe_regex, RegexObject): +def probably_regex(maybe_regex: ResourcePattern) -> bool: + if isinstance(maybe_regex, re.Pattern): return True - else: - common_regex_chars = ["*", "\\", "]", "?", "$", "^", "[", "]", "(", ")"] - # Use common characters used in regular expressions as a proxy - # for if this string is in fact a regex. - return any(c in maybe_regex for c in common_regex_chars) + + # Use characters common in regular expressions as a proxy for whether + # this string is in fact a regex. + return not _REGEX_HINT_CHARS.isdisjoint(maybe_regex) -def re_fix(reg): +def re_fix(reg: ResourcePattern) -> ResourcePattern: """ Replace the invalid regex r'*' with the valid, wildcard regex r'/.*' to enable the CORS app extension to have a more user friendly api. @@ -281,58 +403,57 @@ def re_fix(reg): return r".*" if reg == r"*" else reg -def try_match_any_pattern(inst, patterns, caseSensitive=True): +def try_match_any_pattern(inst: str, patterns: Iterable[ResourcePattern], caseSensitive: bool = True) -> bool: return any(try_match_pattern(inst, pattern, caseSensitive) for pattern in patterns) -def try_match_pattern(value, pattern, caseSensitive=True): + +def try_match_pattern(value: Any, pattern: ResourcePattern, caseSensitive: bool = True) -> bool: """ - Safely attempts to match a pattern or string to a value. This - function can be used to match request origins, headers, or paths. - The value of caseSensitive should be set in accordance to the - data being compared e.g. origins and headers are case insensitive - whereas paths are case-sensitive + Match a value against a *resolved* pattern: a compiled ``re.Pattern`` + (whose case-sensitivity is already baked in) or a literal string. Used to + match request origins, headers, or paths; ``caseSensitive`` only affects + the literal comparison (origins and headers are case insensitive, paths are + case sensitive). """ - if isinstance(pattern, RegexObject): - return re.match(pattern, value) - if probably_regex(pattern): - flags = 0 if caseSensitive else re.IGNORECASE - try: - return re.match(pattern, value, flags=flags) - except re.error: - return False + if isinstance(pattern, re.Pattern): + return bool(pattern.match(value)) try: v = str(value) p = str(pattern) - return v == p if caseSensitive else v.casefold() == p.casefold() except Exception: - return value == pattern + return bool(value == pattern) + return v == p if caseSensitive else v.casefold() == p.casefold() -def get_cors_options(appInstance, *dicts): + +def merge_options(appInstance: Flask | Blueprint | None, *dicts: Mapping[str, Any]) -> dict[str, Any]: """ - Compute CORS options for an application by combining the DEFAULT_OPTIONS, - the app's configuration-specified options and any dictionaries passed. The - last specified option wins. + Merge the DEFAULT_OPTIONS, the app's configuration-specified options and any + dictionaries passed into a single raw options mapping. The last specified + option wins. """ - options = DEFAULT_OPTIONS.copy() + options: dict[str, Any] = dict(DEFAULT_OPTIONS) options.update(get_app_kwarg_dict(appInstance)) - if dicts: - for d in dicts: - options.update(d) - - return serialize_options(options) + for d in dicts: + options.update(d) + return options -def get_app_kwarg_dict(appInstance=None): +def get_cors_options(appInstance: Flask | Blueprint | None, *dicts: Mapping[str, Any]) -> _ComputedCorsOptions: + """Compute the resolved CORS options for an application (see merge_options).""" + return serialize_options(merge_options(appInstance, *dicts)) + + +def get_app_kwarg_dict(appInstance: Flask | Blueprint | None = None) -> dict[str, Any]: """Returns the dictionary of CORS specific app configurations.""" app = appInstance or current_app # In order to support blueprints which do not have a config attribute app_config = getattr(app, "config", {}) - return {k.lower().replace("cors_", ""): app_config.get(k) for k in CONFIG_OPTIONS if app_config.get(k) is not None} + return {k.lower().replace("cors_", ""): value for k in CONFIG_OPTIONS if (value := app_config.get(k)) is not None} -def flexible_str(obj): +def flexible_str(obj: Any) -> str | None: """ A more flexible str function which intelligently handles stringifying strings, lists and other iterables. The results are lexographically sorted @@ -347,53 +468,98 @@ def flexible_str(obj): return str(obj) -def serialize_option(options_dict, key, upper=False): - if key in options_dict: - value = flexible_str(options_dict[key]) - options_dict[key] = value.upper() if upper else value - - -def ensure_iterable(inst): +def ensure_iterable(inst: Any) -> Iterable[Any]: """ Wraps scalars or string types as a list, or returns the iterable instance. """ if isinstance(inst, str) or not isinstance(inst, Iterable): return [inst] else: - return inst + return cast("Iterable[Any]", inst) -def sanitize_regex_param(param): +def sanitize_regex_param(param: Any) -> list[ResourcePattern]: return [re_fix(x) for x in ensure_iterable(param)] -def serialize_options(opts): - """ - A helper method to serialize and processes the options dictionary. - """ - options = (opts or {}).copy() +def _resolve_pattern(pattern: ResourcePattern, *, ignore_case: bool) -> ResourcePattern: + """Resolve a single raw pattern once, at configuration time. - for key in opts.keys(): - if key not in DEFAULT_OPTIONS: + A regex-like string is compiled to a ``re.Pattern`` (with the correct + case-sensitivity baked in); a literal string and an already-compiled + pattern are returned unchanged. An invalid regular expression raises a + ``ValueError`` so the misconfiguration fails fast at setup time rather than + silently never matching. + """ + if isinstance(pattern, str) and probably_regex(pattern): + try: + return re.compile(pattern, re.IGNORECASE if ignore_case else 0) + except re.error as exc: + raise ValueError(f"Invalid regular expression in Flask-CORS configuration: {pattern!r}") from exc + return pattern + + +def _resolve_patterns(patterns: Iterable[ResourcePattern], *, ignore_case: bool) -> list[ResourcePattern]: + """Resolve a list of patterns (see :func:`_resolve_pattern`). + + After this, the request path can simply test ``isinstance(p, re.Pattern)`` + instead of re-running the :func:`probably_regex` heuristic on every request. + """ + return [_resolve_pattern(p, ignore_case=ignore_case) for p in patterns] + + +def serialize_options(opts: Mapping[str, Any]) -> _ComputedCorsOptions: + """ + Normalize a raw options mapping into a strongly-typed :class:`_ComputedCorsOptions`. + + This is the single boundary where loosely-typed user input (arbitrary + keyword arguments, app config, resource dictionaries) is parsed into the + concrete, per-field types the rest of the package relies on. + """ + for key in opts: + if key not in _KNOWN_OPTIONS: LOG.warning("Unknown option passed to Flask-CORS: %s", key) - # Ensure origins is a list of allowed origins with at least one entry. - options["origins"] = sanitize_regex_param(options.get("origins")) - options["allow_headers"] = sanitize_regex_param(options.get("allow_headers")) + # Ensure origins is a list of allowed origins with at least one entry. The + # wildcard is detected before the patterns are compiled (afterwards ``.*`` + # is just another compiled regex, indistinguishable from a real one). + sanitized_origins = sanitize_regex_param(opts.get("origins")) + allow_all_origins = r".*" in sanitized_origins + supports_credentials = bool(opts.get("supports_credentials")) + send_wildcard = bool(opts.get("send_wildcard")) # This is expressly forbidden by the spec. Raise a value error so people # don't get burned in production. - if r".*" in options["origins"] and options["supports_credentials"] and options["send_wildcard"]: + if allow_all_origins and supports_credentials and send_wildcard: raise ValueError( "Cannot use supports_credentials in conjunction with" "an origin string of '*'. See: " "http://www.w3.org/TR/cors/#resource-requests" ) - serialize_option(options, "expose_headers") - serialize_option(options, "methods", upper=True) + origins = _resolve_patterns(sanitized_origins, ignore_case=True) + allow_headers = _resolve_patterns(sanitize_regex_param(opts.get("allow_headers")), ignore_case=True) - if isinstance(options.get("max_age"), timedelta): - options["max_age"] = str(int(options["max_age"].total_seconds())) + methods = flexible_str(opts.get("methods")) + if methods is not None: + methods = methods.upper() - return options + max_age = opts.get("max_age") + if isinstance(max_age, timedelta): + max_age = str(int(max_age.total_seconds())) + + return _ComputedCorsOptions( + origins=origins, + allow_headers=allow_headers, + allow_all_origins=allow_all_origins, + methods=methods, + expose_headers=flexible_str(opts.get("expose_headers")), + max_age=max_age, + supports_credentials=supports_credentials, + send_wildcard=send_wildcard, + automatic_options=bool(opts.get("automatic_options")), + vary_header=bool(opts.get("vary_header")), + intercept_exceptions=bool(opts.get("intercept_exceptions")), + always_send=bool(opts.get("always_send")), + allow_private_network=bool(opts.get("allow_private_network")), + ) diff --git a/libs/flask_cors/decorator.py b/libs/flask_cors/decorator.py index f8915b62d..d3c883d4b 100644 --- a/libs/flask_cors/decorator.py +++ b/libs/flask_cors/decorator.py @@ -1,14 +1,23 @@ +from __future__ import annotations + import logging +from collections.abc import Callable from functools import update_wrapper +from typing import TYPE_CHECKING, Any, TypeVar, cast -from flask import current_app, make_response, request +from flask import Response, current_app, make_response, request -from .core import FLASK_CORS_EVALUATED, get_cors_options, set_cors_headers +from .core import FLASK_CORS_EVALUATED, CrossOriginOptionsInput, get_cors_options, set_cors_headers + +if TYPE_CHECKING: + from typing_extensions import Unpack LOG = logging.getLogger(__name__) +F = TypeVar("F", bound=Callable[..., Any]) -def cross_origin(*args, **kwargs): + +def cross_origin(*args: Any, **kwargs: Unpack[CrossOriginOptionsInput]) -> Callable[[F], F]: """ This function is the decorator which is used to wrap a Flask route with. In the simplest case, simply use the default parameters to allow all @@ -96,7 +105,7 @@ def cross_origin(*args, **kwargs): """ _options = kwargs - def decorator(f): + def decorator(f: F) -> F: LOG.debug("Enabling %s for cross_origin using options:%s", f, _options) # If True, intercept OPTIONS requests by modifying the view function, @@ -107,15 +116,20 @@ def cross_origin(*args, **kwargs): # decorator (which is actually wraps the function object we return) # intercepts OPTIONS handling, and requests will not have CORS headers if _options.get("automatic_options", True): - f.required_methods = getattr(f, "required_methods", set()) - f.required_methods.add("OPTIONS") - f.provide_automatic_options = False + # ``f`` is a plain view function; these attributes are read by + # Flask's routing. Use an ``Any`` alias so the dynamic attribute + # assignment type-checks. + view_func: Any = f + required_methods = set(getattr(f, "required_methods", set())) + required_methods.add("OPTIONS") + view_func.required_methods = required_methods + view_func.provide_automatic_options = False - def wrapped_function(*args, **kwargs): + def wrapped_function(*args: Any, **kwargs: Any) -> Response: # Handle setting of Flask-Cors parameters options = get_cors_options(current_app, _options) - if options.get("automatic_options") and request.method == "OPTIONS": + if options.automatic_options and request.method == "OPTIONS": resp = current_app.make_default_options_response() else: resp = make_response(f(*args, **kwargs)) @@ -124,6 +138,6 @@ def cross_origin(*args, **kwargs): setattr(resp, FLASK_CORS_EVALUATED, True) return resp - return update_wrapper(wrapped_function, f) + return cast(F, update_wrapper(wrapped_function, f)) return decorator diff --git a/libs/flask_cors/extension.py b/libs/flask_cors/extension.py index 434f65eaa..1e172142c 100644 --- a/libs/flask_cors/extension.py +++ b/libs/flask_cors/extension.py @@ -1,9 +1,29 @@ +from __future__ import annotations + import logging +from typing import TYPE_CHECKING, Any from urllib.parse import unquote -from flask import request +from flask import Blueprint, Flask, Response, request -from .core import ACL_ORIGIN, get_cors_options, get_regexp_pattern, parse_resources, set_cors_headers, try_match_pattern +from .core import ( + ACL_ORIGIN, + CorsOptionsInput, + ResourcePattern, + _ComputedCorsOptions, + get_cors_options, + get_regexp_pattern, + merge_options, + parse_resources, + serialize_options, + set_cors_headers, + try_match_pattern, +) + +if TYPE_CHECKING: + from collections.abc import Callable + + from typing_extensions import Unpack LOG = logging.getLogger(__name__) @@ -140,50 +160,61 @@ class CORS: :type allow_private_network: bool """ - def __init__(self, app=None, **kwargs): + def __init__(self, app: Flask | Blueprint | None = None, **kwargs: Unpack[CorsOptionsInput]) -> None: self._options = kwargs if app is not None: self.init_app(app, **kwargs) - def init_app(self, app, **kwargs): + def init_app(self, app: Flask | Blueprint, **kwargs: Unpack[CorsOptionsInput]) -> None: # The resources and options may be specified in the App Config, the CORS constructor # or the kwargs to the call to init_app. - options = get_cors_options(app, self._options, kwargs) + merged = merge_options(app, self._options, kwargs) + options = serialize_options(merged) # Flatten our resources into a list of the form - # (pattern_or_regexp, dictionary_of_options) - resources = parse_resources(options.get("resources")) + # (pattern_or_regexp, dictionary_of_raw_options). ``resources`` is a raw + # spec consumed only here, so it is read from the merged mapping rather + # than carried on the resolved options. + resources = parse_resources(merged.get("resources", r"/*")) - # Compute the options for each resource by combining the options from - # the app's configuration, the constructor, the kwargs to init_app, and - # finally the options specified in the resources dictionary. - resources = [(pattern, get_cors_options(app, options, opts)) for (pattern, opts) in resources] + # Compute the options for each resource from the raw inputs (defaults, + # app config, constructor kwargs, init_app kwargs) plus the per-resource + # overrides. Re-merging the raw inputs rather than the already-resolved + # ``options`` avoids re-normalizing compiled regexes. + resolved_resources: list[tuple[ResourcePattern, _ComputedCorsOptions]] = [ + (pattern, get_cors_options(app, self._options, kwargs, opts)) for (pattern, opts) in resources + ] # Create a human-readable form of these resources by converting the compiled # regular expressions into strings. - resources_human = {get_regexp_pattern(pattern): opts for (pattern, opts) in resources} + resources_human = {get_regexp_pattern(pattern): opts for (pattern, opts) in resolved_resources} LOG.debug("Configuring CORS with resources: %s", resources_human) - cors_after_request = make_after_request_function(resources) + cors_after_request = make_after_request_function(resolved_resources) app.after_request(cors_after_request) - # Wrap exception handlers with cross_origin - # These error handlers will still respect the behavior of the route - if options.get("intercept_exceptions", True): + # Wrap exception handlers with cross_origin so error responses also + # receive CORS headers. Blueprints have no ``handle_exception`` / + # ``make_response``, so the ``hasattr`` guard skips them; the ``Any`` + # alias lets us read and reassign those Flask-only methods without + # tripping mypy's attr-defined / method-assign checks for ``Blueprint``. + if options.intercept_exceptions and hasattr(app, "handle_exception"): + app_any: Any = app - def _after_request_decorator(f): - def wrapped_function(*args, **kwargs): - return cors_after_request(app.make_response(f(*args, **kwargs))) + def _after_request_decorator(f: Callable[..., Any]) -> Callable[..., Response]: + def wrapped_function(*args: Any, **kwargs: Any) -> Response: + return cors_after_request(app_any.make_response(f(*args, **kwargs))) return wrapped_function - if hasattr(app, "handle_exception"): - app.handle_exception = _after_request_decorator(app.handle_exception) - app.handle_user_exception = _after_request_decorator(app.handle_user_exception) + app_any.handle_exception = _after_request_decorator(app_any.handle_exception) + app_any.handle_user_exception = _after_request_decorator(app_any.handle_user_exception) -def make_after_request_function(resources): - def cors_after_request(resp): +def make_after_request_function( + resources: list[tuple[ResourcePattern, _ComputedCorsOptions]], +) -> Callable[[Response], Response]: + def cors_after_request(resp: Response) -> Response: # If CORS headers are set in a view decorator, pass if resp.headers is not None and resp.headers.get(ACL_ORIGIN): LOG.debug("CORS have been already evaluated, skipping") diff --git a/libs/chardet/metadata/__init__.py b/libs/flask_cors/py.typed similarity index 100% rename from libs/chardet/metadata/__init__.py rename to libs/flask_cors/py.typed diff --git a/libs/flask_cors/version.py b/libs/flask_cors/version.py deleted file mode 100644 index 7f229cf18..000000000 --- a/libs/flask_cors/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "6.0.2" diff --git a/libs/guess_language/__init__.py b/libs/guess_language/__init__.py index 5a915b385..66dc8becd 100644 --- a/libs/guess_language/__init__.py +++ b/libs/guess_language/__init__.py @@ -1,11 +1,12 @@ +# -*- coding: utf-8 -*- """Guess the natural language of a text """ # © 2012 spirit -# https://bitbucket.org/spirit/guess_language +# https://github.com/hiddenspirit/guess_language # # Original Python package: # Copyright (c) 2008, Kent S Johnson -# http://code.google.com/p/guess-language/ +# https://github.com/kent37/guess-language # # Original C++ version for KDE: # Copyright (c) 2006 Jacob R Rideout @@ -32,6 +33,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +from __future__ import unicode_literals + import functools import re import warnings @@ -314,18 +317,21 @@ class UNKNOWN(str): def __bool__(self): return False + def __nonzero__(self): + return False + UNKNOWN = UNKNOWN("UNKNOWN") -def guess_language(text: str, hints=None): +def guess_language(text, hints=None): """Return the ISO 639-1 language code. """ words = WORD_RE.findall(text[:MAX_LENGTH].replace("’", "'")) return identify(words, find_runs(words), hints) -def guess_language_info(text: str, hints=None): +def guess_language_info(text, hints=None): """Return LanguageInfo(tag, id, name). """ tag = guess_language(text, hints) @@ -340,13 +346,13 @@ def guess_language_info(text: str, hints=None): guess_language_tag = guess_language -def guess_language_id(text: str, hints=None): +def guess_language_id(text, hints=None): """Return the language ID. """ return _get_id(guess_language(text, hints)) -def guess_language_name(text: str, hints=None): +def guess_language_name(text, hints=None): """Return the language name (in English). """ return _get_name(guess_language(text, hints)) @@ -558,7 +564,7 @@ else: best_score = score best_tag = tag - if best_score / len(words) < threshold: + if float(best_score) / len(words) < threshold: return UNKNOWN return best_tag diff --git a/libs/guess_language/data/models/ar.py b/libs/guess_language/data/models/ar.py index a761f22b7..b6cf70a4a 100644 --- a/libs/guess_language/data/models/ar.py +++ b/libs/guess_language/data/models/ar.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' ال': 0, 'الع': 1, diff --git a/libs/guess_language/data/models/az.py b/libs/guess_language/data/models/az.py index c33473bff..299cbaf58 100644 --- a/libs/guess_language/data/models/az.py +++ b/libs/guess_language/data/models/az.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'lər': 0, 'in ': 1, diff --git a/libs/guess_language/data/models/bg.py b/libs/guess_language/data/models/bg.py index 98eb2990a..eb5abee00 100644 --- a/libs/guess_language/data/models/bg.py +++ b/libs/guess_language/data/models/bg.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'на ': 0, ' на': 1, diff --git a/libs/guess_language/data/models/cs.py b/libs/guess_language/data/models/cs.py index 107a0cbe3..ba4fac789 100644 --- a/libs/guess_language/data/models/cs.py +++ b/libs/guess_language/data/models/cs.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' pr': 0, ' po': 1, diff --git a/libs/guess_language/data/models/cy.py b/libs/guess_language/data/models/cy.py index cf58865cd..015837d53 100644 --- a/libs/guess_language/data/models/cy.py +++ b/libs/guess_language/data/models/cy.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'yn ': 0, 'dd ': 1, diff --git a/libs/guess_language/data/models/da.py b/libs/guess_language/data/models/da.py index 43b2a7c39..f76d97759 100644 --- a/libs/guess_language/data/models/da.py +++ b/libs/guess_language/data/models/da.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'er ': 0, 'en ': 1, diff --git a/libs/guess_language/data/models/de.py b/libs/guess_language/data/models/de.py index be15cd0a8..23704a3c1 100644 --- a/libs/guess_language/data/models/de.py +++ b/libs/guess_language/data/models/de.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'en ': 0, 'er ': 1, diff --git a/libs/guess_language/data/models/eo.py b/libs/guess_language/data/models/eo.py index 72e841b3b..70710ef99 100644 --- a/libs/guess_language/data/models/eo.py +++ b/libs/guess_language/data/models/eo.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' la': 0, 'la ': 1, diff --git a/libs/guess_language/data/models/es.py b/libs/guess_language/data/models/es.py index 3d1dad667..636b6ff4f 100644 --- a/libs/guess_language/data/models/es.py +++ b/libs/guess_language/data/models/es.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' de': 0, 'de ': 1, diff --git a/libs/guess_language/data/models/et.py b/libs/guess_language/data/models/et.py index 3b798436b..4397a6997 100644 --- a/libs/guess_language/data/models/et.py +++ b/libs/guess_language/data/models/et.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'st ': 0, ' ka': 1, diff --git a/libs/guess_language/data/models/fa.py b/libs/guess_language/data/models/fa.py index 6e013bcf4..33cfacf12 100644 --- a/libs/guess_language/data/models/fa.py +++ b/libs/guess_language/data/models/fa.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ان ': 0, 'ای ': 1, diff --git a/libs/guess_language/data/models/fi.py b/libs/guess_language/data/models/fi.py index 2926f855b..ee7669267 100644 --- a/libs/guess_language/data/models/fi.py +++ b/libs/guess_language/data/models/fi.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'en ': 0, 'in ': 1, diff --git a/libs/guess_language/data/models/fr.py b/libs/guess_language/data/models/fr.py index a37f0a929..c01ba9cec 100644 --- a/libs/guess_language/data/models/fr.py +++ b/libs/guess_language/data/models/fr.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'es ': 0, ' de': 1, diff --git a/libs/guess_language/data/models/ha.py b/libs/guess_language/data/models/ha.py index bd2d5025e..13d46680e 100644 --- a/libs/guess_language/data/models/ha.py +++ b/libs/guess_language/data/models/ha.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' da': 0, 'da ': 1, diff --git a/libs/guess_language/data/models/hi.py b/libs/guess_language/data/models/hi.py index 3b6bc7532..5679ebdc4 100644 --- a/libs/guess_language/data/models/hi.py +++ b/libs/guess_language/data/models/hi.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ें ': 0, ' है': 1, diff --git a/libs/guess_language/data/models/hr.py b/libs/guess_language/data/models/hr.py index 62f1627f8..67c39d3fb 100644 --- a/libs/guess_language/data/models/hr.py +++ b/libs/guess_language/data/models/hr.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'je ': 0, ' na': 1, diff --git a/libs/guess_language/data/models/hu.py b/libs/guess_language/data/models/hu.py index 5d1b334d9..a16285209 100644 --- a/libs/guess_language/data/models/hu.py +++ b/libs/guess_language/data/models/hu.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' a ': 0, ' az': 1, diff --git a/libs/guess_language/data/models/is.py b/libs/guess_language/data/models/is.py index b6c139b1c..2e261d663 100644 --- a/libs/guess_language/data/models/is.py +++ b/libs/guess_language/data/models/is.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'að ': 0, 'um ': 1, diff --git a/libs/guess_language/data/models/it.py b/libs/guess_language/data/models/it.py index b3d0341ec..afd584252 100644 --- a/libs/guess_language/data/models/it.py +++ b/libs/guess_language/data/models/it.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' di': 0, 'to ': 1, diff --git a/libs/guess_language/data/models/kk.py b/libs/guess_language/data/models/kk.py index 72bd72499..0bc11ae6f 100644 --- a/libs/guess_language/data/models/kk.py +++ b/libs/guess_language/data/models/kk.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ан ': 0, 'ен ': 1, diff --git a/libs/guess_language/data/models/ky.py b/libs/guess_language/data/models/ky.py index 37fe8ffd0..657dd88bb 100644 --- a/libs/guess_language/data/models/ky.py +++ b/libs/guess_language/data/models/ky.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ын ': 0, 'ан ': 1, diff --git a/libs/guess_language/data/models/lt.py b/libs/guess_language/data/models/lt.py index 69c89b30d..ab372cc2b 100644 --- a/libs/guess_language/data/models/lt.py +++ b/libs/guess_language/data/models/lt.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'as ': 0, ' pa': 1, diff --git a/libs/guess_language/data/models/lv.py b/libs/guess_language/data/models/lv.py index 421f2af8d..d01c8c2e0 100644 --- a/libs/guess_language/data/models/lv.py +++ b/libs/guess_language/data/models/lv.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'as ': 0, ' la': 1, diff --git a/libs/guess_language/data/models/mk.py b/libs/guess_language/data/models/mk.py index 525e84f74..dfa014831 100644 --- a/libs/guess_language/data/models/mk.py +++ b/libs/guess_language/data/models/mk.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'на ': 0, ' на': 1, diff --git a/libs/guess_language/data/models/mn.py b/libs/guess_language/data/models/mn.py index edb24e161..94bd17978 100644 --- a/libs/guess_language/data/models/mn.py +++ b/libs/guess_language/data/models/mn.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ын ': 0, ' ба': 1, diff --git a/libs/guess_language/data/models/nb.py b/libs/guess_language/data/models/nb.py index 973a97745..d1755dc42 100644 --- a/libs/guess_language/data/models/nb.py +++ b/libs/guess_language/data/models/nb.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'er ': 0, 'en ': 1, diff --git a/libs/guess_language/data/models/ne.py b/libs/guess_language/data/models/ne.py index 60e092696..d4ebd7cf7 100644 --- a/libs/guess_language/data/models/ne.py +++ b/libs/guess_language/data/models/ne.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'को ': 0, 'का ': 1, diff --git a/libs/guess_language/data/models/pl.py b/libs/guess_language/data/models/pl.py index 3c7c8ef08..71e60a0eb 100644 --- a/libs/guess_language/data/models/pl.py +++ b/libs/guess_language/data/models/pl.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ie ': 0, 'nie': 1, diff --git a/libs/guess_language/data/models/ps.py b/libs/guess_language/data/models/ps.py index a31f09135..794520ed9 100644 --- a/libs/guess_language/data/models/ps.py +++ b/libs/guess_language/data/models/ps.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' د ': 0, 'اؤ ': 1, diff --git a/libs/guess_language/data/models/pt.py b/libs/guess_language/data/models/pt.py index 140041f5d..5dae0b5a1 100644 --- a/libs/guess_language/data/models/pt.py +++ b/libs/guess_language/data/models/pt.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'de ': 0, ' de': 1, diff --git a/libs/guess_language/data/models/ro.py b/libs/guess_language/data/models/ro.py index 735201987..a9d867957 100644 --- a/libs/guess_language/data/models/ro.py +++ b/libs/guess_language/data/models/ro.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' de': 0, ' în': 1, diff --git a/libs/guess_language/data/models/ru.py b/libs/guess_language/data/models/ru.py index 87aefcf51..fe3d569b2 100644 --- a/libs/guess_language/data/models/ru.py +++ b/libs/guess_language/data/models/ru.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' на': 0, ' пр': 1, diff --git a/libs/guess_language/data/models/sk.py b/libs/guess_language/data/models/sk.py index 5b24e927c..2ab44d1f9 100644 --- a/libs/guess_language/data/models/sk.py +++ b/libs/guess_language/data/models/sk.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' pr': 0, ' po': 1, diff --git a/libs/guess_language/data/models/sl.py b/libs/guess_language/data/models/sl.py index b0888dc53..4396fdbc3 100644 --- a/libs/guess_language/data/models/sl.py +++ b/libs/guess_language/data/models/sl.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'je ': 0, ' pr': 1, diff --git a/libs/guess_language/data/models/sq.py b/libs/guess_language/data/models/sq.py index 90251a9a3..4707b3cad 100644 --- a/libs/guess_language/data/models/sq.py +++ b/libs/guess_language/data/models/sq.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'të ': 0, ' të': 1, diff --git a/libs/guess_language/data/models/sr.py b/libs/guess_language/data/models/sr.py index e81b79574..dac99875e 100644 --- a/libs/guess_language/data/models/sr.py +++ b/libs/guess_language/data/models/sr.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' на': 0, ' је': 1, diff --git a/libs/guess_language/data/models/sv.py b/libs/guess_language/data/models/sv.py index 01957cc1b..4c6057d26 100644 --- a/libs/guess_language/data/models/sv.py +++ b/libs/guess_language/data/models/sv.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'en ': 0, ' de': 1, diff --git a/libs/guess_language/data/models/tr.py b/libs/guess_language/data/models/tr.py index 83b016665..1fe176708 100644 --- a/libs/guess_language/data/models/tr.py +++ b/libs/guess_language/data/models/tr.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'lar': 0, 'en ': 1, diff --git a/libs/guess_language/data/models/ts.py b/libs/guess_language/data/models/ts.py index 9036e99a2..646cd4fbf 100644 --- a/libs/guess_language/data/models/ts.py +++ b/libs/guess_language/data/models/ts.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' ku': 0, 'ku ': 1, diff --git a/libs/guess_language/data/models/uk.py b/libs/guess_language/data/models/uk.py index cdd4ade4d..d2df15d15 100644 --- a/libs/guess_language/data/models/uk.py +++ b/libs/guess_language/data/models/uk.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { ' на': 0, ' за': 1, diff --git a/libs/guess_language/data/models/ur.py b/libs/guess_language/data/models/ur.py index 7206e46d4..a3c70e0d5 100644 --- a/libs/guess_language/data/models/ur.py +++ b/libs/guess_language/data/models/ur.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'یں ': 0, ' کی': 1, diff --git a/libs/guess_language/data/models/uz.py b/libs/guess_language/data/models/uz.py index 9777c37e0..7ac7886cd 100644 --- a/libs/guess_language/data/models/uz.py +++ b/libs/guess_language/data/models/uz.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ан ': 0, 'ган': 1, diff --git a/libs/guess_language/data/models/vi.py b/libs/guess_language/data/models/vi.py index 485b0d00e..ce86df627 100644 --- a/libs/guess_language/data/models/vi.py +++ b/libs/guess_language/data/models/vi.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals model = { 'ng ': 0, ' th': 1, diff --git a/libs/guess_language_spirit-0.5.3.dist-info/RECORD b/libs/guess_language_spirit-0.5.3.dist-info/RECORD deleted file mode 100644 index 513ef3268..000000000 --- a/libs/guess_language_spirit-0.5.3.dist-info/RECORD +++ /dev/null @@ -1,77 +0,0 @@ -guess_language/__init__.py,sha256=P19gHTgdXWxZRp2_riFqFzguDNjByKYDCtwgu47QYSI,16971 -guess_language/__main__.py,sha256=c36xW1eOH2gn0t2gQigtWwSzmwtWVLEbGKsCScEjjB4,1364 -guess_language/console_mode.py,sha256=HnYYJ4cfXrWsJzMT4hMeWWT5UQNZ81MfsPhG0aw0Oks,2073 -guess_language/data/__init__.py,sha256=fJLLHbXI2BJjIxyyci7QZroxPISmZsHoXP0iFkj9k0g,7162 -guess_language/data/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -guess_language/data/models/af.py,sha256=RCPboHHVo4xgMyn1KEBGs7yVm-OgWpfIVikMc2AZWoA,3826 -guess_language/data/models/ar.py,sha256=MF0Y29NDpbM8o-OiQXMVltwaHv4rlOJS5Ev88tjSvmA,4550 -guess_language/data/models/az.py,sha256=KX6GCHhUJN0dRRVUGQ_eseqYfatfAGhXS97_MO47TZs,3934 -guess_language/data/models/bg.py,sha256=GnWvqGWVRoKa1OnnhyhjzDBdDugGy2jXk9IYneqmhLQ,4576 -guess_language/data/models/ca.py,sha256=MXBK3ykdTMr-dv_zm_BPJHjSFX8uG3VoYeVhieBj-Ts,3826 -guess_language/data/models/ceb.py,sha256=24M65MaQDR-2_zCraAVJDwOg7vppnaFiUXgav2y3eME,3826 -guess_language/data/models/cs.py,sha256=CPy92_4HMYs0ZA5S5TcE_9tjSXFjnoG_OjvyUIEz8bs,3904 -guess_language/data/models/cy.py,sha256=TUPhyPGvcLtppcyILL-HL5qjzvJjth1YqrobMLVReYM,3827 -guess_language/data/models/da.py,sha256=8lcTsz7cJRlRDrUtp8U_5Cg_aFPulsuUUrMXirIE_gI,3839 -guess_language/data/models/de.py,sha256=pLZ1NsyI0MOV2NySEod2oa7eLxRz9i11m7KVzN1wBcY,3834 -guess_language/data/models/en.py,sha256=gcToQORD2tSigOkOPiIATh1tnSOevl94ZR_rGvKmBwE,3826 -guess_language/data/models/eo.py,sha256=oBrZVqiFVjNMDnQP3hm4eiHkSknyjdPIwMiEUsIZWWo,3836 -guess_language/data/models/es.py,sha256=uz900seHIEsE6A_nzxozZIw8l89iHyq5V_7_0Y9ao1c,3832 -guess_language/data/models/et.py,sha256=f1gLWQ_9JhjvSt9NG2TiiYKlp3hfeEW_iPoRVhwhrd4,3845 -guess_language/data/models/eu.py,sha256=raA6oBnV3jC6fRTPcuDywTFbjJlpcrX_C-cHJpNdncI,3826 -guess_language/data/models/fa.py,sha256=KMegu5voIGFV3lmVLhudoZ39UHLiumtypHR2fww9frI,4532 -guess_language/data/models/fi.py,sha256=vVJtp557aaMhfy6xnZrZqBcCfK-czPVn3G4LvIe8PxU,3865 -guess_language/data/models/fr.py,sha256=B_yxRIkVue3f44l3yqYevZztKRU9IPaGsqsMUEVbO0g,3847 -guess_language/data/models/ha.py,sha256=fDk-9rGG9GSZIEBpPt9mtATCDSjhl0QoJNln1Rr1SZU,3834 -guess_language/data/models/haw.py,sha256=Yd4LGH7IVurbjsPUDtiSrpWeq293uD7RLCe2g1yvq_k,3826 -guess_language/data/models/hi.py,sha256=5HbZamR5vP3u0Y7b9eaRCvGNPvq0ihq8YBTq_OpkcH0,5212 -guess_language/data/models/hr.py,sha256=rAj3npZJAU_4Yw4dir_37nifQSP7mlK8MFyhTTCxcOc,3836 -guess_language/data/models/hu.py,sha256=nkVUHKIJXqKPcwaD1n4aZW1SGDvOjoXXnHY3Lw3iZgM,3900 -guess_language/data/models/id.py,sha256=QxIP942IWTlOZiS1lclWPMVSa7aBKOSFavQVq-bTFLI,3826 -guess_language/data/models/is.py,sha256=M1icEBl2MLESDZeJYn5QsURgwZaoeOtiAQmE0SW3Two,3906 -guess_language/data/models/it.py,sha256=Ak2mMc8raXtd8T-RDzyifN7wKAcR1jfCFvLcwXD7wuI,3829 -guess_language/data/models/kk.py,sha256=O9usYkb03rZM0_O82hLrxgok4dKT7aHWDoF9hXazSzw,4593 -guess_language/data/models/ky.py,sha256=PZ9g9SfWtbroFCgb95m_-4U1GOE-ADm8aY1CFskdhDs,4593 -guess_language/data/models/la.py,sha256=fUGNxNCxY0srHUy4HPlW4ntfXNhsiSgsrLhMdphkQqw,3826 -guess_language/data/models/lt.py,sha256=hxXyqGgNBfwAG_X0vckrGtin9OhWmA7cp2zlQOjFqUo,3856 -guess_language/data/models/lv.py,sha256=eDpfRYrgcklM-d68_g0WkmPCC5Smy31OxossvSIyAoY,3882 -guess_language/data/models/mk.py,sha256=iDZ6CJP6uhMIqGHPFU4-n1-L_Aab0ZA6P_v1dDwDlZM,4581 -guess_language/data/models/mn.py,sha256=FfQDxLy_awHbQd8Omq-UaUZ2U_iXnVSPy8kZh074opM,4569 -guess_language/data/models/nb.py,sha256=yHd7h-wEB0LHk5pfn2e4CVdwC4quzCEghwvP7EsXvGA,3831 -guess_language/data/models/ne.py,sha256=HGqK-j_ThNOoa9qMtIfkZRB4LF71D1rG-EiydT2JxQw,5340 -guess_language/data/models/nl.py,sha256=WH7DinFOlgOgDbV-ZPTAx57piQ1MaEYD67dEgSeKH6w,3826 -guess_language/data/models/nr.py,sha256=Ey1T-XkIOEcdDPZQcP0WQmiwK14Wv8bYQcBtA2XK2h0,3826 -guess_language/data/models/nso.py,sha256=RqVWpRuVnL1OpVsK0bRodi-1JxNABk5DUJbOE0nYQq4,3826 -guess_language/data/models/pl.py,sha256=g4PK1jWT_oEfx1qRPCwwyIq6HFYXDBS2JCGuQ9DOMo0,3871 -guess_language/data/models/ps.py,sha256=5IruloXrJ4q3LRKjuVWOxyWKk7M3N3VSx2hoGI-VdEU,4503 -guess_language/data/models/pt.py,sha256=kprtWXI2mTuLJEuqYeJxZLTEOfXjP7Q1eA7sxT-sZPY,3844 -guess_language/data/models/pt_br.py,sha256=KRt5b6Gb0ftMVdlQtOsfE5m7J-nZIUNy1fNHAvHpODk,3826 -guess_language/data/models/pt_pt.py,sha256=JEAtQZAb_f-tGeUNwaYX19d73WVaMHeWxVXFhvnTGng,3826 -guess_language/data/models/ro.py,sha256=4UwcI-SYffItHIeJ_c8SqhZolv1OzvNBQFvmBqsPA84,3857 -guess_language/data/models/ru.py,sha256=Ou2U2nTJDnz0s77IAek2P-dytiyLfTmqLPVWug9BFwk,4585 -guess_language/data/models/sk.py,sha256=nsPiJ2qC-EGEOSSE4iJHf7ut2gTQhxTIdk8bwYNACBA,3871 -guess_language/data/models/sl.py,sha256=fdR7gPSekLohYJjG32jMN0Lo2k-nmo5UV_wwhW1Z2ok,3836 -guess_language/data/models/so.py,sha256=_j4neKjN-QyxoNG0loYMOfJA7G5COXKhZQrwbhbZVWo,3826 -guess_language/data/models/sq.py,sha256=9Pw4ogB1Q91IxWRGD2AYALCPtF16WUTXNzRLdpEvjII,3894 -guess_language/data/models/sr.py,sha256=RsMshD49Boi67LHp8_BgIdAQe_nxPn_xtUIyPgORgrU,4583 -guess_language/data/models/ss.py,sha256=6fDymq7CicGHreaiUDZFBQuyqPrPsQVVydI8i6c59X0,3826 -guess_language/data/models/st.py,sha256=ogUgu_Axf_b2FB1NpMc_U47nALzwlt7xYJl91wJLYEA,3826 -guess_language/data/models/sv.py,sha256=tKuDmpF6ezGC9d2Eu1SbJ73uqT9ocg3wf1bTzJxidFk,3852 -guess_language/data/models/sw.py,sha256=M7kgCORuTl4SWbb2tHTzcXXffO6ptCfWbWb8IriozGY,3826 -guess_language/data/models/tl.py,sha256=qHTxK2kg_sRAR5yv7zSpPEUA2nA9AFb-olfGm0X7X-s,3826 -guess_language/data/models/tlh.py,sha256=kufd5oUnOfUOFz4aeLZxaiZ3dGPmhwvo1PgfZ1MShxc,3826 -guess_language/data/models/tn.py,sha256=pKCJnXcm6ZHd2mzEQYQFkfIdjnCoNraP2-fJixDUgmI,3826 -guess_language/data/models/tr.py,sha256=wfdSUYnzol-bo81GldaALmHzAwMAUbswNEYe7KTYA5Q,3913 -guess_language/data/models/ts.py,sha256=90rUALztRfD6gLNxehXkxS1CiaCiAjbIoSJBCNBdtVI,3826 -guess_language/data/models/uk.py,sha256=P-y-n1VBhCDge65tUIMnUycjXz03d68j0WdT9J31Gbk,4574 -guess_language/data/models/ur.py,sha256=ZmQQPtvjz0nUusQNiYTJXRIeXXjtM3gap-h_C4oFX3o,4508 -guess_language/data/models/uz.py,sha256=kSoBx7mD3NswKKZbvmg-nhcoSVquymDc2YrUDRGAwsg,4599 -guess_language/data/models/ve.py,sha256=2ubi0gkXIApgJrPBch1S-DqjhOZfVtCKxdCrUN_VmEQ,3826 -guess_language/data/models/vi.py,sha256=A1-bGDXVRx4r_dGqAvHEHDvuJiQpXfIYa9DYLzLfef0,4115 -guess_language/data/models/xh.py,sha256=gkQ8i_DixgzThg17YqU0vp3eP5-kcMFHWEdpgjbUSXQ,3826 -guess_language/data/models/zu.py,sha256=SguRTk-d_A7mVyznuVbP2joBLkPpwRYdaZClc3hUMHw,3826 -guess_language_spirit-0.5.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -guess_language_spirit-0.5.3.dist-info/METADATA,sha256=dRD1NMRlJ4Y6thoCyMbwet796dnl4u3zDjqIYTJYWYs,2062 -guess_language_spirit-0.5.3.dist-info/RECORD,, -guess_language_spirit-0.5.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -guess_language_spirit-0.5.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -guess_language_spirit-0.5.3.dist-info/top_level.txt,sha256=SN1QhobUTAAeXA3O17ZbzzUa9VIGm5XvcXM6rR8ask0,15 diff --git a/libs/guess_language_spirit-0.5.3.dist-info/INSTALLER b/libs/guess_language_spirit-0.5.4.dist-info/INSTALLER similarity index 100% rename from libs/guess_language_spirit-0.5.3.dist-info/INSTALLER rename to libs/guess_language_spirit-0.5.4.dist-info/INSTALLER diff --git a/libs/guess_language_spirit-0.5.3.dist-info/METADATA b/libs/guess_language_spirit-0.5.4.dist-info/METADATA similarity index 78% rename from libs/guess_language_spirit-0.5.3.dist-info/METADATA rename to libs/guess_language_spirit-0.5.4.dist-info/METADATA index d78d4d343..4ad1c41d7 100644 --- a/libs/guess_language_spirit-0.5.3.dist-info/METADATA +++ b/libs/guess_language_spirit-0.5.4.dist-info/METADATA @@ -1,9 +1,9 @@ Metadata-Version: 2.4 Name: guess_language-spirit -Version: 0.5.3 +Version: 0.5.4 Summary: Guess the natural language of a text -Home-page: https://bitbucket.org/spirit/guess_language -Download-URL: https://bitbucket.org/spirit/guess_language/downloads +Home-page: https://github.com/hiddenspirit/guess_language +Download-URL: https://github.com/hiddenspirit/guess_language/releases Author: spirit Author-email: hiddenspirit@gmail.com Classifier: Development Status :: 5 - Production/Stable @@ -11,12 +11,15 @@ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or l Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 +License-File: COPYING +License-File: COPYING.LESSER Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: download-url Dynamic: home-page +Dynamic: license-file Dynamic: summary guess_language – Guess the natural language of a text @@ -60,8 +63,8 @@ To install with pip, use:: $ pip install guess_language-spirit -On Windows, you may use one of the MSI binary packages provided on the -`download page `_. +On Windows, you may use one of the binary packages provided on the +`releases page `_. Prerequisites @@ -69,10 +72,10 @@ Prerequisites - `Python 3.2+ `_ (or 2.7) - `PyEnchant `_ (optional) -- `lib3to2 `_ +- `lib3to2 `_ (if installing for Python 2) This is a Python 3 version of -`guess-language `_ +`guess-language `_ with further improvements. diff --git a/libs/guess_language_spirit-0.5.4.dist-info/RECORD b/libs/guess_language_spirit-0.5.4.dist-info/RECORD new file mode 100644 index 000000000..9f17ba8b8 --- /dev/null +++ b/libs/guess_language_spirit-0.5.4.dist-info/RECORD @@ -0,0 +1,79 @@ +guess_language/__init__.py,sha256=NdUHvgKRQaOm3C8uHB1PWWef9tTfHHYKqo8ovfCRw-w,17075 +guess_language/__main__.py,sha256=c36xW1eOH2gn0t2gQigtWwSzmwtWVLEbGKsCScEjjB4,1364 +guess_language/console_mode.py,sha256=HnYYJ4cfXrWsJzMT4hMeWWT5UQNZ81MfsPhG0aw0Oks,2073 +guess_language/data/__init__.py,sha256=fJLLHbXI2BJjIxyyci7QZroxPISmZsHoXP0iFkj9k0g,7162 +guess_language/data/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +guess_language/data/models/af.py,sha256=RCPboHHVo4xgMyn1KEBGs7yVm-OgWpfIVikMc2AZWoA,3826 +guess_language/data/models/ar.py,sha256=Q-vXGXVtW8FfpHhASbU45RXN0kJZx8R3TGTH2UjMzRY,4590 +guess_language/data/models/az.py,sha256=v-Hu5jdcfrZJfv_DkiveerxtmzjKQmpl6t5LPlMkSII,3974 +guess_language/data/models/bg.py,sha256=dNHZWQraYJYlXmmQKlbNlgwRBBkmbkVXIoclGZr0cyA,4616 +guess_language/data/models/ca.py,sha256=MXBK3ykdTMr-dv_zm_BPJHjSFX8uG3VoYeVhieBj-Ts,3826 +guess_language/data/models/ceb.py,sha256=24M65MaQDR-2_zCraAVJDwOg7vppnaFiUXgav2y3eME,3826 +guess_language/data/models/cs.py,sha256=V_RtWfoY9YfB9LNUbzwc5Q9-9QqspMwOnOVbS7wJCrw,3944 +guess_language/data/models/cy.py,sha256=35KDLpbeR3Cwkq1q7S_HiALVM3EWZ3FMt3Uu7z9ux9Y,3867 +guess_language/data/models/da.py,sha256=rA6oBnyO9I7QClSfeCFdEQ7gEKwoDrdq0Ip9kYsWYJI,3879 +guess_language/data/models/de.py,sha256=r_cpOmyoo-bQX_g7rydnCunj7YcP5QDjsI-nidrKVSQ,3874 +guess_language/data/models/en.py,sha256=gcToQORD2tSigOkOPiIATh1tnSOevl94ZR_rGvKmBwE,3826 +guess_language/data/models/eo.py,sha256=xQVqz3SIRumy8fqUa2t-oBgHlALINt5m4UYC_YYxRe8,3876 +guess_language/data/models/es.py,sha256=oj1W1qNZgn_8nxoNQtwvfh0oT1O29672Q4pmw_lkdXk,3872 +guess_language/data/models/et.py,sha256=0-qifzmGsgya0sYOo9YIYf-1b3-gFHc1lQb_dtb8FVM,3885 +guess_language/data/models/eu.py,sha256=raA6oBnV3jC6fRTPcuDywTFbjJlpcrX_C-cHJpNdncI,3826 +guess_language/data/models/fa.py,sha256=H9hZ6MzDApebzuy1sq8qxfvbkpgabDI6XDd84w98Ol4,4572 +guess_language/data/models/fi.py,sha256=3oleAUfekfywuiExBDKnavsK7ZhIhqED00JPSdsNAQg,3905 +guess_language/data/models/fr.py,sha256=F3SjVV0YFBgPsIwHa7j1Iu-ZsLe3V_hIrPyN9KGZyZc,3887 +guess_language/data/models/ha.py,sha256=yVfSxCaYFZLRTmtvIN-KinYke3g5iqA6igZlR7JwIEk,3874 +guess_language/data/models/haw.py,sha256=Yd4LGH7IVurbjsPUDtiSrpWeq293uD7RLCe2g1yvq_k,3826 +guess_language/data/models/hi.py,sha256=SoSFwpELyBorwwgPNQ7m4q5RmZyQOYJEuEhZJJE--Ks,5252 +guess_language/data/models/hr.py,sha256=xDU-20Iabxtxg9rI6VlVFxorcbEgaB9TbzNQjtp5rEs,3876 +guess_language/data/models/hu.py,sha256=sZf1a6L8qcgMbV5d4hdiv_Vkvz_YEBRO0uQKpSGp43s,3940 +guess_language/data/models/id.py,sha256=QxIP942IWTlOZiS1lclWPMVSa7aBKOSFavQVq-bTFLI,3826 +guess_language/data/models/is.py,sha256=Ec6BiRagu4Uum8ka9DIDBPJ8xdlt2Uk8WXbaRgJceZQ,3946 +guess_language/data/models/it.py,sha256=ezLv1LgoeOETwWg5kt8gR9OAx3XdvJaJCwSLmUB1oJs,3869 +guess_language/data/models/kk.py,sha256=_rKpPqiAfo54wQHnsy5ykIoK82-1mei1tFz1PF-mVLA,4633 +guess_language/data/models/ky.py,sha256=le4uyAhEQBW_vDbPb4MRL-6yzotyDDEVKeBzgSbytys,4633 +guess_language/data/models/la.py,sha256=fUGNxNCxY0srHUy4HPlW4ntfXNhsiSgsrLhMdphkQqw,3826 +guess_language/data/models/lt.py,sha256=5OM7pqpKfBhR8nnFqQJHpLnglyYjXbo3NZVRh36z4tU,3896 +guess_language/data/models/lv.py,sha256=E_qh_ucghrUkIzsQaGdyy3mvsVtBmY6bSSn-_BEKrQw,3922 +guess_language/data/models/mk.py,sha256=nmz_4g-EDF0GSkf9jtWEUC70j0nG0oVK6Tnv0ls9SeA,4621 +guess_language/data/models/mn.py,sha256=AbmmHK3zCEiCxZCMlHoQmHJg2KVyAPmIOqkzwPuUpRk,4609 +guess_language/data/models/nb.py,sha256=3iORy5Rjdd-GbX6-XA7qCkzlv3diQxKB1awIUuqwUdY,3871 +guess_language/data/models/ne.py,sha256=HFyZfGk7K2_gjm4z-6hVhNxJp0PAScTy0knzq5tDwwc,5380 +guess_language/data/models/nl.py,sha256=WH7DinFOlgOgDbV-ZPTAx57piQ1MaEYD67dEgSeKH6w,3826 +guess_language/data/models/nr.py,sha256=Ey1T-XkIOEcdDPZQcP0WQmiwK14Wv8bYQcBtA2XK2h0,3826 +guess_language/data/models/nso.py,sha256=RqVWpRuVnL1OpVsK0bRodi-1JxNABk5DUJbOE0nYQq4,3826 +guess_language/data/models/pl.py,sha256=IwofKBbKun4ZeSLJGmL5AWxSMw1PKPWrE9prnOzmGFk,3911 +guess_language/data/models/ps.py,sha256=b7cOGct70XjopTx8-6ifj931nymwA5fBMXmKGsiQnmI,4543 +guess_language/data/models/pt.py,sha256=A8WPfA5Mhnxx0hHF4XJCjwjVSKtEne5wj7JOYHJr104,3884 +guess_language/data/models/pt_br.py,sha256=KRt5b6Gb0ftMVdlQtOsfE5m7J-nZIUNy1fNHAvHpODk,3826 +guess_language/data/models/pt_pt.py,sha256=JEAtQZAb_f-tGeUNwaYX19d73WVaMHeWxVXFhvnTGng,3826 +guess_language/data/models/ro.py,sha256=wXngpLKAb3Lm9qjWzdjite6Jk1OnxT0mAHBRyj4MaoU,3897 +guess_language/data/models/ru.py,sha256=WNuDb2NMlQxc3WGZvSKga54k3Jabgk_qf0isfUWSF3c,4625 +guess_language/data/models/sk.py,sha256=KZpto0nl67OUg3Gr_dKK-vxvu4iHFXKGVhv-qCoA5yM,3911 +guess_language/data/models/sl.py,sha256=eMfMkuIOmm3siogB0HRb5OjfjGjbR0GEPsdbPKoMD_o,3876 +guess_language/data/models/so.py,sha256=_j4neKjN-QyxoNG0loYMOfJA7G5COXKhZQrwbhbZVWo,3826 +guess_language/data/models/sq.py,sha256=djyhuRaqM4HyC9t3pzS1qgywT0G7Is7BG5r0EGbYzGo,3934 +guess_language/data/models/sr.py,sha256=_3N9kUlhOMNEYFV_cVtaF05HG3R4Cg7X6Jx5gOdH-YI,4623 +guess_language/data/models/ss.py,sha256=6fDymq7CicGHreaiUDZFBQuyqPrPsQVVydI8i6c59X0,3826 +guess_language/data/models/st.py,sha256=ogUgu_Axf_b2FB1NpMc_U47nALzwlt7xYJl91wJLYEA,3826 +guess_language/data/models/sv.py,sha256=8IZWAmCb_qLrIyJ6t50rphel51o_Qs7pzqnWUuQ3Mc0,3892 +guess_language/data/models/sw.py,sha256=M7kgCORuTl4SWbb2tHTzcXXffO6ptCfWbWb8IriozGY,3826 +guess_language/data/models/tl.py,sha256=qHTxK2kg_sRAR5yv7zSpPEUA2nA9AFb-olfGm0X7X-s,3826 +guess_language/data/models/tlh.py,sha256=kufd5oUnOfUOFz4aeLZxaiZ3dGPmhwvo1PgfZ1MShxc,3826 +guess_language/data/models/tn.py,sha256=pKCJnXcm6ZHd2mzEQYQFkfIdjnCoNraP2-fJixDUgmI,3826 +guess_language/data/models/tr.py,sha256=RRmLa2z2dN6BORIiG5gag6Od5B-VOI4aEBnRNhulMKw,3953 +guess_language/data/models/ts.py,sha256=0GBoAcz2T9P6XqjEtlu991rBQvAty5fvjLwEhOF8Dmw,3866 +guess_language/data/models/uk.py,sha256=gYRxLe4SKSUdphfD-sgbkNACnnbhYKVYEnxJO0_XUE4,4614 +guess_language/data/models/ur.py,sha256=vi4G8dnWxbLoVw2HPSecRKUymY_oOtQb0V_Q5PjP9pc,4548 +guess_language/data/models/uz.py,sha256=t2AIP5UG0jPsHVh9GNy_SFD2G5L3sejcBZ16WPi9X8A,4639 +guess_language/data/models/ve.py,sha256=2ubi0gkXIApgJrPBch1S-DqjhOZfVtCKxdCrUN_VmEQ,3826 +guess_language/data/models/vi.py,sha256=N3NDv04KXMP5Vm4cLZv1FkLb3WOnQtyNZj5RyjpcXcQ,4155 +guess_language/data/models/xh.py,sha256=gkQ8i_DixgzThg17YqU0vp3eP5-kcMFHWEdpgjbUSXQ,3826 +guess_language/data/models/zu.py,sha256=SguRTk-d_A7mVyznuVbP2joBLkPpwRYdaZClc3hUMHw,3826 +guess_language_spirit-0.5.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +guess_language_spirit-0.5.4.dist-info/METADATA,sha256=aEoaFZ3RiXzLjtPqQ28T9iVw5RDKgDv584eKAhE6qb8,2131 +guess_language_spirit-0.5.4.dist-info/RECORD,, +guess_language_spirit-0.5.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +guess_language_spirit-0.5.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +guess_language_spirit-0.5.4.dist-info/licenses/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147 +guess_language_spirit-0.5.4.dist-info/licenses/COPYING.LESSER,sha256=2n6rt7r999OuXp8iOqW9we7ORaxWncIbOwN1ILRGR2g,7651 +guess_language_spirit-0.5.4.dist-info/top_level.txt,sha256=SN1QhobUTAAeXA3O17ZbzzUa9VIGm5XvcXM6rR8ask0,15 diff --git a/libs/guess_language_spirit-0.5.3.dist-info/REQUESTED b/libs/guess_language_spirit-0.5.4.dist-info/REQUESTED similarity index 100% rename from libs/guess_language_spirit-0.5.3.dist-info/REQUESTED rename to libs/guess_language_spirit-0.5.4.dist-info/REQUESTED diff --git a/libs/flask_cors-6.0.2.dist-info/WHEEL b/libs/guess_language_spirit-0.5.4.dist-info/WHEEL similarity index 100% rename from libs/flask_cors-6.0.2.dist-info/WHEEL rename to libs/guess_language_spirit-0.5.4.dist-info/WHEEL diff --git a/libs/guess_language_spirit-0.5.4.dist-info/licenses/COPYING b/libs/guess_language_spirit-0.5.4.dist-info/licenses/COPYING new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/libs/guess_language_spirit-0.5.4.dist-info/licenses/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/libs/guess_language_spirit-0.5.4.dist-info/licenses/COPYING.LESSER b/libs/guess_language_spirit-0.5.4.dist-info/licenses/COPYING.LESSER new file mode 100644 index 000000000..65c5ca88a --- /dev/null +++ b/libs/guess_language_spirit-0.5.4.dist-info/licenses/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/libs/guess_language_spirit-0.5.3.dist-info/top_level.txt b/libs/guess_language_spirit-0.5.4.dist-info/top_level.txt similarity index 100% rename from libs/guess_language_spirit-0.5.3.dist-info/top_level.txt rename to libs/guess_language_spirit-0.5.4.dist-info/top_level.txt diff --git a/libs/idna-3.11.dist-info/METADATA b/libs/idna-3.11.dist-info/METADATA deleted file mode 100644 index 7a4a4b7a7..000000000 --- a/libs/idna-3.11.dist-info/METADATA +++ /dev/null @@ -1,209 +0,0 @@ -Metadata-Version: 2.4 -Name: idna -Version: 3.11 -Summary: Internationalized Domain Names in Applications (IDNA) -Author-email: Kim Davies -Requires-Python: >=3.8 -Description-Content-Type: text/x-rst -License-Expression: BSD-3-Clause -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: System Administrators -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Programming Language :: Python :: Implementation :: PyPy -Classifier: Topic :: Internet :: Name Service (DNS) -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: Utilities -License-File: LICENSE.md -Requires-Dist: ruff >= 0.6.2 ; extra == "all" -Requires-Dist: mypy >= 1.11.2 ; extra == "all" -Requires-Dist: pytest >= 8.3.2 ; extra == "all" -Requires-Dist: flake8 >= 7.1.1 ; extra == "all" -Project-URL: Changelog, https://github.com/kjd/idna/blob/master/HISTORY.rst -Project-URL: Issue tracker, https://github.com/kjd/idna/issues -Project-URL: Source, https://github.com/kjd/idna -Provides-Extra: all - -Internationalized Domain Names in Applications (IDNA) -===================================================== - -Support for `Internationalized Domain Names in -Applications (IDNA) `_ -and `Unicode IDNA Compatibility Processing -`_. - -The latest versions of these standards supplied here provide -more comprehensive language coverage and reduce the potential of -allowing domains with known security vulnerabilities. This library -is a suitable replacement for the “encodings.idna” -module that comes with the Python standard library, but which -only supports an older superseded IDNA specification from 2003. - -Basic functions are simply executed: - -.. code-block:: pycon - - >>> import idna - >>> idna.encode('ドメイン.テスト') - b'xn--eckwd4c7c.xn--zckzah' - >>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) - ドメイン.テスト - - -Installation ------------- - -This package is available for installation from PyPI via the -typical mechanisms, such as: - -.. code-block:: bash - - $ python3 -m pip install idna - - -Usage ------ - -For typical usage, the ``encode`` and ``decode`` functions will take a -domain name argument and perform a conversion to ASCII compatible encoding -(known as A-labels), or to Unicode strings (known as U-labels) -respectively. - -.. code-block:: pycon - - >>> import idna - >>> idna.encode('ドメイン.テスト') - b'xn--eckwd4c7c.xn--zckzah' - >>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) - ドメイン.テスト - -Conversions can be applied at a per-label basis using the ``ulabel`` or -``alabel`` functions if necessary: - -.. code-block:: pycon - - >>> idna.alabel('测试') - b'xn--0zwm56d' - - -Compatibility Mapping (UTS #46) -+++++++++++++++++++++++++++++++ - -This library provides support for `Unicode IDNA Compatibility -Processing `_ which normalizes input from -different potential ways a user may input a domain prior to performing the IDNA -conversion operations. This functionality, known as a -`mapping `_, is considered by the -specification to be a local user-interface issue distinct from IDNA -conversion functionality. - -For example, “Königsgäßchen” is not a permissible label as *LATIN -CAPITAL LETTER K* is not allowed (nor are capital letters in general). -UTS 46 will convert this into lower case prior to applying the IDNA -conversion. - -.. code-block:: pycon - - >>> import idna - >>> idna.encode('Königsgäßchen') - ... - idna.core.InvalidCodepoint: Codepoint U+004B at position 1 of 'Königsgäßchen' not allowed - >>> idna.encode('Königsgäßchen', uts46=True) - b'xn--knigsgchen-b4a3dun' - >>> print(idna.decode('xn--knigsgchen-b4a3dun')) - königsgäßchen - - -Exceptions ----------- - -All errors raised during the conversion following the specification -should raise an exception derived from the ``idna.IDNAError`` base -class. - -More specific exceptions that may be generated as ``idna.IDNABidiError`` -when the error reflects an illegal combination of left-to-right and -right-to-left characters in a label; ``idna.InvalidCodepoint`` when -a specific codepoint is an illegal character in an IDN label (i.e. -INVALID); and ``idna.InvalidCodepointContext`` when the codepoint is -illegal based on its position in the string (i.e. it is CONTEXTO or CONTEXTJ -but the contextual requirements are not satisfied.) - -Building and Diagnostics ------------------------- - -The IDNA and UTS 46 functionality relies upon pre-calculated lookup -tables for performance. These tables are derived from computing against -eligibility criteria in the respective standards using the command-line -script ``tools/idna-data``. - -This tool will fetch relevant codepoint data from the Unicode repository -and perform the required calculations to identify eligibility. There are -three main modes: - -* ``idna-data make-libdata``. Generates ``idnadata.py`` and - ``uts46data.py``, the pre-calculated lookup tables used for IDNA and - UTS 46 conversions. Implementers who wish to track this library against - a different Unicode version may use this tool to manually generate a - different version of the ``idnadata.py`` and ``uts46data.py`` files. - -* ``idna-data make-table``. Generate a table of the IDNA disposition - (e.g. PVALID, CONTEXTJ, CONTEXTO) in the format found in Appendix - B.1 of RFC 5892 and the pre-computed tables published by `IANA - `_. - -* ``idna-data U+0061``. Prints debugging output on the various - properties associated with an individual Unicode codepoint (in this - case, U+0061), that are used to assess the IDNA and UTS 46 status of a - codepoint. This is helpful in debugging or analysis. - -The tool accepts a number of arguments, described using ``idna-data --h``. Most notably, the ``--version`` argument allows the specification -of the version of Unicode to be used in computing the table data. For -example, ``idna-data --version 9.0.0 make-libdata`` will generate -library data against Unicode 9.0.0. - - -Additional Notes ----------------- - -* **Packages**. The latest tagged release version is published in the - `Python Package Index `_. - -* **Version support**. This library supports Python 3.8 and higher. - As this library serves as a low-level toolkit for a variety of - applications, many of which strive for broad compatibility with older - Python versions, there is no rush to remove older interpreter support. - Support for older versions are likely to be removed from new releases - as automated tests can no longer easily be run, i.e. once the Python - version is officially end-of-life. - -* **Testing**. The library has a test suite based on each rule of the - IDNA specification, as well as tests that are provided as part of the - Unicode Technical Standard 46, `Unicode IDNA Compatibility Processing - `_. - -* **Emoji**. It is an occasional request to support emoji domains in - this library. Encoding of symbols like emoji is expressly prohibited by - the technical standard IDNA 2008 and emoji domains are broadly phased - out across the domain industry due to associated security risks. For - now, applications that need to support these non-compliant labels - may wish to consider trying the encode/decode operation in this library - first, and then falling back to using `encodings.idna`. See `the Github - project `_ for more discussion. - -* **Transitional processing**. Unicode 16.0.0 removed transitional - processing so the `transitional` argument for the encode() method - no longer has any effect and will be removed at a later date. - diff --git a/libs/idna-3.11.dist-info/RECORD b/libs/idna-3.11.dist-info/RECORD deleted file mode 100644 index e9b095ac7..000000000 --- a/libs/idna-3.11.dist-info/RECORD +++ /dev/null @@ -1,15 +0,0 @@ -idna-3.11.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -idna-3.11.dist-info/METADATA,sha256=fCwSww9SuiN8TIHllFSASUQCW55hAs8dzKnr9RaEEbA,8378 -idna-3.11.dist-info/RECORD,, -idna-3.11.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -idna-3.11.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 -idna-3.11.dist-info/licenses/LICENSE.md,sha256=t6M2q_OwThgOwGXN0W5wXQeeHMehT5EKpukYfza5zYc,1541 -idna/__init__.py,sha256=MPqNDLZbXqGaNdXxAFhiqFPKEQXju2jNQhCey6-5eJM,868 -idna/codec.py,sha256=M2SGWN7cs_6B32QmKTyTN6xQGZeYQgQ2wiX3_DR6loE,3438 -idna/compat.py,sha256=RzLy6QQCdl9784aFhb2EX9EKGCJjg0P3PilGdeXXcx8,316 -idna/core.py,sha256=P26_XVycuMTZ1R2mNK1ZREVzM5mvTzdabBXfyZVU1Lc,13246 -idna/idnadata.py,sha256=SG8jhaGE53iiD6B49pt2pwTv_UvClciWE-N54oR2p4U,79623 -idna/intranges.py,sha256=amUtkdhYcQG8Zr-CoMM_kVRacxkivC1WgxN1b63KKdU,1898 -idna/package_data.py,sha256=_CUavOxobnbyNG2FLyHoN8QHP3QM9W1tKuw7eq9QwBk,21 -idna/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -idna/uts46data.py,sha256=H9J35VkD0F9L9mKOqjeNGd2A-Va6FlPoz6Jz4K7h-ps,243725 diff --git a/libs/idna-3.11.dist-info/INSTALLER b/libs/idna-3.18.dist-info/INSTALLER similarity index 100% rename from libs/idna-3.11.dist-info/INSTALLER rename to libs/idna-3.18.dist-info/INSTALLER diff --git a/libs/idna-3.18.dist-info/METADATA b/libs/idna-3.18.dist-info/METADATA new file mode 100644 index 000000000..6c4bf89b2 --- /dev/null +++ b/libs/idna-3.18.dist-info/METADATA @@ -0,0 +1,155 @@ +Metadata-Version: 2.4 +Name: idna +Version: 3.18 +Summary: Internationalized Domain Names in Applications (IDNA) +Author-email: Kim Davies +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: Name Service (DNS) +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Utilities +License-File: LICENSE.md +Requires-Dist: ruff >= 0.6.2 ; extra == "all" +Requires-Dist: mypy >= 1.11.2 ; extra == "all" +Requires-Dist: pytest >= 8.3.2 ; extra == "all" +Project-URL: Changelog, https://github.com/kjd/idna/blob/master/HISTORY.md +Project-URL: Issue tracker, https://github.com/kjd/idna/issues +Project-URL: Source, https://github.com/kjd/idna +Provides-Extra: all + +# Internationalized Domain Names in Applications (IDNA) + +Support for [Internationalized Domain Names in Applications +(IDNA)](https://tools.ietf.org/html/rfc5891) and [Unicode IDNA +Compatibility Processing](https://unicode.org/reports/tr46/). It +supersedes the standard library's `encodings.idna`, which only +implements the 2003 specification, offering broader script coverage and +limiting domains with known security vulnerabilities. + +## Usage + +Package may be installed from [PyPI](https://pypi.org/project/idna/) via +the typical methods (e.g. `python3 -m pip install idna`) + +For typical usage, the `encode` and `decode` functions will take a +domain name argument and perform a conversion to ASCII-compatible encoding +(known as A-labels), or to Unicode strings (known as U-labels) +respectively. + +```pycon +>>> import idna +>>> idna.encode('ドメイン.テスト') +b'xn--eckwd4c7c.xn--zckzah' +>>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) +ドメイン.テスト +``` + +Conversions can be applied at a per-label basis using the `ulabel` or +`alabel` functions for specialized use cases. + + +### Compatibility Mapping (UTS #46) + +This library provides support for [Unicode IDNA Compatibility +Processing](https://unicode.org/reports/tr46/) which normalizes input from +different potential ways a user may input a domain prior to performing the IDNA +conversion operations. This functionality, known as a +[mapping](https://tools.ietf.org/html/rfc5895), is considered by the +specification to be a local user-interface issue distinct from IDNA +conversion functionality. + +For example, "Königsgäßchen" is not a permissible label as capital letters +are not allowed. UTS 46 will convert this into lower case prior to applying +the IDNA conversion. + +```pycon +>>> import idna +>>> idna.encode('Königsgäßchen') +... +idna.core.InvalidCodepoint: Codepoint U+004B at position 1 of 'Königsgäßchen' not allowed +>>> idna.encode('Königsgäßchen', uts46=True) +b'xn--knigsgchen-b4a3dun' +>>> idna.decode('xn--knigsgchen-b4a3dun') +'königsgäßchen' +``` + +When performing a decode operation for display purposes, `decode()` +accepts a `display=True` argument that leaves any `xn--` label that +fails to decode unchanged. This is useful for user interface display +where a domain is in use, the A-label form can be presented when it +is not a valid IDN. + + +## Exceptions + +All errors raised during conversion derive from the `idna.IDNAError` +base class. The more specific exceptions are: + +* `idna.IDNABidiError` — raised when a label contains an illegal + combination of left-to-right and right-to-left characters. +* `idna.InvalidCodepoint` — raised when a label contains a codepoint + that is INVALID for IDNA. +* `idna.InvalidCodepointContext` — raised when a CONTEXTO or CONTEXTJ + codepoint appears in a position whose contextual requirements are + not satisfied. + + +## Command-line tool + +The package supports command-line usage to convert domain names +between their Unicode and ASCII-compatible forms. It can be run either +as a module (`python3 -m idna`) or, once installed (such as with `uv +tool` or `pipx`), via the `idna` script: + +```bash +$ uv tool install idna +$ idna xn--e1afmkfd.xn--p1ai +пример.рф +$ idna пример.рф +xn--e1afmkfd.xn--p1ai +``` + +Mode can be specified with `-e`/`--encode` or `-d`/`--decode`, otherwise +it will be chosen automatically based on the first input. Multiple +domains can be supplied either as arguments or through standard input. +UTS #46 mapping is applied by default, which lets the tool accept +inputs that aren't strictly valid IDNA 2008 by normalising them first, +pass `--strict` to disable UTS #46. + +Conversion failures are reported on stderr together with the +offending input; processing continues with the remaining domains and +the tool exits with a non-zero status if any conversion failed. + + +## Additional Notes + +* **Version support**. This library supports Python 3.9 and higher. + As this library serves as a low-level toolkit for a variety of + applications, we strive to support all versions of Python that are + not beyond end-of-life. + +* **Emoji**. It is an occasional request to support emoji domains in + this library. Encoding of symbols like emoji is expressly prohibited by + the IDNA technical standard, and emoji domains are broadly phased + out across the domain industry due to associated security risks. + +* **Regenerating lookup tables**. The IDNA and UTS 46 functionality + relies upon pre-calculated lookup tables, generated using the + `idna-data` script in [`tools/`](tools/README.md). + diff --git a/libs/idna-3.18.dist-info/RECORD b/libs/idna-3.18.dist-info/RECORD new file mode 100644 index 000000000..a395a2b53 --- /dev/null +++ b/libs/idna-3.18.dist-info/RECORD @@ -0,0 +1,19 @@ +../../bin/idna,sha256=zJ1SeflQaVPzU0KRUlolLgGxSXJ2aAyLGN09-zld6L0,183 +idna-3.18.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +idna-3.18.dist-info/METADATA,sha256=Rt_m5axGLQ9oDs2avPZugptqIzSCS02eOXmzETXK8oE,6119 +idna-3.18.dist-info/RECORD,, +idna-3.18.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +idna-3.18.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +idna-3.18.dist-info/entry_points.txt,sha256=7H3nGOHap3jnLE5e7q7Ywr9Vq8axB7WIj5-C_4N2vhw,38 +idna-3.18.dist-info/licenses/LICENSE.md,sha256=GppPDj1HmickDd1ZqRN6ZqtKD539yMphiMwL_YUYfwQ,1541 +idna/__init__.py,sha256=MPqNDLZbXqGaNdXxAFhiqFPKEQXju2jNQhCey6-5eJM,868 +idna/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83 +idna/cli.py,sha256=swqJLMNc8Uzs60KziNpbWnHuqlG3WRQwJSbo4n8xDAo,4139 +idna/codec.py,sha256=JRbo-f7pEkLdWeiH89Z72UR4VBYhvKDFrQBeNX6sRDE,5040 +idna/compat.py,sha256=AepA39ceRHxkfHP41-FvKW5Ki-f4PfUZ90RUMlCNdmo,1353 +idna/core.py,sha256=SfOr1xO3PoE0RDYx7bMciAnjiyjJPbPw_93AB5IUYOw,24685 +idna/idnadata.py,sha256=Af-mo8WBmkhAK6TyXKOQH88OX0mQNDKtdL7UWtQpppk,44862 +idna/intranges.py,sha256=g49scLSkqJtAhLmOODa7hVHriSjmb60tiTsEoocJdBI,1851 +idna/package_data.py,sha256=TeI94EqAFAFaXfBJwsOPUMLn2969uirPa-DaeaceAyU,21 +idna/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +idna/uts46data.py,sha256=jujNz5QqWMcJf-XYLv4X1jBvb5FlI0t6-e1mILsgbPk,234325 diff --git a/libs/idna-3.11.dist-info/REQUESTED b/libs/idna-3.18.dist-info/REQUESTED similarity index 100% rename from libs/idna-3.11.dist-info/REQUESTED rename to libs/idna-3.18.dist-info/REQUESTED diff --git a/libs/idna-3.11.dist-info/WHEEL b/libs/idna-3.18.dist-info/WHEEL similarity index 100% rename from libs/idna-3.11.dist-info/WHEEL rename to libs/idna-3.18.dist-info/WHEEL diff --git a/libs/idna-3.18.dist-info/entry_points.txt b/libs/idna-3.18.dist-info/entry_points.txt new file mode 100644 index 000000000..59ca7ac02 --- /dev/null +++ b/libs/idna-3.18.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +idna=idna.cli:main + diff --git a/libs/idna-3.11.dist-info/licenses/LICENSE.md b/libs/idna-3.18.dist-info/licenses/LICENSE.md similarity index 96% rename from libs/idna-3.11.dist-info/licenses/LICENSE.md rename to libs/idna-3.18.dist-info/licenses/LICENSE.md index 256ba90cd..f706835ab 100644 --- a/libs/idna-3.11.dist-info/licenses/LICENSE.md +++ b/libs/idna-3.18.dist-info/licenses/LICENSE.md @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2013-2025, Kim Davies and contributors. +Copyright (c) 2013-2026, Kim Davies and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/libs/idna/__main__.py b/libs/idna/__main__.py new file mode 100644 index 000000000..dbdd06617 --- /dev/null +++ b/libs/idna/__main__.py @@ -0,0 +1,6 @@ +import sys + +from .cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/idna/cli.py b/libs/idna/cli.py new file mode 100644 index 000000000..4acda2c0f --- /dev/null +++ b/libs/idna/cli.py @@ -0,0 +1,128 @@ +"""Command-line interface for the :mod:`idna` package. + +Invoked via ``python -m idna``. See :func:`main` for the entry point. +""" + +import argparse +import sys +from collections.abc import Iterable +from itertools import chain +from typing import IO, Optional + +from . import IDNAError, decode, encode +from .core import _alabel_prefix, _unicode_dots_re +from .package_data import __version__ + + +def _looks_like_alabel(s: str) -> bool: + """Return True if any label in ``s`` carries the ``xn--`` ACE prefix.""" + prefix = _alabel_prefix.decode("ascii") + return any(label.lower().startswith(prefix) for label in _unicode_dots_re.split(s)) + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="python -m idna", + description=( + "Convert a domain name between its Unicode (U-label) and " + "ASCII-compatible (A-label) forms. With no mode flag, the " + "direction is chosen from the first input — if it contains " + "an xn-- label the stream is decoded, otherwise it is " + "encoded — and the same mode is applied to every remaining " + "input. UTS #46 mapping is applied by default; pass " + "--strict to disable it. When no domains are given on the " + "command line and stdin is piped, one domain per line is " + "read from stdin." + ), + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "-e", + "--encode", + dest="mode", + action="store_const", + const="encode", + help="Encode the input to its ASCII A-label form.", + ) + mode.add_argument( + "-d", + "--decode", + dest="mode", + action="store_const", + const="decode", + help="Decode the input from its ASCII A-label form.", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Disable the default UTS #46 mapping and apply IDNA 2008 rules verbatim.", + ) + parser.add_argument( + "--version", + action="version", + version=f"idna {__version__}", + ) + parser.add_argument( + "domain", + nargs="*", + help="One or more domain names to convert. Omit to read from stdin.", + ) + return parser + + +def _iter_stdin(stream: IO[str]) -> Iterable[str]: + """Yield non-empty stripped lines from ``stream``, ignoring blanks.""" + for line in stream: + stripped = line.strip() + if stripped: + yield stripped + + +def _convert_one(domain: str, mode: str, uts46: bool) -> bool: + """Convert ``domain`` and write the result; return ``False`` on failure.""" + try: + if mode == "decode": + print(decode(domain, uts46=uts46)) + else: + print(encode(domain, uts46=uts46).decode("ascii")) + except IDNAError as err: + print(f"idna: {mode} failed for {domain!r}: {err}", file=sys.stderr) + return False + return True + + +def main(argv: Optional[list[str]] = None) -> int: + """Entry point for ``python -m idna``. + + When more than one domain is supplied (via positional arguments or + piped stdin) and no mode flag is given, the first input determines + the direction and that mode is applied uniformly to the rest. + + :param argv: Argument list excluding the program name. Defaults to + :data:`sys.argv` when ``None``. + :returns: ``0`` on success, ``1`` if any conversion fails. + """ + parser = _build_parser() + args = parser.parse_args(argv) + uts46 = not args.strict + + if args.domain: + domains: Iterable[str] = args.domain + elif not sys.stdin.isatty(): + domains = _iter_stdin(sys.stdin) + else: + parser.error("a domain argument is required when stdin is a terminal") + + iterator = iter(domains) + first = next(iterator, None) + if first is None: + return 0 + + mode = args.mode or ("decode" if _looks_like_alabel(first) else "encode") + + results = [_convert_one(domain, mode, uts46) for domain in chain([first], iterator)] + return 0 if all(results) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/idna/codec.py b/libs/idna/codec.py index cbc2e4ff4..83b42fe42 100644 --- a/libs/idna/codec.py +++ b/libs/idna/codec.py @@ -1,25 +1,32 @@ import codecs -import re -from typing import Any, Optional, Tuple +from typing import Any, Optional -from .core import IDNAError, alabel, decode, encode, ulabel - -_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") +from .core import IDNAError, _unicode_dots_re, alabel, decode, encode, ulabel class Codec(codecs.Codec): - def encode(self, data: str, errors: str = "strict") -> Tuple[bytes, int]: + """Stateless IDNA 2008 codec. + + Implements the :class:`codecs.Codec` protocol so that the whole-domain + encoder (:func:`idna.encode`) and decoder (:func:`idna.decode`) are + accessible through the standard codec machinery as ``"idna2008"``. + + Only the ``"strict"`` error handler is supported; any other handler + raises :exc:`~idna.IDNAError`. + """ + + def encode(self, data: str, errors: str = "strict") -> tuple[bytes, int]: # ty: ignore[invalid-method-override] if errors != "strict": - raise IDNAError('Unsupported error handling "{}"'.format(errors)) + raise IDNAError(f'Unsupported error handling "{errors}"') if not data: return b"", 0 return encode(data), len(data) - def decode(self, data: bytes, errors: str = "strict") -> Tuple[str, int]: + def decode(self, data: bytes, errors: str = "strict") -> tuple[str, int]: # ty: ignore[invalid-method-override] if errors != "strict": - raise IDNAError('Unsupported error handling "{}"'.format(errors)) + raise IDNAError(f'Unsupported error handling "{errors}"') if not data: return "", 0 @@ -28,9 +35,20 @@ class Codec(codecs.Codec): class IncrementalEncoder(codecs.BufferedIncrementalEncoder): - def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]: + """Incremental IDNA 2008 encoder. + + Buffers a partial trailing label across calls until either the next + label separator is seen or ``final=True``, so that streamed input is + encoded one whole label at a time. Any of the four Unicode label + separators (``U+002E``, ``U+3002``, ``U+FF0E``, ``U+FF61``) ends a + label; the result always uses ``U+002E`` as the separator. + + Only the ``"strict"`` error handler is supported. + """ + + def _buffer_encode(self, data: str, errors: str, final: bool) -> tuple[bytes, int]: # ty: ignore[invalid-method-override] if errors != "strict": - raise IDNAError('Unsupported error handling "{}"'.format(errors)) + raise IDNAError(f'Unsupported error handling "{errors}"') if not data: return b"", 0 @@ -62,9 +80,18 @@ class IncrementalEncoder(codecs.BufferedIncrementalEncoder): class IncrementalDecoder(codecs.BufferedIncrementalDecoder): - def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]: + """Incremental IDNA 2008 decoder. + + Buffers a partial trailing label across calls until either the next + label separator is seen or ``final=True``, so that streamed input is + decoded one whole label at a time. + + Only the ``"strict"`` error handler is supported. + """ + + def _buffer_decode(self, data: Any, errors: str, final: bool) -> tuple[str, int]: # ty: ignore[invalid-method-override] if errors != "strict": - raise IDNAError('Unsupported error handling "{}"'.format(errors)) + raise IDNAError(f'Unsupported error handling "{errors}"') if not data: return ("", 0) @@ -106,6 +133,16 @@ class StreamReader(Codec, codecs.StreamReader): def search_function(name: str) -> Optional[codecs.CodecInfo]: + """Codec search function registered with :mod:`codecs`. + + Returns a :class:`codecs.CodecInfo` for the ``"idna2008"`` codec name + so that ``str.encode("idna2008")`` and ``bytes.decode("idna2008")`` + invoke the IDNA 2008 codec defined in this module. + + :param name: The codec name being looked up. + :returns: A :class:`codecs.CodecInfo` instance if ``name`` is + ``"idna2008"``, otherwise ``None``. + """ if name != "idna2008": return None return codecs.CodecInfo( diff --git a/libs/idna/compat.py b/libs/idna/compat.py index 1df9f2a70..1d01e3d97 100644 --- a/libs/idna/compat.py +++ b/libs/idna/compat.py @@ -4,12 +4,38 @@ from .core import decode, encode def ToASCII(label: str) -> bytes: + """Compatibility shim for :rfc:`3490` ``ToASCII``. + + Delegates to :func:`idna.encode` (IDNA 2008). Provided to ease porting + of code written against the legacy :mod:`encodings.idna` API; new code + should call :func:`idna.encode` directly. + + :param label: The label or domain to encode. + :returns: The encoded form as ASCII :class:`bytes`. + """ return encode(label) def ToUnicode(label: Union[bytes, bytearray]) -> str: + """Compatibility shim for :rfc:`3490` ``ToUnicode``. + + Delegates to :func:`idna.decode` (IDNA 2008). Provided to ease porting + of code written against the legacy :mod:`encodings.idna` API; new code + should call :func:`idna.decode` directly. + + :param label: The label or domain to decode. + :returns: The decoded Unicode form. + """ return decode(label) def nameprep(s: Any) -> None: + """Stub for :rfc:`3491` Nameprep, which is not used by IDNA 2008. + + IDNA 2008 (:rfc:`5891`) replaces Nameprep with the per-codepoint + validity classes from :rfc:`5892`; this function exists only to + return a clear error if legacy code attempts to call it. + + :raises NotImplementedError: Always. + """ raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol") diff --git a/libs/idna/core.py b/libs/idna/core.py index 8177bf7a3..1ccbd1f33 100644 --- a/libs/idna/core.py +++ b/libs/idna/core.py @@ -1,6 +1,7 @@ import bisect import re import unicodedata +import warnings from typing import Optional, Union from . import idnadata @@ -8,38 +9,49 @@ from .intranges import intranges_contain _virama_combining_class = 9 _alabel_prefix = b"xn--" +_max_input_length = 1024 _unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") +# Bidi category sets from RFC 5893, hoisted out of the per-codepoint loop +_bidi_rtl_first = frozenset({"R", "AL"}) +_bidi_rtl_categories = frozenset({"R", "AL", "AN"}) +_bidi_rtl_allowed = frozenset({"R", "AL", "AN", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}) +_bidi_rtl_valid_ending = frozenset({"R", "AL", "EN", "AN"}) +_bidi_rtl_numeric = frozenset({"AN", "EN"}) +_bidi_ltr_allowed = frozenset({"L", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}) +_bidi_ltr_valid_ending = frozenset({"L", "EN"}) +_bidi_joiner_l_or_d = frozenset({"L", "D"}) +_bidi_joiner_r_or_d = frozenset({"R", "D"}) + + +def _joining_type(cp: int) -> Optional[str]: + for jt, ranges in idnadata.joining_types.items(): + if intranges_contain(cp, ranges): + return jt + return None + + class IDNAError(UnicodeError): """Base exception for all IDNA-encoding related problems""" - pass - class IDNABidiError(IDNAError): """Exception when bidirectional requirements are not satisfied""" - pass - class InvalidCodepoint(IDNAError): """Exception when a disallowed or unallocated codepoint is used""" - pass - class InvalidCodepointContext(IDNAError): """Exception when the codepoint is not valid in the context it is used""" - pass - def _combining_class(cp: int) -> int: v = unicodedata.combining(chr(cp)) - if v == 0: - if not unicodedata.name(chr(cp)): - raise ValueError("Unknown character in unicodedata") + if v == 0 and not unicodedata.name(chr(cp)): + raise ValueError("Unknown character in unicodedata") return v @@ -52,42 +64,76 @@ def _punycode(s: str) -> bytes: def _unot(s: int) -> str: - return "U+{:04X}".format(s) + return f"U+{s:04X}" def valid_label_length(label: Union[bytes, str]) -> bool: - if len(label) > 63: - return False - return True + """Check that a label does not exceed the maximum permitted length. + + Per :rfc:`1035` (and :rfc:`5891` §4.2.4) a DNS label must not exceed + 63 octets. The argument may be either a :class:`str` (a U-label, where + length is measured in characters) or :class:`bytes` (an A-label, where + length is measured in octets). + + :param label: The label to check. + :returns: ``True`` if the label is within the length limit, otherwise + ``False``. + """ + return len(label) <= 63 -def valid_string_length(label: Union[bytes, str], trailing_dot: bool) -> bool: - if len(label) > (254 if trailing_dot else 253): - return False - return True +def valid_string_length(domain: Union[bytes, str], trailing_dot: bool) -> bool: + """Check that a full domain name does not exceed the maximum length. + + Per :rfc:`1035`, a domain name is limited to 253 octets when no trailing + dot is present, or 254 octets when one is included. + + :param domain: The full (possibly multi-label) domain name. + :param trailing_dot: ``True`` if ``domain`` includes a trailing ``.``. + :returns: ``True`` if the domain is within the length limit, otherwise + ``False``. + """ + return len(domain) <= (254 if trailing_dot else 253) def check_bidi(label: str, check_ltr: bool = False) -> bool: + """Validate the Bidi Rule from :rfc:`5893` for a single label. + + The Bidi Rule constrains how bidirectional characters (Hebrew, Arabic, + etc.) may appear within a label. By default the check is only applied + when the label contains at least one right-to-left character (Unicode + bidirectional categories ``R``, ``AL``, or ``AN``); set ``check_ltr`` + to ``True`` to apply it to LTR-only labels as well. + + :param label: The label to validate, as a Unicode string. + :param check_ltr: If ``True``, apply the rules even when the label + contains no RTL characters. + :returns: ``True`` if the label satisfies the Bidi Rule. + :raises IDNABidiError: If any of Bidi Rule conditions 1-6 are violated, + or if the directional category of a codepoint cannot be determined. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") # Bidi rules should only be applied if string contains RTL characters bidi_label = False for idx, cp in enumerate(label, 1): direction = unicodedata.bidirectional(cp) if direction == "": # String likely comes from a newer version of Unicode - raise IDNABidiError("Unknown directionality in label {} at position {}".format(repr(label), idx)) - if direction in ["R", "AL", "AN"]: + raise IDNABidiError(f"Unknown directionality in label {label!r} at position {idx}") + if direction in _bidi_rtl_categories: bidi_label = True if not bidi_label and not check_ltr: return True # Bidi rule 1 direction = unicodedata.bidirectional(label[0]) - if direction in ["R", "AL"]: + if direction in _bidi_rtl_first: rtl = True elif direction == "L": rtl = False else: - raise IDNABidiError("First codepoint in label {} must be directionality L, R or AL".format(repr(label))) + raise IDNABidiError(f"First codepoint in label {label!r} must be directionality L, R or AL") valid_ending = False number_type: Optional[str] = None @@ -96,37 +142,25 @@ def check_bidi(label: str, check_ltr: bool = False) -> bool: if rtl: # Bidi rule 2 - if direction not in [ - "R", - "AL", - "AN", - "EN", - "ES", - "CS", - "ET", - "ON", - "BN", - "NSM", - ]: - raise IDNABidiError("Invalid direction for codepoint at position {} in a right-to-left label".format(idx)) + if direction not in _bidi_rtl_allowed: + raise IDNABidiError(f"Invalid direction for codepoint at position {idx} in a right-to-left label") # Bidi rule 3 - if direction in ["R", "AL", "EN", "AN"]: + if direction in _bidi_rtl_valid_ending: valid_ending = True elif direction != "NSM": valid_ending = False # Bidi rule 4 - if direction in ["AN", "EN"]: + if direction in _bidi_rtl_numeric: if not number_type: number_type = direction - else: - if number_type != direction: - raise IDNABidiError("Can not mix numeral types in a right-to-left label") + elif number_type != direction: + raise IDNABidiError("Can not mix numeral types in a right-to-left label") else: # Bidi rule 5 - if direction not in ["L", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"]: - raise IDNABidiError("Invalid direction for codepoint at position {} in a left-to-right label".format(idx)) + if direction not in _bidi_ltr_allowed: + raise IDNABidiError(f"Invalid direction for codepoint at position {idx} in a left-to-right label") # Bidi rule 6 - if direction in ["L", "EN"]: + if direction in _bidi_ltr_valid_ending: valid_ending = True elif direction != "NSM": valid_ending = False @@ -138,12 +172,31 @@ def check_bidi(label: str, check_ltr: bool = False) -> bool: def check_initial_combiner(label: str) -> bool: + """Reject labels that begin with a combining mark. + + Per :rfc:`5891` §4.2.3.2 a label must not start with a character of + Unicode general category ``M`` (Mark). + + :param label: The label to check. + :returns: ``True`` if the first character is not a combining mark. + :raises IDNAError: If the label begins with a combining character. + """ if unicodedata.category(label[0])[0] == "M": raise IDNAError("Label begins with an illegal combining character") return True def check_hyphen_ok(label: str) -> bool: + """Validate the hyphen restrictions for a label. + + Per :rfc:`5891` §4.2.3.1 a label must not start or end with a hyphen + (``U+002D``), and must not have hyphens in both the third and fourth + positions (the prefix reserved for A-labels). + + :param label: The label to check. + :returns: ``True`` if the hyphen restrictions are satisfied. + :raises IDNAError: If any of the hyphen restrictions are violated. + """ if label[2:4] == "--": raise IDNAError("Label has disallowed hyphens in 3rd and 4th position") if label[0] == "-" or label[-1] == "-": @@ -152,74 +205,104 @@ def check_hyphen_ok(label: str) -> bool: def check_nfc(label: str) -> None: + """Require that a label is in Unicode Normalization Form C. + + :param label: The label to check. + :raises IDNAError: If ``label`` differs from its NFC normalisation. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") if unicodedata.normalize("NFC", label) != label: raise IDNAError("Label must be in Normalization Form C") def valid_contextj(label: str, pos: int) -> bool: + """Validate the CONTEXTJ rules from :rfc:`5892` Appendix A. + + These rules govern the contextual use of the joiner codepoints + ``U+200C`` (ZERO WIDTH NON-JOINER, Appendix A.1) and ``U+200D`` + (ZERO WIDTH JOINER, Appendix A.2) within a label. + + :param label: The label containing the codepoint. + :param pos: Index of the joiner codepoint within ``label``. + :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTJ + rule, ``False`` otherwise (including when the codepoint at + ``pos`` is not a recognised joiner). + :raises ValueError: If an adjacent codepoint has no Unicode name when + determining its combining class. + :raises IDNAError: If ``label`` exceeds the defensive input length limit. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") cp_value = ord(label[pos]) if cp_value == 0x200C: - if pos > 0: - if _combining_class(ord(label[pos - 1])) == _virama_combining_class: - return True + if pos > 0 and _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True ok = False for i in range(pos - 1, -1, -1): - joining_type = idnadata.joining_types.get(ord(label[i])) - if joining_type == ord("T"): + joining_type = _joining_type(ord(label[i])) + if joining_type == "T": continue - elif joining_type in [ord("L"), ord("D")]: + if joining_type in _bidi_joiner_l_or_d: ok = True break - else: - break + break if not ok: return False ok = False for i in range(pos + 1, len(label)): - joining_type = idnadata.joining_types.get(ord(label[i])) - if joining_type == ord("T"): + joining_type = _joining_type(ord(label[i])) + if joining_type == "T": continue - elif joining_type in [ord("R"), ord("D")]: + if joining_type in _bidi_joiner_r_or_d: ok = True break - else: - break + break return ok if cp_value == 0x200D: - if pos > 0: - if _combining_class(ord(label[pos - 1])) == _virama_combining_class: - return True - return False + return pos > 0 and _combining_class(ord(label[pos - 1])) == _virama_combining_class - else: - return False + return False def valid_contexto(label: str, pos: int, exception: bool = False) -> bool: + """Validate the CONTEXTO rules from :rfc:`5892` Appendix A. + + Covers the contextual rules for codepoints such as MIDDLE DOT + (``U+00B7``), Greek lower numeral sign, Hebrew punctuation, Katakana + middle dot, and the Arabic-Indic / Extended Arabic-Indic digit ranges. + + :param label: The label containing the codepoint. + :param pos: Index of the codepoint within ``label``. + :param exception: Reserved for forward compatibility; currently unused. + :returns: ``True`` if the codepoint at ``pos`` satisfies its CONTEXTO + rule, ``False`` otherwise (including when the codepoint is not a + recognised CONTEXTO codepoint). + :raises IDNAError: If ``label`` exceeds the defensive input length limit. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") cp_value = ord(label[pos]) if cp_value == 0x00B7: - if 0 < pos < len(label) - 1: - if ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C: - return True - return False + return 0 < pos < len(label) - 1 and ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C - elif cp_value == 0x0375: + if cp_value == 0x0375: if pos < len(label) - 1 and len(label) > 1: return _is_script(label[pos + 1], "Greek") return False - elif cp_value == 0x05F3 or cp_value == 0x05F4: + if cp_value in {0x05F3, 0x05F4}: if pos > 0: return _is_script(label[pos - 1], "Hebrew") return False - elif cp_value == 0x30FB: + if cp_value == 0x30FB: for cp in label: if cp == "\u30fb": continue @@ -227,27 +310,45 @@ def valid_contexto(label: str, pos: int, exception: bool = False) -> bool: return True return False - elif 0x660 <= cp_value <= 0x669: - for cp in label: - if 0x6F0 <= ord(cp) <= 0x06F9: - return False - return True + if 0x660 <= cp_value <= 0x669: + return not any(0x6F0 <= ord(cp) <= 0x06F9 for cp in label) - elif 0x6F0 <= cp_value <= 0x6F9: - for cp in label: - if 0x660 <= ord(cp) <= 0x0669: - return False - return True + if 0x6F0 <= cp_value <= 0x6F9: + return not any(0x660 <= ord(cp) <= 0x0669 for cp in label) return False def check_label(label: Union[str, bytes, bytearray]) -> None: + """Run the full set of IDNA 2008 validity checks on a single label. + + Applies, in order: NFC normalisation (:func:`check_nfc`), hyphen + restrictions (:func:`check_hyphen_ok`), the no-leading-combiner rule + (:func:`check_initial_combiner`), per-codepoint validity (PVALID, + CONTEXTJ, CONTEXTO classes from :rfc:`5892`), and the Bidi Rule + (:func:`check_bidi`). + + :param label: The label to validate. ``bytes`` or ``bytearray`` input + is decoded as UTF-8 first. + :raises IDNAError: If the label is empty or fails a structural rule. + :raises InvalidCodepoint: If the label contains a DISALLOWED or + UNASSIGNED codepoint. + :raises InvalidCodepointContext: If a CONTEXTJ or CONTEXTO codepoint + is not valid in its context. + :raises IDNABidiError: If the Bidi Rule is violated. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") if isinstance(label, (bytes, bytearray)): label = label.decode("utf-8") if len(label) == 0: raise IDNAError("Empty Label") + # Reject on domain length rather than label length so support some UTS 46 + # use cases, still reducing processing of label contextual rules + if not valid_string_length(label, trailing_dot=True): + raise IDNAError("Label too long") + check_nfc(label) check_hyphen_ok(label) check_initial_combiner(label) @@ -256,40 +357,47 @@ def check_label(label: Union[str, bytes, bytearray]) -> None: cp_value = ord(cp) if intranges_contain(cp_value, idnadata.codepoint_classes["PVALID"]): continue - elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]): + if intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]): try: if not valid_contextj(label, pos): - raise InvalidCodepointContext( - "Joiner {} not allowed at position {} in {}".format(_unot(cp_value), pos + 1, repr(label)) - ) - except ValueError: + raise InvalidCodepointContext(f"Joiner {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}") + except ValueError as err: raise IDNAError( - "Unknown codepoint adjacent to joiner {} at position {} in {}".format( - _unot(cp_value), pos + 1, repr(label) - ) - ) + f"Unknown codepoint adjacent to joiner {_unot(cp_value)} at position {pos + 1} in {label!r}" + ) from err elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTO"]): if not valid_contexto(label, pos): - raise InvalidCodepointContext( - "Codepoint {} not allowed at position {} in {}".format(_unot(cp_value), pos + 1, repr(label)) - ) + raise InvalidCodepointContext(f"Codepoint {_unot(cp_value)} not allowed at position {pos + 1} in {label!r}") else: - raise InvalidCodepoint( - "Codepoint {} at position {} of {} not allowed".format(_unot(cp_value), pos + 1, repr(label)) - ) + raise InvalidCodepoint(f"Codepoint {_unot(cp_value)} at position {pos + 1} of {label!r} not allowed") check_bidi(label) def alabel(label: str) -> bytes: + """Convert a single U-label into its A-label form. + + The result is the ASCII-Compatible Encoding (ACE) form per :rfc:`5891` + §4: the label is validated, Punycode-encoded, and prefixed with + ``xn--``. Pure ASCII labels that are already valid IDNA labels are + returned unchanged (as :class:`bytes`). + + :param label: The label to convert, as a Unicode string. + :returns: The A-label as ASCII-encoded :class:`bytes`. + :raises IDNAError: If the label is invalid or the resulting A-label + exceeds 63 octets. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") try: label_bytes = label.encode("ascii") + except UnicodeEncodeError: + pass + else: ulabel(label_bytes) if not valid_label_length(label_bytes): raise IDNAError("Label too long") return label_bytes - except UnicodeEncodeError: - pass check_label(label) label_bytes = _alabel_prefix + _punycode(label) @@ -301,6 +409,20 @@ def alabel(label: str) -> bytes: def ulabel(label: Union[str, bytes, bytearray]) -> str: + """Convert a single A-label into its U-label form. + + Performs the inverse of :func:`alabel`: an ``xn--``-prefixed label is + Punycode-decoded and validated. Labels that are already Unicode (or + plain ASCII without the ACE prefix) are validated and returned as a + Unicode string. + + :param label: The label to convert. ``bytes`` or ``bytearray`` input + is treated as ASCII. + :returns: The U-label as a Unicode string. + :raises IDNAError: If the label is malformed or fails validation. + """ + if len(label) > _max_input_length: + raise IDNAError("Label too long") if not isinstance(label, (bytes, bytearray)): try: label_bytes = label.encode("ascii") @@ -315,7 +437,7 @@ def ulabel(label: Union[str, bytes, bytearray]) -> str: label_bytes = label_bytes[len(_alabel_prefix) :] if not label_bytes: raise IDNAError("Malformed A-label, no Punycode eligible content found") - if label_bytes.decode("ascii")[-1] == "-": + if label_bytes.endswith(b"-"): raise IDNAError("A-label must not end with a hyphen") else: check_label(label_bytes) @@ -323,42 +445,64 @@ def ulabel(label: Union[str, bytes, bytearray]) -> str: try: label = label_bytes.decode("punycode") - except UnicodeError: - raise IDNAError("Invalid A-label") + except UnicodeError as err: + raise IDNAError("Invalid A-label") from err check_label(label) return label def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str: - """Re-map the characters in the string according to UTS46 processing.""" - from .uts46data import uts46data + """Apply the UTS #46 character mapping to a domain string. + + Implements the mapping table from `UTS #46 §4 + `_: each character is kept, + replaced, or rejected based on its status (``V``, ``M``, ``D``, ``3``, + ``I``). The result is returned in Normalisation Form C. + + :param domain: The full domain name to remap. + :param std3_rules: If ``True``, apply the stricter STD3 ASCII rules + (status ``3`` codepoints raise instead of being kept or mapped). + :param transitional: If ``True``, use transitional processing (status + ``D`` codepoints are mapped instead of kept). Transitional + processing has been removed from UTS #46 and this option is + retained only for backwards compatibility. + :returns: The remapped domain, in Normalisation Form C. + :raises InvalidCodepoint: If the domain contains a disallowed + codepoint under the chosen rules. + :raises IDNAError: If ``domain`` exceeds the defensive input length limit. + """ + if len(domain) > _max_input_length: + raise IDNAError("Domain too long") + from .uts46data import uts46_replacements, uts46_starts, uts46_statuses output = "" for pos, char in enumerate(domain): code_point = ord(char) - try: - uts46row = uts46data[code_point if code_point < 256 else bisect.bisect_left(uts46data, (code_point, "Z")) - 1] - status = uts46row[1] - replacement: Optional[str] = None - if len(uts46row) == 3: - replacement = uts46row[2] - if ( - status == "V" - or (status == "D" and not transitional) - or (status == "3" and not std3_rules and replacement is None) - ): - output += char - elif replacement is not None and ( - status == "M" or (status == "3" and not std3_rules) or (status == "D" and transitional) - ): - output += replacement - elif status != "I": - raise IndexError() - except IndexError: - raise InvalidCodepoint( - "Codepoint {} not allowed at position {} in {}".format(_unot(code_point), pos + 1, repr(domain)) - ) + i = code_point if code_point < 256 else bisect.bisect_right(uts46_starts, code_point) - 1 + status = chr(uts46_statuses[i]) + replacement: Optional[str] = uts46_replacements[i] + + # UTS #46 §4: V is always valid, D is deviation (kept unless transitional), + # 3 is disallowed-STD3 (kept unmapped if std3_rules is off and no mapping). + keep_as_is = ( + status == "V" or (status == "D" and not transitional) or (status == "3" and not std3_rules and replacement is None) + ) + # M is mapped, 3-with-replacement and transitional D fall through to the + # same replacement output path. + use_replacement = replacement is not None and ( + status == "M" or (status == "3" and not std3_rules) or (status == "D" and transitional) + ) + + if keep_as_is: + output += char + elif use_replacement: + assert replacement is not None # narrowed by use_replacement + output += replacement + elif status == "I": + continue + else: + raise InvalidCodepoint(f"Codepoint {_unot(code_point)} not allowed at position {pos + 1} in {domain!r}") return unicodedata.normalize("NFC", output) @@ -370,19 +514,52 @@ def encode( std3_rules: bool = False, transitional: bool = False, ) -> bytes: + """Encode a Unicode domain name into its ASCII (A-label) form. + + Splits the input on label separators (only ``U+002E`` if ``strict`` is + set; otherwise also IDEOGRAPHIC FULL STOP ``U+3002``, FULLWIDTH FULL + STOP ``U+FF0E``, and HALFWIDTH IDEOGRAPHIC FULL STOP ``U+FF61``), + encodes each label with :func:`alabel`, and rejoins them with ``.``. + Optionally pre-processes the input through :func:`uts46_remap`. + + :param s: The domain name to encode. + :param strict: If ``True``, only ``U+002E`` is recognised as a label + separator. + :param uts46: If ``True``, apply UTS #46 mapping before encoding. + :param std3_rules: Forwarded to :func:`uts46_remap` when ``uts46`` is + ``True``. + :param transitional: Forwarded to :func:`uts46_remap` when ``uts46`` + is ``True``. Deprecated: emits a :class:`DeprecationWarning` and + will be removed in a future version. + :returns: The encoded domain as ASCII :class:`bytes`. + :raises IDNAError: If the domain is empty, contains an invalid label, + or exceeds the maximum domain length. + """ + if transitional: + warnings.warn( + "Transitional processing has been removed from UTS #46. " + "The transitional argument will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) if not isinstance(s, str): try: s = str(s, "ascii") - except UnicodeDecodeError: - raise IDNAError("should pass a unicode string to the function rather than a byte string.") + except (UnicodeDecodeError, TypeError) as err: + raise IDNAError("should pass a unicode string to the function rather than a byte string.") from err + if len(s) > _max_input_length: + raise IDNAError("Domain too long") if uts46: s = uts46_remap(s, std3_rules, transitional) + + # Reject inputs that exceed the maximum DNS domain length up-front + # to avoid expensive computation on long inputs. + if not valid_string_length(s, trailing_dot=True): + raise IDNAError("Domain too long") + trailing_dot = False result = [] - if strict: - labels = s.split(".") - else: - labels = _unicode_dots_re.split(s) + labels = s.split(".") if strict else _unicode_dots_re.split(s) if not labels or labels == [""]: raise IDNAError("Empty domain") if labels[-1] == "": @@ -407,29 +584,63 @@ def decode( strict: bool = False, uts46: bool = False, std3_rules: bool = False, + display: bool = False, ) -> str: - try: - if not isinstance(s, str): + """Decode an A-label-encoded domain name back to Unicode. + + Splits the input on label separators (see :func:`encode` for the + rules), decodes each label with :func:`ulabel`, and rejoins them + with ``.``. Optionally pre-processes the input through + :func:`uts46_remap`. + + :param s: The domain name to decode. + :param strict: If ``True``, only ``U+002E`` is recognised as a label + separator. + :param uts46: If ``True``, apply UTS #46 mapping before decoding. + :param std3_rules: Forwarded to :func:`uts46_remap` when ``uts46`` is + ``True``. + :param display: If ``True``, any ``xn--`` label that fails IDNA + validation is passed through unchanged (lowercased) rather than + aborting the whole call. Intended for "decode for display" + consumers (e.g. URL libraries, HTTP clients) that want to show + the user the label as it appears on the wire when it cannot be + rendered as Unicode. Matches the per-label recovery prescribed + by UTS #46 §4 and the WHATWG URL "domain to Unicode" algorithm. + :returns: The decoded domain as a Unicode string. + :raises IDNAError: If the input is not valid ASCII, contains an + invalid label, or is empty. + """ + if not isinstance(s, str): + try: s = str(s, "ascii") - except UnicodeDecodeError: - raise IDNAError("Invalid ASCII in A-label") + except (UnicodeDecodeError, TypeError) as err: + raise IDNAError("Invalid ASCII in A-label") from err + if len(s) > _max_input_length: + raise IDNAError("Domain too long") if uts46: s = uts46_remap(s, std3_rules, False) + # Reject inputs that exceed the maximum DNS domain length up-front + # to avoid expensive computation on long inputs. + if not valid_string_length(s, trailing_dot=True): + raise IDNAError("Domain too long") trailing_dot = False result = [] - if not strict: - labels = _unicode_dots_re.split(s) - else: - labels = s.split(".") + labels = s.split(".") if strict else _unicode_dots_re.split(s) if not labels or labels == [""]: raise IDNAError("Empty domain") if not labels[-1]: del labels[-1] trailing_dot = True for label in labels: - s = ulabel(label) - if s: - result.append(s) + try: + u = ulabel(label) + except IDNAError: + if display and label[:4].lower() == "xn--": + u = label.lower() + else: + raise + if u: + result.append(u) else: raise IDNAError("Empty label") if trailing_dot: diff --git a/libs/idna/idnadata.py b/libs/idna/idnadata.py index ded47cae0..f2ab38897 100644 --- a/libs/idna/idnadata.py +++ b/libs/idna/idnadata.py @@ -1,6 +1,6 @@ # This file is automatically generated by tools/idna-data -__version__ = "16.0.0" +__version__ = "17.0.0" scripts = { "Greek": ( @@ -54,16 +54,15 @@ scripts = { 0xF9000000FA6E, 0xFA700000FADA, 0x16FE200016FE4, - 0x16FF000016FF2, + 0x16FF000016FF7, 0x200000002A6E0, - 0x2A7000002B73A, - 0x2B7400002B81E, - 0x2B8200002CEA2, + 0x2A7000002B81E, + 0x2B8200002CEAE, 0x2CEB00002EBE1, 0x2EBF00002EE5E, 0x2F8000002FA1E, 0x300000003134B, - 0x31350000323B0, + 0x313500003347A, ), "Hebrew": ( 0x591000005C8, @@ -101,2970 +100,551 @@ scripts = { 0x1B1640001B168, ), } + + joining_types = { - 0xAD: 84, - 0x300: 84, - 0x301: 84, - 0x302: 84, - 0x303: 84, - 0x304: 84, - 0x305: 84, - 0x306: 84, - 0x307: 84, - 0x308: 84, - 0x309: 84, - 0x30A: 84, - 0x30B: 84, - 0x30C: 84, - 0x30D: 84, - 0x30E: 84, - 0x30F: 84, - 0x310: 84, - 0x311: 84, - 0x312: 84, - 0x313: 84, - 0x314: 84, - 0x315: 84, - 0x316: 84, - 0x317: 84, - 0x318: 84, - 0x319: 84, - 0x31A: 84, - 0x31B: 84, - 0x31C: 84, - 0x31D: 84, - 0x31E: 84, - 0x31F: 84, - 0x320: 84, - 0x321: 84, - 0x322: 84, - 0x323: 84, - 0x324: 84, - 0x325: 84, - 0x326: 84, - 0x327: 84, - 0x328: 84, - 0x329: 84, - 0x32A: 84, - 0x32B: 84, - 0x32C: 84, - 0x32D: 84, - 0x32E: 84, - 0x32F: 84, - 0x330: 84, - 0x331: 84, - 0x332: 84, - 0x333: 84, - 0x334: 84, - 0x335: 84, - 0x336: 84, - 0x337: 84, - 0x338: 84, - 0x339: 84, - 0x33A: 84, - 0x33B: 84, - 0x33C: 84, - 0x33D: 84, - 0x33E: 84, - 0x33F: 84, - 0x340: 84, - 0x341: 84, - 0x342: 84, - 0x343: 84, - 0x344: 84, - 0x345: 84, - 0x346: 84, - 0x347: 84, - 0x348: 84, - 0x349: 84, - 0x34A: 84, - 0x34B: 84, - 0x34C: 84, - 0x34D: 84, - 0x34E: 84, - 0x34F: 84, - 0x350: 84, - 0x351: 84, - 0x352: 84, - 0x353: 84, - 0x354: 84, - 0x355: 84, - 0x356: 84, - 0x357: 84, - 0x358: 84, - 0x359: 84, - 0x35A: 84, - 0x35B: 84, - 0x35C: 84, - 0x35D: 84, - 0x35E: 84, - 0x35F: 84, - 0x360: 84, - 0x361: 84, - 0x362: 84, - 0x363: 84, - 0x364: 84, - 0x365: 84, - 0x366: 84, - 0x367: 84, - 0x368: 84, - 0x369: 84, - 0x36A: 84, - 0x36B: 84, - 0x36C: 84, - 0x36D: 84, - 0x36E: 84, - 0x36F: 84, - 0x483: 84, - 0x484: 84, - 0x485: 84, - 0x486: 84, - 0x487: 84, - 0x488: 84, - 0x489: 84, - 0x591: 84, - 0x592: 84, - 0x593: 84, - 0x594: 84, - 0x595: 84, - 0x596: 84, - 0x597: 84, - 0x598: 84, - 0x599: 84, - 0x59A: 84, - 0x59B: 84, - 0x59C: 84, - 0x59D: 84, - 0x59E: 84, - 0x59F: 84, - 0x5A0: 84, - 0x5A1: 84, - 0x5A2: 84, - 0x5A3: 84, - 0x5A4: 84, - 0x5A5: 84, - 0x5A6: 84, - 0x5A7: 84, - 0x5A8: 84, - 0x5A9: 84, - 0x5AA: 84, - 0x5AB: 84, - 0x5AC: 84, - 0x5AD: 84, - 0x5AE: 84, - 0x5AF: 84, - 0x5B0: 84, - 0x5B1: 84, - 0x5B2: 84, - 0x5B3: 84, - 0x5B4: 84, - 0x5B5: 84, - 0x5B6: 84, - 0x5B7: 84, - 0x5B8: 84, - 0x5B9: 84, - 0x5BA: 84, - 0x5BB: 84, - 0x5BC: 84, - 0x5BD: 84, - 0x5BF: 84, - 0x5C1: 84, - 0x5C2: 84, - 0x5C4: 84, - 0x5C5: 84, - 0x5C7: 84, - 0x610: 84, - 0x611: 84, - 0x612: 84, - 0x613: 84, - 0x614: 84, - 0x615: 84, - 0x616: 84, - 0x617: 84, - 0x618: 84, - 0x619: 84, - 0x61A: 84, - 0x61C: 84, - 0x620: 68, - 0x622: 82, - 0x623: 82, - 0x624: 82, - 0x625: 82, - 0x626: 68, - 0x627: 82, - 0x628: 68, - 0x629: 82, - 0x62A: 68, - 0x62B: 68, - 0x62C: 68, - 0x62D: 68, - 0x62E: 68, - 0x62F: 82, - 0x630: 82, - 0x631: 82, - 0x632: 82, - 0x633: 68, - 0x634: 68, - 0x635: 68, - 0x636: 68, - 0x637: 68, - 0x638: 68, - 0x639: 68, - 0x63A: 68, - 0x63B: 68, - 0x63C: 68, - 0x63D: 68, - 0x63E: 68, - 0x63F: 68, - 0x640: 67, - 0x641: 68, - 0x642: 68, - 0x643: 68, - 0x644: 68, - 0x645: 68, - 0x646: 68, - 0x647: 68, - 0x648: 82, - 0x649: 68, - 0x64A: 68, - 0x64B: 84, - 0x64C: 84, - 0x64D: 84, - 0x64E: 84, - 0x64F: 84, - 0x650: 84, - 0x651: 84, - 0x652: 84, - 0x653: 84, - 0x654: 84, - 0x655: 84, - 0x656: 84, - 0x657: 84, - 0x658: 84, - 0x659: 84, - 0x65A: 84, - 0x65B: 84, - 0x65C: 84, - 0x65D: 84, - 0x65E: 84, - 0x65F: 84, - 0x66E: 68, - 0x66F: 68, - 0x670: 84, - 0x671: 82, - 0x672: 82, - 0x673: 82, - 0x675: 82, - 0x676: 82, - 0x677: 82, - 0x678: 68, - 0x679: 68, - 0x67A: 68, - 0x67B: 68, - 0x67C: 68, - 0x67D: 68, - 0x67E: 68, - 0x67F: 68, - 0x680: 68, - 0x681: 68, - 0x682: 68, - 0x683: 68, - 0x684: 68, - 0x685: 68, - 0x686: 68, - 0x687: 68, - 0x688: 82, - 0x689: 82, - 0x68A: 82, - 0x68B: 82, - 0x68C: 82, - 0x68D: 82, - 0x68E: 82, - 0x68F: 82, - 0x690: 82, - 0x691: 82, - 0x692: 82, - 0x693: 82, - 0x694: 82, - 0x695: 82, - 0x696: 82, - 0x697: 82, - 0x698: 82, - 0x699: 82, - 0x69A: 68, - 0x69B: 68, - 0x69C: 68, - 0x69D: 68, - 0x69E: 68, - 0x69F: 68, - 0x6A0: 68, - 0x6A1: 68, - 0x6A2: 68, - 0x6A3: 68, - 0x6A4: 68, - 0x6A5: 68, - 0x6A6: 68, - 0x6A7: 68, - 0x6A8: 68, - 0x6A9: 68, - 0x6AA: 68, - 0x6AB: 68, - 0x6AC: 68, - 0x6AD: 68, - 0x6AE: 68, - 0x6AF: 68, - 0x6B0: 68, - 0x6B1: 68, - 0x6B2: 68, - 0x6B3: 68, - 0x6B4: 68, - 0x6B5: 68, - 0x6B6: 68, - 0x6B7: 68, - 0x6B8: 68, - 0x6B9: 68, - 0x6BA: 68, - 0x6BB: 68, - 0x6BC: 68, - 0x6BD: 68, - 0x6BE: 68, - 0x6BF: 68, - 0x6C0: 82, - 0x6C1: 68, - 0x6C2: 68, - 0x6C3: 82, - 0x6C4: 82, - 0x6C5: 82, - 0x6C6: 82, - 0x6C7: 82, - 0x6C8: 82, - 0x6C9: 82, - 0x6CA: 82, - 0x6CB: 82, - 0x6CC: 68, - 0x6CD: 82, - 0x6CE: 68, - 0x6CF: 82, - 0x6D0: 68, - 0x6D1: 68, - 0x6D2: 82, - 0x6D3: 82, - 0x6D5: 82, - 0x6D6: 84, - 0x6D7: 84, - 0x6D8: 84, - 0x6D9: 84, - 0x6DA: 84, - 0x6DB: 84, - 0x6DC: 84, - 0x6DF: 84, - 0x6E0: 84, - 0x6E1: 84, - 0x6E2: 84, - 0x6E3: 84, - 0x6E4: 84, - 0x6E7: 84, - 0x6E8: 84, - 0x6EA: 84, - 0x6EB: 84, - 0x6EC: 84, - 0x6ED: 84, - 0x6EE: 82, - 0x6EF: 82, - 0x6FA: 68, - 0x6FB: 68, - 0x6FC: 68, - 0x6FF: 68, - 0x70F: 84, - 0x710: 82, - 0x711: 84, - 0x712: 68, - 0x713: 68, - 0x714: 68, - 0x715: 82, - 0x716: 82, - 0x717: 82, - 0x718: 82, - 0x719: 82, - 0x71A: 68, - 0x71B: 68, - 0x71C: 68, - 0x71D: 68, - 0x71E: 82, - 0x71F: 68, - 0x720: 68, - 0x721: 68, - 0x722: 68, - 0x723: 68, - 0x724: 68, - 0x725: 68, - 0x726: 68, - 0x727: 68, - 0x728: 82, - 0x729: 68, - 0x72A: 82, - 0x72B: 68, - 0x72C: 82, - 0x72D: 68, - 0x72E: 68, - 0x72F: 82, - 0x730: 84, - 0x731: 84, - 0x732: 84, - 0x733: 84, - 0x734: 84, - 0x735: 84, - 0x736: 84, - 0x737: 84, - 0x738: 84, - 0x739: 84, - 0x73A: 84, - 0x73B: 84, - 0x73C: 84, - 0x73D: 84, - 0x73E: 84, - 0x73F: 84, - 0x740: 84, - 0x741: 84, - 0x742: 84, - 0x743: 84, - 0x744: 84, - 0x745: 84, - 0x746: 84, - 0x747: 84, - 0x748: 84, - 0x749: 84, - 0x74A: 84, - 0x74D: 82, - 0x74E: 68, - 0x74F: 68, - 0x750: 68, - 0x751: 68, - 0x752: 68, - 0x753: 68, - 0x754: 68, - 0x755: 68, - 0x756: 68, - 0x757: 68, - 0x758: 68, - 0x759: 82, - 0x75A: 82, - 0x75B: 82, - 0x75C: 68, - 0x75D: 68, - 0x75E: 68, - 0x75F: 68, - 0x760: 68, - 0x761: 68, - 0x762: 68, - 0x763: 68, - 0x764: 68, - 0x765: 68, - 0x766: 68, - 0x767: 68, - 0x768: 68, - 0x769: 68, - 0x76A: 68, - 0x76B: 82, - 0x76C: 82, - 0x76D: 68, - 0x76E: 68, - 0x76F: 68, - 0x770: 68, - 0x771: 82, - 0x772: 68, - 0x773: 82, - 0x774: 82, - 0x775: 68, - 0x776: 68, - 0x777: 68, - 0x778: 82, - 0x779: 82, - 0x77A: 68, - 0x77B: 68, - 0x77C: 68, - 0x77D: 68, - 0x77E: 68, - 0x77F: 68, - 0x7A6: 84, - 0x7A7: 84, - 0x7A8: 84, - 0x7A9: 84, - 0x7AA: 84, - 0x7AB: 84, - 0x7AC: 84, - 0x7AD: 84, - 0x7AE: 84, - 0x7AF: 84, - 0x7B0: 84, - 0x7CA: 68, - 0x7CB: 68, - 0x7CC: 68, - 0x7CD: 68, - 0x7CE: 68, - 0x7CF: 68, - 0x7D0: 68, - 0x7D1: 68, - 0x7D2: 68, - 0x7D3: 68, - 0x7D4: 68, - 0x7D5: 68, - 0x7D6: 68, - 0x7D7: 68, - 0x7D8: 68, - 0x7D9: 68, - 0x7DA: 68, - 0x7DB: 68, - 0x7DC: 68, - 0x7DD: 68, - 0x7DE: 68, - 0x7DF: 68, - 0x7E0: 68, - 0x7E1: 68, - 0x7E2: 68, - 0x7E3: 68, - 0x7E4: 68, - 0x7E5: 68, - 0x7E6: 68, - 0x7E7: 68, - 0x7E8: 68, - 0x7E9: 68, - 0x7EA: 68, - 0x7EB: 84, - 0x7EC: 84, - 0x7ED: 84, - 0x7EE: 84, - 0x7EF: 84, - 0x7F0: 84, - 0x7F1: 84, - 0x7F2: 84, - 0x7F3: 84, - 0x7FA: 67, - 0x7FD: 84, - 0x816: 84, - 0x817: 84, - 0x818: 84, - 0x819: 84, - 0x81B: 84, - 0x81C: 84, - 0x81D: 84, - 0x81E: 84, - 0x81F: 84, - 0x820: 84, - 0x821: 84, - 0x822: 84, - 0x823: 84, - 0x825: 84, - 0x826: 84, - 0x827: 84, - 0x829: 84, - 0x82A: 84, - 0x82B: 84, - 0x82C: 84, - 0x82D: 84, - 0x840: 82, - 0x841: 68, - 0x842: 68, - 0x843: 68, - 0x844: 68, - 0x845: 68, - 0x846: 82, - 0x847: 82, - 0x848: 68, - 0x849: 82, - 0x84A: 68, - 0x84B: 68, - 0x84C: 68, - 0x84D: 68, - 0x84E: 68, - 0x84F: 68, - 0x850: 68, - 0x851: 68, - 0x852: 68, - 0x853: 68, - 0x854: 82, - 0x855: 68, - 0x856: 82, - 0x857: 82, - 0x858: 82, - 0x859: 84, - 0x85A: 84, - 0x85B: 84, - 0x860: 68, - 0x862: 68, - 0x863: 68, - 0x864: 68, - 0x865: 68, - 0x867: 82, - 0x868: 68, - 0x869: 82, - 0x86A: 82, - 0x870: 82, - 0x871: 82, - 0x872: 82, - 0x873: 82, - 0x874: 82, - 0x875: 82, - 0x876: 82, - 0x877: 82, - 0x878: 82, - 0x879: 82, - 0x87A: 82, - 0x87B: 82, - 0x87C: 82, - 0x87D: 82, - 0x87E: 82, - 0x87F: 82, - 0x880: 82, - 0x881: 82, - 0x882: 82, - 0x883: 67, - 0x884: 67, - 0x885: 67, - 0x886: 68, - 0x889: 68, - 0x88A: 68, - 0x88B: 68, - 0x88C: 68, - 0x88D: 68, - 0x88E: 82, - 0x897: 84, - 0x898: 84, - 0x899: 84, - 0x89A: 84, - 0x89B: 84, - 0x89C: 84, - 0x89D: 84, - 0x89E: 84, - 0x89F: 84, - 0x8A0: 68, - 0x8A1: 68, - 0x8A2: 68, - 0x8A3: 68, - 0x8A4: 68, - 0x8A5: 68, - 0x8A6: 68, - 0x8A7: 68, - 0x8A8: 68, - 0x8A9: 68, - 0x8AA: 82, - 0x8AB: 82, - 0x8AC: 82, - 0x8AE: 82, - 0x8AF: 68, - 0x8B0: 68, - 0x8B1: 82, - 0x8B2: 82, - 0x8B3: 68, - 0x8B4: 68, - 0x8B5: 68, - 0x8B6: 68, - 0x8B7: 68, - 0x8B8: 68, - 0x8B9: 82, - 0x8BA: 68, - 0x8BB: 68, - 0x8BC: 68, - 0x8BD: 68, - 0x8BE: 68, - 0x8BF: 68, - 0x8C0: 68, - 0x8C1: 68, - 0x8C2: 68, - 0x8C3: 68, - 0x8C4: 68, - 0x8C5: 68, - 0x8C6: 68, - 0x8C7: 68, - 0x8C8: 68, - 0x8CA: 84, - 0x8CB: 84, - 0x8CC: 84, - 0x8CD: 84, - 0x8CE: 84, - 0x8CF: 84, - 0x8D0: 84, - 0x8D1: 84, - 0x8D2: 84, - 0x8D3: 84, - 0x8D4: 84, - 0x8D5: 84, - 0x8D6: 84, - 0x8D7: 84, - 0x8D8: 84, - 0x8D9: 84, - 0x8DA: 84, - 0x8DB: 84, - 0x8DC: 84, - 0x8DD: 84, - 0x8DE: 84, - 0x8DF: 84, - 0x8E0: 84, - 0x8E1: 84, - 0x8E3: 84, - 0x8E4: 84, - 0x8E5: 84, - 0x8E6: 84, - 0x8E7: 84, - 0x8E8: 84, - 0x8E9: 84, - 0x8EA: 84, - 0x8EB: 84, - 0x8EC: 84, - 0x8ED: 84, - 0x8EE: 84, - 0x8EF: 84, - 0x8F0: 84, - 0x8F1: 84, - 0x8F2: 84, - 0x8F3: 84, - 0x8F4: 84, - 0x8F5: 84, - 0x8F6: 84, - 0x8F7: 84, - 0x8F8: 84, - 0x8F9: 84, - 0x8FA: 84, - 0x8FB: 84, - 0x8FC: 84, - 0x8FD: 84, - 0x8FE: 84, - 0x8FF: 84, - 0x900: 84, - 0x901: 84, - 0x902: 84, - 0x93A: 84, - 0x93C: 84, - 0x941: 84, - 0x942: 84, - 0x943: 84, - 0x944: 84, - 0x945: 84, - 0x946: 84, - 0x947: 84, - 0x948: 84, - 0x94D: 84, - 0x951: 84, - 0x952: 84, - 0x953: 84, - 0x954: 84, - 0x955: 84, - 0x956: 84, - 0x957: 84, - 0x962: 84, - 0x963: 84, - 0x981: 84, - 0x9BC: 84, - 0x9C1: 84, - 0x9C2: 84, - 0x9C3: 84, - 0x9C4: 84, - 0x9CD: 84, - 0x9E2: 84, - 0x9E3: 84, - 0x9FE: 84, - 0xA01: 84, - 0xA02: 84, - 0xA3C: 84, - 0xA41: 84, - 0xA42: 84, - 0xA47: 84, - 0xA48: 84, - 0xA4B: 84, - 0xA4C: 84, - 0xA4D: 84, - 0xA51: 84, - 0xA70: 84, - 0xA71: 84, - 0xA75: 84, - 0xA81: 84, - 0xA82: 84, - 0xABC: 84, - 0xAC1: 84, - 0xAC2: 84, - 0xAC3: 84, - 0xAC4: 84, - 0xAC5: 84, - 0xAC7: 84, - 0xAC8: 84, - 0xACD: 84, - 0xAE2: 84, - 0xAE3: 84, - 0xAFA: 84, - 0xAFB: 84, - 0xAFC: 84, - 0xAFD: 84, - 0xAFE: 84, - 0xAFF: 84, - 0xB01: 84, - 0xB3C: 84, - 0xB3F: 84, - 0xB41: 84, - 0xB42: 84, - 0xB43: 84, - 0xB44: 84, - 0xB4D: 84, - 0xB55: 84, - 0xB56: 84, - 0xB62: 84, - 0xB63: 84, - 0xB82: 84, - 0xBC0: 84, - 0xBCD: 84, - 0xC00: 84, - 0xC04: 84, - 0xC3C: 84, - 0xC3E: 84, - 0xC3F: 84, - 0xC40: 84, - 0xC46: 84, - 0xC47: 84, - 0xC48: 84, - 0xC4A: 84, - 0xC4B: 84, - 0xC4C: 84, - 0xC4D: 84, - 0xC55: 84, - 0xC56: 84, - 0xC62: 84, - 0xC63: 84, - 0xC81: 84, - 0xCBC: 84, - 0xCBF: 84, - 0xCC6: 84, - 0xCCC: 84, - 0xCCD: 84, - 0xCE2: 84, - 0xCE3: 84, - 0xD00: 84, - 0xD01: 84, - 0xD3B: 84, - 0xD3C: 84, - 0xD41: 84, - 0xD42: 84, - 0xD43: 84, - 0xD44: 84, - 0xD4D: 84, - 0xD62: 84, - 0xD63: 84, - 0xD81: 84, - 0xDCA: 84, - 0xDD2: 84, - 0xDD3: 84, - 0xDD4: 84, - 0xDD6: 84, - 0xE31: 84, - 0xE34: 84, - 0xE35: 84, - 0xE36: 84, - 0xE37: 84, - 0xE38: 84, - 0xE39: 84, - 0xE3A: 84, - 0xE47: 84, - 0xE48: 84, - 0xE49: 84, - 0xE4A: 84, - 0xE4B: 84, - 0xE4C: 84, - 0xE4D: 84, - 0xE4E: 84, - 0xEB1: 84, - 0xEB4: 84, - 0xEB5: 84, - 0xEB6: 84, - 0xEB7: 84, - 0xEB8: 84, - 0xEB9: 84, - 0xEBA: 84, - 0xEBB: 84, - 0xEBC: 84, - 0xEC8: 84, - 0xEC9: 84, - 0xECA: 84, - 0xECB: 84, - 0xECC: 84, - 0xECD: 84, - 0xECE: 84, - 0xF18: 84, - 0xF19: 84, - 0xF35: 84, - 0xF37: 84, - 0xF39: 84, - 0xF71: 84, - 0xF72: 84, - 0xF73: 84, - 0xF74: 84, - 0xF75: 84, - 0xF76: 84, - 0xF77: 84, - 0xF78: 84, - 0xF79: 84, - 0xF7A: 84, - 0xF7B: 84, - 0xF7C: 84, - 0xF7D: 84, - 0xF7E: 84, - 0xF80: 84, - 0xF81: 84, - 0xF82: 84, - 0xF83: 84, - 0xF84: 84, - 0xF86: 84, - 0xF87: 84, - 0xF8D: 84, - 0xF8E: 84, - 0xF8F: 84, - 0xF90: 84, - 0xF91: 84, - 0xF92: 84, - 0xF93: 84, - 0xF94: 84, - 0xF95: 84, - 0xF96: 84, - 0xF97: 84, - 0xF99: 84, - 0xF9A: 84, - 0xF9B: 84, - 0xF9C: 84, - 0xF9D: 84, - 0xF9E: 84, - 0xF9F: 84, - 0xFA0: 84, - 0xFA1: 84, - 0xFA2: 84, - 0xFA3: 84, - 0xFA4: 84, - 0xFA5: 84, - 0xFA6: 84, - 0xFA7: 84, - 0xFA8: 84, - 0xFA9: 84, - 0xFAA: 84, - 0xFAB: 84, - 0xFAC: 84, - 0xFAD: 84, - 0xFAE: 84, - 0xFAF: 84, - 0xFB0: 84, - 0xFB1: 84, - 0xFB2: 84, - 0xFB3: 84, - 0xFB4: 84, - 0xFB5: 84, - 0xFB6: 84, - 0xFB7: 84, - 0xFB8: 84, - 0xFB9: 84, - 0xFBA: 84, - 0xFBB: 84, - 0xFBC: 84, - 0xFC6: 84, - 0x102D: 84, - 0x102E: 84, - 0x102F: 84, - 0x1030: 84, - 0x1032: 84, - 0x1033: 84, - 0x1034: 84, - 0x1035: 84, - 0x1036: 84, - 0x1037: 84, - 0x1039: 84, - 0x103A: 84, - 0x103D: 84, - 0x103E: 84, - 0x1058: 84, - 0x1059: 84, - 0x105E: 84, - 0x105F: 84, - 0x1060: 84, - 0x1071: 84, - 0x1072: 84, - 0x1073: 84, - 0x1074: 84, - 0x1082: 84, - 0x1085: 84, - 0x1086: 84, - 0x108D: 84, - 0x109D: 84, - 0x135D: 84, - 0x135E: 84, - 0x135F: 84, - 0x1712: 84, - 0x1713: 84, - 0x1714: 84, - 0x1732: 84, - 0x1733: 84, - 0x1752: 84, - 0x1753: 84, - 0x1772: 84, - 0x1773: 84, - 0x17B4: 84, - 0x17B5: 84, - 0x17B7: 84, - 0x17B8: 84, - 0x17B9: 84, - 0x17BA: 84, - 0x17BB: 84, - 0x17BC: 84, - 0x17BD: 84, - 0x17C6: 84, - 0x17C9: 84, - 0x17CA: 84, - 0x17CB: 84, - 0x17CC: 84, - 0x17CD: 84, - 0x17CE: 84, - 0x17CF: 84, - 0x17D0: 84, - 0x17D1: 84, - 0x17D2: 84, - 0x17D3: 84, - 0x17DD: 84, - 0x1807: 68, - 0x180A: 67, - 0x180B: 84, - 0x180C: 84, - 0x180D: 84, - 0x180F: 84, - 0x1820: 68, - 0x1821: 68, - 0x1822: 68, - 0x1823: 68, - 0x1824: 68, - 0x1825: 68, - 0x1826: 68, - 0x1827: 68, - 0x1828: 68, - 0x1829: 68, - 0x182A: 68, - 0x182B: 68, - 0x182C: 68, - 0x182D: 68, - 0x182E: 68, - 0x182F: 68, - 0x1830: 68, - 0x1831: 68, - 0x1832: 68, - 0x1833: 68, - 0x1834: 68, - 0x1835: 68, - 0x1836: 68, - 0x1837: 68, - 0x1838: 68, - 0x1839: 68, - 0x183A: 68, - 0x183B: 68, - 0x183C: 68, - 0x183D: 68, - 0x183E: 68, - 0x183F: 68, - 0x1840: 68, - 0x1841: 68, - 0x1842: 68, - 0x1843: 68, - 0x1844: 68, - 0x1845: 68, - 0x1846: 68, - 0x1847: 68, - 0x1848: 68, - 0x1849: 68, - 0x184A: 68, - 0x184B: 68, - 0x184C: 68, - 0x184D: 68, - 0x184E: 68, - 0x184F: 68, - 0x1850: 68, - 0x1851: 68, - 0x1852: 68, - 0x1853: 68, - 0x1854: 68, - 0x1855: 68, - 0x1856: 68, - 0x1857: 68, - 0x1858: 68, - 0x1859: 68, - 0x185A: 68, - 0x185B: 68, - 0x185C: 68, - 0x185D: 68, - 0x185E: 68, - 0x185F: 68, - 0x1860: 68, - 0x1861: 68, - 0x1862: 68, - 0x1863: 68, - 0x1864: 68, - 0x1865: 68, - 0x1866: 68, - 0x1867: 68, - 0x1868: 68, - 0x1869: 68, - 0x186A: 68, - 0x186B: 68, - 0x186C: 68, - 0x186D: 68, - 0x186E: 68, - 0x186F: 68, - 0x1870: 68, - 0x1871: 68, - 0x1872: 68, - 0x1873: 68, - 0x1874: 68, - 0x1875: 68, - 0x1876: 68, - 0x1877: 68, - 0x1878: 68, - 0x1885: 84, - 0x1886: 84, - 0x1887: 68, - 0x1888: 68, - 0x1889: 68, - 0x188A: 68, - 0x188B: 68, - 0x188C: 68, - 0x188D: 68, - 0x188E: 68, - 0x188F: 68, - 0x1890: 68, - 0x1891: 68, - 0x1892: 68, - 0x1893: 68, - 0x1894: 68, - 0x1895: 68, - 0x1896: 68, - 0x1897: 68, - 0x1898: 68, - 0x1899: 68, - 0x189A: 68, - 0x189B: 68, - 0x189C: 68, - 0x189D: 68, - 0x189E: 68, - 0x189F: 68, - 0x18A0: 68, - 0x18A1: 68, - 0x18A2: 68, - 0x18A3: 68, - 0x18A4: 68, - 0x18A5: 68, - 0x18A6: 68, - 0x18A7: 68, - 0x18A8: 68, - 0x18A9: 84, - 0x18AA: 68, - 0x1920: 84, - 0x1921: 84, - 0x1922: 84, - 0x1927: 84, - 0x1928: 84, - 0x1932: 84, - 0x1939: 84, - 0x193A: 84, - 0x193B: 84, - 0x1A17: 84, - 0x1A18: 84, - 0x1A1B: 84, - 0x1A56: 84, - 0x1A58: 84, - 0x1A59: 84, - 0x1A5A: 84, - 0x1A5B: 84, - 0x1A5C: 84, - 0x1A5D: 84, - 0x1A5E: 84, - 0x1A60: 84, - 0x1A62: 84, - 0x1A65: 84, - 0x1A66: 84, - 0x1A67: 84, - 0x1A68: 84, - 0x1A69: 84, - 0x1A6A: 84, - 0x1A6B: 84, - 0x1A6C: 84, - 0x1A73: 84, - 0x1A74: 84, - 0x1A75: 84, - 0x1A76: 84, - 0x1A77: 84, - 0x1A78: 84, - 0x1A79: 84, - 0x1A7A: 84, - 0x1A7B: 84, - 0x1A7C: 84, - 0x1A7F: 84, - 0x1AB0: 84, - 0x1AB1: 84, - 0x1AB2: 84, - 0x1AB3: 84, - 0x1AB4: 84, - 0x1AB5: 84, - 0x1AB6: 84, - 0x1AB7: 84, - 0x1AB8: 84, - 0x1AB9: 84, - 0x1ABA: 84, - 0x1ABB: 84, - 0x1ABC: 84, - 0x1ABD: 84, - 0x1ABE: 84, - 0x1ABF: 84, - 0x1AC0: 84, - 0x1AC1: 84, - 0x1AC2: 84, - 0x1AC3: 84, - 0x1AC4: 84, - 0x1AC5: 84, - 0x1AC6: 84, - 0x1AC7: 84, - 0x1AC8: 84, - 0x1AC9: 84, - 0x1ACA: 84, - 0x1ACB: 84, - 0x1ACC: 84, - 0x1ACD: 84, - 0x1ACE: 84, - 0x1B00: 84, - 0x1B01: 84, - 0x1B02: 84, - 0x1B03: 84, - 0x1B34: 84, - 0x1B36: 84, - 0x1B37: 84, - 0x1B38: 84, - 0x1B39: 84, - 0x1B3A: 84, - 0x1B3C: 84, - 0x1B42: 84, - 0x1B6B: 84, - 0x1B6C: 84, - 0x1B6D: 84, - 0x1B6E: 84, - 0x1B6F: 84, - 0x1B70: 84, - 0x1B71: 84, - 0x1B72: 84, - 0x1B73: 84, - 0x1B80: 84, - 0x1B81: 84, - 0x1BA2: 84, - 0x1BA3: 84, - 0x1BA4: 84, - 0x1BA5: 84, - 0x1BA8: 84, - 0x1BA9: 84, - 0x1BAB: 84, - 0x1BAC: 84, - 0x1BAD: 84, - 0x1BE6: 84, - 0x1BE8: 84, - 0x1BE9: 84, - 0x1BED: 84, - 0x1BEF: 84, - 0x1BF0: 84, - 0x1BF1: 84, - 0x1C2C: 84, - 0x1C2D: 84, - 0x1C2E: 84, - 0x1C2F: 84, - 0x1C30: 84, - 0x1C31: 84, - 0x1C32: 84, - 0x1C33: 84, - 0x1C36: 84, - 0x1C37: 84, - 0x1CD0: 84, - 0x1CD1: 84, - 0x1CD2: 84, - 0x1CD4: 84, - 0x1CD5: 84, - 0x1CD6: 84, - 0x1CD7: 84, - 0x1CD8: 84, - 0x1CD9: 84, - 0x1CDA: 84, - 0x1CDB: 84, - 0x1CDC: 84, - 0x1CDD: 84, - 0x1CDE: 84, - 0x1CDF: 84, - 0x1CE0: 84, - 0x1CE2: 84, - 0x1CE3: 84, - 0x1CE4: 84, - 0x1CE5: 84, - 0x1CE6: 84, - 0x1CE7: 84, - 0x1CE8: 84, - 0x1CED: 84, - 0x1CF4: 84, - 0x1CF8: 84, - 0x1CF9: 84, - 0x1DC0: 84, - 0x1DC1: 84, - 0x1DC2: 84, - 0x1DC3: 84, - 0x1DC4: 84, - 0x1DC5: 84, - 0x1DC6: 84, - 0x1DC7: 84, - 0x1DC8: 84, - 0x1DC9: 84, - 0x1DCA: 84, - 0x1DCB: 84, - 0x1DCC: 84, - 0x1DCD: 84, - 0x1DCE: 84, - 0x1DCF: 84, - 0x1DD0: 84, - 0x1DD1: 84, - 0x1DD2: 84, - 0x1DD3: 84, - 0x1DD4: 84, - 0x1DD5: 84, - 0x1DD6: 84, - 0x1DD7: 84, - 0x1DD8: 84, - 0x1DD9: 84, - 0x1DDA: 84, - 0x1DDB: 84, - 0x1DDC: 84, - 0x1DDD: 84, - 0x1DDE: 84, - 0x1DDF: 84, - 0x1DE0: 84, - 0x1DE1: 84, - 0x1DE2: 84, - 0x1DE3: 84, - 0x1DE4: 84, - 0x1DE5: 84, - 0x1DE6: 84, - 0x1DE7: 84, - 0x1DE8: 84, - 0x1DE9: 84, - 0x1DEA: 84, - 0x1DEB: 84, - 0x1DEC: 84, - 0x1DED: 84, - 0x1DEE: 84, - 0x1DEF: 84, - 0x1DF0: 84, - 0x1DF1: 84, - 0x1DF2: 84, - 0x1DF3: 84, - 0x1DF4: 84, - 0x1DF5: 84, - 0x1DF6: 84, - 0x1DF7: 84, - 0x1DF8: 84, - 0x1DF9: 84, - 0x1DFA: 84, - 0x1DFB: 84, - 0x1DFC: 84, - 0x1DFD: 84, - 0x1DFE: 84, - 0x1DFF: 84, - 0x200B: 84, - 0x200D: 67, - 0x200E: 84, - 0x200F: 84, - 0x202A: 84, - 0x202B: 84, - 0x202C: 84, - 0x202D: 84, - 0x202E: 84, - 0x2060: 84, - 0x2061: 84, - 0x2062: 84, - 0x2063: 84, - 0x2064: 84, - 0x206A: 84, - 0x206B: 84, - 0x206C: 84, - 0x206D: 84, - 0x206E: 84, - 0x206F: 84, - 0x20D0: 84, - 0x20D1: 84, - 0x20D2: 84, - 0x20D3: 84, - 0x20D4: 84, - 0x20D5: 84, - 0x20D6: 84, - 0x20D7: 84, - 0x20D8: 84, - 0x20D9: 84, - 0x20DA: 84, - 0x20DB: 84, - 0x20DC: 84, - 0x20DD: 84, - 0x20DE: 84, - 0x20DF: 84, - 0x20E0: 84, - 0x20E1: 84, - 0x20E2: 84, - 0x20E3: 84, - 0x20E4: 84, - 0x20E5: 84, - 0x20E6: 84, - 0x20E7: 84, - 0x20E8: 84, - 0x20E9: 84, - 0x20EA: 84, - 0x20EB: 84, - 0x20EC: 84, - 0x20ED: 84, - 0x20EE: 84, - 0x20EF: 84, - 0x20F0: 84, - 0x2CEF: 84, - 0x2CF0: 84, - 0x2CF1: 84, - 0x2D7F: 84, - 0x2DE0: 84, - 0x2DE1: 84, - 0x2DE2: 84, - 0x2DE3: 84, - 0x2DE4: 84, - 0x2DE5: 84, - 0x2DE6: 84, - 0x2DE7: 84, - 0x2DE8: 84, - 0x2DE9: 84, - 0x2DEA: 84, - 0x2DEB: 84, - 0x2DEC: 84, - 0x2DED: 84, - 0x2DEE: 84, - 0x2DEF: 84, - 0x2DF0: 84, - 0x2DF1: 84, - 0x2DF2: 84, - 0x2DF3: 84, - 0x2DF4: 84, - 0x2DF5: 84, - 0x2DF6: 84, - 0x2DF7: 84, - 0x2DF8: 84, - 0x2DF9: 84, - 0x2DFA: 84, - 0x2DFB: 84, - 0x2DFC: 84, - 0x2DFD: 84, - 0x2DFE: 84, - 0x2DFF: 84, - 0x302A: 84, - 0x302B: 84, - 0x302C: 84, - 0x302D: 84, - 0x3099: 84, - 0x309A: 84, - 0xA66F: 84, - 0xA670: 84, - 0xA671: 84, - 0xA672: 84, - 0xA674: 84, - 0xA675: 84, - 0xA676: 84, - 0xA677: 84, - 0xA678: 84, - 0xA679: 84, - 0xA67A: 84, - 0xA67B: 84, - 0xA67C: 84, - 0xA67D: 84, - 0xA69E: 84, - 0xA69F: 84, - 0xA6F0: 84, - 0xA6F1: 84, - 0xA802: 84, - 0xA806: 84, - 0xA80B: 84, - 0xA825: 84, - 0xA826: 84, - 0xA82C: 84, - 0xA840: 68, - 0xA841: 68, - 0xA842: 68, - 0xA843: 68, - 0xA844: 68, - 0xA845: 68, - 0xA846: 68, - 0xA847: 68, - 0xA848: 68, - 0xA849: 68, - 0xA84A: 68, - 0xA84B: 68, - 0xA84C: 68, - 0xA84D: 68, - 0xA84E: 68, - 0xA84F: 68, - 0xA850: 68, - 0xA851: 68, - 0xA852: 68, - 0xA853: 68, - 0xA854: 68, - 0xA855: 68, - 0xA856: 68, - 0xA857: 68, - 0xA858: 68, - 0xA859: 68, - 0xA85A: 68, - 0xA85B: 68, - 0xA85C: 68, - 0xA85D: 68, - 0xA85E: 68, - 0xA85F: 68, - 0xA860: 68, - 0xA861: 68, - 0xA862: 68, - 0xA863: 68, - 0xA864: 68, - 0xA865: 68, - 0xA866: 68, - 0xA867: 68, - 0xA868: 68, - 0xA869: 68, - 0xA86A: 68, - 0xA86B: 68, - 0xA86C: 68, - 0xA86D: 68, - 0xA86E: 68, - 0xA86F: 68, - 0xA870: 68, - 0xA871: 68, - 0xA872: 76, - 0xA8C4: 84, - 0xA8C5: 84, - 0xA8E0: 84, - 0xA8E1: 84, - 0xA8E2: 84, - 0xA8E3: 84, - 0xA8E4: 84, - 0xA8E5: 84, - 0xA8E6: 84, - 0xA8E7: 84, - 0xA8E8: 84, - 0xA8E9: 84, - 0xA8EA: 84, - 0xA8EB: 84, - 0xA8EC: 84, - 0xA8ED: 84, - 0xA8EE: 84, - 0xA8EF: 84, - 0xA8F0: 84, - 0xA8F1: 84, - 0xA8FF: 84, - 0xA926: 84, - 0xA927: 84, - 0xA928: 84, - 0xA929: 84, - 0xA92A: 84, - 0xA92B: 84, - 0xA92C: 84, - 0xA92D: 84, - 0xA947: 84, - 0xA948: 84, - 0xA949: 84, - 0xA94A: 84, - 0xA94B: 84, - 0xA94C: 84, - 0xA94D: 84, - 0xA94E: 84, - 0xA94F: 84, - 0xA950: 84, - 0xA951: 84, - 0xA980: 84, - 0xA981: 84, - 0xA982: 84, - 0xA9B3: 84, - 0xA9B6: 84, - 0xA9B7: 84, - 0xA9B8: 84, - 0xA9B9: 84, - 0xA9BC: 84, - 0xA9BD: 84, - 0xA9E5: 84, - 0xAA29: 84, - 0xAA2A: 84, - 0xAA2B: 84, - 0xAA2C: 84, - 0xAA2D: 84, - 0xAA2E: 84, - 0xAA31: 84, - 0xAA32: 84, - 0xAA35: 84, - 0xAA36: 84, - 0xAA43: 84, - 0xAA4C: 84, - 0xAA7C: 84, - 0xAAB0: 84, - 0xAAB2: 84, - 0xAAB3: 84, - 0xAAB4: 84, - 0xAAB7: 84, - 0xAAB8: 84, - 0xAABE: 84, - 0xAABF: 84, - 0xAAC1: 84, - 0xAAEC: 84, - 0xAAED: 84, - 0xAAF6: 84, - 0xABE5: 84, - 0xABE8: 84, - 0xABED: 84, - 0xFB1E: 84, - 0xFE00: 84, - 0xFE01: 84, - 0xFE02: 84, - 0xFE03: 84, - 0xFE04: 84, - 0xFE05: 84, - 0xFE06: 84, - 0xFE07: 84, - 0xFE08: 84, - 0xFE09: 84, - 0xFE0A: 84, - 0xFE0B: 84, - 0xFE0C: 84, - 0xFE0D: 84, - 0xFE0E: 84, - 0xFE0F: 84, - 0xFE20: 84, - 0xFE21: 84, - 0xFE22: 84, - 0xFE23: 84, - 0xFE24: 84, - 0xFE25: 84, - 0xFE26: 84, - 0xFE27: 84, - 0xFE28: 84, - 0xFE29: 84, - 0xFE2A: 84, - 0xFE2B: 84, - 0xFE2C: 84, - 0xFE2D: 84, - 0xFE2E: 84, - 0xFE2F: 84, - 0xFEFF: 84, - 0xFFF9: 84, - 0xFFFA: 84, - 0xFFFB: 84, - 0x101FD: 84, - 0x102E0: 84, - 0x10376: 84, - 0x10377: 84, - 0x10378: 84, - 0x10379: 84, - 0x1037A: 84, - 0x10A01: 84, - 0x10A02: 84, - 0x10A03: 84, - 0x10A05: 84, - 0x10A06: 84, - 0x10A0C: 84, - 0x10A0D: 84, - 0x10A0E: 84, - 0x10A0F: 84, - 0x10A38: 84, - 0x10A39: 84, - 0x10A3A: 84, - 0x10A3F: 84, - 0x10AC0: 68, - 0x10AC1: 68, - 0x10AC2: 68, - 0x10AC3: 68, - 0x10AC4: 68, - 0x10AC5: 82, - 0x10AC7: 82, - 0x10AC9: 82, - 0x10ACA: 82, - 0x10ACD: 76, - 0x10ACE: 82, - 0x10ACF: 82, - 0x10AD0: 82, - 0x10AD1: 82, - 0x10AD2: 82, - 0x10AD3: 68, - 0x10AD4: 68, - 0x10AD5: 68, - 0x10AD6: 68, - 0x10AD7: 76, - 0x10AD8: 68, - 0x10AD9: 68, - 0x10ADA: 68, - 0x10ADB: 68, - 0x10ADC: 68, - 0x10ADD: 82, - 0x10ADE: 68, - 0x10ADF: 68, - 0x10AE0: 68, - 0x10AE1: 82, - 0x10AE4: 82, - 0x10AE5: 84, - 0x10AE6: 84, - 0x10AEB: 68, - 0x10AEC: 68, - 0x10AED: 68, - 0x10AEE: 68, - 0x10AEF: 82, - 0x10B80: 68, - 0x10B81: 82, - 0x10B82: 68, - 0x10B83: 82, - 0x10B84: 82, - 0x10B85: 82, - 0x10B86: 68, - 0x10B87: 68, - 0x10B88: 68, - 0x10B89: 82, - 0x10B8A: 68, - 0x10B8B: 68, - 0x10B8C: 82, - 0x10B8D: 68, - 0x10B8E: 82, - 0x10B8F: 82, - 0x10B90: 68, - 0x10B91: 82, - 0x10BA9: 82, - 0x10BAA: 82, - 0x10BAB: 82, - 0x10BAC: 82, - 0x10BAD: 68, - 0x10BAE: 68, - 0x10D00: 76, - 0x10D01: 68, - 0x10D02: 68, - 0x10D03: 68, - 0x10D04: 68, - 0x10D05: 68, - 0x10D06: 68, - 0x10D07: 68, - 0x10D08: 68, - 0x10D09: 68, - 0x10D0A: 68, - 0x10D0B: 68, - 0x10D0C: 68, - 0x10D0D: 68, - 0x10D0E: 68, - 0x10D0F: 68, - 0x10D10: 68, - 0x10D11: 68, - 0x10D12: 68, - 0x10D13: 68, - 0x10D14: 68, - 0x10D15: 68, - 0x10D16: 68, - 0x10D17: 68, - 0x10D18: 68, - 0x10D19: 68, - 0x10D1A: 68, - 0x10D1B: 68, - 0x10D1C: 68, - 0x10D1D: 68, - 0x10D1E: 68, - 0x10D1F: 68, - 0x10D20: 68, - 0x10D21: 68, - 0x10D22: 82, - 0x10D23: 68, - 0x10D24: 84, - 0x10D25: 84, - 0x10D26: 84, - 0x10D27: 84, - 0x10D69: 84, - 0x10D6A: 84, - 0x10D6B: 84, - 0x10D6C: 84, - 0x10D6D: 84, - 0x10EAB: 84, - 0x10EAC: 84, - 0x10EC2: 82, - 0x10EC3: 68, - 0x10EC4: 68, - 0x10EFC: 84, - 0x10EFD: 84, - 0x10EFE: 84, - 0x10EFF: 84, - 0x10F30: 68, - 0x10F31: 68, - 0x10F32: 68, - 0x10F33: 82, - 0x10F34: 68, - 0x10F35: 68, - 0x10F36: 68, - 0x10F37: 68, - 0x10F38: 68, - 0x10F39: 68, - 0x10F3A: 68, - 0x10F3B: 68, - 0x10F3C: 68, - 0x10F3D: 68, - 0x10F3E: 68, - 0x10F3F: 68, - 0x10F40: 68, - 0x10F41: 68, - 0x10F42: 68, - 0x10F43: 68, - 0x10F44: 68, - 0x10F46: 84, - 0x10F47: 84, - 0x10F48: 84, - 0x10F49: 84, - 0x10F4A: 84, - 0x10F4B: 84, - 0x10F4C: 84, - 0x10F4D: 84, - 0x10F4E: 84, - 0x10F4F: 84, - 0x10F50: 84, - 0x10F51: 68, - 0x10F52: 68, - 0x10F53: 68, - 0x10F54: 82, - 0x10F70: 68, - 0x10F71: 68, - 0x10F72: 68, - 0x10F73: 68, - 0x10F74: 82, - 0x10F75: 82, - 0x10F76: 68, - 0x10F77: 68, - 0x10F78: 68, - 0x10F79: 68, - 0x10F7A: 68, - 0x10F7B: 68, - 0x10F7C: 68, - 0x10F7D: 68, - 0x10F7E: 68, - 0x10F7F: 68, - 0x10F80: 68, - 0x10F81: 68, - 0x10F82: 84, - 0x10F83: 84, - 0x10F84: 84, - 0x10F85: 84, - 0x10FB0: 68, - 0x10FB2: 68, - 0x10FB3: 68, - 0x10FB4: 82, - 0x10FB5: 82, - 0x10FB6: 82, - 0x10FB8: 68, - 0x10FB9: 82, - 0x10FBA: 82, - 0x10FBB: 68, - 0x10FBC: 68, - 0x10FBD: 82, - 0x10FBE: 68, - 0x10FBF: 68, - 0x10FC1: 68, - 0x10FC2: 82, - 0x10FC3: 82, - 0x10FC4: 68, - 0x10FC9: 82, - 0x10FCA: 68, - 0x10FCB: 76, - 0x11001: 84, - 0x11038: 84, - 0x11039: 84, - 0x1103A: 84, - 0x1103B: 84, - 0x1103C: 84, - 0x1103D: 84, - 0x1103E: 84, - 0x1103F: 84, - 0x11040: 84, - 0x11041: 84, - 0x11042: 84, - 0x11043: 84, - 0x11044: 84, - 0x11045: 84, - 0x11046: 84, - 0x11070: 84, - 0x11073: 84, - 0x11074: 84, - 0x1107F: 84, - 0x11080: 84, - 0x11081: 84, - 0x110B3: 84, - 0x110B4: 84, - 0x110B5: 84, - 0x110B6: 84, - 0x110B9: 84, - 0x110BA: 84, - 0x110C2: 84, - 0x11100: 84, - 0x11101: 84, - 0x11102: 84, - 0x11127: 84, - 0x11128: 84, - 0x11129: 84, - 0x1112A: 84, - 0x1112B: 84, - 0x1112D: 84, - 0x1112E: 84, - 0x1112F: 84, - 0x11130: 84, - 0x11131: 84, - 0x11132: 84, - 0x11133: 84, - 0x11134: 84, - 0x11173: 84, - 0x11180: 84, - 0x11181: 84, - 0x111B6: 84, - 0x111B7: 84, - 0x111B8: 84, - 0x111B9: 84, - 0x111BA: 84, - 0x111BB: 84, - 0x111BC: 84, - 0x111BD: 84, - 0x111BE: 84, - 0x111C9: 84, - 0x111CA: 84, - 0x111CB: 84, - 0x111CC: 84, - 0x111CF: 84, - 0x1122F: 84, - 0x11230: 84, - 0x11231: 84, - 0x11234: 84, - 0x11236: 84, - 0x11237: 84, - 0x1123E: 84, - 0x11241: 84, - 0x112DF: 84, - 0x112E3: 84, - 0x112E4: 84, - 0x112E5: 84, - 0x112E6: 84, - 0x112E7: 84, - 0x112E8: 84, - 0x112E9: 84, - 0x112EA: 84, - 0x11300: 84, - 0x11301: 84, - 0x1133B: 84, - 0x1133C: 84, - 0x11340: 84, - 0x11366: 84, - 0x11367: 84, - 0x11368: 84, - 0x11369: 84, - 0x1136A: 84, - 0x1136B: 84, - 0x1136C: 84, - 0x11370: 84, - 0x11371: 84, - 0x11372: 84, - 0x11373: 84, - 0x11374: 84, - 0x113BB: 84, - 0x113BC: 84, - 0x113BD: 84, - 0x113BE: 84, - 0x113BF: 84, - 0x113C0: 84, - 0x113CE: 84, - 0x113D0: 84, - 0x113D2: 84, - 0x113E1: 84, - 0x113E2: 84, - 0x11438: 84, - 0x11439: 84, - 0x1143A: 84, - 0x1143B: 84, - 0x1143C: 84, - 0x1143D: 84, - 0x1143E: 84, - 0x1143F: 84, - 0x11442: 84, - 0x11443: 84, - 0x11444: 84, - 0x11446: 84, - 0x1145E: 84, - 0x114B3: 84, - 0x114B4: 84, - 0x114B5: 84, - 0x114B6: 84, - 0x114B7: 84, - 0x114B8: 84, - 0x114BA: 84, - 0x114BF: 84, - 0x114C0: 84, - 0x114C2: 84, - 0x114C3: 84, - 0x115B2: 84, - 0x115B3: 84, - 0x115B4: 84, - 0x115B5: 84, - 0x115BC: 84, - 0x115BD: 84, - 0x115BF: 84, - 0x115C0: 84, - 0x115DC: 84, - 0x115DD: 84, - 0x11633: 84, - 0x11634: 84, - 0x11635: 84, - 0x11636: 84, - 0x11637: 84, - 0x11638: 84, - 0x11639: 84, - 0x1163A: 84, - 0x1163D: 84, - 0x1163F: 84, - 0x11640: 84, - 0x116AB: 84, - 0x116AD: 84, - 0x116B0: 84, - 0x116B1: 84, - 0x116B2: 84, - 0x116B3: 84, - 0x116B4: 84, - 0x116B5: 84, - 0x116B7: 84, - 0x1171D: 84, - 0x1171F: 84, - 0x11722: 84, - 0x11723: 84, - 0x11724: 84, - 0x11725: 84, - 0x11727: 84, - 0x11728: 84, - 0x11729: 84, - 0x1172A: 84, - 0x1172B: 84, - 0x1182F: 84, - 0x11830: 84, - 0x11831: 84, - 0x11832: 84, - 0x11833: 84, - 0x11834: 84, - 0x11835: 84, - 0x11836: 84, - 0x11837: 84, - 0x11839: 84, - 0x1183A: 84, - 0x1193B: 84, - 0x1193C: 84, - 0x1193E: 84, - 0x11943: 84, - 0x119D4: 84, - 0x119D5: 84, - 0x119D6: 84, - 0x119D7: 84, - 0x119DA: 84, - 0x119DB: 84, - 0x119E0: 84, - 0x11A01: 84, - 0x11A02: 84, - 0x11A03: 84, - 0x11A04: 84, - 0x11A05: 84, - 0x11A06: 84, - 0x11A07: 84, - 0x11A08: 84, - 0x11A09: 84, - 0x11A0A: 84, - 0x11A33: 84, - 0x11A34: 84, - 0x11A35: 84, - 0x11A36: 84, - 0x11A37: 84, - 0x11A38: 84, - 0x11A3B: 84, - 0x11A3C: 84, - 0x11A3D: 84, - 0x11A3E: 84, - 0x11A47: 84, - 0x11A51: 84, - 0x11A52: 84, - 0x11A53: 84, - 0x11A54: 84, - 0x11A55: 84, - 0x11A56: 84, - 0x11A59: 84, - 0x11A5A: 84, - 0x11A5B: 84, - 0x11A8A: 84, - 0x11A8B: 84, - 0x11A8C: 84, - 0x11A8D: 84, - 0x11A8E: 84, - 0x11A8F: 84, - 0x11A90: 84, - 0x11A91: 84, - 0x11A92: 84, - 0x11A93: 84, - 0x11A94: 84, - 0x11A95: 84, - 0x11A96: 84, - 0x11A98: 84, - 0x11A99: 84, - 0x11C30: 84, - 0x11C31: 84, - 0x11C32: 84, - 0x11C33: 84, - 0x11C34: 84, - 0x11C35: 84, - 0x11C36: 84, - 0x11C38: 84, - 0x11C39: 84, - 0x11C3A: 84, - 0x11C3B: 84, - 0x11C3C: 84, - 0x11C3D: 84, - 0x11C3F: 84, - 0x11C92: 84, - 0x11C93: 84, - 0x11C94: 84, - 0x11C95: 84, - 0x11C96: 84, - 0x11C97: 84, - 0x11C98: 84, - 0x11C99: 84, - 0x11C9A: 84, - 0x11C9B: 84, - 0x11C9C: 84, - 0x11C9D: 84, - 0x11C9E: 84, - 0x11C9F: 84, - 0x11CA0: 84, - 0x11CA1: 84, - 0x11CA2: 84, - 0x11CA3: 84, - 0x11CA4: 84, - 0x11CA5: 84, - 0x11CA6: 84, - 0x11CA7: 84, - 0x11CAA: 84, - 0x11CAB: 84, - 0x11CAC: 84, - 0x11CAD: 84, - 0x11CAE: 84, - 0x11CAF: 84, - 0x11CB0: 84, - 0x11CB2: 84, - 0x11CB3: 84, - 0x11CB5: 84, - 0x11CB6: 84, - 0x11D31: 84, - 0x11D32: 84, - 0x11D33: 84, - 0x11D34: 84, - 0x11D35: 84, - 0x11D36: 84, - 0x11D3A: 84, - 0x11D3C: 84, - 0x11D3D: 84, - 0x11D3F: 84, - 0x11D40: 84, - 0x11D41: 84, - 0x11D42: 84, - 0x11D43: 84, - 0x11D44: 84, - 0x11D45: 84, - 0x11D47: 84, - 0x11D90: 84, - 0x11D91: 84, - 0x11D95: 84, - 0x11D97: 84, - 0x11EF3: 84, - 0x11EF4: 84, - 0x11F00: 84, - 0x11F01: 84, - 0x11F36: 84, - 0x11F37: 84, - 0x11F38: 84, - 0x11F39: 84, - 0x11F3A: 84, - 0x11F40: 84, - 0x11F42: 84, - 0x11F5A: 84, - 0x13430: 84, - 0x13431: 84, - 0x13432: 84, - 0x13433: 84, - 0x13434: 84, - 0x13435: 84, - 0x13436: 84, - 0x13437: 84, - 0x13438: 84, - 0x13439: 84, - 0x1343A: 84, - 0x1343B: 84, - 0x1343C: 84, - 0x1343D: 84, - 0x1343E: 84, - 0x1343F: 84, - 0x13440: 84, - 0x13447: 84, - 0x13448: 84, - 0x13449: 84, - 0x1344A: 84, - 0x1344B: 84, - 0x1344C: 84, - 0x1344D: 84, - 0x1344E: 84, - 0x1344F: 84, - 0x13450: 84, - 0x13451: 84, - 0x13452: 84, - 0x13453: 84, - 0x13454: 84, - 0x13455: 84, - 0x1611E: 84, - 0x1611F: 84, - 0x16120: 84, - 0x16121: 84, - 0x16122: 84, - 0x16123: 84, - 0x16124: 84, - 0x16125: 84, - 0x16126: 84, - 0x16127: 84, - 0x16128: 84, - 0x16129: 84, - 0x1612D: 84, - 0x1612E: 84, - 0x1612F: 84, - 0x16AF0: 84, - 0x16AF1: 84, - 0x16AF2: 84, - 0x16AF3: 84, - 0x16AF4: 84, - 0x16B30: 84, - 0x16B31: 84, - 0x16B32: 84, - 0x16B33: 84, - 0x16B34: 84, - 0x16B35: 84, - 0x16B36: 84, - 0x16F4F: 84, - 0x16F8F: 84, - 0x16F90: 84, - 0x16F91: 84, - 0x16F92: 84, - 0x16FE4: 84, - 0x1BC9D: 84, - 0x1BC9E: 84, - 0x1BCA0: 84, - 0x1BCA1: 84, - 0x1BCA2: 84, - 0x1BCA3: 84, - 0x1CF00: 84, - 0x1CF01: 84, - 0x1CF02: 84, - 0x1CF03: 84, - 0x1CF04: 84, - 0x1CF05: 84, - 0x1CF06: 84, - 0x1CF07: 84, - 0x1CF08: 84, - 0x1CF09: 84, - 0x1CF0A: 84, - 0x1CF0B: 84, - 0x1CF0C: 84, - 0x1CF0D: 84, - 0x1CF0E: 84, - 0x1CF0F: 84, - 0x1CF10: 84, - 0x1CF11: 84, - 0x1CF12: 84, - 0x1CF13: 84, - 0x1CF14: 84, - 0x1CF15: 84, - 0x1CF16: 84, - 0x1CF17: 84, - 0x1CF18: 84, - 0x1CF19: 84, - 0x1CF1A: 84, - 0x1CF1B: 84, - 0x1CF1C: 84, - 0x1CF1D: 84, - 0x1CF1E: 84, - 0x1CF1F: 84, - 0x1CF20: 84, - 0x1CF21: 84, - 0x1CF22: 84, - 0x1CF23: 84, - 0x1CF24: 84, - 0x1CF25: 84, - 0x1CF26: 84, - 0x1CF27: 84, - 0x1CF28: 84, - 0x1CF29: 84, - 0x1CF2A: 84, - 0x1CF2B: 84, - 0x1CF2C: 84, - 0x1CF2D: 84, - 0x1CF30: 84, - 0x1CF31: 84, - 0x1CF32: 84, - 0x1CF33: 84, - 0x1CF34: 84, - 0x1CF35: 84, - 0x1CF36: 84, - 0x1CF37: 84, - 0x1CF38: 84, - 0x1CF39: 84, - 0x1CF3A: 84, - 0x1CF3B: 84, - 0x1CF3C: 84, - 0x1CF3D: 84, - 0x1CF3E: 84, - 0x1CF3F: 84, - 0x1CF40: 84, - 0x1CF41: 84, - 0x1CF42: 84, - 0x1CF43: 84, - 0x1CF44: 84, - 0x1CF45: 84, - 0x1CF46: 84, - 0x1D167: 84, - 0x1D168: 84, - 0x1D169: 84, - 0x1D173: 84, - 0x1D174: 84, - 0x1D175: 84, - 0x1D176: 84, - 0x1D177: 84, - 0x1D178: 84, - 0x1D179: 84, - 0x1D17A: 84, - 0x1D17B: 84, - 0x1D17C: 84, - 0x1D17D: 84, - 0x1D17E: 84, - 0x1D17F: 84, - 0x1D180: 84, - 0x1D181: 84, - 0x1D182: 84, - 0x1D185: 84, - 0x1D186: 84, - 0x1D187: 84, - 0x1D188: 84, - 0x1D189: 84, - 0x1D18A: 84, - 0x1D18B: 84, - 0x1D1AA: 84, - 0x1D1AB: 84, - 0x1D1AC: 84, - 0x1D1AD: 84, - 0x1D242: 84, - 0x1D243: 84, - 0x1D244: 84, - 0x1DA00: 84, - 0x1DA01: 84, - 0x1DA02: 84, - 0x1DA03: 84, - 0x1DA04: 84, - 0x1DA05: 84, - 0x1DA06: 84, - 0x1DA07: 84, - 0x1DA08: 84, - 0x1DA09: 84, - 0x1DA0A: 84, - 0x1DA0B: 84, - 0x1DA0C: 84, - 0x1DA0D: 84, - 0x1DA0E: 84, - 0x1DA0F: 84, - 0x1DA10: 84, - 0x1DA11: 84, - 0x1DA12: 84, - 0x1DA13: 84, - 0x1DA14: 84, - 0x1DA15: 84, - 0x1DA16: 84, - 0x1DA17: 84, - 0x1DA18: 84, - 0x1DA19: 84, - 0x1DA1A: 84, - 0x1DA1B: 84, - 0x1DA1C: 84, - 0x1DA1D: 84, - 0x1DA1E: 84, - 0x1DA1F: 84, - 0x1DA20: 84, - 0x1DA21: 84, - 0x1DA22: 84, - 0x1DA23: 84, - 0x1DA24: 84, - 0x1DA25: 84, - 0x1DA26: 84, - 0x1DA27: 84, - 0x1DA28: 84, - 0x1DA29: 84, - 0x1DA2A: 84, - 0x1DA2B: 84, - 0x1DA2C: 84, - 0x1DA2D: 84, - 0x1DA2E: 84, - 0x1DA2F: 84, - 0x1DA30: 84, - 0x1DA31: 84, - 0x1DA32: 84, - 0x1DA33: 84, - 0x1DA34: 84, - 0x1DA35: 84, - 0x1DA36: 84, - 0x1DA3B: 84, - 0x1DA3C: 84, - 0x1DA3D: 84, - 0x1DA3E: 84, - 0x1DA3F: 84, - 0x1DA40: 84, - 0x1DA41: 84, - 0x1DA42: 84, - 0x1DA43: 84, - 0x1DA44: 84, - 0x1DA45: 84, - 0x1DA46: 84, - 0x1DA47: 84, - 0x1DA48: 84, - 0x1DA49: 84, - 0x1DA4A: 84, - 0x1DA4B: 84, - 0x1DA4C: 84, - 0x1DA4D: 84, - 0x1DA4E: 84, - 0x1DA4F: 84, - 0x1DA50: 84, - 0x1DA51: 84, - 0x1DA52: 84, - 0x1DA53: 84, - 0x1DA54: 84, - 0x1DA55: 84, - 0x1DA56: 84, - 0x1DA57: 84, - 0x1DA58: 84, - 0x1DA59: 84, - 0x1DA5A: 84, - 0x1DA5B: 84, - 0x1DA5C: 84, - 0x1DA5D: 84, - 0x1DA5E: 84, - 0x1DA5F: 84, - 0x1DA60: 84, - 0x1DA61: 84, - 0x1DA62: 84, - 0x1DA63: 84, - 0x1DA64: 84, - 0x1DA65: 84, - 0x1DA66: 84, - 0x1DA67: 84, - 0x1DA68: 84, - 0x1DA69: 84, - 0x1DA6A: 84, - 0x1DA6B: 84, - 0x1DA6C: 84, - 0x1DA75: 84, - 0x1DA84: 84, - 0x1DA9B: 84, - 0x1DA9C: 84, - 0x1DA9D: 84, - 0x1DA9E: 84, - 0x1DA9F: 84, - 0x1DAA1: 84, - 0x1DAA2: 84, - 0x1DAA3: 84, - 0x1DAA4: 84, - 0x1DAA5: 84, - 0x1DAA6: 84, - 0x1DAA7: 84, - 0x1DAA8: 84, - 0x1DAA9: 84, - 0x1DAAA: 84, - 0x1DAAB: 84, - 0x1DAAC: 84, - 0x1DAAD: 84, - 0x1DAAE: 84, - 0x1DAAF: 84, - 0x1E000: 84, - 0x1E001: 84, - 0x1E002: 84, - 0x1E003: 84, - 0x1E004: 84, - 0x1E005: 84, - 0x1E006: 84, - 0x1E008: 84, - 0x1E009: 84, - 0x1E00A: 84, - 0x1E00B: 84, - 0x1E00C: 84, - 0x1E00D: 84, - 0x1E00E: 84, - 0x1E00F: 84, - 0x1E010: 84, - 0x1E011: 84, - 0x1E012: 84, - 0x1E013: 84, - 0x1E014: 84, - 0x1E015: 84, - 0x1E016: 84, - 0x1E017: 84, - 0x1E018: 84, - 0x1E01B: 84, - 0x1E01C: 84, - 0x1E01D: 84, - 0x1E01E: 84, - 0x1E01F: 84, - 0x1E020: 84, - 0x1E021: 84, - 0x1E023: 84, - 0x1E024: 84, - 0x1E026: 84, - 0x1E027: 84, - 0x1E028: 84, - 0x1E029: 84, - 0x1E02A: 84, - 0x1E08F: 84, - 0x1E130: 84, - 0x1E131: 84, - 0x1E132: 84, - 0x1E133: 84, - 0x1E134: 84, - 0x1E135: 84, - 0x1E136: 84, - 0x1E2AE: 84, - 0x1E2EC: 84, - 0x1E2ED: 84, - 0x1E2EE: 84, - 0x1E2EF: 84, - 0x1E4EC: 84, - 0x1E4ED: 84, - 0x1E4EE: 84, - 0x1E4EF: 84, - 0x1E5EE: 84, - 0x1E5EF: 84, - 0x1E8D0: 84, - 0x1E8D1: 84, - 0x1E8D2: 84, - 0x1E8D3: 84, - 0x1E8D4: 84, - 0x1E8D5: 84, - 0x1E8D6: 84, - 0x1E900: 68, - 0x1E901: 68, - 0x1E902: 68, - 0x1E903: 68, - 0x1E904: 68, - 0x1E905: 68, - 0x1E906: 68, - 0x1E907: 68, - 0x1E908: 68, - 0x1E909: 68, - 0x1E90A: 68, - 0x1E90B: 68, - 0x1E90C: 68, - 0x1E90D: 68, - 0x1E90E: 68, - 0x1E90F: 68, - 0x1E910: 68, - 0x1E911: 68, - 0x1E912: 68, - 0x1E913: 68, - 0x1E914: 68, - 0x1E915: 68, - 0x1E916: 68, - 0x1E917: 68, - 0x1E918: 68, - 0x1E919: 68, - 0x1E91A: 68, - 0x1E91B: 68, - 0x1E91C: 68, - 0x1E91D: 68, - 0x1E91E: 68, - 0x1E91F: 68, - 0x1E920: 68, - 0x1E921: 68, - 0x1E922: 68, - 0x1E923: 68, - 0x1E924: 68, - 0x1E925: 68, - 0x1E926: 68, - 0x1E927: 68, - 0x1E928: 68, - 0x1E929: 68, - 0x1E92A: 68, - 0x1E92B: 68, - 0x1E92C: 68, - 0x1E92D: 68, - 0x1E92E: 68, - 0x1E92F: 68, - 0x1E930: 68, - 0x1E931: 68, - 0x1E932: 68, - 0x1E933: 68, - 0x1E934: 68, - 0x1E935: 68, - 0x1E936: 68, - 0x1E937: 68, - 0x1E938: 68, - 0x1E939: 68, - 0x1E93A: 68, - 0x1E93B: 68, - 0x1E93C: 68, - 0x1E93D: 68, - 0x1E93E: 68, - 0x1E93F: 68, - 0x1E940: 68, - 0x1E941: 68, - 0x1E942: 68, - 0x1E943: 68, - 0x1E944: 84, - 0x1E945: 84, - 0x1E946: 84, - 0x1E947: 84, - 0x1E948: 84, - 0x1E949: 84, - 0x1E94A: 84, - 0x1E94B: 84, - 0xE0001: 84, - 0xE0020: 84, - 0xE0021: 84, - 0xE0022: 84, - 0xE0023: 84, - 0xE0024: 84, - 0xE0025: 84, - 0xE0026: 84, - 0xE0027: 84, - 0xE0028: 84, - 0xE0029: 84, - 0xE002A: 84, - 0xE002B: 84, - 0xE002C: 84, - 0xE002D: 84, - 0xE002E: 84, - 0xE002F: 84, - 0xE0030: 84, - 0xE0031: 84, - 0xE0032: 84, - 0xE0033: 84, - 0xE0034: 84, - 0xE0035: 84, - 0xE0036: 84, - 0xE0037: 84, - 0xE0038: 84, - 0xE0039: 84, - 0xE003A: 84, - 0xE003B: 84, - 0xE003C: 84, - 0xE003D: 84, - 0xE003E: 84, - 0xE003F: 84, - 0xE0040: 84, - 0xE0041: 84, - 0xE0042: 84, - 0xE0043: 84, - 0xE0044: 84, - 0xE0045: 84, - 0xE0046: 84, - 0xE0047: 84, - 0xE0048: 84, - 0xE0049: 84, - 0xE004A: 84, - 0xE004B: 84, - 0xE004C: 84, - 0xE004D: 84, - 0xE004E: 84, - 0xE004F: 84, - 0xE0050: 84, - 0xE0051: 84, - 0xE0052: 84, - 0xE0053: 84, - 0xE0054: 84, - 0xE0055: 84, - 0xE0056: 84, - 0xE0057: 84, - 0xE0058: 84, - 0xE0059: 84, - 0xE005A: 84, - 0xE005B: 84, - 0xE005C: 84, - 0xE005D: 84, - 0xE005E: 84, - 0xE005F: 84, - 0xE0060: 84, - 0xE0061: 84, - 0xE0062: 84, - 0xE0063: 84, - 0xE0064: 84, - 0xE0065: 84, - 0xE0066: 84, - 0xE0067: 84, - 0xE0068: 84, - 0xE0069: 84, - 0xE006A: 84, - 0xE006B: 84, - 0xE006C: 84, - 0xE006D: 84, - 0xE006E: 84, - 0xE006F: 84, - 0xE0070: 84, - 0xE0071: 84, - 0xE0072: 84, - 0xE0073: 84, - 0xE0074: 84, - 0xE0075: 84, - 0xE0076: 84, - 0xE0077: 84, - 0xE0078: 84, - 0xE0079: 84, - 0xE007A: 84, - 0xE007B: 84, - 0xE007C: 84, - 0xE007D: 84, - 0xE007E: 84, - 0xE007F: 84, - 0xE0100: 84, - 0xE0101: 84, - 0xE0102: 84, - 0xE0103: 84, - 0xE0104: 84, - 0xE0105: 84, - 0xE0106: 84, - 0xE0107: 84, - 0xE0108: 84, - 0xE0109: 84, - 0xE010A: 84, - 0xE010B: 84, - 0xE010C: 84, - 0xE010D: 84, - 0xE010E: 84, - 0xE010F: 84, - 0xE0110: 84, - 0xE0111: 84, - 0xE0112: 84, - 0xE0113: 84, - 0xE0114: 84, - 0xE0115: 84, - 0xE0116: 84, - 0xE0117: 84, - 0xE0118: 84, - 0xE0119: 84, - 0xE011A: 84, - 0xE011B: 84, - 0xE011C: 84, - 0xE011D: 84, - 0xE011E: 84, - 0xE011F: 84, - 0xE0120: 84, - 0xE0121: 84, - 0xE0122: 84, - 0xE0123: 84, - 0xE0124: 84, - 0xE0125: 84, - 0xE0126: 84, - 0xE0127: 84, - 0xE0128: 84, - 0xE0129: 84, - 0xE012A: 84, - 0xE012B: 84, - 0xE012C: 84, - 0xE012D: 84, - 0xE012E: 84, - 0xE012F: 84, - 0xE0130: 84, - 0xE0131: 84, - 0xE0132: 84, - 0xE0133: 84, - 0xE0134: 84, - 0xE0135: 84, - 0xE0136: 84, - 0xE0137: 84, - 0xE0138: 84, - 0xE0139: 84, - 0xE013A: 84, - 0xE013B: 84, - 0xE013C: 84, - 0xE013D: 84, - 0xE013E: 84, - 0xE013F: 84, - 0xE0140: 84, - 0xE0141: 84, - 0xE0142: 84, - 0xE0143: 84, - 0xE0144: 84, - 0xE0145: 84, - 0xE0146: 84, - 0xE0147: 84, - 0xE0148: 84, - 0xE0149: 84, - 0xE014A: 84, - 0xE014B: 84, - 0xE014C: 84, - 0xE014D: 84, - 0xE014E: 84, - 0xE014F: 84, - 0xE0150: 84, - 0xE0151: 84, - 0xE0152: 84, - 0xE0153: 84, - 0xE0154: 84, - 0xE0155: 84, - 0xE0156: 84, - 0xE0157: 84, - 0xE0158: 84, - 0xE0159: 84, - 0xE015A: 84, - 0xE015B: 84, - 0xE015C: 84, - 0xE015D: 84, - 0xE015E: 84, - 0xE015F: 84, - 0xE0160: 84, - 0xE0161: 84, - 0xE0162: 84, - 0xE0163: 84, - 0xE0164: 84, - 0xE0165: 84, - 0xE0166: 84, - 0xE0167: 84, - 0xE0168: 84, - 0xE0169: 84, - 0xE016A: 84, - 0xE016B: 84, - 0xE016C: 84, - 0xE016D: 84, - 0xE016E: 84, - 0xE016F: 84, - 0xE0170: 84, - 0xE0171: 84, - 0xE0172: 84, - 0xE0173: 84, - 0xE0174: 84, - 0xE0175: 84, - 0xE0176: 84, - 0xE0177: 84, - 0xE0178: 84, - 0xE0179: 84, - 0xE017A: 84, - 0xE017B: 84, - 0xE017C: 84, - 0xE017D: 84, - 0xE017E: 84, - 0xE017F: 84, - 0xE0180: 84, - 0xE0181: 84, - 0xE0182: 84, - 0xE0183: 84, - 0xE0184: 84, - 0xE0185: 84, - 0xE0186: 84, - 0xE0187: 84, - 0xE0188: 84, - 0xE0189: 84, - 0xE018A: 84, - 0xE018B: 84, - 0xE018C: 84, - 0xE018D: 84, - 0xE018E: 84, - 0xE018F: 84, - 0xE0190: 84, - 0xE0191: 84, - 0xE0192: 84, - 0xE0193: 84, - 0xE0194: 84, - 0xE0195: 84, - 0xE0196: 84, - 0xE0197: 84, - 0xE0198: 84, - 0xE0199: 84, - 0xE019A: 84, - 0xE019B: 84, - 0xE019C: 84, - 0xE019D: 84, - 0xE019E: 84, - 0xE019F: 84, - 0xE01A0: 84, - 0xE01A1: 84, - 0xE01A2: 84, - 0xE01A3: 84, - 0xE01A4: 84, - 0xE01A5: 84, - 0xE01A6: 84, - 0xE01A7: 84, - 0xE01A8: 84, - 0xE01A9: 84, - 0xE01AA: 84, - 0xE01AB: 84, - 0xE01AC: 84, - 0xE01AD: 84, - 0xE01AE: 84, - 0xE01AF: 84, - 0xE01B0: 84, - 0xE01B1: 84, - 0xE01B2: 84, - 0xE01B3: 84, - 0xE01B4: 84, - 0xE01B5: 84, - 0xE01B6: 84, - 0xE01B7: 84, - 0xE01B8: 84, - 0xE01B9: 84, - 0xE01BA: 84, - 0xE01BB: 84, - 0xE01BC: 84, - 0xE01BD: 84, - 0xE01BE: 84, - 0xE01BF: 84, - 0xE01C0: 84, - 0xE01C1: 84, - 0xE01C2: 84, - 0xE01C3: 84, - 0xE01C4: 84, - 0xE01C5: 84, - 0xE01C6: 84, - 0xE01C7: 84, - 0xE01C8: 84, - 0xE01C9: 84, - 0xE01CA: 84, - 0xE01CB: 84, - 0xE01CC: 84, - 0xE01CD: 84, - 0xE01CE: 84, - 0xE01CF: 84, - 0xE01D0: 84, - 0xE01D1: 84, - 0xE01D2: 84, - 0xE01D3: 84, - 0xE01D4: 84, - 0xE01D5: 84, - 0xE01D6: 84, - 0xE01D7: 84, - 0xE01D8: 84, - 0xE01D9: 84, - 0xE01DA: 84, - 0xE01DB: 84, - 0xE01DC: 84, - 0xE01DD: 84, - 0xE01DE: 84, - 0xE01DF: 84, - 0xE01E0: 84, - 0xE01E1: 84, - 0xE01E2: 84, - 0xE01E3: 84, - 0xE01E4: 84, - 0xE01E5: 84, - 0xE01E6: 84, - 0xE01E7: 84, - 0xE01E8: 84, - 0xE01E9: 84, - 0xE01EA: 84, - 0xE01EB: 84, - 0xE01EC: 84, - 0xE01ED: 84, - 0xE01EE: 84, - 0xE01EF: 84, + "C": ( + 0x64000000641, + 0x7FA000007FB, + 0x88300000886, + 0x180A0000180B, + 0x200D0000200E, + ), + "D": ( + 0x62000000621, + 0x62600000627, + 0x62800000629, + 0x62A0000062F, + 0x63300000640, + 0x64100000648, + 0x6490000064B, + 0x66E00000670, + 0x67800000688, + 0x69A000006C0, + 0x6C1000006C3, + 0x6CC000006CD, + 0x6CE000006CF, + 0x6D0000006D2, + 0x6FA000006FD, + 0x6FF00000700, + 0x71200000715, + 0x71A0000071E, + 0x71F00000728, + 0x7290000072A, + 0x72B0000072C, + 0x72D0000072F, + 0x74E00000759, + 0x75C0000076B, + 0x76D00000771, + 0x77200000773, + 0x77500000778, + 0x77A00000780, + 0x7CA000007EB, + 0x84100000846, + 0x84800000849, + 0x84A00000854, + 0x85500000856, + 0x86000000861, + 0x86200000866, + 0x86800000869, + 0x88600000887, + 0x8890000088E, + 0x88F00000890, + 0x8A0000008AA, + 0x8AF000008B1, + 0x8B3000008B9, + 0x8BA000008C9, + 0x180700001808, + 0x182000001879, + 0x1887000018A9, + 0x18AA000018AB, + 0xA8400000A872, + 0x10AC000010AC5, + 0x10AD300010AD7, + 0x10AD800010ADD, + 0x10ADE00010AE1, + 0x10AEB00010AEF, + 0x10B8000010B81, + 0x10B8200010B83, + 0x10B8600010B89, + 0x10B8A00010B8C, + 0x10B8D00010B8E, + 0x10B9000010B91, + 0x10BAD00010BAF, + 0x10D0100010D22, + 0x10D2300010D24, + 0x10EC300010EC5, + 0x10EC600010EC8, + 0x10F3000010F33, + 0x10F3400010F45, + 0x10F5100010F54, + 0x10F7000010F74, + 0x10F7600010F82, + 0x10FB000010FB1, + 0x10FB200010FB4, + 0x10FB800010FB9, + 0x10FBB00010FBD, + 0x10FBE00010FC0, + 0x10FC100010FC2, + 0x10FC400010FC5, + 0x10FCA00010FCB, + 0x1E9000001E944, + ), + "L": ( + 0xA8720000A873, + 0x10ACD00010ACE, + 0x10AD700010AD8, + 0x10D0000010D01, + 0x10FCB00010FCC, + ), + "R": ( + 0x62200000626, + 0x62700000628, + 0x6290000062A, + 0x62F00000633, + 0x64800000649, + 0x67100000674, + 0x67500000678, + 0x6880000069A, + 0x6C0000006C1, + 0x6C3000006CC, + 0x6CD000006CE, + 0x6CF000006D0, + 0x6D2000006D4, + 0x6D5000006D6, + 0x6EE000006F0, + 0x71000000711, + 0x7150000071A, + 0x71E0000071F, + 0x72800000729, + 0x72A0000072B, + 0x72C0000072D, + 0x72F00000730, + 0x74D0000074E, + 0x7590000075C, + 0x76B0000076D, + 0x77100000772, + 0x77300000775, + 0x7780000077A, + 0x84000000841, + 0x84600000848, + 0x8490000084A, + 0x85400000855, + 0x85600000859, + 0x86700000868, + 0x8690000086B, + 0x87000000883, + 0x88E0000088F, + 0x8AA000008AD, + 0x8AE000008AF, + 0x8B1000008B3, + 0x8B9000008BA, + 0x10AC500010AC6, + 0x10AC700010AC8, + 0x10AC900010ACB, + 0x10ACE00010AD3, + 0x10ADD00010ADE, + 0x10AE100010AE2, + 0x10AE400010AE5, + 0x10AEF00010AF0, + 0x10B8100010B82, + 0x10B8300010B86, + 0x10B8900010B8A, + 0x10B8C00010B8D, + 0x10B8E00010B90, + 0x10B9100010B92, + 0x10BA900010BAD, + 0x10D2200010D23, + 0x10EC200010EC3, + 0x10F3300010F34, + 0x10F5400010F55, + 0x10F7400010F76, + 0x10FB400010FB7, + 0x10FB900010FBB, + 0x10FBD00010FBE, + 0x10FC200010FC4, + 0x10FC900010FCA, + ), + "T": ( + 0xAD000000AE, + 0x30000000370, + 0x4830000048A, + 0x591000005BE, + 0x5BF000005C0, + 0x5C1000005C3, + 0x5C4000005C6, + 0x5C7000005C8, + 0x6100000061B, + 0x61C0000061D, + 0x64B00000660, + 0x67000000671, + 0x6D6000006DD, + 0x6DF000006E5, + 0x6E7000006E9, + 0x6EA000006EE, + 0x70F00000710, + 0x71100000712, + 0x7300000074B, + 0x7A6000007B1, + 0x7EB000007F4, + 0x7FD000007FE, + 0x8160000081A, + 0x81B00000824, + 0x82500000828, + 0x8290000082E, + 0x8590000085C, + 0x897000008A0, + 0x8CA000008E2, + 0x8E300000903, + 0x93A0000093B, + 0x93C0000093D, + 0x94100000949, + 0x94D0000094E, + 0x95100000958, + 0x96200000964, + 0x98100000982, + 0x9BC000009BD, + 0x9C1000009C5, + 0x9CD000009CE, + 0x9E2000009E4, + 0x9FE000009FF, + 0xA0100000A03, + 0xA3C00000A3D, + 0xA4100000A43, + 0xA4700000A49, + 0xA4B00000A4E, + 0xA5100000A52, + 0xA7000000A72, + 0xA7500000A76, + 0xA8100000A83, + 0xABC00000ABD, + 0xAC100000AC6, + 0xAC700000AC9, + 0xACD00000ACE, + 0xAE200000AE4, + 0xAFA00000B00, + 0xB0100000B02, + 0xB3C00000B3D, + 0xB3F00000B40, + 0xB4100000B45, + 0xB4D00000B4E, + 0xB5500000B57, + 0xB6200000B64, + 0xB8200000B83, + 0xBC000000BC1, + 0xBCD00000BCE, + 0xC0000000C01, + 0xC0400000C05, + 0xC3C00000C3D, + 0xC3E00000C41, + 0xC4600000C49, + 0xC4A00000C4E, + 0xC5500000C57, + 0xC6200000C64, + 0xC8100000C82, + 0xCBC00000CBD, + 0xCBF00000CC0, + 0xCC600000CC7, + 0xCCC00000CCE, + 0xCE200000CE4, + 0xD0000000D02, + 0xD3B00000D3D, + 0xD4100000D45, + 0xD4D00000D4E, + 0xD6200000D64, + 0xD8100000D82, + 0xDCA00000DCB, + 0xDD200000DD5, + 0xDD600000DD7, + 0xE3100000E32, + 0xE3400000E3B, + 0xE4700000E4F, + 0xEB100000EB2, + 0xEB400000EBD, + 0xEC800000ECF, + 0xF1800000F1A, + 0xF3500000F36, + 0xF3700000F38, + 0xF3900000F3A, + 0xF7100000F7F, + 0xF8000000F85, + 0xF8600000F88, + 0xF8D00000F98, + 0xF9900000FBD, + 0xFC600000FC7, + 0x102D00001031, + 0x103200001038, + 0x10390000103B, + 0x103D0000103F, + 0x10580000105A, + 0x105E00001061, + 0x107100001075, + 0x108200001083, + 0x108500001087, + 0x108D0000108E, + 0x109D0000109E, + 0x135D00001360, + 0x171200001715, + 0x173200001734, + 0x175200001754, + 0x177200001774, + 0x17B4000017B6, + 0x17B7000017BE, + 0x17C6000017C7, + 0x17C9000017D4, + 0x17DD000017DE, + 0x180B0000180E, + 0x180F00001810, + 0x188500001887, + 0x18A9000018AA, + 0x192000001923, + 0x192700001929, + 0x193200001933, + 0x19390000193C, + 0x1A1700001A19, + 0x1A1B00001A1C, + 0x1A5600001A57, + 0x1A5800001A5F, + 0x1A6000001A61, + 0x1A6200001A63, + 0x1A6500001A6D, + 0x1A7300001A7D, + 0x1A7F00001A80, + 0x1AB000001ADE, + 0x1AE000001AEC, + 0x1B0000001B04, + 0x1B3400001B35, + 0x1B3600001B3B, + 0x1B3C00001B3D, + 0x1B4200001B43, + 0x1B6B00001B74, + 0x1B8000001B82, + 0x1BA200001BA6, + 0x1BA800001BAA, + 0x1BAB00001BAE, + 0x1BE600001BE7, + 0x1BE800001BEA, + 0x1BED00001BEE, + 0x1BEF00001BF2, + 0x1C2C00001C34, + 0x1C3600001C38, + 0x1CD000001CD3, + 0x1CD400001CE1, + 0x1CE200001CE9, + 0x1CED00001CEE, + 0x1CF400001CF5, + 0x1CF800001CFA, + 0x1DC000001E00, + 0x200B0000200C, + 0x200E00002010, + 0x202A0000202F, + 0x206000002065, + 0x206A00002070, + 0x20D0000020F1, + 0x2CEF00002CF2, + 0x2D7F00002D80, + 0x2DE000002E00, + 0x302A0000302E, + 0x30990000309B, + 0xA66F0000A673, + 0xA6740000A67E, + 0xA69E0000A6A0, + 0xA6F00000A6F2, + 0xA8020000A803, + 0xA8060000A807, + 0xA80B0000A80C, + 0xA8250000A827, + 0xA82C0000A82D, + 0xA8C40000A8C6, + 0xA8E00000A8F2, + 0xA8FF0000A900, + 0xA9260000A92E, + 0xA9470000A952, + 0xA9800000A983, + 0xA9B30000A9B4, + 0xA9B60000A9BA, + 0xA9BC0000A9BE, + 0xA9E50000A9E6, + 0xAA290000AA2F, + 0xAA310000AA33, + 0xAA350000AA37, + 0xAA430000AA44, + 0xAA4C0000AA4D, + 0xAA7C0000AA7D, + 0xAAB00000AAB1, + 0xAAB20000AAB5, + 0xAAB70000AAB9, + 0xAABE0000AAC0, + 0xAAC10000AAC2, + 0xAAEC0000AAEE, + 0xAAF60000AAF7, + 0xABE50000ABE6, + 0xABE80000ABE9, + 0xABED0000ABEE, + 0xFB1E0000FB1F, + 0xFE000000FE10, + 0xFE200000FE30, + 0xFEFF0000FF00, + 0xFFF90000FFFC, + 0x101FD000101FE, + 0x102E0000102E1, + 0x103760001037B, + 0x10A0100010A04, + 0x10A0500010A07, + 0x10A0C00010A10, + 0x10A3800010A3B, + 0x10A3F00010A40, + 0x10AE500010AE7, + 0x10D2400010D28, + 0x10D6900010D6E, + 0x10EAB00010EAD, + 0x10EFA00010F00, + 0x10F4600010F51, + 0x10F8200010F86, + 0x1100100011002, + 0x1103800011047, + 0x1107000011071, + 0x1107300011075, + 0x1107F00011082, + 0x110B3000110B7, + 0x110B9000110BB, + 0x110C2000110C3, + 0x1110000011103, + 0x111270001112C, + 0x1112D00011135, + 0x1117300011174, + 0x1118000011182, + 0x111B6000111BF, + 0x111C9000111CD, + 0x111CF000111D0, + 0x1122F00011232, + 0x1123400011235, + 0x1123600011238, + 0x1123E0001123F, + 0x1124100011242, + 0x112DF000112E0, + 0x112E3000112EB, + 0x1130000011302, + 0x1133B0001133D, + 0x1134000011341, + 0x113660001136D, + 0x1137000011375, + 0x113BB000113C1, + 0x113CE000113CF, + 0x113D0000113D1, + 0x113D2000113D3, + 0x113E1000113E3, + 0x1143800011440, + 0x1144200011445, + 0x1144600011447, + 0x1145E0001145F, + 0x114B3000114B9, + 0x114BA000114BB, + 0x114BF000114C1, + 0x114C2000114C4, + 0x115B2000115B6, + 0x115BC000115BE, + 0x115BF000115C1, + 0x115DC000115DE, + 0x116330001163B, + 0x1163D0001163E, + 0x1163F00011641, + 0x116AB000116AC, + 0x116AD000116AE, + 0x116B0000116B6, + 0x116B7000116B8, + 0x1171D0001171E, + 0x1171F00011720, + 0x1172200011726, + 0x117270001172C, + 0x1182F00011838, + 0x118390001183B, + 0x1193B0001193D, + 0x1193E0001193F, + 0x1194300011944, + 0x119D4000119D8, + 0x119DA000119DC, + 0x119E0000119E1, + 0x11A0100011A0B, + 0x11A3300011A39, + 0x11A3B00011A3F, + 0x11A4700011A48, + 0x11A5100011A57, + 0x11A5900011A5C, + 0x11A8A00011A97, + 0x11A9800011A9A, + 0x11B6000011B61, + 0x11B6200011B65, + 0x11B6600011B67, + 0x11C3000011C37, + 0x11C3800011C3E, + 0x11C3F00011C40, + 0x11C9200011CA8, + 0x11CAA00011CB1, + 0x11CB200011CB4, + 0x11CB500011CB7, + 0x11D3100011D37, + 0x11D3A00011D3B, + 0x11D3C00011D3E, + 0x11D3F00011D46, + 0x11D4700011D48, + 0x11D9000011D92, + 0x11D9500011D96, + 0x11D9700011D98, + 0x11EF300011EF5, + 0x11F0000011F02, + 0x11F3600011F3B, + 0x11F4000011F41, + 0x11F4200011F43, + 0x11F5A00011F5B, + 0x1343000013441, + 0x1344700013456, + 0x1611E0001612A, + 0x1612D00016130, + 0x16AF000016AF5, + 0x16B3000016B37, + 0x16F4F00016F50, + 0x16F8F00016F93, + 0x16FE400016FE5, + 0x1BC9D0001BC9F, + 0x1BCA00001BCA4, + 0x1CF000001CF2E, + 0x1CF300001CF47, + 0x1D1670001D16A, + 0x1D1730001D183, + 0x1D1850001D18C, + 0x1D1AA0001D1AE, + 0x1D2420001D245, + 0x1DA000001DA37, + 0x1DA3B0001DA6D, + 0x1DA750001DA76, + 0x1DA840001DA85, + 0x1DA9B0001DAA0, + 0x1DAA10001DAB0, + 0x1E0000001E007, + 0x1E0080001E019, + 0x1E01B0001E022, + 0x1E0230001E025, + 0x1E0260001E02B, + 0x1E08F0001E090, + 0x1E1300001E137, + 0x1E2AE0001E2AF, + 0x1E2EC0001E2F0, + 0x1E4EC0001E4F0, + 0x1E5EE0001E5F0, + 0x1E6E30001E6E4, + 0x1E6E60001E6E7, + 0x1E6EE0001E6F0, + 0x1E6F50001E6F6, + 0x1E8D00001E8D7, + 0x1E9440001E94C, + 0xE0001000E0002, + 0xE0020000E0080, + 0xE0100000E01F0, + ), } + + codepoint_classes = { "PVALID": ( 0x2D0000002E, @@ -3366,7 +946,7 @@ codepoint_classes = { 0x8400000085C, 0x8600000086B, 0x87000000888, - 0x8890000088F, + 0x88900000890, 0x897000008E2, 0x8E300000958, 0x96000000964, @@ -3454,7 +1034,7 @@ codepoint_classes = { 0xC4A00000C4E, 0xC5500000C57, 0xC5800000C5B, - 0xC5D00000C5E, + 0xC5C00000C5E, 0xC6000000C64, 0xC6600000C70, 0xC8000000C84, @@ -3467,7 +1047,7 @@ codepoint_classes = { 0xCC600000CC9, 0xCCA00000CCE, 0xCD500000CD7, - 0xCDD00000CDF, + 0xCDC00000CDF, 0xCE000000CE4, 0xCE600000CF0, 0xCF100000CF4, @@ -3594,7 +1174,8 @@ codepoint_classes = { 0x1A9000001A9A, 0x1AA700001AA8, 0x1AB000001ABE, - 0x1ABF00001ACF, + 0x1ABF00001ADE, + 0x1AE000001AEC, 0x1B0000001B4D, 0x1B5000001B5A, 0x1B6B00001B74, @@ -3967,6 +1548,7 @@ codepoint_classes = { 0xA7C80000A7C9, 0xA7CA0000A7CB, 0xA7CD0000A7CE, + 0xA7CF0000A7D0, 0xA7D10000A7D2, 0xA7D30000A7D4, 0xA7D50000A7D6, @@ -4059,6 +1641,7 @@ codepoint_classes = { 0x108F4000108F6, 0x1090000010916, 0x109200001093A, + 0x109400001095A, 0x10980000109B8, 0x109BE000109C0, 0x10A0000010A04, @@ -4086,8 +1669,8 @@ codepoint_classes = { 0x10E8000010EAA, 0x10EAB00010EAD, 0x10EB000010EB2, - 0x10EC200010EC5, - 0x10EFC00010F1D, + 0x10EC200010EC8, + 0x10EFA00010F1D, 0x10F2700010F28, 0x10F3000010F51, 0x10F7000010F86, @@ -4181,6 +1764,7 @@ codepoint_classes = { 0x11A5000011A9A, 0x11A9D00011A9E, 0x11AB000011AF9, + 0x11B6000011B68, 0x11BC000011BE1, 0x11BF000011BFA, 0x11C0000011C09, @@ -4203,6 +1787,8 @@ codepoint_classes = { 0x11D9000011D92, 0x11D9300011D99, 0x11DA000011DAA, + 0x11DB000011DDC, + 0x11DE000011DEA, 0x11EE000011EF7, 0x11F0000011F11, 0x11F1200011F3B, @@ -4232,15 +1818,16 @@ codepoint_classes = { 0x16D4000016D6D, 0x16D7000016D7A, 0x16E6000016E80, + 0x16EBB00016ED4, 0x16F0000016F4B, 0x16F4F00016F88, 0x16F8F00016FA0, 0x16FE000016FE2, 0x16FE300016FE5, - 0x16FF000016FF2, - 0x17000000187F8, - 0x1880000018CD6, - 0x18CFF00018D09, + 0x16FF000016FF4, + 0x1700000018CD6, + 0x18CFF00018D1F, + 0x18D8000018DF3, 0x1AFF00001AFF4, 0x1AFF50001AFFC, 0x1AFFD0001AFFF, @@ -4255,7 +1842,6 @@ codepoint_classes = { 0x1BC800001BC89, 0x1BC900001BC9A, 0x1BC9D0001BC9F, - 0x1CCF00001CCFA, 0x1CF000001CF2E, 0x1CF300001CF47, 0x1DA000001DA37, @@ -4280,6 +1866,9 @@ codepoint_classes = { 0x1E2C00001E2FA, 0x1E4D00001E4FA, 0x1E5D00001E5FB, + 0x1E6C00001E6DF, + 0x1E6E00001E6F6, + 0x1E6FE0001E700, 0x1E7E00001E7E7, 0x1E7E80001E7EC, 0x1E7ED0001E7EF, @@ -4289,13 +1878,12 @@ codepoint_classes = { 0x1E9220001E94C, 0x1E9500001E95A, 0x200000002A6E0, - 0x2A7000002B73A, - 0x2B7400002B81E, - 0x2B8200002CEA2, + 0x2A7000002B81E, + 0x2B8200002CEAE, 0x2CEB00002EBE1, 0x2EBF00002EE5E, 0x300000003134B, - 0x31350000323B0, + 0x313500003347A, ), "CONTEXTJ": (0x200C0000200E,), "CONTEXTO": ( diff --git a/libs/idna/intranges.py b/libs/idna/intranges.py index 7bfaa8d80..19d77810c 100644 --- a/libs/idna/intranges.py +++ b/libs/idna/intranges.py @@ -6,10 +6,9 @@ in the original list?" in time O(log(# runs)). """ import bisect -from typing import List, Tuple -def intranges_from_list(list_: List[int]) -> Tuple[int, ...]: +def intranges_from_list(list_: list[int]) -> tuple[int, ...]: """Represent a list of integers as a sequence of ranges: ((start_0, end_0), (start_1, end_1), ...), such that the original integers are exactly those x such that start_i <= x < end_i for some i. @@ -21,9 +20,8 @@ def intranges_from_list(list_: List[int]) -> Tuple[int, ...]: ranges = [] last_write = -1 for i in range(len(sorted_list)): - if i + 1 < len(sorted_list): - if sorted_list[i] == sorted_list[i + 1] - 1: - continue + if i + 1 < len(sorted_list) and sorted_list[i] == sorted_list[i + 1] - 1: + continue current_range = sorted_list[last_write + 1 : i + 1] ranges.append(_encode_range(current_range[0], current_range[-1] + 1)) last_write = i @@ -35,11 +33,11 @@ def _encode_range(start: int, end: int) -> int: return (start << 32) | end -def _decode_range(r: int) -> Tuple[int, int]: +def _decode_range(r: int) -> tuple[int, int]: return (r >> 32), (r & ((1 << 32) - 1)) -def intranges_contain(int_: int, ranges: Tuple[int, ...]) -> bool: +def intranges_contain(int_: int, ranges: tuple[int, ...]) -> bool: """Determine if `int_` falls into one of the ranges in `ranges`.""" tuple_ = _encode_range(int_, 0) pos = bisect.bisect_left(ranges, tuple_) diff --git a/libs/idna/package_data.py b/libs/idna/package_data.py index 7272c8d92..94e40398c 100644 --- a/libs/idna/package_data.py +++ b/libs/idna/package_data.py @@ -1 +1 @@ -__version__ = "3.11" +__version__ = "3.18" diff --git a/libs/idna/uts46data.py b/libs/idna/uts46data.py index 4610b71da..f2d931fe5 100644 --- a/libs/idna/uts46data.py +++ b/libs/idna/uts46data.py @@ -1,8841 +1,16896 @@ # This file is automatically generated by tools/idna-data -# vim: set fileencoding=utf-8 : -from typing import List, Tuple, Union +from array import array +from typing import Optional """IDNA Mapping Table from UTS46.""" -__version__ = "16.0.0" +__version__ = "17.0.0" +uts46_starts: "array[int]" = array( + "I", + ( + 0x0, + 0x1, + 0x2, + 0x3, + 0x4, + 0x5, + 0x6, + 0x7, + 0x8, + 0x9, + 0xA, + 0xB, + 0xC, + 0xD, + 0xE, + 0xF, + 0x10, + 0x11, + 0x12, + 0x13, + 0x14, + 0x15, + 0x16, + 0x17, + 0x18, + 0x19, + 0x1A, + 0x1B, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0x20, + 0x21, + 0x22, + 0x23, + 0x24, + 0x25, + 0x26, + 0x27, + 0x28, + 0x29, + 0x2A, + 0x2B, + 0x2C, + 0x2D, + 0x2E, + 0x2F, + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + 0x40, + 0x41, + 0x42, + 0x43, + 0x44, + 0x45, + 0x46, + 0x47, + 0x48, + 0x49, + 0x4A, + 0x4B, + 0x4C, + 0x4D, + 0x4E, + 0x4F, + 0x50, + 0x51, + 0x52, + 0x53, + 0x54, + 0x55, + 0x56, + 0x57, + 0x58, + 0x59, + 0x5A, + 0x5B, + 0x5C, + 0x5D, + 0x5E, + 0x5F, + 0x60, + 0x61, + 0x62, + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6A, + 0x6B, + 0x6C, + 0x6D, + 0x6E, + 0x6F, + 0x70, + 0x71, + 0x72, + 0x73, + 0x74, + 0x75, + 0x76, + 0x77, + 0x78, + 0x79, + 0x7A, + 0x7B, + 0x7C, + 0x7D, + 0x7E, + 0x7F, + 0x80, + 0x81, + 0x82, + 0x83, + 0x84, + 0x85, + 0x86, + 0x87, + 0x88, + 0x89, + 0x8A, + 0x8B, + 0x8C, + 0x8D, + 0x8E, + 0x8F, + 0x90, + 0x91, + 0x92, + 0x93, + 0x94, + 0x95, + 0x96, + 0x97, + 0x98, + 0x99, + 0x9A, + 0x9B, + 0x9C, + 0x9D, + 0x9E, + 0x9F, + 0xA0, + 0xA1, + 0xA2, + 0xA3, + 0xA4, + 0xA5, + 0xA6, + 0xA7, + 0xA8, + 0xA9, + 0xAA, + 0xAB, + 0xAC, + 0xAD, + 0xAE, + 0xAF, + 0xB0, + 0xB1, + 0xB2, + 0xB3, + 0xB4, + 0xB5, + 0xB6, + 0xB7, + 0xB8, + 0xB9, + 0xBA, + 0xBB, + 0xBC, + 0xBD, + 0xBE, + 0xBF, + 0xC0, + 0xC1, + 0xC2, + 0xC3, + 0xC4, + 0xC5, + 0xC6, + 0xC7, + 0xC8, + 0xC9, + 0xCA, + 0xCB, + 0xCC, + 0xCD, + 0xCE, + 0xCF, + 0xD0, + 0xD1, + 0xD2, + 0xD3, + 0xD4, + 0xD5, + 0xD6, + 0xD7, + 0xD8, + 0xD9, + 0xDA, + 0xDB, + 0xDC, + 0xDD, + 0xDE, + 0xDF, + 0xE0, + 0xE1, + 0xE2, + 0xE3, + 0xE4, + 0xE5, + 0xE6, + 0xE7, + 0xE8, + 0xE9, + 0xEA, + 0xEB, + 0xEC, + 0xED, + 0xEE, + 0xEF, + 0xF0, + 0xF1, + 0xF2, + 0xF3, + 0xF4, + 0xF5, + 0xF6, + 0xF7, + 0xF8, + 0xF9, + 0xFA, + 0xFB, + 0xFC, + 0xFD, + 0xFE, + 0xFF, + 0x100, + 0x101, + 0x102, + 0x103, + 0x104, + 0x105, + 0x106, + 0x107, + 0x108, + 0x109, + 0x10A, + 0x10B, + 0x10C, + 0x10D, + 0x10E, + 0x10F, + 0x110, + 0x111, + 0x112, + 0x113, + 0x114, + 0x115, + 0x116, + 0x117, + 0x118, + 0x119, + 0x11A, + 0x11B, + 0x11C, + 0x11D, + 0x11E, + 0x11F, + 0x120, + 0x121, + 0x122, + 0x123, + 0x124, + 0x125, + 0x126, + 0x127, + 0x128, + 0x129, + 0x12A, + 0x12B, + 0x12C, + 0x12D, + 0x12E, + 0x12F, + 0x130, + 0x131, + 0x132, + 0x134, + 0x135, + 0x136, + 0x137, + 0x139, + 0x13A, + 0x13B, + 0x13C, + 0x13D, + 0x13E, + 0x13F, + 0x141, + 0x142, + 0x143, + 0x144, + 0x145, + 0x146, + 0x147, + 0x148, + 0x149, + 0x14A, + 0x14B, + 0x14C, + 0x14D, + 0x14E, + 0x14F, + 0x150, + 0x151, + 0x152, + 0x153, + 0x154, + 0x155, + 0x156, + 0x157, + 0x158, + 0x159, + 0x15A, + 0x15B, + 0x15C, + 0x15D, + 0x15E, + 0x15F, + 0x160, + 0x161, + 0x162, + 0x163, + 0x164, + 0x165, + 0x166, + 0x167, + 0x168, + 0x169, + 0x16A, + 0x16B, + 0x16C, + 0x16D, + 0x16E, + 0x16F, + 0x170, + 0x171, + 0x172, + 0x173, + 0x174, + 0x175, + 0x176, + 0x177, + 0x178, + 0x179, + 0x17A, + 0x17B, + 0x17C, + 0x17D, + 0x17E, + 0x17F, + 0x180, + 0x181, + 0x182, + 0x183, + 0x184, + 0x185, + 0x186, + 0x187, + 0x188, + 0x189, + 0x18A, + 0x18B, + 0x18C, + 0x18E, + 0x18F, + 0x190, + 0x191, + 0x192, + 0x193, + 0x194, + 0x195, + 0x196, + 0x197, + 0x198, + 0x199, + 0x19C, + 0x19D, + 0x19E, + 0x19F, + 0x1A0, + 0x1A1, + 0x1A2, + 0x1A3, + 0x1A4, + 0x1A5, + 0x1A6, + 0x1A7, + 0x1A8, + 0x1A9, + 0x1AA, + 0x1AC, + 0x1AD, + 0x1AE, + 0x1AF, + 0x1B0, + 0x1B1, + 0x1B2, + 0x1B3, + 0x1B4, + 0x1B5, + 0x1B6, + 0x1B7, + 0x1B8, + 0x1B9, + 0x1BC, + 0x1BD, + 0x1C4, + 0x1C7, + 0x1CA, + 0x1CD, + 0x1CE, + 0x1CF, + 0x1D0, + 0x1D1, + 0x1D2, + 0x1D3, + 0x1D4, + 0x1D5, + 0x1D6, + 0x1D7, + 0x1D8, + 0x1D9, + 0x1DA, + 0x1DB, + 0x1DC, + 0x1DE, + 0x1DF, + 0x1E0, + 0x1E1, + 0x1E2, + 0x1E3, + 0x1E4, + 0x1E5, + 0x1E6, + 0x1E7, + 0x1E8, + 0x1E9, + 0x1EA, + 0x1EB, + 0x1EC, + 0x1ED, + 0x1EE, + 0x1EF, + 0x1F1, + 0x1F4, + 0x1F5, + 0x1F6, + 0x1F7, + 0x1F8, + 0x1F9, + 0x1FA, + 0x1FB, + 0x1FC, + 0x1FD, + 0x1FE, + 0x1FF, + 0x200, + 0x201, + 0x202, + 0x203, + 0x204, + 0x205, + 0x206, + 0x207, + 0x208, + 0x209, + 0x20A, + 0x20B, + 0x20C, + 0x20D, + 0x20E, + 0x20F, + 0x210, + 0x211, + 0x212, + 0x213, + 0x214, + 0x215, + 0x216, + 0x217, + 0x218, + 0x219, + 0x21A, + 0x21B, + 0x21C, + 0x21D, + 0x21E, + 0x21F, + 0x220, + 0x221, + 0x222, + 0x223, + 0x224, + 0x225, + 0x226, + 0x227, + 0x228, + 0x229, + 0x22A, + 0x22B, + 0x22C, + 0x22D, + 0x22E, + 0x22F, + 0x230, + 0x231, + 0x232, + 0x233, + 0x23A, + 0x23B, + 0x23C, + 0x23D, + 0x23E, + 0x23F, + 0x241, + 0x242, + 0x243, + 0x244, + 0x245, + 0x246, + 0x247, + 0x248, + 0x249, + 0x24A, + 0x24B, + 0x24C, + 0x24D, + 0x24E, + 0x24F, + 0x2B0, + 0x2B1, + 0x2B2, + 0x2B3, + 0x2B4, + 0x2B5, + 0x2B6, + 0x2B7, + 0x2B8, + 0x2B9, + 0x2D8, + 0x2D9, + 0x2DA, + 0x2DB, + 0x2DC, + 0x2DD, + 0x2DE, + 0x2E0, + 0x2E1, + 0x2E2, + 0x2E3, + 0x2E4, + 0x2E5, + 0x340, + 0x341, + 0x342, + 0x343, + 0x344, + 0x345, + 0x346, + 0x34F, + 0x350, + 0x370, + 0x371, + 0x372, + 0x373, + 0x374, + 0x375, + 0x376, + 0x377, + 0x378, + 0x37A, + 0x37B, + 0x37E, + 0x37F, + 0x380, + 0x384, + 0x385, + 0x386, + 0x387, + 0x388, + 0x389, + 0x38A, + 0x38B, + 0x38C, + 0x38D, + 0x38E, + 0x38F, + 0x390, + 0x391, + 0x392, + 0x393, + 0x394, + 0x395, + 0x396, + 0x397, + 0x398, + 0x399, + 0x39A, + 0x39B, + 0x39C, + 0x39D, + 0x39E, + 0x39F, + 0x3A0, + 0x3A1, + 0x3A2, + 0x3A3, + 0x3A4, + 0x3A5, + 0x3A6, + 0x3A7, + 0x3A8, + 0x3A9, + 0x3AA, + 0x3AB, + 0x3AC, + 0x3C2, + 0x3C3, + 0x3CF, + 0x3D0, + 0x3D1, + 0x3D2, + 0x3D3, + 0x3D4, + 0x3D5, + 0x3D6, + 0x3D7, + 0x3D8, + 0x3D9, + 0x3DA, + 0x3DB, + 0x3DC, + 0x3DD, + 0x3DE, + 0x3DF, + 0x3E0, + 0x3E1, + 0x3E2, + 0x3E3, + 0x3E4, + 0x3E5, + 0x3E6, + 0x3E7, + 0x3E8, + 0x3E9, + 0x3EA, + 0x3EB, + 0x3EC, + 0x3ED, + 0x3EE, + 0x3EF, + 0x3F0, + 0x3F1, + 0x3F2, + 0x3F3, + 0x3F4, + 0x3F5, + 0x3F6, + 0x3F7, + 0x3F8, + 0x3F9, + 0x3FA, + 0x3FB, + 0x3FD, + 0x3FE, + 0x3FF, + 0x400, + 0x401, + 0x402, + 0x403, + 0x404, + 0x405, + 0x406, + 0x407, + 0x408, + 0x409, + 0x40A, + 0x40B, + 0x40C, + 0x40D, + 0x40E, + 0x40F, + 0x410, + 0x411, + 0x412, + 0x413, + 0x414, + 0x415, + 0x416, + 0x417, + 0x418, + 0x419, + 0x41A, + 0x41B, + 0x41C, + 0x41D, + 0x41E, + 0x41F, + 0x420, + 0x421, + 0x422, + 0x423, + 0x424, + 0x425, + 0x426, + 0x427, + 0x428, + 0x429, + 0x42A, + 0x42B, + 0x42C, + 0x42D, + 0x42E, + 0x42F, + 0x430, + 0x460, + 0x461, + 0x462, + 0x463, + 0x464, + 0x465, + 0x466, + 0x467, + 0x468, + 0x469, + 0x46A, + 0x46B, + 0x46C, + 0x46D, + 0x46E, + 0x46F, + 0x470, + 0x471, + 0x472, + 0x473, + 0x474, + 0x475, + 0x476, + 0x477, + 0x478, + 0x479, + 0x47A, + 0x47B, + 0x47C, + 0x47D, + 0x47E, + 0x47F, + 0x480, + 0x481, + 0x48A, + 0x48B, + 0x48C, + 0x48D, + 0x48E, + 0x48F, + 0x490, + 0x491, + 0x492, + 0x493, + 0x494, + 0x495, + 0x496, + 0x497, + 0x498, + 0x499, + 0x49A, + 0x49B, + 0x49C, + 0x49D, + 0x49E, + 0x49F, + 0x4A0, + 0x4A1, + 0x4A2, + 0x4A3, + 0x4A4, + 0x4A5, + 0x4A6, + 0x4A7, + 0x4A8, + 0x4A9, + 0x4AA, + 0x4AB, + 0x4AC, + 0x4AD, + 0x4AE, + 0x4AF, + 0x4B0, + 0x4B1, + 0x4B2, + 0x4B3, + 0x4B4, + 0x4B5, + 0x4B6, + 0x4B7, + 0x4B8, + 0x4B9, + 0x4BA, + 0x4BB, + 0x4BC, + 0x4BD, + 0x4BE, + 0x4BF, + 0x4C0, + 0x4C1, + 0x4C2, + 0x4C3, + 0x4C4, + 0x4C5, + 0x4C6, + 0x4C7, + 0x4C8, + 0x4C9, + 0x4CA, + 0x4CB, + 0x4CC, + 0x4CD, + 0x4CE, + 0x4D0, + 0x4D1, + 0x4D2, + 0x4D3, + 0x4D4, + 0x4D5, + 0x4D6, + 0x4D7, + 0x4D8, + 0x4D9, + 0x4DA, + 0x4DB, + 0x4DC, + 0x4DD, + 0x4DE, + 0x4DF, + 0x4E0, + 0x4E1, + 0x4E2, + 0x4E3, + 0x4E4, + 0x4E5, + 0x4E6, + 0x4E7, + 0x4E8, + 0x4E9, + 0x4EA, + 0x4EB, + 0x4EC, + 0x4ED, + 0x4EE, + 0x4EF, + 0x4F0, + 0x4F1, + 0x4F2, + 0x4F3, + 0x4F4, + 0x4F5, + 0x4F6, + 0x4F7, + 0x4F8, + 0x4F9, + 0x4FA, + 0x4FB, + 0x4FC, + 0x4FD, + 0x4FE, + 0x4FF, + 0x500, + 0x501, + 0x502, + 0x503, + 0x504, + 0x505, + 0x506, + 0x507, + 0x508, + 0x509, + 0x50A, + 0x50B, + 0x50C, + 0x50D, + 0x50E, + 0x50F, + 0x510, + 0x511, + 0x512, + 0x513, + 0x514, + 0x515, + 0x516, + 0x517, + 0x518, + 0x519, + 0x51A, + 0x51B, + 0x51C, + 0x51D, + 0x51E, + 0x51F, + 0x520, + 0x521, + 0x522, + 0x523, + 0x524, + 0x525, + 0x526, + 0x527, + 0x528, + 0x529, + 0x52A, + 0x52B, + 0x52C, + 0x52D, + 0x52E, + 0x52F, + 0x530, + 0x531, + 0x532, + 0x533, + 0x534, + 0x535, + 0x536, + 0x537, + 0x538, + 0x539, + 0x53A, + 0x53B, + 0x53C, + 0x53D, + 0x53E, + 0x53F, + 0x540, + 0x541, + 0x542, + 0x543, + 0x544, + 0x545, + 0x546, + 0x547, + 0x548, + 0x549, + 0x54A, + 0x54B, + 0x54C, + 0x54D, + 0x54E, + 0x54F, + 0x550, + 0x551, + 0x552, + 0x553, + 0x554, + 0x555, + 0x556, + 0x557, + 0x559, + 0x587, + 0x588, + 0x58B, + 0x58D, + 0x590, + 0x591, + 0x5C8, + 0x5D0, + 0x5EB, + 0x5EF, + 0x5F5, + 0x606, + 0x61C, + 0x61D, + 0x675, + 0x676, + 0x677, + 0x678, + 0x679, + 0x6DD, + 0x6DE, + 0x70E, + 0x710, + 0x74B, + 0x74D, + 0x7B2, + 0x7C0, + 0x7FB, + 0x7FD, + 0x82E, + 0x830, + 0x83F, + 0x840, + 0x85C, + 0x85E, + 0x85F, + 0x860, + 0x86B, + 0x870, + 0x890, + 0x897, + 0x8E2, + 0x8E3, + 0x958, + 0x959, + 0x95A, + 0x95B, + 0x95C, + 0x95D, + 0x95E, + 0x95F, + 0x960, + 0x984, + 0x985, + 0x98D, + 0x98F, + 0x991, + 0x993, + 0x9A9, + 0x9AA, + 0x9B1, + 0x9B2, + 0x9B3, + 0x9B6, + 0x9BA, + 0x9BC, + 0x9C5, + 0x9C7, + 0x9C9, + 0x9CB, + 0x9CF, + 0x9D7, + 0x9D8, + 0x9DC, + 0x9DD, + 0x9DE, + 0x9DF, + 0x9E0, + 0x9E4, + 0x9E6, + 0x9FF, + 0xA01, + 0xA04, + 0xA05, + 0xA0B, + 0xA0F, + 0xA11, + 0xA13, + 0xA29, + 0xA2A, + 0xA31, + 0xA32, + 0xA33, + 0xA34, + 0xA35, + 0xA36, + 0xA37, + 0xA38, + 0xA3A, + 0xA3C, + 0xA3D, + 0xA3E, + 0xA43, + 0xA47, + 0xA49, + 0xA4B, + 0xA4E, + 0xA51, + 0xA52, + 0xA59, + 0xA5A, + 0xA5B, + 0xA5C, + 0xA5D, + 0xA5E, + 0xA5F, + 0xA66, + 0xA77, + 0xA81, + 0xA84, + 0xA85, + 0xA8E, + 0xA8F, + 0xA92, + 0xA93, + 0xAA9, + 0xAAA, + 0xAB1, + 0xAB2, + 0xAB4, + 0xAB5, + 0xABA, + 0xABC, + 0xAC6, + 0xAC7, + 0xACA, + 0xACB, + 0xACE, + 0xAD0, + 0xAD1, + 0xAE0, + 0xAE4, + 0xAE6, + 0xAF2, + 0xAF9, + 0xB00, + 0xB01, + 0xB04, + 0xB05, + 0xB0D, + 0xB0F, + 0xB11, + 0xB13, + 0xB29, + 0xB2A, + 0xB31, + 0xB32, + 0xB34, + 0xB35, + 0xB3A, + 0xB3C, + 0xB45, + 0xB47, + 0xB49, + 0xB4B, + 0xB4E, + 0xB55, + 0xB58, + 0xB5C, + 0xB5D, + 0xB5E, + 0xB5F, + 0xB64, + 0xB66, + 0xB78, + 0xB82, + 0xB84, + 0xB85, + 0xB8B, + 0xB8E, + 0xB91, + 0xB92, + 0xB96, + 0xB99, + 0xB9B, + 0xB9C, + 0xB9D, + 0xB9E, + 0xBA0, + 0xBA3, + 0xBA5, + 0xBA8, + 0xBAB, + 0xBAE, + 0xBBA, + 0xBBE, + 0xBC3, + 0xBC6, + 0xBC9, + 0xBCA, + 0xBCE, + 0xBD0, + 0xBD1, + 0xBD7, + 0xBD8, + 0xBE6, + 0xBFB, + 0xC00, + 0xC0D, + 0xC0E, + 0xC11, + 0xC12, + 0xC29, + 0xC2A, + 0xC3A, + 0xC3C, + 0xC45, + 0xC46, + 0xC49, + 0xC4A, + 0xC4E, + 0xC55, + 0xC57, + 0xC58, + 0xC5B, + 0xC5C, + 0xC5E, + 0xC60, + 0xC64, + 0xC66, + 0xC70, + 0xC77, + 0xC8D, + 0xC8E, + 0xC91, + 0xC92, + 0xCA9, + 0xCAA, + 0xCB4, + 0xCB5, + 0xCBA, + 0xCBC, + 0xCC5, + 0xCC6, + 0xCC9, + 0xCCA, + 0xCCE, + 0xCD5, + 0xCD7, + 0xCDC, + 0xCDF, + 0xCE0, + 0xCE4, + 0xCE6, + 0xCF0, + 0xCF1, + 0xCF4, + 0xD00, + 0xD0D, + 0xD0E, + 0xD11, + 0xD12, + 0xD45, + 0xD46, + 0xD49, + 0xD4A, + 0xD50, + 0xD54, + 0xD64, + 0xD66, + 0xD80, + 0xD81, + 0xD84, + 0xD85, + 0xD97, + 0xD9A, + 0xDB2, + 0xDB3, + 0xDBC, + 0xDBD, + 0xDBE, + 0xDC0, + 0xDC7, + 0xDCA, + 0xDCB, + 0xDCF, + 0xDD5, + 0xDD6, + 0xDD7, + 0xDD8, + 0xDE0, + 0xDE6, + 0xDF0, + 0xDF2, + 0xDF5, + 0xE01, + 0xE33, + 0xE34, + 0xE3B, + 0xE3F, + 0xE5C, + 0xE81, + 0xE83, + 0xE84, + 0xE85, + 0xE86, + 0xE8B, + 0xE8C, + 0xEA4, + 0xEA5, + 0xEA6, + 0xEA7, + 0xEB3, + 0xEB4, + 0xEBE, + 0xEC0, + 0xEC5, + 0xEC6, + 0xEC7, + 0xEC8, + 0xECF, + 0xED0, + 0xEDA, + 0xEDC, + 0xEDD, + 0xEDE, + 0xEE0, + 0xF00, + 0xF0C, + 0xF0D, + 0xF43, + 0xF44, + 0xF48, + 0xF49, + 0xF4D, + 0xF4E, + 0xF52, + 0xF53, + 0xF57, + 0xF58, + 0xF5C, + 0xF5D, + 0xF69, + 0xF6A, + 0xF6D, + 0xF71, + 0xF73, + 0xF74, + 0xF75, + 0xF76, + 0xF77, + 0xF78, + 0xF79, + 0xF7A, + 0xF81, + 0xF82, + 0xF93, + 0xF94, + 0xF98, + 0xF99, + 0xF9D, + 0xF9E, + 0xFA2, + 0xFA3, + 0xFA7, + 0xFA8, + 0xFAC, + 0xFAD, + 0xFB9, + 0xFBA, + 0xFBD, + 0xFBE, + 0xFCD, + 0xFCE, + 0xFDB, + 0x1000, + 0x10A0, + 0x10A1, + 0x10A2, + 0x10A3, + 0x10A4, + 0x10A5, + 0x10A6, + 0x10A7, + 0x10A8, + 0x10A9, + 0x10AA, + 0x10AB, + 0x10AC, + 0x10AD, + 0x10AE, + 0x10AF, + 0x10B0, + 0x10B1, + 0x10B2, + 0x10B3, + 0x10B4, + 0x10B5, + 0x10B6, + 0x10B7, + 0x10B8, + 0x10B9, + 0x10BA, + 0x10BB, + 0x10BC, + 0x10BD, + 0x10BE, + 0x10BF, + 0x10C0, + 0x10C1, + 0x10C2, + 0x10C3, + 0x10C4, + 0x10C5, + 0x10C6, + 0x10C7, + 0x10C8, + 0x10CD, + 0x10CE, + 0x10D0, + 0x10FC, + 0x10FD, + 0x115F, + 0x1161, + 0x1249, + 0x124A, + 0x124E, + 0x1250, + 0x1257, + 0x1258, + 0x1259, + 0x125A, + 0x125E, + 0x1260, + 0x1289, + 0x128A, + 0x128E, + 0x1290, + 0x12B1, + 0x12B2, + 0x12B6, + 0x12B8, + 0x12BF, + 0x12C0, + 0x12C1, + 0x12C2, + 0x12C6, + 0x12C8, + 0x12D7, + 0x12D8, + 0x1311, + 0x1312, + 0x1316, + 0x1318, + 0x135B, + 0x135D, + 0x137D, + 0x1380, + 0x139A, + 0x13A0, + 0x13F6, + 0x13F8, + 0x13F9, + 0x13FA, + 0x13FB, + 0x13FC, + 0x13FD, + 0x13FE, + 0x1400, + 0x1680, + 0x1681, + 0x169D, + 0x16A0, + 0x16F9, + 0x1700, + 0x1716, + 0x171F, + 0x1737, + 0x1740, + 0x1754, + 0x1760, + 0x176D, + 0x176E, + 0x1771, + 0x1772, + 0x1774, + 0x1780, + 0x17B4, + 0x17B6, + 0x17DE, + 0x17E0, + 0x17EA, + 0x17F0, + 0x17FA, + 0x1800, + 0x180B, + 0x1810, + 0x181A, + 0x1820, + 0x1879, + 0x1880, + 0x18AB, + 0x18B0, + 0x18F6, + 0x1900, + 0x191F, + 0x1920, + 0x192C, + 0x1930, + 0x193C, + 0x1940, + 0x1941, + 0x1944, + 0x196E, + 0x1970, + 0x1975, + 0x1980, + 0x19AC, + 0x19B0, + 0x19CA, + 0x19D0, + 0x19DB, + 0x19DE, + 0x1A1C, + 0x1A1E, + 0x1A5F, + 0x1A60, + 0x1A7D, + 0x1A7F, + 0x1A8A, + 0x1A90, + 0x1A9A, + 0x1AA0, + 0x1AAE, + 0x1AB0, + 0x1ADE, + 0x1AE0, + 0x1AEC, + 0x1B00, + 0x1B4D, + 0x1B4E, + 0x1BF4, + 0x1BFC, + 0x1C38, + 0x1C3B, + 0x1C4A, + 0x1C4D, + 0x1C80, + 0x1C81, + 0x1C82, + 0x1C83, + 0x1C84, + 0x1C86, + 0x1C87, + 0x1C88, + 0x1C89, + 0x1C8A, + 0x1C8B, + 0x1C90, + 0x1C91, + 0x1C92, + 0x1C93, + 0x1C94, + 0x1C95, + 0x1C96, + 0x1C97, + 0x1C98, + 0x1C99, + 0x1C9A, + 0x1C9B, + 0x1C9C, + 0x1C9D, + 0x1C9E, + 0x1C9F, + 0x1CA0, + 0x1CA1, + 0x1CA2, + 0x1CA3, + 0x1CA4, + 0x1CA5, + 0x1CA6, + 0x1CA7, + 0x1CA8, + 0x1CA9, + 0x1CAA, + 0x1CAB, + 0x1CAC, + 0x1CAD, + 0x1CAE, + 0x1CAF, + 0x1CB0, + 0x1CB1, + 0x1CB2, + 0x1CB3, + 0x1CB4, + 0x1CB5, + 0x1CB6, + 0x1CB7, + 0x1CB8, + 0x1CB9, + 0x1CBA, + 0x1CBB, + 0x1CBD, + 0x1CBE, + 0x1CBF, + 0x1CC0, + 0x1CC8, + 0x1CD0, + 0x1CFB, + 0x1D00, + 0x1D2C, + 0x1D2D, + 0x1D2E, + 0x1D2F, + 0x1D30, + 0x1D31, + 0x1D32, + 0x1D33, + 0x1D34, + 0x1D35, + 0x1D36, + 0x1D37, + 0x1D38, + 0x1D39, + 0x1D3A, + 0x1D3B, + 0x1D3C, + 0x1D3D, + 0x1D3E, + 0x1D3F, + 0x1D40, + 0x1D41, + 0x1D42, + 0x1D43, + 0x1D44, + 0x1D45, + 0x1D46, + 0x1D47, + 0x1D48, + 0x1D49, + 0x1D4A, + 0x1D4B, + 0x1D4C, + 0x1D4D, + 0x1D4E, + 0x1D4F, + 0x1D50, + 0x1D51, + 0x1D52, + 0x1D53, + 0x1D54, + 0x1D55, + 0x1D56, + 0x1D57, + 0x1D58, + 0x1D59, + 0x1D5A, + 0x1D5B, + 0x1D5C, + 0x1D5D, + 0x1D5E, + 0x1D5F, + 0x1D60, + 0x1D61, + 0x1D62, + 0x1D63, + 0x1D64, + 0x1D65, + 0x1D66, + 0x1D67, + 0x1D68, + 0x1D69, + 0x1D6A, + 0x1D6B, + 0x1D78, + 0x1D79, + 0x1D9B, + 0x1D9C, + 0x1D9D, + 0x1D9E, + 0x1D9F, + 0x1DA0, + 0x1DA1, + 0x1DA2, + 0x1DA3, + 0x1DA4, + 0x1DA5, + 0x1DA6, + 0x1DA7, + 0x1DA8, + 0x1DA9, + 0x1DAA, + 0x1DAB, + 0x1DAC, + 0x1DAD, + 0x1DAE, + 0x1DAF, + 0x1DB0, + 0x1DB1, + 0x1DB2, + 0x1DB3, + 0x1DB4, + 0x1DB5, + 0x1DB6, + 0x1DB7, + 0x1DB8, + 0x1DB9, + 0x1DBA, + 0x1DBB, + 0x1DBC, + 0x1DBD, + 0x1DBE, + 0x1DBF, + 0x1DC0, + 0x1E00, + 0x1E01, + 0x1E02, + 0x1E03, + 0x1E04, + 0x1E05, + 0x1E06, + 0x1E07, + 0x1E08, + 0x1E09, + 0x1E0A, + 0x1E0B, + 0x1E0C, + 0x1E0D, + 0x1E0E, + 0x1E0F, + 0x1E10, + 0x1E11, + 0x1E12, + 0x1E13, + 0x1E14, + 0x1E15, + 0x1E16, + 0x1E17, + 0x1E18, + 0x1E19, + 0x1E1A, + 0x1E1B, + 0x1E1C, + 0x1E1D, + 0x1E1E, + 0x1E1F, + 0x1E20, + 0x1E21, + 0x1E22, + 0x1E23, + 0x1E24, + 0x1E25, + 0x1E26, + 0x1E27, + 0x1E28, + 0x1E29, + 0x1E2A, + 0x1E2B, + 0x1E2C, + 0x1E2D, + 0x1E2E, + 0x1E2F, + 0x1E30, + 0x1E31, + 0x1E32, + 0x1E33, + 0x1E34, + 0x1E35, + 0x1E36, + 0x1E37, + 0x1E38, + 0x1E39, + 0x1E3A, + 0x1E3B, + 0x1E3C, + 0x1E3D, + 0x1E3E, + 0x1E3F, + 0x1E40, + 0x1E41, + 0x1E42, + 0x1E43, + 0x1E44, + 0x1E45, + 0x1E46, + 0x1E47, + 0x1E48, + 0x1E49, + 0x1E4A, + 0x1E4B, + 0x1E4C, + 0x1E4D, + 0x1E4E, + 0x1E4F, + 0x1E50, + 0x1E51, + 0x1E52, + 0x1E53, + 0x1E54, + 0x1E55, + 0x1E56, + 0x1E57, + 0x1E58, + 0x1E59, + 0x1E5A, + 0x1E5B, + 0x1E5C, + 0x1E5D, + 0x1E5E, + 0x1E5F, + 0x1E60, + 0x1E61, + 0x1E62, + 0x1E63, + 0x1E64, + 0x1E65, + 0x1E66, + 0x1E67, + 0x1E68, + 0x1E69, + 0x1E6A, + 0x1E6B, + 0x1E6C, + 0x1E6D, + 0x1E6E, + 0x1E6F, + 0x1E70, + 0x1E71, + 0x1E72, + 0x1E73, + 0x1E74, + 0x1E75, + 0x1E76, + 0x1E77, + 0x1E78, + 0x1E79, + 0x1E7A, + 0x1E7B, + 0x1E7C, + 0x1E7D, + 0x1E7E, + 0x1E7F, + 0x1E80, + 0x1E81, + 0x1E82, + 0x1E83, + 0x1E84, + 0x1E85, + 0x1E86, + 0x1E87, + 0x1E88, + 0x1E89, + 0x1E8A, + 0x1E8B, + 0x1E8C, + 0x1E8D, + 0x1E8E, + 0x1E8F, + 0x1E90, + 0x1E91, + 0x1E92, + 0x1E93, + 0x1E94, + 0x1E95, + 0x1E9A, + 0x1E9B, + 0x1E9C, + 0x1E9E, + 0x1E9F, + 0x1EA0, + 0x1EA1, + 0x1EA2, + 0x1EA3, + 0x1EA4, + 0x1EA5, + 0x1EA6, + 0x1EA7, + 0x1EA8, + 0x1EA9, + 0x1EAA, + 0x1EAB, + 0x1EAC, + 0x1EAD, + 0x1EAE, + 0x1EAF, + 0x1EB0, + 0x1EB1, + 0x1EB2, + 0x1EB3, + 0x1EB4, + 0x1EB5, + 0x1EB6, + 0x1EB7, + 0x1EB8, + 0x1EB9, + 0x1EBA, + 0x1EBB, + 0x1EBC, + 0x1EBD, + 0x1EBE, + 0x1EBF, + 0x1EC0, + 0x1EC1, + 0x1EC2, + 0x1EC3, + 0x1EC4, + 0x1EC5, + 0x1EC6, + 0x1EC7, + 0x1EC8, + 0x1EC9, + 0x1ECA, + 0x1ECB, + 0x1ECC, + 0x1ECD, + 0x1ECE, + 0x1ECF, + 0x1ED0, + 0x1ED1, + 0x1ED2, + 0x1ED3, + 0x1ED4, + 0x1ED5, + 0x1ED6, + 0x1ED7, + 0x1ED8, + 0x1ED9, + 0x1EDA, + 0x1EDB, + 0x1EDC, + 0x1EDD, + 0x1EDE, + 0x1EDF, + 0x1EE0, + 0x1EE1, + 0x1EE2, + 0x1EE3, + 0x1EE4, + 0x1EE5, + 0x1EE6, + 0x1EE7, + 0x1EE8, + 0x1EE9, + 0x1EEA, + 0x1EEB, + 0x1EEC, + 0x1EED, + 0x1EEE, + 0x1EEF, + 0x1EF0, + 0x1EF1, + 0x1EF2, + 0x1EF3, + 0x1EF4, + 0x1EF5, + 0x1EF6, + 0x1EF7, + 0x1EF8, + 0x1EF9, + 0x1EFA, + 0x1EFB, + 0x1EFC, + 0x1EFD, + 0x1EFE, + 0x1EFF, + 0x1F08, + 0x1F09, + 0x1F0A, + 0x1F0B, + 0x1F0C, + 0x1F0D, + 0x1F0E, + 0x1F0F, + 0x1F10, + 0x1F16, + 0x1F18, + 0x1F19, + 0x1F1A, + 0x1F1B, + 0x1F1C, + 0x1F1D, + 0x1F1E, + 0x1F20, + 0x1F28, + 0x1F29, + 0x1F2A, + 0x1F2B, + 0x1F2C, + 0x1F2D, + 0x1F2E, + 0x1F2F, + 0x1F30, + 0x1F38, + 0x1F39, + 0x1F3A, + 0x1F3B, + 0x1F3C, + 0x1F3D, + 0x1F3E, + 0x1F3F, + 0x1F40, + 0x1F46, + 0x1F48, + 0x1F49, + 0x1F4A, + 0x1F4B, + 0x1F4C, + 0x1F4D, + 0x1F4E, + 0x1F50, + 0x1F58, + 0x1F59, + 0x1F5A, + 0x1F5B, + 0x1F5C, + 0x1F5D, + 0x1F5E, + 0x1F5F, + 0x1F60, + 0x1F68, + 0x1F69, + 0x1F6A, + 0x1F6B, + 0x1F6C, + 0x1F6D, + 0x1F6E, + 0x1F6F, + 0x1F70, + 0x1F71, + 0x1F72, + 0x1F73, + 0x1F74, + 0x1F75, + 0x1F76, + 0x1F77, + 0x1F78, + 0x1F79, + 0x1F7A, + 0x1F7B, + 0x1F7C, + 0x1F7D, + 0x1F7E, + 0x1F80, + 0x1F81, + 0x1F82, + 0x1F83, + 0x1F84, + 0x1F85, + 0x1F86, + 0x1F87, + 0x1F88, + 0x1F89, + 0x1F8A, + 0x1F8B, + 0x1F8C, + 0x1F8D, + 0x1F8E, + 0x1F8F, + 0x1F90, + 0x1F91, + 0x1F92, + 0x1F93, + 0x1F94, + 0x1F95, + 0x1F96, + 0x1F97, + 0x1F98, + 0x1F99, + 0x1F9A, + 0x1F9B, + 0x1F9C, + 0x1F9D, + 0x1F9E, + 0x1F9F, + 0x1FA0, + 0x1FA1, + 0x1FA2, + 0x1FA3, + 0x1FA4, + 0x1FA5, + 0x1FA6, + 0x1FA7, + 0x1FA8, + 0x1FA9, + 0x1FAA, + 0x1FAB, + 0x1FAC, + 0x1FAD, + 0x1FAE, + 0x1FAF, + 0x1FB0, + 0x1FB2, + 0x1FB3, + 0x1FB4, + 0x1FB5, + 0x1FB6, + 0x1FB7, + 0x1FB8, + 0x1FB9, + 0x1FBA, + 0x1FBB, + 0x1FBC, + 0x1FBD, + 0x1FBE, + 0x1FBF, + 0x1FC0, + 0x1FC1, + 0x1FC2, + 0x1FC3, + 0x1FC4, + 0x1FC5, + 0x1FC6, + 0x1FC7, + 0x1FC8, + 0x1FC9, + 0x1FCA, + 0x1FCB, + 0x1FCC, + 0x1FCD, + 0x1FCE, + 0x1FCF, + 0x1FD0, + 0x1FD3, + 0x1FD4, + 0x1FD6, + 0x1FD8, + 0x1FD9, + 0x1FDA, + 0x1FDB, + 0x1FDC, + 0x1FDD, + 0x1FDE, + 0x1FDF, + 0x1FE0, + 0x1FE3, + 0x1FE4, + 0x1FE8, + 0x1FE9, + 0x1FEA, + 0x1FEB, + 0x1FEC, + 0x1FED, + 0x1FEE, + 0x1FEF, + 0x1FF0, + 0x1FF2, + 0x1FF3, + 0x1FF4, + 0x1FF5, + 0x1FF6, + 0x1FF7, + 0x1FF8, + 0x1FF9, + 0x1FFA, + 0x1FFB, + 0x1FFC, + 0x1FFD, + 0x1FFE, + 0x1FFF, + 0x2000, + 0x200B, + 0x200C, + 0x200E, + 0x2010, + 0x2011, + 0x2012, + 0x2017, + 0x2018, + 0x2024, + 0x2027, + 0x2028, + 0x202F, + 0x2030, + 0x2033, + 0x2034, + 0x2035, + 0x2036, + 0x2037, + 0x2038, + 0x203C, + 0x203D, + 0x203E, + 0x203F, + 0x2047, + 0x2048, + 0x2049, + 0x204A, + 0x2057, + 0x2058, + 0x205F, + 0x2060, + 0x2065, + 0x206A, + 0x2070, + 0x2071, + 0x2072, + 0x2074, + 0x2075, + 0x2076, + 0x2077, + 0x2078, + 0x2079, + 0x207A, + 0x207B, + 0x207C, + 0x207D, + 0x207E, + 0x207F, + 0x2080, + 0x2081, + 0x2082, + 0x2083, + 0x2084, + 0x2085, + 0x2086, + 0x2087, + 0x2088, + 0x2089, + 0x208A, + 0x208B, + 0x208C, + 0x208D, + 0x208E, + 0x208F, + 0x2090, + 0x2091, + 0x2092, + 0x2093, + 0x2094, + 0x2095, + 0x2096, + 0x2097, + 0x2098, + 0x2099, + 0x209A, + 0x209B, + 0x209C, + 0x209D, + 0x20A0, + 0x20A8, + 0x20A9, + 0x20C2, + 0x20D0, + 0x20F1, + 0x2100, + 0x2101, + 0x2102, + 0x2103, + 0x2104, + 0x2105, + 0x2106, + 0x2107, + 0x2108, + 0x2109, + 0x210A, + 0x210B, + 0x210F, + 0x2110, + 0x2112, + 0x2114, + 0x2115, + 0x2116, + 0x2117, + 0x2119, + 0x211A, + 0x211B, + 0x211E, + 0x2120, + 0x2121, + 0x2122, + 0x2123, + 0x2124, + 0x2125, + 0x2126, + 0x2127, + 0x2128, + 0x2129, + 0x212A, + 0x212B, + 0x212C, + 0x212D, + 0x212E, + 0x212F, + 0x2131, + 0x2132, + 0x2133, + 0x2134, + 0x2135, + 0x2136, + 0x2137, + 0x2138, + 0x2139, + 0x213A, + 0x213B, + 0x213C, + 0x213D, + 0x213F, + 0x2140, + 0x2141, + 0x2145, + 0x2147, + 0x2148, + 0x2149, + 0x214A, + 0x2150, + 0x2151, + 0x2152, + 0x2153, + 0x2154, + 0x2155, + 0x2156, + 0x2157, + 0x2158, + 0x2159, + 0x215A, + 0x215B, + 0x215C, + 0x215D, + 0x215E, + 0x215F, + 0x2160, + 0x2161, + 0x2162, + 0x2163, + 0x2164, + 0x2165, + 0x2166, + 0x2167, + 0x2168, + 0x2169, + 0x216A, + 0x216B, + 0x216C, + 0x216D, + 0x216E, + 0x216F, + 0x2170, + 0x2171, + 0x2172, + 0x2173, + 0x2174, + 0x2175, + 0x2176, + 0x2177, + 0x2178, + 0x2179, + 0x217A, + 0x217B, + 0x217C, + 0x217D, + 0x217E, + 0x217F, + 0x2180, + 0x2183, + 0x2184, + 0x2189, + 0x218A, + 0x218C, + 0x2190, + 0x222C, + 0x222D, + 0x222E, + 0x222F, + 0x2230, + 0x2231, + 0x2329, + 0x232A, + 0x232B, + 0x242A, + 0x2440, + 0x244B, + 0x2460, + 0x2461, + 0x2462, + 0x2463, + 0x2464, + 0x2465, + 0x2466, + 0x2467, + 0x2468, + 0x2469, + 0x246A, + 0x246B, + 0x246C, + 0x246D, + 0x246E, + 0x246F, + 0x2470, + 0x2471, + 0x2472, + 0x2473, + 0x2474, + 0x2475, + 0x2476, + 0x2477, + 0x2478, + 0x2479, + 0x247A, + 0x247B, + 0x247C, + 0x247D, + 0x247E, + 0x247F, + 0x2480, + 0x2481, + 0x2482, + 0x2483, + 0x2484, + 0x2485, + 0x2486, + 0x2487, + 0x2488, + 0x249C, + 0x249D, + 0x249E, + 0x249F, + 0x24A0, + 0x24A1, + 0x24A2, + 0x24A3, + 0x24A4, + 0x24A5, + 0x24A6, + 0x24A7, + 0x24A8, + 0x24A9, + 0x24AA, + 0x24AB, + 0x24AC, + 0x24AD, + 0x24AE, + 0x24AF, + 0x24B0, + 0x24B1, + 0x24B2, + 0x24B3, + 0x24B4, + 0x24B5, + 0x24B6, + 0x24B7, + 0x24B8, + 0x24B9, + 0x24BA, + 0x24BB, + 0x24BC, + 0x24BD, + 0x24BE, + 0x24BF, + 0x24C0, + 0x24C1, + 0x24C2, + 0x24C3, + 0x24C4, + 0x24C5, + 0x24C6, + 0x24C7, + 0x24C8, + 0x24C9, + 0x24CA, + 0x24CB, + 0x24CC, + 0x24CD, + 0x24CE, + 0x24CF, + 0x24D0, + 0x24D1, + 0x24D2, + 0x24D3, + 0x24D4, + 0x24D5, + 0x24D6, + 0x24D7, + 0x24D8, + 0x24D9, + 0x24DA, + 0x24DB, + 0x24DC, + 0x24DD, + 0x24DE, + 0x24DF, + 0x24E0, + 0x24E1, + 0x24E2, + 0x24E3, + 0x24E4, + 0x24E5, + 0x24E6, + 0x24E7, + 0x24E8, + 0x24E9, + 0x24EA, + 0x24EB, + 0x2A0C, + 0x2A0D, + 0x2A74, + 0x2A75, + 0x2A76, + 0x2A77, + 0x2ADC, + 0x2ADD, + 0x2B74, + 0x2B76, + 0x2C00, + 0x2C01, + 0x2C02, + 0x2C03, + 0x2C04, + 0x2C05, + 0x2C06, + 0x2C07, + 0x2C08, + 0x2C09, + 0x2C0A, + 0x2C0B, + 0x2C0C, + 0x2C0D, + 0x2C0E, + 0x2C0F, + 0x2C10, + 0x2C11, + 0x2C12, + 0x2C13, + 0x2C14, + 0x2C15, + 0x2C16, + 0x2C17, + 0x2C18, + 0x2C19, + 0x2C1A, + 0x2C1B, + 0x2C1C, + 0x2C1D, + 0x2C1E, + 0x2C1F, + 0x2C20, + 0x2C21, + 0x2C22, + 0x2C23, + 0x2C24, + 0x2C25, + 0x2C26, + 0x2C27, + 0x2C28, + 0x2C29, + 0x2C2A, + 0x2C2B, + 0x2C2C, + 0x2C2D, + 0x2C2E, + 0x2C2F, + 0x2C30, + 0x2C60, + 0x2C61, + 0x2C62, + 0x2C63, + 0x2C64, + 0x2C65, + 0x2C67, + 0x2C68, + 0x2C69, + 0x2C6A, + 0x2C6B, + 0x2C6C, + 0x2C6D, + 0x2C6E, + 0x2C6F, + 0x2C70, + 0x2C71, + 0x2C72, + 0x2C73, + 0x2C75, + 0x2C76, + 0x2C7C, + 0x2C7D, + 0x2C7E, + 0x2C7F, + 0x2C80, + 0x2C81, + 0x2C82, + 0x2C83, + 0x2C84, + 0x2C85, + 0x2C86, + 0x2C87, + 0x2C88, + 0x2C89, + 0x2C8A, + 0x2C8B, + 0x2C8C, + 0x2C8D, + 0x2C8E, + 0x2C8F, + 0x2C90, + 0x2C91, + 0x2C92, + 0x2C93, + 0x2C94, + 0x2C95, + 0x2C96, + 0x2C97, + 0x2C98, + 0x2C99, + 0x2C9A, + 0x2C9B, + 0x2C9C, + 0x2C9D, + 0x2C9E, + 0x2C9F, + 0x2CA0, + 0x2CA1, + 0x2CA2, + 0x2CA3, + 0x2CA4, + 0x2CA5, + 0x2CA6, + 0x2CA7, + 0x2CA8, + 0x2CA9, + 0x2CAA, + 0x2CAB, + 0x2CAC, + 0x2CAD, + 0x2CAE, + 0x2CAF, + 0x2CB0, + 0x2CB1, + 0x2CB2, + 0x2CB3, + 0x2CB4, + 0x2CB5, + 0x2CB6, + 0x2CB7, + 0x2CB8, + 0x2CB9, + 0x2CBA, + 0x2CBB, + 0x2CBC, + 0x2CBD, + 0x2CBE, + 0x2CBF, + 0x2CC0, + 0x2CC1, + 0x2CC2, + 0x2CC3, + 0x2CC4, + 0x2CC5, + 0x2CC6, + 0x2CC7, + 0x2CC8, + 0x2CC9, + 0x2CCA, + 0x2CCB, + 0x2CCC, + 0x2CCD, + 0x2CCE, + 0x2CCF, + 0x2CD0, + 0x2CD1, + 0x2CD2, + 0x2CD3, + 0x2CD4, + 0x2CD5, + 0x2CD6, + 0x2CD7, + 0x2CD8, + 0x2CD9, + 0x2CDA, + 0x2CDB, + 0x2CDC, + 0x2CDD, + 0x2CDE, + 0x2CDF, + 0x2CE0, + 0x2CE1, + 0x2CE2, + 0x2CE3, + 0x2CEB, + 0x2CEC, + 0x2CED, + 0x2CEE, + 0x2CF2, + 0x2CF3, + 0x2CF4, + 0x2CF9, + 0x2D26, + 0x2D27, + 0x2D28, + 0x2D2D, + 0x2D2E, + 0x2D30, + 0x2D68, + 0x2D6F, + 0x2D70, + 0x2D71, + 0x2D7F, + 0x2D97, + 0x2DA0, + 0x2DA7, + 0x2DA8, + 0x2DAF, + 0x2DB0, + 0x2DB7, + 0x2DB8, + 0x2DBF, + 0x2DC0, + 0x2DC7, + 0x2DC8, + 0x2DCF, + 0x2DD0, + 0x2DD7, + 0x2DD8, + 0x2DDF, + 0x2DE0, + 0x2E5E, + 0x2E80, + 0x2E9A, + 0x2E9B, + 0x2E9F, + 0x2EA0, + 0x2EF3, + 0x2EF4, + 0x2F00, + 0x2F01, + 0x2F02, + 0x2F03, + 0x2F04, + 0x2F05, + 0x2F06, + 0x2F07, + 0x2F08, + 0x2F09, + 0x2F0A, + 0x2F0B, + 0x2F0C, + 0x2F0D, + 0x2F0E, + 0x2F0F, + 0x2F10, + 0x2F11, + 0x2F12, + 0x2F13, + 0x2F14, + 0x2F15, + 0x2F16, + 0x2F17, + 0x2F18, + 0x2F19, + 0x2F1A, + 0x2F1B, + 0x2F1C, + 0x2F1D, + 0x2F1E, + 0x2F1F, + 0x2F20, + 0x2F21, + 0x2F22, + 0x2F23, + 0x2F24, + 0x2F25, + 0x2F26, + 0x2F27, + 0x2F28, + 0x2F29, + 0x2F2A, + 0x2F2B, + 0x2F2C, + 0x2F2D, + 0x2F2E, + 0x2F2F, + 0x2F30, + 0x2F31, + 0x2F32, + 0x2F33, + 0x2F34, + 0x2F35, + 0x2F36, + 0x2F37, + 0x2F38, + 0x2F39, + 0x2F3A, + 0x2F3B, + 0x2F3C, + 0x2F3D, + 0x2F3E, + 0x2F3F, + 0x2F40, + 0x2F41, + 0x2F42, + 0x2F43, + 0x2F44, + 0x2F45, + 0x2F46, + 0x2F47, + 0x2F48, + 0x2F49, + 0x2F4A, + 0x2F4B, + 0x2F4C, + 0x2F4D, + 0x2F4E, + 0x2F4F, + 0x2F50, + 0x2F51, + 0x2F52, + 0x2F53, + 0x2F54, + 0x2F55, + 0x2F56, + 0x2F57, + 0x2F58, + 0x2F59, + 0x2F5A, + 0x2F5B, + 0x2F5C, + 0x2F5D, + 0x2F5E, + 0x2F5F, + 0x2F60, + 0x2F61, + 0x2F62, + 0x2F63, + 0x2F64, + 0x2F65, + 0x2F66, + 0x2F67, + 0x2F68, + 0x2F69, + 0x2F6A, + 0x2F6B, + 0x2F6C, + 0x2F6D, + 0x2F6E, + 0x2F6F, + 0x2F70, + 0x2F71, + 0x2F72, + 0x2F73, + 0x2F74, + 0x2F75, + 0x2F76, + 0x2F77, + 0x2F78, + 0x2F79, + 0x2F7A, + 0x2F7B, + 0x2F7C, + 0x2F7D, + 0x2F7E, + 0x2F7F, + 0x2F80, + 0x2F81, + 0x2F82, + 0x2F83, + 0x2F84, + 0x2F85, + 0x2F86, + 0x2F87, + 0x2F88, + 0x2F89, + 0x2F8A, + 0x2F8B, + 0x2F8C, + 0x2F8D, + 0x2F8E, + 0x2F8F, + 0x2F90, + 0x2F91, + 0x2F92, + 0x2F93, + 0x2F94, + 0x2F95, + 0x2F96, + 0x2F97, + 0x2F98, + 0x2F99, + 0x2F9A, + 0x2F9B, + 0x2F9C, + 0x2F9D, + 0x2F9E, + 0x2F9F, + 0x2FA0, + 0x2FA1, + 0x2FA2, + 0x2FA3, + 0x2FA4, + 0x2FA5, + 0x2FA6, + 0x2FA7, + 0x2FA8, + 0x2FA9, + 0x2FAA, + 0x2FAB, + 0x2FAC, + 0x2FAD, + 0x2FAE, + 0x2FAF, + 0x2FB0, + 0x2FB1, + 0x2FB2, + 0x2FB3, + 0x2FB4, + 0x2FB5, + 0x2FB6, + 0x2FB7, + 0x2FB8, + 0x2FB9, + 0x2FBA, + 0x2FBB, + 0x2FBC, + 0x2FBD, + 0x2FBE, + 0x2FBF, + 0x2FC0, + 0x2FC1, + 0x2FC2, + 0x2FC3, + 0x2FC4, + 0x2FC5, + 0x2FC6, + 0x2FC7, + 0x2FC8, + 0x2FC9, + 0x2FCA, + 0x2FCB, + 0x2FCC, + 0x2FCD, + 0x2FCE, + 0x2FCF, + 0x2FD0, + 0x2FD1, + 0x2FD2, + 0x2FD3, + 0x2FD4, + 0x2FD5, + 0x2FD6, + 0x3000, + 0x3001, + 0x3002, + 0x3003, + 0x3036, + 0x3037, + 0x3038, + 0x3039, + 0x303A, + 0x303B, + 0x3040, + 0x3041, + 0x3097, + 0x3099, + 0x309B, + 0x309C, + 0x309D, + 0x309F, + 0x30A0, + 0x30FF, + 0x3100, + 0x3105, + 0x3130, + 0x3131, + 0x3132, + 0x3133, + 0x3134, + 0x3135, + 0x3136, + 0x3137, + 0x3138, + 0x3139, + 0x313A, + 0x313B, + 0x313C, + 0x313D, + 0x313E, + 0x313F, + 0x3140, + 0x3141, + 0x3142, + 0x3143, + 0x3144, + 0x3145, + 0x3146, + 0x3147, + 0x3148, + 0x3149, + 0x314A, + 0x314B, + 0x314C, + 0x314D, + 0x314E, + 0x314F, + 0x3150, + 0x3151, + 0x3152, + 0x3153, + 0x3154, + 0x3155, + 0x3156, + 0x3157, + 0x3158, + 0x3159, + 0x315A, + 0x315B, + 0x315C, + 0x315D, + 0x315E, + 0x315F, + 0x3160, + 0x3161, + 0x3162, + 0x3163, + 0x3164, + 0x3165, + 0x3166, + 0x3167, + 0x3168, + 0x3169, + 0x316A, + 0x316B, + 0x316C, + 0x316D, + 0x316E, + 0x316F, + 0x3170, + 0x3171, + 0x3172, + 0x3173, + 0x3174, + 0x3175, + 0x3176, + 0x3177, + 0x3178, + 0x3179, + 0x317A, + 0x317B, + 0x317C, + 0x317D, + 0x317E, + 0x317F, + 0x3180, + 0x3181, + 0x3182, + 0x3183, + 0x3184, + 0x3185, + 0x3186, + 0x3187, + 0x3188, + 0x3189, + 0x318A, + 0x318B, + 0x318C, + 0x318D, + 0x318E, + 0x318F, + 0x3190, + 0x3192, + 0x3193, + 0x3194, + 0x3195, + 0x3196, + 0x3197, + 0x3198, + 0x3199, + 0x319A, + 0x319B, + 0x319C, + 0x319D, + 0x319E, + 0x319F, + 0x31A0, + 0x31E6, + 0x31F0, + 0x3200, + 0x3201, + 0x3202, + 0x3203, + 0x3204, + 0x3205, + 0x3206, + 0x3207, + 0x3208, + 0x3209, + 0x320A, + 0x320B, + 0x320C, + 0x320D, + 0x320E, + 0x320F, + 0x3210, + 0x3211, + 0x3212, + 0x3213, + 0x3214, + 0x3215, + 0x3216, + 0x3217, + 0x3218, + 0x3219, + 0x321A, + 0x321B, + 0x321C, + 0x321D, + 0x321E, + 0x321F, + 0x3220, + 0x3221, + 0x3222, + 0x3223, + 0x3224, + 0x3225, + 0x3226, + 0x3227, + 0x3228, + 0x3229, + 0x322A, + 0x322B, + 0x322C, + 0x322D, + 0x322E, + 0x322F, + 0x3230, + 0x3231, + 0x3232, + 0x3233, + 0x3234, + 0x3235, + 0x3236, + 0x3237, + 0x3238, + 0x3239, + 0x323A, + 0x323B, + 0x323C, + 0x323D, + 0x323E, + 0x323F, + 0x3240, + 0x3241, + 0x3242, + 0x3243, + 0x3244, + 0x3245, + 0x3246, + 0x3247, + 0x3248, + 0x3250, + 0x3251, + 0x3252, + 0x3253, + 0x3254, + 0x3255, + 0x3256, + 0x3257, + 0x3258, + 0x3259, + 0x325A, + 0x325B, + 0x325C, + 0x325D, + 0x325E, + 0x325F, + 0x3260, + 0x3261, + 0x3262, + 0x3263, + 0x3264, + 0x3265, + 0x3266, + 0x3267, + 0x3268, + 0x3269, + 0x326A, + 0x326B, + 0x326C, + 0x326D, + 0x326E, + 0x326F, + 0x3270, + 0x3271, + 0x3272, + 0x3273, + 0x3274, + 0x3275, + 0x3276, + 0x3277, + 0x3278, + 0x3279, + 0x327A, + 0x327B, + 0x327C, + 0x327D, + 0x327E, + 0x327F, + 0x3280, + 0x3281, + 0x3282, + 0x3283, + 0x3284, + 0x3285, + 0x3286, + 0x3287, + 0x3288, + 0x3289, + 0x328A, + 0x328B, + 0x328C, + 0x328D, + 0x328E, + 0x328F, + 0x3290, + 0x3291, + 0x3292, + 0x3293, + 0x3294, + 0x3295, + 0x3296, + 0x3297, + 0x3298, + 0x3299, + 0x329A, + 0x329B, + 0x329C, + 0x329D, + 0x329E, + 0x329F, + 0x32A0, + 0x32A1, + 0x32A2, + 0x32A3, + 0x32A4, + 0x32A5, + 0x32A6, + 0x32A7, + 0x32A8, + 0x32A9, + 0x32AA, + 0x32AB, + 0x32AC, + 0x32AD, + 0x32AE, + 0x32AF, + 0x32B0, + 0x32B1, + 0x32B2, + 0x32B3, + 0x32B4, + 0x32B5, + 0x32B6, + 0x32B7, + 0x32B8, + 0x32B9, + 0x32BA, + 0x32BB, + 0x32BC, + 0x32BD, + 0x32BE, + 0x32BF, + 0x32C0, + 0x32C1, + 0x32C2, + 0x32C3, + 0x32C4, + 0x32C5, + 0x32C6, + 0x32C7, + 0x32C8, + 0x32C9, + 0x32CA, + 0x32CB, + 0x32CC, + 0x32CD, + 0x32CE, + 0x32CF, + 0x32D0, + 0x32D1, + 0x32D2, + 0x32D3, + 0x32D4, + 0x32D5, + 0x32D6, + 0x32D7, + 0x32D8, + 0x32D9, + 0x32DA, + 0x32DB, + 0x32DC, + 0x32DD, + 0x32DE, + 0x32DF, + 0x32E0, + 0x32E1, + 0x32E2, + 0x32E3, + 0x32E4, + 0x32E5, + 0x32E6, + 0x32E7, + 0x32E8, + 0x32E9, + 0x32EA, + 0x32EB, + 0x32EC, + 0x32ED, + 0x32EE, + 0x32EF, + 0x32F0, + 0x32F1, + 0x32F2, + 0x32F3, + 0x32F4, + 0x32F5, + 0x32F6, + 0x32F7, + 0x32F8, + 0x32F9, + 0x32FA, + 0x32FB, + 0x32FC, + 0x32FD, + 0x32FE, + 0x32FF, + 0x3300, + 0x3301, + 0x3302, + 0x3303, + 0x3304, + 0x3305, + 0x3306, + 0x3307, + 0x3308, + 0x3309, + 0x330A, + 0x330B, + 0x330C, + 0x330D, + 0x330E, + 0x330F, + 0x3310, + 0x3311, + 0x3312, + 0x3313, + 0x3314, + 0x3315, + 0x3316, + 0x3317, + 0x3318, + 0x3319, + 0x331A, + 0x331B, + 0x331C, + 0x331D, + 0x331E, + 0x331F, + 0x3320, + 0x3321, + 0x3322, + 0x3323, + 0x3324, + 0x3325, + 0x3326, + 0x3327, + 0x3328, + 0x3329, + 0x332A, + 0x332B, + 0x332C, + 0x332D, + 0x332E, + 0x332F, + 0x3330, + 0x3331, + 0x3332, + 0x3333, + 0x3334, + 0x3335, + 0x3336, + 0x3337, + 0x3338, + 0x3339, + 0x333A, + 0x333B, + 0x333C, + 0x333D, + 0x333E, + 0x333F, + 0x3340, + 0x3341, + 0x3342, + 0x3343, + 0x3344, + 0x3345, + 0x3346, + 0x3347, + 0x3348, + 0x3349, + 0x334A, + 0x334B, + 0x334C, + 0x334D, + 0x334E, + 0x334F, + 0x3350, + 0x3351, + 0x3352, + 0x3353, + 0x3354, + 0x3355, + 0x3356, + 0x3357, + 0x3358, + 0x3359, + 0x335A, + 0x335B, + 0x335C, + 0x335D, + 0x335E, + 0x335F, + 0x3360, + 0x3361, + 0x3362, + 0x3363, + 0x3364, + 0x3365, + 0x3366, + 0x3367, + 0x3368, + 0x3369, + 0x336A, + 0x336B, + 0x336C, + 0x336D, + 0x336E, + 0x336F, + 0x3370, + 0x3371, + 0x3372, + 0x3373, + 0x3374, + 0x3375, + 0x3376, + 0x3377, + 0x3378, + 0x3379, + 0x337A, + 0x337B, + 0x337C, + 0x337D, + 0x337E, + 0x337F, + 0x3380, + 0x3381, + 0x3382, + 0x3383, + 0x3384, + 0x3385, + 0x3386, + 0x3387, + 0x3388, + 0x3389, + 0x338A, + 0x338B, + 0x338C, + 0x338D, + 0x338E, + 0x338F, + 0x3390, + 0x3391, + 0x3392, + 0x3393, + 0x3394, + 0x3395, + 0x3396, + 0x3397, + 0x3398, + 0x3399, + 0x339A, + 0x339B, + 0x339C, + 0x339D, + 0x339E, + 0x339F, + 0x33A0, + 0x33A1, + 0x33A2, + 0x33A3, + 0x33A4, + 0x33A5, + 0x33A6, + 0x33A7, + 0x33A8, + 0x33A9, + 0x33AA, + 0x33AB, + 0x33AC, + 0x33AD, + 0x33AE, + 0x33AF, + 0x33B0, + 0x33B1, + 0x33B2, + 0x33B3, + 0x33B4, + 0x33B5, + 0x33B6, + 0x33B7, + 0x33B8, + 0x33B9, + 0x33BA, + 0x33BB, + 0x33BC, + 0x33BD, + 0x33BE, + 0x33BF, + 0x33C0, + 0x33C1, + 0x33C2, + 0x33C3, + 0x33C4, + 0x33C5, + 0x33C6, + 0x33C7, + 0x33C8, + 0x33C9, + 0x33CA, + 0x33CB, + 0x33CC, + 0x33CD, + 0x33CE, + 0x33CF, + 0x33D0, + 0x33D1, + 0x33D2, + 0x33D3, + 0x33D4, + 0x33D5, + 0x33D6, + 0x33D7, + 0x33D8, + 0x33D9, + 0x33DA, + 0x33DB, + 0x33DC, + 0x33DD, + 0x33DE, + 0x33DF, + 0x33E0, + 0x33E1, + 0x33E2, + 0x33E3, + 0x33E4, + 0x33E5, + 0x33E6, + 0x33E7, + 0x33E8, + 0x33E9, + 0x33EA, + 0x33EB, + 0x33EC, + 0x33ED, + 0x33EE, + 0x33EF, + 0x33F0, + 0x33F1, + 0x33F2, + 0x33F3, + 0x33F4, + 0x33F5, + 0x33F6, + 0x33F7, + 0x33F8, + 0x33F9, + 0x33FA, + 0x33FB, + 0x33FC, + 0x33FD, + 0x33FE, + 0x33FF, + 0x3400, + 0xA48D, + 0xA490, + 0xA4C7, + 0xA4D0, + 0xA62C, + 0xA640, + 0xA641, + 0xA642, + 0xA643, + 0xA644, + 0xA645, + 0xA646, + 0xA647, + 0xA648, + 0xA649, + 0xA64A, + 0xA64B, + 0xA64C, + 0xA64D, + 0xA64E, + 0xA64F, + 0xA650, + 0xA651, + 0xA652, + 0xA653, + 0xA654, + 0xA655, + 0xA656, + 0xA657, + 0xA658, + 0xA659, + 0xA65A, + 0xA65B, + 0xA65C, + 0xA65D, + 0xA65E, + 0xA65F, + 0xA660, + 0xA661, + 0xA662, + 0xA663, + 0xA664, + 0xA665, + 0xA666, + 0xA667, + 0xA668, + 0xA669, + 0xA66A, + 0xA66B, + 0xA66C, + 0xA66D, + 0xA680, + 0xA681, + 0xA682, + 0xA683, + 0xA684, + 0xA685, + 0xA686, + 0xA687, + 0xA688, + 0xA689, + 0xA68A, + 0xA68B, + 0xA68C, + 0xA68D, + 0xA68E, + 0xA68F, + 0xA690, + 0xA691, + 0xA692, + 0xA693, + 0xA694, + 0xA695, + 0xA696, + 0xA697, + 0xA698, + 0xA699, + 0xA69A, + 0xA69B, + 0xA69C, + 0xA69D, + 0xA69E, + 0xA6F8, + 0xA700, + 0xA722, + 0xA723, + 0xA724, + 0xA725, + 0xA726, + 0xA727, + 0xA728, + 0xA729, + 0xA72A, + 0xA72B, + 0xA72C, + 0xA72D, + 0xA72E, + 0xA72F, + 0xA732, + 0xA733, + 0xA734, + 0xA735, + 0xA736, + 0xA737, + 0xA738, + 0xA739, + 0xA73A, + 0xA73B, + 0xA73C, + 0xA73D, + 0xA73E, + 0xA73F, + 0xA740, + 0xA741, + 0xA742, + 0xA743, + 0xA744, + 0xA745, + 0xA746, + 0xA747, + 0xA748, + 0xA749, + 0xA74A, + 0xA74B, + 0xA74C, + 0xA74D, + 0xA74E, + 0xA74F, + 0xA750, + 0xA751, + 0xA752, + 0xA753, + 0xA754, + 0xA755, + 0xA756, + 0xA757, + 0xA758, + 0xA759, + 0xA75A, + 0xA75B, + 0xA75C, + 0xA75D, + 0xA75E, + 0xA75F, + 0xA760, + 0xA761, + 0xA762, + 0xA763, + 0xA764, + 0xA765, + 0xA766, + 0xA767, + 0xA768, + 0xA769, + 0xA76A, + 0xA76B, + 0xA76C, + 0xA76D, + 0xA76E, + 0xA76F, + 0xA770, + 0xA771, + 0xA779, + 0xA77A, + 0xA77B, + 0xA77C, + 0xA77D, + 0xA77E, + 0xA77F, + 0xA780, + 0xA781, + 0xA782, + 0xA783, + 0xA784, + 0xA785, + 0xA786, + 0xA787, + 0xA78B, + 0xA78C, + 0xA78D, + 0xA78E, + 0xA790, + 0xA791, + 0xA792, + 0xA793, + 0xA796, + 0xA797, + 0xA798, + 0xA799, + 0xA79A, + 0xA79B, + 0xA79C, + 0xA79D, + 0xA79E, + 0xA79F, + 0xA7A0, + 0xA7A1, + 0xA7A2, + 0xA7A3, + 0xA7A4, + 0xA7A5, + 0xA7A6, + 0xA7A7, + 0xA7A8, + 0xA7A9, + 0xA7AA, + 0xA7AB, + 0xA7AC, + 0xA7AD, + 0xA7AE, + 0xA7AF, + 0xA7B0, + 0xA7B1, + 0xA7B2, + 0xA7B3, + 0xA7B4, + 0xA7B5, + 0xA7B6, + 0xA7B7, + 0xA7B8, + 0xA7B9, + 0xA7BA, + 0xA7BB, + 0xA7BC, + 0xA7BD, + 0xA7BE, + 0xA7BF, + 0xA7C0, + 0xA7C1, + 0xA7C2, + 0xA7C3, + 0xA7C4, + 0xA7C5, + 0xA7C6, + 0xA7C7, + 0xA7C8, + 0xA7C9, + 0xA7CA, + 0xA7CB, + 0xA7CC, + 0xA7CD, + 0xA7CE, + 0xA7CF, + 0xA7D0, + 0xA7D1, + 0xA7D2, + 0xA7D3, + 0xA7D4, + 0xA7D5, + 0xA7D6, + 0xA7D7, + 0xA7D8, + 0xA7D9, + 0xA7DA, + 0xA7DB, + 0xA7DC, + 0xA7DD, + 0xA7F1, + 0xA7F2, + 0xA7F3, + 0xA7F4, + 0xA7F5, + 0xA7F6, + 0xA7F8, + 0xA7F9, + 0xA7FA, + 0xA82D, + 0xA830, + 0xA83A, + 0xA840, + 0xA878, + 0xA880, + 0xA8C6, + 0xA8CE, + 0xA8DA, + 0xA8E0, + 0xA954, + 0xA95F, + 0xA97D, + 0xA980, + 0xA9CE, + 0xA9CF, + 0xA9DA, + 0xA9DE, + 0xA9FF, + 0xAA00, + 0xAA37, + 0xAA40, + 0xAA4E, + 0xAA50, + 0xAA5A, + 0xAA5C, + 0xAAC3, + 0xAADB, + 0xAAF7, + 0xAB01, + 0xAB07, + 0xAB09, + 0xAB0F, + 0xAB11, + 0xAB17, + 0xAB20, + 0xAB27, + 0xAB28, + 0xAB2F, + 0xAB30, + 0xAB5C, + 0xAB5D, + 0xAB5E, + 0xAB5F, + 0xAB60, + 0xAB69, + 0xAB6A, + 0xAB6C, + 0xAB70, + 0xAB71, + 0xAB72, + 0xAB73, + 0xAB74, + 0xAB75, + 0xAB76, + 0xAB77, + 0xAB78, + 0xAB79, + 0xAB7A, + 0xAB7B, + 0xAB7C, + 0xAB7D, + 0xAB7E, + 0xAB7F, + 0xAB80, + 0xAB81, + 0xAB82, + 0xAB83, + 0xAB84, + 0xAB85, + 0xAB86, + 0xAB87, + 0xAB88, + 0xAB89, + 0xAB8A, + 0xAB8B, + 0xAB8C, + 0xAB8D, + 0xAB8E, + 0xAB8F, + 0xAB90, + 0xAB91, + 0xAB92, + 0xAB93, + 0xAB94, + 0xAB95, + 0xAB96, + 0xAB97, + 0xAB98, + 0xAB99, + 0xAB9A, + 0xAB9B, + 0xAB9C, + 0xAB9D, + 0xAB9E, + 0xAB9F, + 0xABA0, + 0xABA1, + 0xABA2, + 0xABA3, + 0xABA4, + 0xABA5, + 0xABA6, + 0xABA7, + 0xABA8, + 0xABA9, + 0xABAA, + 0xABAB, + 0xABAC, + 0xABAD, + 0xABAE, + 0xABAF, + 0xABB0, + 0xABB1, + 0xABB2, + 0xABB3, + 0xABB4, + 0xABB5, + 0xABB6, + 0xABB7, + 0xABB8, + 0xABB9, + 0xABBA, + 0xABBB, + 0xABBC, + 0xABBD, + 0xABBE, + 0xABBF, + 0xABC0, + 0xABEE, + 0xABF0, + 0xABFA, + 0xAC00, + 0xD7A4, + 0xD7B0, + 0xD7C7, + 0xD7CB, + 0xD7FC, + 0xF900, + 0xF901, + 0xF902, + 0xF903, + 0xF904, + 0xF905, + 0xF906, + 0xF907, + 0xF909, + 0xF90A, + 0xF90B, + 0xF90C, + 0xF90D, + 0xF90E, + 0xF90F, + 0xF910, + 0xF911, + 0xF912, + 0xF913, + 0xF914, + 0xF915, + 0xF916, + 0xF917, + 0xF918, + 0xF919, + 0xF91A, + 0xF91B, + 0xF91C, + 0xF91D, + 0xF91E, + 0xF91F, + 0xF920, + 0xF921, + 0xF922, + 0xF923, + 0xF924, + 0xF925, + 0xF926, + 0xF927, + 0xF928, + 0xF929, + 0xF92A, + 0xF92B, + 0xF92C, + 0xF92D, + 0xF92E, + 0xF92F, + 0xF930, + 0xF931, + 0xF932, + 0xF933, + 0xF934, + 0xF935, + 0xF936, + 0xF937, + 0xF938, + 0xF939, + 0xF93A, + 0xF93B, + 0xF93C, + 0xF93D, + 0xF93E, + 0xF93F, + 0xF940, + 0xF941, + 0xF942, + 0xF943, + 0xF944, + 0xF945, + 0xF946, + 0xF947, + 0xF948, + 0xF949, + 0xF94A, + 0xF94B, + 0xF94C, + 0xF94D, + 0xF94E, + 0xF94F, + 0xF950, + 0xF951, + 0xF952, + 0xF953, + 0xF954, + 0xF955, + 0xF956, + 0xF957, + 0xF958, + 0xF959, + 0xF95A, + 0xF95B, + 0xF95C, + 0xF95D, + 0xF95E, + 0xF95F, + 0xF960, + 0xF961, + 0xF962, + 0xF963, + 0xF964, + 0xF965, + 0xF966, + 0xF967, + 0xF968, + 0xF969, + 0xF96A, + 0xF96B, + 0xF96C, + 0xF96D, + 0xF96E, + 0xF96F, + 0xF970, + 0xF971, + 0xF972, + 0xF973, + 0xF974, + 0xF975, + 0xF976, + 0xF977, + 0xF978, + 0xF979, + 0xF97A, + 0xF97B, + 0xF97C, + 0xF97D, + 0xF97E, + 0xF97F, + 0xF980, + 0xF981, + 0xF982, + 0xF983, + 0xF984, + 0xF985, + 0xF986, + 0xF987, + 0xF988, + 0xF989, + 0xF98A, + 0xF98B, + 0xF98C, + 0xF98D, + 0xF98E, + 0xF98F, + 0xF990, + 0xF991, + 0xF992, + 0xF993, + 0xF994, + 0xF995, + 0xF996, + 0xF997, + 0xF998, + 0xF999, + 0xF99A, + 0xF99B, + 0xF99C, + 0xF99D, + 0xF99E, + 0xF99F, + 0xF9A0, + 0xF9A1, + 0xF9A2, + 0xF9A3, + 0xF9A4, + 0xF9A5, + 0xF9A6, + 0xF9A7, + 0xF9A8, + 0xF9A9, + 0xF9AA, + 0xF9AB, + 0xF9AC, + 0xF9AD, + 0xF9AE, + 0xF9AF, + 0xF9B0, + 0xF9B1, + 0xF9B2, + 0xF9B3, + 0xF9B4, + 0xF9B5, + 0xF9B6, + 0xF9B7, + 0xF9B8, + 0xF9B9, + 0xF9BA, + 0xF9BB, + 0xF9BC, + 0xF9BD, + 0xF9BE, + 0xF9BF, + 0xF9C0, + 0xF9C1, + 0xF9C2, + 0xF9C3, + 0xF9C4, + 0xF9C5, + 0xF9C6, + 0xF9C7, + 0xF9C8, + 0xF9C9, + 0xF9CA, + 0xF9CB, + 0xF9CC, + 0xF9CD, + 0xF9CE, + 0xF9CF, + 0xF9D0, + 0xF9D1, + 0xF9D2, + 0xF9D3, + 0xF9D4, + 0xF9D5, + 0xF9D6, + 0xF9D7, + 0xF9D8, + 0xF9D9, + 0xF9DA, + 0xF9DB, + 0xF9DC, + 0xF9DD, + 0xF9DE, + 0xF9DF, + 0xF9E0, + 0xF9E1, + 0xF9E2, + 0xF9E3, + 0xF9E4, + 0xF9E5, + 0xF9E6, + 0xF9E7, + 0xF9E8, + 0xF9E9, + 0xF9EA, + 0xF9EB, + 0xF9EC, + 0xF9ED, + 0xF9EE, + 0xF9EF, + 0xF9F0, + 0xF9F1, + 0xF9F2, + 0xF9F3, + 0xF9F4, + 0xF9F5, + 0xF9F6, + 0xF9F7, + 0xF9F8, + 0xF9F9, + 0xF9FA, + 0xF9FB, + 0xF9FC, + 0xF9FD, + 0xF9FE, + 0xF9FF, + 0xFA00, + 0xFA01, + 0xFA02, + 0xFA03, + 0xFA04, + 0xFA05, + 0xFA06, + 0xFA07, + 0xFA08, + 0xFA09, + 0xFA0A, + 0xFA0B, + 0xFA0C, + 0xFA0D, + 0xFA0E, + 0xFA10, + 0xFA11, + 0xFA12, + 0xFA13, + 0xFA15, + 0xFA16, + 0xFA17, + 0xFA18, + 0xFA19, + 0xFA1A, + 0xFA1B, + 0xFA1C, + 0xFA1D, + 0xFA1E, + 0xFA1F, + 0xFA20, + 0xFA21, + 0xFA22, + 0xFA23, + 0xFA25, + 0xFA26, + 0xFA27, + 0xFA2A, + 0xFA2B, + 0xFA2C, + 0xFA2D, + 0xFA2E, + 0xFA2F, + 0xFA30, + 0xFA31, + 0xFA32, + 0xFA33, + 0xFA34, + 0xFA35, + 0xFA36, + 0xFA37, + 0xFA38, + 0xFA39, + 0xFA3A, + 0xFA3B, + 0xFA3C, + 0xFA3D, + 0xFA3E, + 0xFA3F, + 0xFA40, + 0xFA41, + 0xFA42, + 0xFA43, + 0xFA44, + 0xFA45, + 0xFA46, + 0xFA47, + 0xFA48, + 0xFA49, + 0xFA4A, + 0xFA4B, + 0xFA4C, + 0xFA4D, + 0xFA4E, + 0xFA4F, + 0xFA50, + 0xFA51, + 0xFA52, + 0xFA53, + 0xFA54, + 0xFA55, + 0xFA56, + 0xFA57, + 0xFA58, + 0xFA59, + 0xFA5A, + 0xFA5B, + 0xFA5C, + 0xFA5D, + 0xFA5F, + 0xFA60, + 0xFA61, + 0xFA62, + 0xFA63, + 0xFA64, + 0xFA65, + 0xFA66, + 0xFA67, + 0xFA68, + 0xFA69, + 0xFA6A, + 0xFA6B, + 0xFA6C, + 0xFA6D, + 0xFA6E, + 0xFA70, + 0xFA71, + 0xFA72, + 0xFA73, + 0xFA74, + 0xFA75, + 0xFA76, + 0xFA77, + 0xFA78, + 0xFA79, + 0xFA7A, + 0xFA7B, + 0xFA7C, + 0xFA7D, + 0xFA7E, + 0xFA7F, + 0xFA80, + 0xFA81, + 0xFA82, + 0xFA83, + 0xFA84, + 0xFA85, + 0xFA86, + 0xFA87, + 0xFA88, + 0xFA89, + 0xFA8A, + 0xFA8B, + 0xFA8C, + 0xFA8D, + 0xFA8E, + 0xFA8F, + 0xFA90, + 0xFA91, + 0xFA92, + 0xFA93, + 0xFA94, + 0xFA95, + 0xFA96, + 0xFA97, + 0xFA98, + 0xFA99, + 0xFA9A, + 0xFA9B, + 0xFA9C, + 0xFA9D, + 0xFA9E, + 0xFA9F, + 0xFAA0, + 0xFAA1, + 0xFAA2, + 0xFAA3, + 0xFAA4, + 0xFAA5, + 0xFAA6, + 0xFAA7, + 0xFAA8, + 0xFAA9, + 0xFAAA, + 0xFAAB, + 0xFAAC, + 0xFAAD, + 0xFAAE, + 0xFAAF, + 0xFAB0, + 0xFAB1, + 0xFAB2, + 0xFAB3, + 0xFAB4, + 0xFAB5, + 0xFAB6, + 0xFAB7, + 0xFAB8, + 0xFAB9, + 0xFABA, + 0xFABB, + 0xFABC, + 0xFABD, + 0xFABE, + 0xFABF, + 0xFAC0, + 0xFAC1, + 0xFAC2, + 0xFAC3, + 0xFAC4, + 0xFAC5, + 0xFAC6, + 0xFAC7, + 0xFAC8, + 0xFAC9, + 0xFACA, + 0xFACB, + 0xFACC, + 0xFACD, + 0xFACE, + 0xFACF, + 0xFAD0, + 0xFAD1, + 0xFAD2, + 0xFAD3, + 0xFAD4, + 0xFAD5, + 0xFAD6, + 0xFAD7, + 0xFAD8, + 0xFAD9, + 0xFADA, + 0xFB00, + 0xFB01, + 0xFB02, + 0xFB03, + 0xFB04, + 0xFB05, + 0xFB07, + 0xFB13, + 0xFB14, + 0xFB15, + 0xFB16, + 0xFB17, + 0xFB18, + 0xFB1D, + 0xFB1E, + 0xFB1F, + 0xFB20, + 0xFB21, + 0xFB22, + 0xFB23, + 0xFB24, + 0xFB25, + 0xFB26, + 0xFB27, + 0xFB28, + 0xFB29, + 0xFB2A, + 0xFB2B, + 0xFB2C, + 0xFB2D, + 0xFB2E, + 0xFB2F, + 0xFB30, + 0xFB31, + 0xFB32, + 0xFB33, + 0xFB34, + 0xFB35, + 0xFB36, + 0xFB37, + 0xFB38, + 0xFB39, + 0xFB3A, + 0xFB3B, + 0xFB3C, + 0xFB3D, + 0xFB3E, + 0xFB3F, + 0xFB40, + 0xFB41, + 0xFB42, + 0xFB43, + 0xFB44, + 0xFB45, + 0xFB46, + 0xFB47, + 0xFB48, + 0xFB49, + 0xFB4A, + 0xFB4B, + 0xFB4C, + 0xFB4D, + 0xFB4E, + 0xFB4F, + 0xFB50, + 0xFB52, + 0xFB56, + 0xFB5A, + 0xFB5E, + 0xFB62, + 0xFB66, + 0xFB6A, + 0xFB6E, + 0xFB72, + 0xFB76, + 0xFB7A, + 0xFB7E, + 0xFB82, + 0xFB84, + 0xFB86, + 0xFB88, + 0xFB8A, + 0xFB8C, + 0xFB8E, + 0xFB92, + 0xFB96, + 0xFB9A, + 0xFB9E, + 0xFBA0, + 0xFBA4, + 0xFBA6, + 0xFBAA, + 0xFBAE, + 0xFBB0, + 0xFBB2, + 0xFBD3, + 0xFBD7, + 0xFBD9, + 0xFBDB, + 0xFBDD, + 0xFBDE, + 0xFBE0, + 0xFBE2, + 0xFBE4, + 0xFBE8, + 0xFBEA, + 0xFBEC, + 0xFBEE, + 0xFBF0, + 0xFBF2, + 0xFBF4, + 0xFBF6, + 0xFBF9, + 0xFBFC, + 0xFC00, + 0xFC01, + 0xFC02, + 0xFC03, + 0xFC04, + 0xFC05, + 0xFC06, + 0xFC07, + 0xFC08, + 0xFC09, + 0xFC0A, + 0xFC0B, + 0xFC0C, + 0xFC0D, + 0xFC0E, + 0xFC0F, + 0xFC10, + 0xFC11, + 0xFC12, + 0xFC13, + 0xFC14, + 0xFC15, + 0xFC16, + 0xFC17, + 0xFC18, + 0xFC19, + 0xFC1A, + 0xFC1B, + 0xFC1C, + 0xFC1D, + 0xFC1E, + 0xFC1F, + 0xFC20, + 0xFC21, + 0xFC22, + 0xFC23, + 0xFC24, + 0xFC25, + 0xFC26, + 0xFC27, + 0xFC28, + 0xFC29, + 0xFC2A, + 0xFC2B, + 0xFC2C, + 0xFC2D, + 0xFC2E, + 0xFC2F, + 0xFC30, + 0xFC31, + 0xFC32, + 0xFC33, + 0xFC34, + 0xFC35, + 0xFC36, + 0xFC37, + 0xFC38, + 0xFC39, + 0xFC3A, + 0xFC3B, + 0xFC3C, + 0xFC3D, + 0xFC3E, + 0xFC3F, + 0xFC40, + 0xFC41, + 0xFC42, + 0xFC43, + 0xFC44, + 0xFC45, + 0xFC46, + 0xFC47, + 0xFC48, + 0xFC49, + 0xFC4A, + 0xFC4B, + 0xFC4C, + 0xFC4D, + 0xFC4E, + 0xFC4F, + 0xFC50, + 0xFC51, + 0xFC52, + 0xFC53, + 0xFC54, + 0xFC55, + 0xFC56, + 0xFC57, + 0xFC58, + 0xFC59, + 0xFC5A, + 0xFC5B, + 0xFC5C, + 0xFC5D, + 0xFC5E, + 0xFC5F, + 0xFC60, + 0xFC61, + 0xFC62, + 0xFC63, + 0xFC64, + 0xFC65, + 0xFC66, + 0xFC67, + 0xFC68, + 0xFC69, + 0xFC6A, + 0xFC6B, + 0xFC6C, + 0xFC6D, + 0xFC6E, + 0xFC6F, + 0xFC70, + 0xFC71, + 0xFC72, + 0xFC73, + 0xFC74, + 0xFC75, + 0xFC76, + 0xFC77, + 0xFC78, + 0xFC79, + 0xFC7A, + 0xFC7B, + 0xFC7C, + 0xFC7D, + 0xFC7E, + 0xFC7F, + 0xFC80, + 0xFC81, + 0xFC82, + 0xFC83, + 0xFC84, + 0xFC85, + 0xFC86, + 0xFC87, + 0xFC88, + 0xFC89, + 0xFC8A, + 0xFC8B, + 0xFC8C, + 0xFC8D, + 0xFC8E, + 0xFC8F, + 0xFC90, + 0xFC91, + 0xFC92, + 0xFC93, + 0xFC94, + 0xFC95, + 0xFC96, + 0xFC97, + 0xFC98, + 0xFC99, + 0xFC9A, + 0xFC9B, + 0xFC9C, + 0xFC9D, + 0xFC9E, + 0xFC9F, + 0xFCA0, + 0xFCA1, + 0xFCA2, + 0xFCA3, + 0xFCA4, + 0xFCA5, + 0xFCA6, + 0xFCA7, + 0xFCA8, + 0xFCA9, + 0xFCAA, + 0xFCAB, + 0xFCAC, + 0xFCAD, + 0xFCAE, + 0xFCAF, + 0xFCB0, + 0xFCB1, + 0xFCB2, + 0xFCB3, + 0xFCB4, + 0xFCB5, + 0xFCB6, + 0xFCB7, + 0xFCB8, + 0xFCB9, + 0xFCBA, + 0xFCBB, + 0xFCBC, + 0xFCBD, + 0xFCBE, + 0xFCBF, + 0xFCC0, + 0xFCC1, + 0xFCC2, + 0xFCC3, + 0xFCC4, + 0xFCC5, + 0xFCC6, + 0xFCC7, + 0xFCC8, + 0xFCC9, + 0xFCCA, + 0xFCCB, + 0xFCCC, + 0xFCCD, + 0xFCCE, + 0xFCCF, + 0xFCD0, + 0xFCD1, + 0xFCD2, + 0xFCD3, + 0xFCD4, + 0xFCD5, + 0xFCD6, + 0xFCD7, + 0xFCD8, + 0xFCD9, + 0xFCDA, + 0xFCDB, + 0xFCDC, + 0xFCDD, + 0xFCDE, + 0xFCDF, + 0xFCE0, + 0xFCE1, + 0xFCE2, + 0xFCE3, + 0xFCE4, + 0xFCE5, + 0xFCE6, + 0xFCE7, + 0xFCE8, + 0xFCE9, + 0xFCEA, + 0xFCEB, + 0xFCEC, + 0xFCED, + 0xFCEE, + 0xFCEF, + 0xFCF0, + 0xFCF1, + 0xFCF2, + 0xFCF3, + 0xFCF4, + 0xFCF5, + 0xFCF6, + 0xFCF7, + 0xFCF8, + 0xFCF9, + 0xFCFA, + 0xFCFB, + 0xFCFC, + 0xFCFD, + 0xFCFE, + 0xFCFF, + 0xFD00, + 0xFD01, + 0xFD02, + 0xFD03, + 0xFD04, + 0xFD05, + 0xFD06, + 0xFD07, + 0xFD08, + 0xFD09, + 0xFD0A, + 0xFD0B, + 0xFD0C, + 0xFD0D, + 0xFD0E, + 0xFD0F, + 0xFD10, + 0xFD11, + 0xFD12, + 0xFD13, + 0xFD14, + 0xFD15, + 0xFD16, + 0xFD17, + 0xFD18, + 0xFD19, + 0xFD1A, + 0xFD1B, + 0xFD1C, + 0xFD1D, + 0xFD1E, + 0xFD1F, + 0xFD20, + 0xFD21, + 0xFD22, + 0xFD23, + 0xFD24, + 0xFD25, + 0xFD26, + 0xFD27, + 0xFD28, + 0xFD29, + 0xFD2A, + 0xFD2B, + 0xFD2C, + 0xFD2D, + 0xFD2E, + 0xFD2F, + 0xFD30, + 0xFD31, + 0xFD32, + 0xFD33, + 0xFD34, + 0xFD35, + 0xFD36, + 0xFD37, + 0xFD38, + 0xFD39, + 0xFD3A, + 0xFD3B, + 0xFD3C, + 0xFD3E, + 0xFD50, + 0xFD51, + 0xFD53, + 0xFD54, + 0xFD55, + 0xFD56, + 0xFD57, + 0xFD58, + 0xFD5A, + 0xFD5B, + 0xFD5C, + 0xFD5D, + 0xFD5E, + 0xFD5F, + 0xFD61, + 0xFD62, + 0xFD64, + 0xFD66, + 0xFD67, + 0xFD69, + 0xFD6A, + 0xFD6C, + 0xFD6E, + 0xFD6F, + 0xFD71, + 0xFD73, + 0xFD74, + 0xFD75, + 0xFD76, + 0xFD78, + 0xFD79, + 0xFD7A, + 0xFD7B, + 0xFD7C, + 0xFD7E, + 0xFD7F, + 0xFD80, + 0xFD81, + 0xFD82, + 0xFD83, + 0xFD85, + 0xFD87, + 0xFD89, + 0xFD8A, + 0xFD8B, + 0xFD8C, + 0xFD8D, + 0xFD8E, + 0xFD8F, + 0xFD90, + 0xFD92, + 0xFD93, + 0xFD94, + 0xFD95, + 0xFD96, + 0xFD97, + 0xFD99, + 0xFD9A, + 0xFD9B, + 0xFD9C, + 0xFD9E, + 0xFD9F, + 0xFDA0, + 0xFDA1, + 0xFDA2, + 0xFDA3, + 0xFDA4, + 0xFDA5, + 0xFDA6, + 0xFDA7, + 0xFDA8, + 0xFDA9, + 0xFDAA, + 0xFDAB, + 0xFDAC, + 0xFDAD, + 0xFDAE, + 0xFDAF, + 0xFDB0, + 0xFDB1, + 0xFDB2, + 0xFDB3, + 0xFDB4, + 0xFDB5, + 0xFDB6, + 0xFDB7, + 0xFDB8, + 0xFDB9, + 0xFDBA, + 0xFDBB, + 0xFDBC, + 0xFDBD, + 0xFDBE, + 0xFDBF, + 0xFDC0, + 0xFDC1, + 0xFDC2, + 0xFDC3, + 0xFDC4, + 0xFDC5, + 0xFDC6, + 0xFDC7, + 0xFDC8, + 0xFDD0, + 0xFDF0, + 0xFDF1, + 0xFDF2, + 0xFDF3, + 0xFDF4, + 0xFDF5, + 0xFDF6, + 0xFDF7, + 0xFDF8, + 0xFDF9, + 0xFDFA, + 0xFDFB, + 0xFDFC, + 0xFDFD, + 0xFE00, + 0xFE10, + 0xFE11, + 0xFE12, + 0xFE13, + 0xFE14, + 0xFE15, + 0xFE16, + 0xFE17, + 0xFE18, + 0xFE19, + 0xFE20, + 0xFE30, + 0xFE31, + 0xFE32, + 0xFE33, + 0xFE35, + 0xFE36, + 0xFE37, + 0xFE38, + 0xFE39, + 0xFE3A, + 0xFE3B, + 0xFE3C, + 0xFE3D, + 0xFE3E, + 0xFE3F, + 0xFE40, + 0xFE41, + 0xFE42, + 0xFE43, + 0xFE44, + 0xFE45, + 0xFE47, + 0xFE48, + 0xFE49, + 0xFE4D, + 0xFE50, + 0xFE51, + 0xFE52, + 0xFE54, + 0xFE55, + 0xFE56, + 0xFE57, + 0xFE58, + 0xFE59, + 0xFE5A, + 0xFE5B, + 0xFE5C, + 0xFE5D, + 0xFE5E, + 0xFE5F, + 0xFE60, + 0xFE61, + 0xFE62, + 0xFE63, + 0xFE64, + 0xFE65, + 0xFE66, + 0xFE67, + 0xFE68, + 0xFE69, + 0xFE6A, + 0xFE6B, + 0xFE6C, + 0xFE70, + 0xFE71, + 0xFE72, + 0xFE73, + 0xFE74, + 0xFE75, + 0xFE76, + 0xFE77, + 0xFE78, + 0xFE79, + 0xFE7A, + 0xFE7B, + 0xFE7C, + 0xFE7D, + 0xFE7E, + 0xFE7F, + 0xFE80, + 0xFE81, + 0xFE83, + 0xFE85, + 0xFE87, + 0xFE89, + 0xFE8D, + 0xFE8F, + 0xFE93, + 0xFE95, + 0xFE99, + 0xFE9D, + 0xFEA1, + 0xFEA5, + 0xFEA9, + 0xFEAB, + 0xFEAD, + 0xFEAF, + 0xFEB1, + 0xFEB5, + 0xFEB9, + 0xFEBD, + 0xFEC1, + 0xFEC5, + 0xFEC9, + 0xFECD, + 0xFED1, + 0xFED5, + 0xFED9, + 0xFEDD, + 0xFEE1, + 0xFEE5, + 0xFEE9, + 0xFEED, + 0xFEEF, + 0xFEF1, + 0xFEF5, + 0xFEF7, + 0xFEF9, + 0xFEFB, + 0xFEFD, + 0xFEFF, + 0xFF00, + 0xFF01, + 0xFF02, + 0xFF03, + 0xFF04, + 0xFF05, + 0xFF06, + 0xFF07, + 0xFF08, + 0xFF09, + 0xFF0A, + 0xFF0B, + 0xFF0C, + 0xFF0D, + 0xFF0E, + 0xFF0F, + 0xFF10, + 0xFF11, + 0xFF12, + 0xFF13, + 0xFF14, + 0xFF15, + 0xFF16, + 0xFF17, + 0xFF18, + 0xFF19, + 0xFF1A, + 0xFF1B, + 0xFF1C, + 0xFF1D, + 0xFF1E, + 0xFF1F, + 0xFF20, + 0xFF21, + 0xFF22, + 0xFF23, + 0xFF24, + 0xFF25, + 0xFF26, + 0xFF27, + 0xFF28, + 0xFF29, + 0xFF2A, + 0xFF2B, + 0xFF2C, + 0xFF2D, + 0xFF2E, + 0xFF2F, + 0xFF30, + 0xFF31, + 0xFF32, + 0xFF33, + 0xFF34, + 0xFF35, + 0xFF36, + 0xFF37, + 0xFF38, + 0xFF39, + 0xFF3A, + 0xFF3B, + 0xFF3C, + 0xFF3D, + 0xFF3E, + 0xFF3F, + 0xFF40, + 0xFF41, + 0xFF42, + 0xFF43, + 0xFF44, + 0xFF45, + 0xFF46, + 0xFF47, + 0xFF48, + 0xFF49, + 0xFF4A, + 0xFF4B, + 0xFF4C, + 0xFF4D, + 0xFF4E, + 0xFF4F, + 0xFF50, + 0xFF51, + 0xFF52, + 0xFF53, + 0xFF54, + 0xFF55, + 0xFF56, + 0xFF57, + 0xFF58, + 0xFF59, + 0xFF5A, + 0xFF5B, + 0xFF5C, + 0xFF5D, + 0xFF5E, + 0xFF5F, + 0xFF60, + 0xFF61, + 0xFF62, + 0xFF63, + 0xFF64, + 0xFF65, + 0xFF66, + 0xFF67, + 0xFF68, + 0xFF69, + 0xFF6A, + 0xFF6B, + 0xFF6C, + 0xFF6D, + 0xFF6E, + 0xFF6F, + 0xFF70, + 0xFF71, + 0xFF72, + 0xFF73, + 0xFF74, + 0xFF75, + 0xFF76, + 0xFF77, + 0xFF78, + 0xFF79, + 0xFF7A, + 0xFF7B, + 0xFF7C, + 0xFF7D, + 0xFF7E, + 0xFF7F, + 0xFF80, + 0xFF81, + 0xFF82, + 0xFF83, + 0xFF84, + 0xFF85, + 0xFF86, + 0xFF87, + 0xFF88, + 0xFF89, + 0xFF8A, + 0xFF8B, + 0xFF8C, + 0xFF8D, + 0xFF8E, + 0xFF8F, + 0xFF90, + 0xFF91, + 0xFF92, + 0xFF93, + 0xFF94, + 0xFF95, + 0xFF96, + 0xFF97, + 0xFF98, + 0xFF99, + 0xFF9A, + 0xFF9B, + 0xFF9C, + 0xFF9D, + 0xFF9E, + 0xFF9F, + 0xFFA0, + 0xFFA1, + 0xFFA2, + 0xFFA3, + 0xFFA4, + 0xFFA5, + 0xFFA6, + 0xFFA7, + 0xFFA8, + 0xFFA9, + 0xFFAA, + 0xFFAB, + 0xFFAC, + 0xFFAD, + 0xFFAE, + 0xFFAF, + 0xFFB0, + 0xFFB1, + 0xFFB2, + 0xFFB3, + 0xFFB4, + 0xFFB5, + 0xFFB6, + 0xFFB7, + 0xFFB8, + 0xFFB9, + 0xFFBA, + 0xFFBB, + 0xFFBC, + 0xFFBD, + 0xFFBE, + 0xFFBF, + 0xFFC2, + 0xFFC3, + 0xFFC4, + 0xFFC5, + 0xFFC6, + 0xFFC7, + 0xFFC8, + 0xFFCA, + 0xFFCB, + 0xFFCC, + 0xFFCD, + 0xFFCE, + 0xFFCF, + 0xFFD0, + 0xFFD2, + 0xFFD3, + 0xFFD4, + 0xFFD5, + 0xFFD6, + 0xFFD7, + 0xFFD8, + 0xFFDA, + 0xFFDB, + 0xFFDC, + 0xFFDD, + 0xFFE0, + 0xFFE1, + 0xFFE2, + 0xFFE3, + 0xFFE4, + 0xFFE5, + 0xFFE6, + 0xFFE7, + 0xFFE8, + 0xFFE9, + 0xFFEA, + 0xFFEB, + 0xFFEC, + 0xFFED, + 0xFFEE, + 0xFFEF, + 0x10000, + 0x1000C, + 0x1000D, + 0x10027, + 0x10028, + 0x1003B, + 0x1003C, + 0x1003E, + 0x1003F, + 0x1004E, + 0x10050, + 0x1005E, + 0x10080, + 0x100FB, + 0x10100, + 0x10103, + 0x10107, + 0x10134, + 0x10137, + 0x1018F, + 0x10190, + 0x1019D, + 0x101A0, + 0x101A1, + 0x101D0, + 0x101FE, + 0x10280, + 0x1029D, + 0x102A0, + 0x102D1, + 0x102E0, + 0x102FC, + 0x10300, + 0x10324, + 0x1032D, + 0x1034B, + 0x10350, + 0x1037B, + 0x10380, + 0x1039E, + 0x1039F, + 0x103C4, + 0x103C8, + 0x103D6, + 0x10400, + 0x10401, + 0x10402, + 0x10403, + 0x10404, + 0x10405, + 0x10406, + 0x10407, + 0x10408, + 0x10409, + 0x1040A, + 0x1040B, + 0x1040C, + 0x1040D, + 0x1040E, + 0x1040F, + 0x10410, + 0x10411, + 0x10412, + 0x10413, + 0x10414, + 0x10415, + 0x10416, + 0x10417, + 0x10418, + 0x10419, + 0x1041A, + 0x1041B, + 0x1041C, + 0x1041D, + 0x1041E, + 0x1041F, + 0x10420, + 0x10421, + 0x10422, + 0x10423, + 0x10424, + 0x10425, + 0x10426, + 0x10427, + 0x10428, + 0x1049E, + 0x104A0, + 0x104AA, + 0x104B0, + 0x104B1, + 0x104B2, + 0x104B3, + 0x104B4, + 0x104B5, + 0x104B6, + 0x104B7, + 0x104B8, + 0x104B9, + 0x104BA, + 0x104BB, + 0x104BC, + 0x104BD, + 0x104BE, + 0x104BF, + 0x104C0, + 0x104C1, + 0x104C2, + 0x104C3, + 0x104C4, + 0x104C5, + 0x104C6, + 0x104C7, + 0x104C8, + 0x104C9, + 0x104CA, + 0x104CB, + 0x104CC, + 0x104CD, + 0x104CE, + 0x104CF, + 0x104D0, + 0x104D1, + 0x104D2, + 0x104D3, + 0x104D4, + 0x104D8, + 0x104FC, + 0x10500, + 0x10528, + 0x10530, + 0x10564, + 0x1056F, + 0x10570, + 0x10571, + 0x10572, + 0x10573, + 0x10574, + 0x10575, + 0x10576, + 0x10577, + 0x10578, + 0x10579, + 0x1057A, + 0x1057B, + 0x1057C, + 0x1057D, + 0x1057E, + 0x1057F, + 0x10580, + 0x10581, + 0x10582, + 0x10583, + 0x10584, + 0x10585, + 0x10586, + 0x10587, + 0x10588, + 0x10589, + 0x1058A, + 0x1058B, + 0x1058C, + 0x1058D, + 0x1058E, + 0x1058F, + 0x10590, + 0x10591, + 0x10592, + 0x10593, + 0x10594, + 0x10595, + 0x10596, + 0x10597, + 0x105A2, + 0x105A3, + 0x105B2, + 0x105B3, + 0x105BA, + 0x105BB, + 0x105BD, + 0x105C0, + 0x105F4, + 0x10600, + 0x10737, + 0x10740, + 0x10756, + 0x10760, + 0x10768, + 0x10780, + 0x10781, + 0x10782, + 0x10783, + 0x10784, + 0x10785, + 0x10786, + 0x10787, + 0x10788, + 0x10789, + 0x1078A, + 0x1078B, + 0x1078C, + 0x1078D, + 0x1078E, + 0x1078F, + 0x10790, + 0x10791, + 0x10792, + 0x10793, + 0x10794, + 0x10795, + 0x10796, + 0x10797, + 0x10798, + 0x10799, + 0x1079A, + 0x1079B, + 0x1079C, + 0x1079D, + 0x1079E, + 0x1079F, + 0x107A0, + 0x107A1, + 0x107A2, + 0x107A3, + 0x107A4, + 0x107A5, + 0x107A6, + 0x107A7, + 0x107A8, + 0x107A9, + 0x107AA, + 0x107AB, + 0x107AC, + 0x107AD, + 0x107AE, + 0x107AF, + 0x107B0, + 0x107B1, + 0x107B2, + 0x107B3, + 0x107B4, + 0x107B5, + 0x107B6, + 0x107B7, + 0x107B8, + 0x107B9, + 0x107BA, + 0x107BB, + 0x10800, + 0x10806, + 0x10808, + 0x10809, + 0x1080A, + 0x10836, + 0x10837, + 0x10839, + 0x1083C, + 0x1083D, + 0x1083F, + 0x10856, + 0x10857, + 0x1089F, + 0x108A7, + 0x108B0, + 0x108E0, + 0x108F3, + 0x108F4, + 0x108F6, + 0x108FB, + 0x1091C, + 0x1091F, + 0x1093A, + 0x1093F, + 0x1095A, + 0x10980, + 0x109B8, + 0x109BC, + 0x109D0, + 0x109D2, + 0x10A04, + 0x10A05, + 0x10A07, + 0x10A0C, + 0x10A14, + 0x10A15, + 0x10A18, + 0x10A19, + 0x10A36, + 0x10A38, + 0x10A3B, + 0x10A3F, + 0x10A49, + 0x10A50, + 0x10A59, + 0x10A60, + 0x10AA0, + 0x10AC0, + 0x10AE7, + 0x10AEB, + 0x10AF7, + 0x10B00, + 0x10B36, + 0x10B39, + 0x10B56, + 0x10B58, + 0x10B73, + 0x10B78, + 0x10B92, + 0x10B99, + 0x10B9D, + 0x10BA9, + 0x10BB0, + 0x10C00, + 0x10C49, + 0x10C80, + 0x10C81, + 0x10C82, + 0x10C83, + 0x10C84, + 0x10C85, + 0x10C86, + 0x10C87, + 0x10C88, + 0x10C89, + 0x10C8A, + 0x10C8B, + 0x10C8C, + 0x10C8D, + 0x10C8E, + 0x10C8F, + 0x10C90, + 0x10C91, + 0x10C92, + 0x10C93, + 0x10C94, + 0x10C95, + 0x10C96, + 0x10C97, + 0x10C98, + 0x10C99, + 0x10C9A, + 0x10C9B, + 0x10C9C, + 0x10C9D, + 0x10C9E, + 0x10C9F, + 0x10CA0, + 0x10CA1, + 0x10CA2, + 0x10CA3, + 0x10CA4, + 0x10CA5, + 0x10CA6, + 0x10CA7, + 0x10CA8, + 0x10CA9, + 0x10CAA, + 0x10CAB, + 0x10CAC, + 0x10CAD, + 0x10CAE, + 0x10CAF, + 0x10CB0, + 0x10CB1, + 0x10CB2, + 0x10CB3, + 0x10CC0, + 0x10CF3, + 0x10CFA, + 0x10D28, + 0x10D30, + 0x10D3A, + 0x10D40, + 0x10D50, + 0x10D51, + 0x10D52, + 0x10D53, + 0x10D54, + 0x10D55, + 0x10D56, + 0x10D57, + 0x10D58, + 0x10D59, + 0x10D5A, + 0x10D5B, + 0x10D5C, + 0x10D5D, + 0x10D5E, + 0x10D5F, + 0x10D60, + 0x10D61, + 0x10D62, + 0x10D63, + 0x10D64, + 0x10D65, + 0x10D66, + 0x10D69, + 0x10D86, + 0x10D8E, + 0x10D90, + 0x10E60, + 0x10E7F, + 0x10E80, + 0x10EAA, + 0x10EAB, + 0x10EAE, + 0x10EB0, + 0x10EB2, + 0x10EC2, + 0x10EC8, + 0x10ED0, + 0x10ED9, + 0x10EFA, + 0x10F28, + 0x10F30, + 0x10F5A, + 0x10F70, + 0x10F8A, + 0x10FB0, + 0x10FCC, + 0x10FE0, + 0x10FF7, + 0x11000, + 0x1104E, + 0x11052, + 0x11076, + 0x1107F, + 0x110BD, + 0x110BE, + 0x110C3, + 0x110D0, + 0x110E9, + 0x110F0, + 0x110FA, + 0x11100, + 0x11135, + 0x11136, + 0x11148, + 0x11150, + 0x11177, + 0x11180, + 0x111E0, + 0x111E1, + 0x111F5, + 0x11200, + 0x11212, + 0x11213, + 0x11242, + 0x11280, + 0x11287, + 0x11288, + 0x11289, + 0x1128A, + 0x1128E, + 0x1128F, + 0x1129E, + 0x1129F, + 0x112AA, + 0x112B0, + 0x112EB, + 0x112F0, + 0x112FA, + 0x11300, + 0x11304, + 0x11305, + 0x1130D, + 0x1130F, + 0x11311, + 0x11313, + 0x11329, + 0x1132A, + 0x11331, + 0x11332, + 0x11334, + 0x11335, + 0x1133A, + 0x1133B, + 0x11345, + 0x11347, + 0x11349, + 0x1134B, + 0x1134E, + 0x11350, + 0x11351, + 0x11357, + 0x11358, + 0x1135D, + 0x11364, + 0x11366, + 0x1136D, + 0x11370, + 0x11375, + 0x11380, + 0x1138A, + 0x1138B, + 0x1138C, + 0x1138E, + 0x1138F, + 0x11390, + 0x113B6, + 0x113B7, + 0x113C1, + 0x113C2, + 0x113C3, + 0x113C5, + 0x113C6, + 0x113C7, + 0x113CB, + 0x113CC, + 0x113D6, + 0x113D7, + 0x113D9, + 0x113E1, + 0x113E3, + 0x11400, + 0x1145C, + 0x1145D, + 0x11462, + 0x11480, + 0x114C8, + 0x114D0, + 0x114DA, + 0x11580, + 0x115B6, + 0x115B8, + 0x115DE, + 0x11600, + 0x11645, + 0x11650, + 0x1165A, + 0x11660, + 0x1166D, + 0x11680, + 0x116BA, + 0x116C0, + 0x116CA, + 0x116D0, + 0x116E4, + 0x11700, + 0x1171B, + 0x1171D, + 0x1172C, + 0x11730, + 0x11747, + 0x11800, + 0x1183C, + 0x118A0, + 0x118A1, + 0x118A2, + 0x118A3, + 0x118A4, + 0x118A5, + 0x118A6, + 0x118A7, + 0x118A8, + 0x118A9, + 0x118AA, + 0x118AB, + 0x118AC, + 0x118AD, + 0x118AE, + 0x118AF, + 0x118B0, + 0x118B1, + 0x118B2, + 0x118B3, + 0x118B4, + 0x118B5, + 0x118B6, + 0x118B7, + 0x118B8, + 0x118B9, + 0x118BA, + 0x118BB, + 0x118BC, + 0x118BD, + 0x118BE, + 0x118BF, + 0x118C0, + 0x118F3, + 0x118FF, + 0x11907, + 0x11909, + 0x1190A, + 0x1190C, + 0x11914, + 0x11915, + 0x11917, + 0x11918, + 0x11936, + 0x11937, + 0x11939, + 0x1193B, + 0x11947, + 0x11950, + 0x1195A, + 0x119A0, + 0x119A8, + 0x119AA, + 0x119D8, + 0x119DA, + 0x119E5, + 0x11A00, + 0x11A48, + 0x11A50, + 0x11AA3, + 0x11AB0, + 0x11AF9, + 0x11B00, + 0x11B0A, + 0x11B60, + 0x11B68, + 0x11BC0, + 0x11BE2, + 0x11BF0, + 0x11BFA, + 0x11C00, + 0x11C09, + 0x11C0A, + 0x11C37, + 0x11C38, + 0x11C46, + 0x11C50, + 0x11C6D, + 0x11C70, + 0x11C90, + 0x11C92, + 0x11CA8, + 0x11CA9, + 0x11CB7, + 0x11D00, + 0x11D07, + 0x11D08, + 0x11D0A, + 0x11D0B, + 0x11D37, + 0x11D3A, + 0x11D3B, + 0x11D3C, + 0x11D3E, + 0x11D3F, + 0x11D48, + 0x11D50, + 0x11D5A, + 0x11D60, + 0x11D66, + 0x11D67, + 0x11D69, + 0x11D6A, + 0x11D8F, + 0x11D90, + 0x11D92, + 0x11D93, + 0x11D99, + 0x11DA0, + 0x11DAA, + 0x11DB0, + 0x11DDC, + 0x11DE0, + 0x11DEA, + 0x11EE0, + 0x11EF9, + 0x11F00, + 0x11F11, + 0x11F12, + 0x11F3B, + 0x11F3E, + 0x11F5B, + 0x11FB0, + 0x11FB1, + 0x11FC0, + 0x11FF2, + 0x11FFF, + 0x1239A, + 0x12400, + 0x1246F, + 0x12470, + 0x12475, + 0x12480, + 0x12544, + 0x12F90, + 0x12FF3, + 0x13000, + 0x13430, + 0x13440, + 0x13456, + 0x13460, + 0x143FB, + 0x14400, + 0x14647, + 0x16100, + 0x1613A, + 0x16800, + 0x16A39, + 0x16A40, + 0x16A5F, + 0x16A60, + 0x16A6A, + 0x16A6E, + 0x16ABF, + 0x16AC0, + 0x16ACA, + 0x16AD0, + 0x16AEE, + 0x16AF0, + 0x16AF6, + 0x16B00, + 0x16B46, + 0x16B50, + 0x16B5A, + 0x16B5B, + 0x16B62, + 0x16B63, + 0x16B78, + 0x16B7D, + 0x16B90, + 0x16D40, + 0x16D7A, + 0x16E40, + 0x16E41, + 0x16E42, + 0x16E43, + 0x16E44, + 0x16E45, + 0x16E46, + 0x16E47, + 0x16E48, + 0x16E49, + 0x16E4A, + 0x16E4B, + 0x16E4C, + 0x16E4D, + 0x16E4E, + 0x16E4F, + 0x16E50, + 0x16E51, + 0x16E52, + 0x16E53, + 0x16E54, + 0x16E55, + 0x16E56, + 0x16E57, + 0x16E58, + 0x16E59, + 0x16E5A, + 0x16E5B, + 0x16E5C, + 0x16E5D, + 0x16E5E, + 0x16E5F, + 0x16E60, + 0x16E9B, + 0x16EA0, + 0x16EA1, + 0x16EA2, + 0x16EA3, + 0x16EA4, + 0x16EA5, + 0x16EA6, + 0x16EA7, + 0x16EA8, + 0x16EA9, + 0x16EAA, + 0x16EAB, + 0x16EAC, + 0x16EAD, + 0x16EAE, + 0x16EAF, + 0x16EB0, + 0x16EB1, + 0x16EB2, + 0x16EB3, + 0x16EB4, + 0x16EB5, + 0x16EB6, + 0x16EB7, + 0x16EB8, + 0x16EB9, + 0x16EBB, + 0x16ED4, + 0x16F00, + 0x16F4B, + 0x16F4F, + 0x16F88, + 0x16F8F, + 0x16FA0, + 0x16FE0, + 0x16FE5, + 0x16FF0, + 0x16FF7, + 0x17000, + 0x18CD6, + 0x18CFF, + 0x18D1F, + 0x18D80, + 0x18DF3, + 0x1AFF0, + 0x1AFF4, + 0x1AFF5, + 0x1AFFC, + 0x1AFFD, + 0x1AFFF, + 0x1B000, + 0x1B123, + 0x1B132, + 0x1B133, + 0x1B150, + 0x1B153, + 0x1B155, + 0x1B156, + 0x1B164, + 0x1B168, + 0x1B170, + 0x1B2FC, + 0x1BC00, + 0x1BC6B, + 0x1BC70, + 0x1BC7D, + 0x1BC80, + 0x1BC89, + 0x1BC90, + 0x1BC9A, + 0x1BC9C, + 0x1BCA0, + 0x1BCA4, + 0x1CC00, + 0x1CCD6, + 0x1CCD7, + 0x1CCD8, + 0x1CCD9, + 0x1CCDA, + 0x1CCDB, + 0x1CCDC, + 0x1CCDD, + 0x1CCDE, + 0x1CCDF, + 0x1CCE0, + 0x1CCE1, + 0x1CCE2, + 0x1CCE3, + 0x1CCE4, + 0x1CCE5, + 0x1CCE6, + 0x1CCE7, + 0x1CCE8, + 0x1CCE9, + 0x1CCEA, + 0x1CCEB, + 0x1CCEC, + 0x1CCED, + 0x1CCEE, + 0x1CCEF, + 0x1CCF0, + 0x1CCF1, + 0x1CCF2, + 0x1CCF3, + 0x1CCF4, + 0x1CCF5, + 0x1CCF6, + 0x1CCF7, + 0x1CCF8, + 0x1CCF9, + 0x1CCFA, + 0x1CCFD, + 0x1CD00, + 0x1CEB4, + 0x1CEBA, + 0x1CED1, + 0x1CEE0, + 0x1CEF1, + 0x1CF00, + 0x1CF2E, + 0x1CF30, + 0x1CF47, + 0x1CF50, + 0x1CFC4, + 0x1D000, + 0x1D0F6, + 0x1D100, + 0x1D127, + 0x1D129, + 0x1D15E, + 0x1D15F, + 0x1D160, + 0x1D161, + 0x1D162, + 0x1D163, + 0x1D164, + 0x1D165, + 0x1D173, + 0x1D17B, + 0x1D1BB, + 0x1D1BC, + 0x1D1BD, + 0x1D1BE, + 0x1D1BF, + 0x1D1C0, + 0x1D1C1, + 0x1D1EB, + 0x1D200, + 0x1D246, + 0x1D2C0, + 0x1D2D4, + 0x1D2E0, + 0x1D2F4, + 0x1D300, + 0x1D357, + 0x1D360, + 0x1D379, + 0x1D400, + 0x1D401, + 0x1D402, + 0x1D403, + 0x1D404, + 0x1D405, + 0x1D406, + 0x1D407, + 0x1D408, + 0x1D409, + 0x1D40A, + 0x1D40B, + 0x1D40C, + 0x1D40D, + 0x1D40E, + 0x1D40F, + 0x1D410, + 0x1D411, + 0x1D412, + 0x1D413, + 0x1D414, + 0x1D415, + 0x1D416, + 0x1D417, + 0x1D418, + 0x1D419, + 0x1D41A, + 0x1D41B, + 0x1D41C, + 0x1D41D, + 0x1D41E, + 0x1D41F, + 0x1D420, + 0x1D421, + 0x1D422, + 0x1D423, + 0x1D424, + 0x1D425, + 0x1D426, + 0x1D427, + 0x1D428, + 0x1D429, + 0x1D42A, + 0x1D42B, + 0x1D42C, + 0x1D42D, + 0x1D42E, + 0x1D42F, + 0x1D430, + 0x1D431, + 0x1D432, + 0x1D433, + 0x1D434, + 0x1D435, + 0x1D436, + 0x1D437, + 0x1D438, + 0x1D439, + 0x1D43A, + 0x1D43B, + 0x1D43C, + 0x1D43D, + 0x1D43E, + 0x1D43F, + 0x1D440, + 0x1D441, + 0x1D442, + 0x1D443, + 0x1D444, + 0x1D445, + 0x1D446, + 0x1D447, + 0x1D448, + 0x1D449, + 0x1D44A, + 0x1D44B, + 0x1D44C, + 0x1D44D, + 0x1D44E, + 0x1D44F, + 0x1D450, + 0x1D451, + 0x1D452, + 0x1D453, + 0x1D454, + 0x1D455, + 0x1D456, + 0x1D457, + 0x1D458, + 0x1D459, + 0x1D45A, + 0x1D45B, + 0x1D45C, + 0x1D45D, + 0x1D45E, + 0x1D45F, + 0x1D460, + 0x1D461, + 0x1D462, + 0x1D463, + 0x1D464, + 0x1D465, + 0x1D466, + 0x1D467, + 0x1D468, + 0x1D469, + 0x1D46A, + 0x1D46B, + 0x1D46C, + 0x1D46D, + 0x1D46E, + 0x1D46F, + 0x1D470, + 0x1D471, + 0x1D472, + 0x1D473, + 0x1D474, + 0x1D475, + 0x1D476, + 0x1D477, + 0x1D478, + 0x1D479, + 0x1D47A, + 0x1D47B, + 0x1D47C, + 0x1D47D, + 0x1D47E, + 0x1D47F, + 0x1D480, + 0x1D481, + 0x1D482, + 0x1D483, + 0x1D484, + 0x1D485, + 0x1D486, + 0x1D487, + 0x1D488, + 0x1D489, + 0x1D48A, + 0x1D48B, + 0x1D48C, + 0x1D48D, + 0x1D48E, + 0x1D48F, + 0x1D490, + 0x1D491, + 0x1D492, + 0x1D493, + 0x1D494, + 0x1D495, + 0x1D496, + 0x1D497, + 0x1D498, + 0x1D499, + 0x1D49A, + 0x1D49B, + 0x1D49C, + 0x1D49D, + 0x1D49E, + 0x1D49F, + 0x1D4A0, + 0x1D4A2, + 0x1D4A3, + 0x1D4A5, + 0x1D4A6, + 0x1D4A7, + 0x1D4A9, + 0x1D4AA, + 0x1D4AB, + 0x1D4AC, + 0x1D4AD, + 0x1D4AE, + 0x1D4AF, + 0x1D4B0, + 0x1D4B1, + 0x1D4B2, + 0x1D4B3, + 0x1D4B4, + 0x1D4B5, + 0x1D4B6, + 0x1D4B7, + 0x1D4B8, + 0x1D4B9, + 0x1D4BA, + 0x1D4BB, + 0x1D4BC, + 0x1D4BD, + 0x1D4BE, + 0x1D4BF, + 0x1D4C0, + 0x1D4C1, + 0x1D4C2, + 0x1D4C3, + 0x1D4C4, + 0x1D4C5, + 0x1D4C6, + 0x1D4C7, + 0x1D4C8, + 0x1D4C9, + 0x1D4CA, + 0x1D4CB, + 0x1D4CC, + 0x1D4CD, + 0x1D4CE, + 0x1D4CF, + 0x1D4D0, + 0x1D4D1, + 0x1D4D2, + 0x1D4D3, + 0x1D4D4, + 0x1D4D5, + 0x1D4D6, + 0x1D4D7, + 0x1D4D8, + 0x1D4D9, + 0x1D4DA, + 0x1D4DB, + 0x1D4DC, + 0x1D4DD, + 0x1D4DE, + 0x1D4DF, + 0x1D4E0, + 0x1D4E1, + 0x1D4E2, + 0x1D4E3, + 0x1D4E4, + 0x1D4E5, + 0x1D4E6, + 0x1D4E7, + 0x1D4E8, + 0x1D4E9, + 0x1D4EA, + 0x1D4EB, + 0x1D4EC, + 0x1D4ED, + 0x1D4EE, + 0x1D4EF, + 0x1D4F0, + 0x1D4F1, + 0x1D4F2, + 0x1D4F3, + 0x1D4F4, + 0x1D4F5, + 0x1D4F6, + 0x1D4F7, + 0x1D4F8, + 0x1D4F9, + 0x1D4FA, + 0x1D4FB, + 0x1D4FC, + 0x1D4FD, + 0x1D4FE, + 0x1D4FF, + 0x1D500, + 0x1D501, + 0x1D502, + 0x1D503, + 0x1D504, + 0x1D505, + 0x1D506, + 0x1D507, + 0x1D508, + 0x1D509, + 0x1D50A, + 0x1D50B, + 0x1D50D, + 0x1D50E, + 0x1D50F, + 0x1D510, + 0x1D511, + 0x1D512, + 0x1D513, + 0x1D514, + 0x1D515, + 0x1D516, + 0x1D517, + 0x1D518, + 0x1D519, + 0x1D51A, + 0x1D51B, + 0x1D51C, + 0x1D51D, + 0x1D51E, + 0x1D51F, + 0x1D520, + 0x1D521, + 0x1D522, + 0x1D523, + 0x1D524, + 0x1D525, + 0x1D526, + 0x1D527, + 0x1D528, + 0x1D529, + 0x1D52A, + 0x1D52B, + 0x1D52C, + 0x1D52D, + 0x1D52E, + 0x1D52F, + 0x1D530, + 0x1D531, + 0x1D532, + 0x1D533, + 0x1D534, + 0x1D535, + 0x1D536, + 0x1D537, + 0x1D538, + 0x1D539, + 0x1D53A, + 0x1D53B, + 0x1D53C, + 0x1D53D, + 0x1D53E, + 0x1D53F, + 0x1D540, + 0x1D541, + 0x1D542, + 0x1D543, + 0x1D544, + 0x1D545, + 0x1D546, + 0x1D547, + 0x1D54A, + 0x1D54B, + 0x1D54C, + 0x1D54D, + 0x1D54E, + 0x1D54F, + 0x1D550, + 0x1D551, + 0x1D552, + 0x1D553, + 0x1D554, + 0x1D555, + 0x1D556, + 0x1D557, + 0x1D558, + 0x1D559, + 0x1D55A, + 0x1D55B, + 0x1D55C, + 0x1D55D, + 0x1D55E, + 0x1D55F, + 0x1D560, + 0x1D561, + 0x1D562, + 0x1D563, + 0x1D564, + 0x1D565, + 0x1D566, + 0x1D567, + 0x1D568, + 0x1D569, + 0x1D56A, + 0x1D56B, + 0x1D56C, + 0x1D56D, + 0x1D56E, + 0x1D56F, + 0x1D570, + 0x1D571, + 0x1D572, + 0x1D573, + 0x1D574, + 0x1D575, + 0x1D576, + 0x1D577, + 0x1D578, + 0x1D579, + 0x1D57A, + 0x1D57B, + 0x1D57C, + 0x1D57D, + 0x1D57E, + 0x1D57F, + 0x1D580, + 0x1D581, + 0x1D582, + 0x1D583, + 0x1D584, + 0x1D585, + 0x1D586, + 0x1D587, + 0x1D588, + 0x1D589, + 0x1D58A, + 0x1D58B, + 0x1D58C, + 0x1D58D, + 0x1D58E, + 0x1D58F, + 0x1D590, + 0x1D591, + 0x1D592, + 0x1D593, + 0x1D594, + 0x1D595, + 0x1D596, + 0x1D597, + 0x1D598, + 0x1D599, + 0x1D59A, + 0x1D59B, + 0x1D59C, + 0x1D59D, + 0x1D59E, + 0x1D59F, + 0x1D5A0, + 0x1D5A1, + 0x1D5A2, + 0x1D5A3, + 0x1D5A4, + 0x1D5A5, + 0x1D5A6, + 0x1D5A7, + 0x1D5A8, + 0x1D5A9, + 0x1D5AA, + 0x1D5AB, + 0x1D5AC, + 0x1D5AD, + 0x1D5AE, + 0x1D5AF, + 0x1D5B0, + 0x1D5B1, + 0x1D5B2, + 0x1D5B3, + 0x1D5B4, + 0x1D5B5, + 0x1D5B6, + 0x1D5B7, + 0x1D5B8, + 0x1D5B9, + 0x1D5BA, + 0x1D5BB, + 0x1D5BC, + 0x1D5BD, + 0x1D5BE, + 0x1D5BF, + 0x1D5C0, + 0x1D5C1, + 0x1D5C2, + 0x1D5C3, + 0x1D5C4, + 0x1D5C5, + 0x1D5C6, + 0x1D5C7, + 0x1D5C8, + 0x1D5C9, + 0x1D5CA, + 0x1D5CB, + 0x1D5CC, + 0x1D5CD, + 0x1D5CE, + 0x1D5CF, + 0x1D5D0, + 0x1D5D1, + 0x1D5D2, + 0x1D5D3, + 0x1D5D4, + 0x1D5D5, + 0x1D5D6, + 0x1D5D7, + 0x1D5D8, + 0x1D5D9, + 0x1D5DA, + 0x1D5DB, + 0x1D5DC, + 0x1D5DD, + 0x1D5DE, + 0x1D5DF, + 0x1D5E0, + 0x1D5E1, + 0x1D5E2, + 0x1D5E3, + 0x1D5E4, + 0x1D5E5, + 0x1D5E6, + 0x1D5E7, + 0x1D5E8, + 0x1D5E9, + 0x1D5EA, + 0x1D5EB, + 0x1D5EC, + 0x1D5ED, + 0x1D5EE, + 0x1D5EF, + 0x1D5F0, + 0x1D5F1, + 0x1D5F2, + 0x1D5F3, + 0x1D5F4, + 0x1D5F5, + 0x1D5F6, + 0x1D5F7, + 0x1D5F8, + 0x1D5F9, + 0x1D5FA, + 0x1D5FB, + 0x1D5FC, + 0x1D5FD, + 0x1D5FE, + 0x1D5FF, + 0x1D600, + 0x1D601, + 0x1D602, + 0x1D603, + 0x1D604, + 0x1D605, + 0x1D606, + 0x1D607, + 0x1D608, + 0x1D609, + 0x1D60A, + 0x1D60B, + 0x1D60C, + 0x1D60D, + 0x1D60E, + 0x1D60F, + 0x1D610, + 0x1D611, + 0x1D612, + 0x1D613, + 0x1D614, + 0x1D615, + 0x1D616, + 0x1D617, + 0x1D618, + 0x1D619, + 0x1D61A, + 0x1D61B, + 0x1D61C, + 0x1D61D, + 0x1D61E, + 0x1D61F, + 0x1D620, + 0x1D621, + 0x1D622, + 0x1D623, + 0x1D624, + 0x1D625, + 0x1D626, + 0x1D627, + 0x1D628, + 0x1D629, + 0x1D62A, + 0x1D62B, + 0x1D62C, + 0x1D62D, + 0x1D62E, + 0x1D62F, + 0x1D630, + 0x1D631, + 0x1D632, + 0x1D633, + 0x1D634, + 0x1D635, + 0x1D636, + 0x1D637, + 0x1D638, + 0x1D639, + 0x1D63A, + 0x1D63B, + 0x1D63C, + 0x1D63D, + 0x1D63E, + 0x1D63F, + 0x1D640, + 0x1D641, + 0x1D642, + 0x1D643, + 0x1D644, + 0x1D645, + 0x1D646, + 0x1D647, + 0x1D648, + 0x1D649, + 0x1D64A, + 0x1D64B, + 0x1D64C, + 0x1D64D, + 0x1D64E, + 0x1D64F, + 0x1D650, + 0x1D651, + 0x1D652, + 0x1D653, + 0x1D654, + 0x1D655, + 0x1D656, + 0x1D657, + 0x1D658, + 0x1D659, + 0x1D65A, + 0x1D65B, + 0x1D65C, + 0x1D65D, + 0x1D65E, + 0x1D65F, + 0x1D660, + 0x1D661, + 0x1D662, + 0x1D663, + 0x1D664, + 0x1D665, + 0x1D666, + 0x1D667, + 0x1D668, + 0x1D669, + 0x1D66A, + 0x1D66B, + 0x1D66C, + 0x1D66D, + 0x1D66E, + 0x1D66F, + 0x1D670, + 0x1D671, + 0x1D672, + 0x1D673, + 0x1D674, + 0x1D675, + 0x1D676, + 0x1D677, + 0x1D678, + 0x1D679, + 0x1D67A, + 0x1D67B, + 0x1D67C, + 0x1D67D, + 0x1D67E, + 0x1D67F, + 0x1D680, + 0x1D681, + 0x1D682, + 0x1D683, + 0x1D684, + 0x1D685, + 0x1D686, + 0x1D687, + 0x1D688, + 0x1D689, + 0x1D68A, + 0x1D68B, + 0x1D68C, + 0x1D68D, + 0x1D68E, + 0x1D68F, + 0x1D690, + 0x1D691, + 0x1D692, + 0x1D693, + 0x1D694, + 0x1D695, + 0x1D696, + 0x1D697, + 0x1D698, + 0x1D699, + 0x1D69A, + 0x1D69B, + 0x1D69C, + 0x1D69D, + 0x1D69E, + 0x1D69F, + 0x1D6A0, + 0x1D6A1, + 0x1D6A2, + 0x1D6A3, + 0x1D6A4, + 0x1D6A5, + 0x1D6A6, + 0x1D6A8, + 0x1D6A9, + 0x1D6AA, + 0x1D6AB, + 0x1D6AC, + 0x1D6AD, + 0x1D6AE, + 0x1D6AF, + 0x1D6B0, + 0x1D6B1, + 0x1D6B2, + 0x1D6B3, + 0x1D6B4, + 0x1D6B5, + 0x1D6B6, + 0x1D6B7, + 0x1D6B8, + 0x1D6B9, + 0x1D6BA, + 0x1D6BB, + 0x1D6BC, + 0x1D6BD, + 0x1D6BE, + 0x1D6BF, + 0x1D6C0, + 0x1D6C1, + 0x1D6C2, + 0x1D6C3, + 0x1D6C4, + 0x1D6C5, + 0x1D6C6, + 0x1D6C7, + 0x1D6C8, + 0x1D6C9, + 0x1D6CA, + 0x1D6CB, + 0x1D6CC, + 0x1D6CD, + 0x1D6CE, + 0x1D6CF, + 0x1D6D0, + 0x1D6D1, + 0x1D6D2, + 0x1D6D3, + 0x1D6D5, + 0x1D6D6, + 0x1D6D7, + 0x1D6D8, + 0x1D6D9, + 0x1D6DA, + 0x1D6DB, + 0x1D6DC, + 0x1D6DD, + 0x1D6DE, + 0x1D6DF, + 0x1D6E0, + 0x1D6E1, + 0x1D6E2, + 0x1D6E3, + 0x1D6E4, + 0x1D6E5, + 0x1D6E6, + 0x1D6E7, + 0x1D6E8, + 0x1D6E9, + 0x1D6EA, + 0x1D6EB, + 0x1D6EC, + 0x1D6ED, + 0x1D6EE, + 0x1D6EF, + 0x1D6F0, + 0x1D6F1, + 0x1D6F2, + 0x1D6F3, + 0x1D6F4, + 0x1D6F5, + 0x1D6F6, + 0x1D6F7, + 0x1D6F8, + 0x1D6F9, + 0x1D6FA, + 0x1D6FB, + 0x1D6FC, + 0x1D6FD, + 0x1D6FE, + 0x1D6FF, + 0x1D700, + 0x1D701, + 0x1D702, + 0x1D703, + 0x1D704, + 0x1D705, + 0x1D706, + 0x1D707, + 0x1D708, + 0x1D709, + 0x1D70A, + 0x1D70B, + 0x1D70C, + 0x1D70D, + 0x1D70F, + 0x1D710, + 0x1D711, + 0x1D712, + 0x1D713, + 0x1D714, + 0x1D715, + 0x1D716, + 0x1D717, + 0x1D718, + 0x1D719, + 0x1D71A, + 0x1D71B, + 0x1D71C, + 0x1D71D, + 0x1D71E, + 0x1D71F, + 0x1D720, + 0x1D721, + 0x1D722, + 0x1D723, + 0x1D724, + 0x1D725, + 0x1D726, + 0x1D727, + 0x1D728, + 0x1D729, + 0x1D72A, + 0x1D72B, + 0x1D72C, + 0x1D72D, + 0x1D72E, + 0x1D72F, + 0x1D730, + 0x1D731, + 0x1D732, + 0x1D733, + 0x1D734, + 0x1D735, + 0x1D736, + 0x1D737, + 0x1D738, + 0x1D739, + 0x1D73A, + 0x1D73B, + 0x1D73C, + 0x1D73D, + 0x1D73E, + 0x1D73F, + 0x1D740, + 0x1D741, + 0x1D742, + 0x1D743, + 0x1D744, + 0x1D745, + 0x1D746, + 0x1D747, + 0x1D749, + 0x1D74A, + 0x1D74B, + 0x1D74C, + 0x1D74D, + 0x1D74E, + 0x1D74F, + 0x1D750, + 0x1D751, + 0x1D752, + 0x1D753, + 0x1D754, + 0x1D755, + 0x1D756, + 0x1D757, + 0x1D758, + 0x1D759, + 0x1D75A, + 0x1D75B, + 0x1D75C, + 0x1D75D, + 0x1D75E, + 0x1D75F, + 0x1D760, + 0x1D761, + 0x1D762, + 0x1D763, + 0x1D764, + 0x1D765, + 0x1D766, + 0x1D767, + 0x1D768, + 0x1D769, + 0x1D76A, + 0x1D76B, + 0x1D76C, + 0x1D76D, + 0x1D76E, + 0x1D76F, + 0x1D770, + 0x1D771, + 0x1D772, + 0x1D773, + 0x1D774, + 0x1D775, + 0x1D776, + 0x1D777, + 0x1D778, + 0x1D779, + 0x1D77A, + 0x1D77B, + 0x1D77C, + 0x1D77D, + 0x1D77E, + 0x1D77F, + 0x1D780, + 0x1D781, + 0x1D783, + 0x1D784, + 0x1D785, + 0x1D786, + 0x1D787, + 0x1D788, + 0x1D789, + 0x1D78A, + 0x1D78B, + 0x1D78C, + 0x1D78D, + 0x1D78E, + 0x1D78F, + 0x1D790, + 0x1D791, + 0x1D792, + 0x1D793, + 0x1D794, + 0x1D795, + 0x1D796, + 0x1D797, + 0x1D798, + 0x1D799, + 0x1D79A, + 0x1D79B, + 0x1D79C, + 0x1D79D, + 0x1D79E, + 0x1D79F, + 0x1D7A0, + 0x1D7A1, + 0x1D7A2, + 0x1D7A3, + 0x1D7A4, + 0x1D7A5, + 0x1D7A6, + 0x1D7A7, + 0x1D7A8, + 0x1D7A9, + 0x1D7AA, + 0x1D7AB, + 0x1D7AC, + 0x1D7AD, + 0x1D7AE, + 0x1D7AF, + 0x1D7B0, + 0x1D7B1, + 0x1D7B2, + 0x1D7B3, + 0x1D7B4, + 0x1D7B5, + 0x1D7B6, + 0x1D7B7, + 0x1D7B8, + 0x1D7B9, + 0x1D7BA, + 0x1D7BB, + 0x1D7BD, + 0x1D7BE, + 0x1D7BF, + 0x1D7C0, + 0x1D7C1, + 0x1D7C2, + 0x1D7C3, + 0x1D7C4, + 0x1D7C5, + 0x1D7C6, + 0x1D7C7, + 0x1D7C8, + 0x1D7C9, + 0x1D7CA, + 0x1D7CC, + 0x1D7CE, + 0x1D7CF, + 0x1D7D0, + 0x1D7D1, + 0x1D7D2, + 0x1D7D3, + 0x1D7D4, + 0x1D7D5, + 0x1D7D6, + 0x1D7D7, + 0x1D7D8, + 0x1D7D9, + 0x1D7DA, + 0x1D7DB, + 0x1D7DC, + 0x1D7DD, + 0x1D7DE, + 0x1D7DF, + 0x1D7E0, + 0x1D7E1, + 0x1D7E2, + 0x1D7E3, + 0x1D7E4, + 0x1D7E5, + 0x1D7E6, + 0x1D7E7, + 0x1D7E8, + 0x1D7E9, + 0x1D7EA, + 0x1D7EB, + 0x1D7EC, + 0x1D7ED, + 0x1D7EE, + 0x1D7EF, + 0x1D7F0, + 0x1D7F1, + 0x1D7F2, + 0x1D7F3, + 0x1D7F4, + 0x1D7F5, + 0x1D7F6, + 0x1D7F7, + 0x1D7F8, + 0x1D7F9, + 0x1D7FA, + 0x1D7FB, + 0x1D7FC, + 0x1D7FD, + 0x1D7FE, + 0x1D7FF, + 0x1D800, + 0x1DA8C, + 0x1DA9B, + 0x1DAA0, + 0x1DAA1, + 0x1DAB0, + 0x1DF00, + 0x1DF1F, + 0x1DF25, + 0x1DF2B, + 0x1E000, + 0x1E007, + 0x1E008, + 0x1E019, + 0x1E01B, + 0x1E022, + 0x1E023, + 0x1E025, + 0x1E026, + 0x1E02B, + 0x1E030, + 0x1E031, + 0x1E032, + 0x1E033, + 0x1E034, + 0x1E035, + 0x1E036, + 0x1E037, + 0x1E038, + 0x1E039, + 0x1E03A, + 0x1E03B, + 0x1E03C, + 0x1E03D, + 0x1E03E, + 0x1E03F, + 0x1E040, + 0x1E041, + 0x1E042, + 0x1E043, + 0x1E044, + 0x1E045, + 0x1E046, + 0x1E047, + 0x1E048, + 0x1E049, + 0x1E04A, + 0x1E04B, + 0x1E04C, + 0x1E04D, + 0x1E04E, + 0x1E04F, + 0x1E050, + 0x1E051, + 0x1E052, + 0x1E053, + 0x1E054, + 0x1E055, + 0x1E056, + 0x1E057, + 0x1E058, + 0x1E059, + 0x1E05A, + 0x1E05B, + 0x1E05C, + 0x1E05D, + 0x1E05E, + 0x1E05F, + 0x1E060, + 0x1E061, + 0x1E062, + 0x1E063, + 0x1E064, + 0x1E065, + 0x1E066, + 0x1E067, + 0x1E068, + 0x1E069, + 0x1E06A, + 0x1E06B, + 0x1E06C, + 0x1E06D, + 0x1E06E, + 0x1E08F, + 0x1E090, + 0x1E100, + 0x1E12D, + 0x1E130, + 0x1E13E, + 0x1E140, + 0x1E14A, + 0x1E14E, + 0x1E150, + 0x1E290, + 0x1E2AF, + 0x1E2C0, + 0x1E2FA, + 0x1E2FF, + 0x1E300, + 0x1E4D0, + 0x1E4FA, + 0x1E5D0, + 0x1E5FB, + 0x1E5FF, + 0x1E600, + 0x1E6C0, + 0x1E6DF, + 0x1E6E0, + 0x1E6F6, + 0x1E6FE, + 0x1E700, + 0x1E7E0, + 0x1E7E7, + 0x1E7E8, + 0x1E7EC, + 0x1E7ED, + 0x1E7EF, + 0x1E7F0, + 0x1E7FF, + 0x1E800, + 0x1E8C5, + 0x1E8C7, + 0x1E8D7, + 0x1E900, + 0x1E901, + 0x1E902, + 0x1E903, + 0x1E904, + 0x1E905, + 0x1E906, + 0x1E907, + 0x1E908, + 0x1E909, + 0x1E90A, + 0x1E90B, + 0x1E90C, + 0x1E90D, + 0x1E90E, + 0x1E90F, + 0x1E910, + 0x1E911, + 0x1E912, + 0x1E913, + 0x1E914, + 0x1E915, + 0x1E916, + 0x1E917, + 0x1E918, + 0x1E919, + 0x1E91A, + 0x1E91B, + 0x1E91C, + 0x1E91D, + 0x1E91E, + 0x1E91F, + 0x1E920, + 0x1E921, + 0x1E922, + 0x1E94C, + 0x1E950, + 0x1E95A, + 0x1E95E, + 0x1E960, + 0x1EC71, + 0x1ECB5, + 0x1ED01, + 0x1ED3E, + 0x1EE00, + 0x1EE01, + 0x1EE02, + 0x1EE03, + 0x1EE04, + 0x1EE05, + 0x1EE06, + 0x1EE07, + 0x1EE08, + 0x1EE09, + 0x1EE0A, + 0x1EE0B, + 0x1EE0C, + 0x1EE0D, + 0x1EE0E, + 0x1EE0F, + 0x1EE10, + 0x1EE11, + 0x1EE12, + 0x1EE13, + 0x1EE14, + 0x1EE15, + 0x1EE16, + 0x1EE17, + 0x1EE18, + 0x1EE19, + 0x1EE1A, + 0x1EE1B, + 0x1EE1C, + 0x1EE1D, + 0x1EE1E, + 0x1EE1F, + 0x1EE20, + 0x1EE21, + 0x1EE22, + 0x1EE23, + 0x1EE24, + 0x1EE25, + 0x1EE27, + 0x1EE28, + 0x1EE29, + 0x1EE2A, + 0x1EE2B, + 0x1EE2C, + 0x1EE2D, + 0x1EE2E, + 0x1EE2F, + 0x1EE30, + 0x1EE31, + 0x1EE32, + 0x1EE33, + 0x1EE34, + 0x1EE35, + 0x1EE36, + 0x1EE37, + 0x1EE38, + 0x1EE39, + 0x1EE3A, + 0x1EE3B, + 0x1EE3C, + 0x1EE42, + 0x1EE43, + 0x1EE47, + 0x1EE48, + 0x1EE49, + 0x1EE4A, + 0x1EE4B, + 0x1EE4C, + 0x1EE4D, + 0x1EE4E, + 0x1EE4F, + 0x1EE50, + 0x1EE51, + 0x1EE52, + 0x1EE53, + 0x1EE54, + 0x1EE55, + 0x1EE57, + 0x1EE58, + 0x1EE59, + 0x1EE5A, + 0x1EE5B, + 0x1EE5C, + 0x1EE5D, + 0x1EE5E, + 0x1EE5F, + 0x1EE60, + 0x1EE61, + 0x1EE62, + 0x1EE63, + 0x1EE64, + 0x1EE65, + 0x1EE67, + 0x1EE68, + 0x1EE69, + 0x1EE6A, + 0x1EE6B, + 0x1EE6C, + 0x1EE6D, + 0x1EE6E, + 0x1EE6F, + 0x1EE70, + 0x1EE71, + 0x1EE72, + 0x1EE73, + 0x1EE74, + 0x1EE75, + 0x1EE76, + 0x1EE77, + 0x1EE78, + 0x1EE79, + 0x1EE7A, + 0x1EE7B, + 0x1EE7C, + 0x1EE7D, + 0x1EE7E, + 0x1EE7F, + 0x1EE80, + 0x1EE81, + 0x1EE82, + 0x1EE83, + 0x1EE84, + 0x1EE85, + 0x1EE86, + 0x1EE87, + 0x1EE88, + 0x1EE89, + 0x1EE8A, + 0x1EE8B, + 0x1EE8C, + 0x1EE8D, + 0x1EE8E, + 0x1EE8F, + 0x1EE90, + 0x1EE91, + 0x1EE92, + 0x1EE93, + 0x1EE94, + 0x1EE95, + 0x1EE96, + 0x1EE97, + 0x1EE98, + 0x1EE99, + 0x1EE9A, + 0x1EE9B, + 0x1EE9C, + 0x1EEA1, + 0x1EEA2, + 0x1EEA3, + 0x1EEA4, + 0x1EEA5, + 0x1EEA6, + 0x1EEA7, + 0x1EEA8, + 0x1EEA9, + 0x1EEAA, + 0x1EEAB, + 0x1EEAC, + 0x1EEAD, + 0x1EEAE, + 0x1EEAF, + 0x1EEB0, + 0x1EEB1, + 0x1EEB2, + 0x1EEB3, + 0x1EEB4, + 0x1EEB5, + 0x1EEB6, + 0x1EEB7, + 0x1EEB8, + 0x1EEB9, + 0x1EEBA, + 0x1EEBB, + 0x1EEBC, + 0x1EEF0, + 0x1EEF2, + 0x1F000, + 0x1F02C, + 0x1F030, + 0x1F094, + 0x1F0A0, + 0x1F0AF, + 0x1F0B1, + 0x1F0C0, + 0x1F0C1, + 0x1F0D0, + 0x1F0D1, + 0x1F0F6, + 0x1F101, + 0x1F102, + 0x1F103, + 0x1F104, + 0x1F105, + 0x1F106, + 0x1F107, + 0x1F108, + 0x1F109, + 0x1F10A, + 0x1F10B, + 0x1F110, + 0x1F111, + 0x1F112, + 0x1F113, + 0x1F114, + 0x1F115, + 0x1F116, + 0x1F117, + 0x1F118, + 0x1F119, + 0x1F11A, + 0x1F11B, + 0x1F11C, + 0x1F11D, + 0x1F11E, + 0x1F11F, + 0x1F120, + 0x1F121, + 0x1F122, + 0x1F123, + 0x1F124, + 0x1F125, + 0x1F126, + 0x1F127, + 0x1F128, + 0x1F129, + 0x1F12A, + 0x1F12B, + 0x1F12C, + 0x1F12D, + 0x1F12E, + 0x1F12F, + 0x1F130, + 0x1F131, + 0x1F132, + 0x1F133, + 0x1F134, + 0x1F135, + 0x1F136, + 0x1F137, + 0x1F138, + 0x1F139, + 0x1F13A, + 0x1F13B, + 0x1F13C, + 0x1F13D, + 0x1F13E, + 0x1F13F, + 0x1F140, + 0x1F141, + 0x1F142, + 0x1F143, + 0x1F144, + 0x1F145, + 0x1F146, + 0x1F147, + 0x1F148, + 0x1F149, + 0x1F14A, + 0x1F14B, + 0x1F14C, + 0x1F14D, + 0x1F14E, + 0x1F14F, + 0x1F150, + 0x1F16A, + 0x1F16B, + 0x1F16C, + 0x1F16D, + 0x1F190, + 0x1F191, + 0x1F1AE, + 0x1F1E6, + 0x1F200, + 0x1F201, + 0x1F202, + 0x1F203, + 0x1F210, + 0x1F211, + 0x1F212, + 0x1F213, + 0x1F214, + 0x1F215, + 0x1F216, + 0x1F217, + 0x1F218, + 0x1F219, + 0x1F21A, + 0x1F21B, + 0x1F21C, + 0x1F21D, + 0x1F21E, + 0x1F21F, + 0x1F220, + 0x1F221, + 0x1F222, + 0x1F223, + 0x1F224, + 0x1F225, + 0x1F226, + 0x1F227, + 0x1F228, + 0x1F229, + 0x1F22A, + 0x1F22B, + 0x1F22C, + 0x1F22D, + 0x1F22E, + 0x1F22F, + 0x1F230, + 0x1F231, + 0x1F232, + 0x1F233, + 0x1F234, + 0x1F235, + 0x1F236, + 0x1F237, + 0x1F238, + 0x1F239, + 0x1F23A, + 0x1F23B, + 0x1F23C, + 0x1F240, + 0x1F241, + 0x1F242, + 0x1F243, + 0x1F244, + 0x1F245, + 0x1F246, + 0x1F247, + 0x1F248, + 0x1F249, + 0x1F250, + 0x1F251, + 0x1F252, + 0x1F260, + 0x1F266, + 0x1F300, + 0x1F6D9, + 0x1F6DC, + 0x1F6ED, + 0x1F6F0, + 0x1F6FD, + 0x1F700, + 0x1F7DA, + 0x1F7E0, + 0x1F7EC, + 0x1F7F0, + 0x1F7F1, + 0x1F800, + 0x1F80C, + 0x1F810, + 0x1F848, + 0x1F850, + 0x1F85A, + 0x1F860, + 0x1F888, + 0x1F890, + 0x1F8AE, + 0x1F8B0, + 0x1F8BC, + 0x1F8C0, + 0x1F8C2, + 0x1F8D0, + 0x1F8D9, + 0x1F900, + 0x1FA58, + 0x1FA60, + 0x1FA6E, + 0x1FA70, + 0x1FA7D, + 0x1FA80, + 0x1FA8B, + 0x1FA8E, + 0x1FAC7, + 0x1FAC8, + 0x1FAC9, + 0x1FACD, + 0x1FADD, + 0x1FADF, + 0x1FAEB, + 0x1FAEF, + 0x1FAF9, + 0x1FB00, + 0x1FB93, + 0x1FB94, + 0x1FBF0, + 0x1FBF1, + 0x1FBF2, + 0x1FBF3, + 0x1FBF4, + 0x1FBF5, + 0x1FBF6, + 0x1FBF7, + 0x1FBF8, + 0x1FBF9, + 0x1FBFA, + 0x1FBFB, + 0x20000, + 0x2A6E0, + 0x2A700, + 0x2B81E, + 0x2B820, + 0x2CEAE, + 0x2CEB0, + 0x2EBE1, + 0x2EBF0, + 0x2EE5E, + 0x2F800, + 0x2F801, + 0x2F802, + 0x2F803, + 0x2F804, + 0x2F805, + 0x2F806, + 0x2F807, + 0x2F808, + 0x2F809, + 0x2F80A, + 0x2F80B, + 0x2F80C, + 0x2F80D, + 0x2F80E, + 0x2F80F, + 0x2F810, + 0x2F811, + 0x2F812, + 0x2F813, + 0x2F814, + 0x2F815, + 0x2F816, + 0x2F817, + 0x2F818, + 0x2F819, + 0x2F81A, + 0x2F81B, + 0x2F81C, + 0x2F81D, + 0x2F81E, + 0x2F81F, + 0x2F820, + 0x2F821, + 0x2F822, + 0x2F823, + 0x2F824, + 0x2F825, + 0x2F826, + 0x2F827, + 0x2F828, + 0x2F829, + 0x2F82A, + 0x2F82B, + 0x2F82C, + 0x2F82D, + 0x2F82E, + 0x2F82F, + 0x2F830, + 0x2F831, + 0x2F834, + 0x2F835, + 0x2F836, + 0x2F837, + 0x2F838, + 0x2F839, + 0x2F83A, + 0x2F83B, + 0x2F83C, + 0x2F83D, + 0x2F83E, + 0x2F83F, + 0x2F840, + 0x2F841, + 0x2F842, + 0x2F843, + 0x2F844, + 0x2F845, + 0x2F847, + 0x2F848, + 0x2F849, + 0x2F84A, + 0x2F84B, + 0x2F84C, + 0x2F84D, + 0x2F84E, + 0x2F84F, + 0x2F850, + 0x2F851, + 0x2F852, + 0x2F853, + 0x2F854, + 0x2F855, + 0x2F856, + 0x2F857, + 0x2F858, + 0x2F859, + 0x2F85A, + 0x2F85B, + 0x2F85C, + 0x2F85D, + 0x2F85E, + 0x2F85F, + 0x2F860, + 0x2F861, + 0x2F862, + 0x2F863, + 0x2F864, + 0x2F865, + 0x2F866, + 0x2F867, + 0x2F868, + 0x2F869, + 0x2F86A, + 0x2F86C, + 0x2F86D, + 0x2F86E, + 0x2F86F, + 0x2F870, + 0x2F871, + 0x2F872, + 0x2F873, + 0x2F874, + 0x2F875, + 0x2F876, + 0x2F877, + 0x2F878, + 0x2F879, + 0x2F87A, + 0x2F87B, + 0x2F87C, + 0x2F87D, + 0x2F87E, + 0x2F87F, + 0x2F880, + 0x2F881, + 0x2F882, + 0x2F883, + 0x2F884, + 0x2F885, + 0x2F886, + 0x2F887, + 0x2F888, + 0x2F889, + 0x2F88A, + 0x2F88B, + 0x2F88C, + 0x2F88D, + 0x2F88E, + 0x2F88F, + 0x2F890, + 0x2F891, + 0x2F893, + 0x2F894, + 0x2F896, + 0x2F897, + 0x2F898, + 0x2F899, + 0x2F89A, + 0x2F89B, + 0x2F89C, + 0x2F89D, + 0x2F89E, + 0x2F89F, + 0x2F8A0, + 0x2F8A1, + 0x2F8A2, + 0x2F8A3, + 0x2F8A4, + 0x2F8A5, + 0x2F8A6, + 0x2F8A7, + 0x2F8A8, + 0x2F8A9, + 0x2F8AA, + 0x2F8AB, + 0x2F8AC, + 0x2F8AD, + 0x2F8AE, + 0x2F8AF, + 0x2F8B0, + 0x2F8B1, + 0x2F8B2, + 0x2F8B3, + 0x2F8B4, + 0x2F8B5, + 0x2F8B6, + 0x2F8B7, + 0x2F8B8, + 0x2F8B9, + 0x2F8BA, + 0x2F8BB, + 0x2F8BC, + 0x2F8BD, + 0x2F8BE, + 0x2F8BF, + 0x2F8C0, + 0x2F8C1, + 0x2F8C2, + 0x2F8C3, + 0x2F8C4, + 0x2F8C5, + 0x2F8C6, + 0x2F8C7, + 0x2F8C8, + 0x2F8C9, + 0x2F8CA, + 0x2F8CB, + 0x2F8CC, + 0x2F8CD, + 0x2F8CE, + 0x2F8CF, + 0x2F8D0, + 0x2F8D1, + 0x2F8D2, + 0x2F8D3, + 0x2F8D4, + 0x2F8D5, + 0x2F8D6, + 0x2F8D7, + 0x2F8D8, + 0x2F8D9, + 0x2F8DA, + 0x2F8DB, + 0x2F8DC, + 0x2F8DD, + 0x2F8DE, + 0x2F8DF, + 0x2F8E0, + 0x2F8E1, + 0x2F8E2, + 0x2F8E3, + 0x2F8E4, + 0x2F8E5, + 0x2F8E6, + 0x2F8E7, + 0x2F8E8, + 0x2F8E9, + 0x2F8EA, + 0x2F8EB, + 0x2F8EC, + 0x2F8ED, + 0x2F8EE, + 0x2F8EF, + 0x2F8F0, + 0x2F8F1, + 0x2F8F2, + 0x2F8F3, + 0x2F8F4, + 0x2F8F5, + 0x2F8F6, + 0x2F8F7, + 0x2F8F8, + 0x2F8F9, + 0x2F8FA, + 0x2F8FB, + 0x2F8FC, + 0x2F8FD, + 0x2F8FE, + 0x2F8FF, + 0x2F900, + 0x2F901, + 0x2F902, + 0x2F903, + 0x2F904, + 0x2F905, + 0x2F906, + 0x2F907, + 0x2F908, + 0x2F909, + 0x2F90A, + 0x2F90B, + 0x2F90C, + 0x2F90D, + 0x2F90E, + 0x2F90F, + 0x2F910, + 0x2F911, + 0x2F912, + 0x2F913, + 0x2F914, + 0x2F915, + 0x2F916, + 0x2F917, + 0x2F918, + 0x2F919, + 0x2F91A, + 0x2F91B, + 0x2F91C, + 0x2F91D, + 0x2F91E, + 0x2F91F, + 0x2F920, + 0x2F921, + 0x2F922, + 0x2F923, + 0x2F924, + 0x2F925, + 0x2F926, + 0x2F927, + 0x2F928, + 0x2F929, + 0x2F92A, + 0x2F92B, + 0x2F92C, + 0x2F92E, + 0x2F92F, + 0x2F930, + 0x2F931, + 0x2F932, + 0x2F933, + 0x2F934, + 0x2F935, + 0x2F936, + 0x2F937, + 0x2F938, + 0x2F939, + 0x2F93A, + 0x2F93B, + 0x2F93C, + 0x2F93D, + 0x2F93E, + 0x2F93F, + 0x2F940, + 0x2F941, + 0x2F942, + 0x2F943, + 0x2F944, + 0x2F945, + 0x2F946, + 0x2F948, + 0x2F949, + 0x2F94A, + 0x2F94B, + 0x2F94C, + 0x2F94D, + 0x2F94E, + 0x2F94F, + 0x2F950, + 0x2F951, + 0x2F952, + 0x2F953, + 0x2F954, + 0x2F955, + 0x2F956, + 0x2F957, + 0x2F958, + 0x2F959, + 0x2F95A, + 0x2F95B, + 0x2F95C, + 0x2F95D, + 0x2F95F, + 0x2F960, + 0x2F961, + 0x2F962, + 0x2F963, + 0x2F964, + 0x2F965, + 0x2F966, + 0x2F967, + 0x2F968, + 0x2F969, + 0x2F96A, + 0x2F96B, + 0x2F96C, + 0x2F96D, + 0x2F96E, + 0x2F96F, + 0x2F970, + 0x2F971, + 0x2F972, + 0x2F973, + 0x2F974, + 0x2F975, + 0x2F976, + 0x2F977, + 0x2F978, + 0x2F979, + 0x2F97A, + 0x2F97B, + 0x2F97C, + 0x2F97D, + 0x2F97E, + 0x2F97F, + 0x2F980, + 0x2F981, + 0x2F982, + 0x2F983, + 0x2F984, + 0x2F985, + 0x2F986, + 0x2F987, + 0x2F988, + 0x2F989, + 0x2F98A, + 0x2F98B, + 0x2F98C, + 0x2F98D, + 0x2F98E, + 0x2F98F, + 0x2F990, + 0x2F991, + 0x2F992, + 0x2F993, + 0x2F994, + 0x2F995, + 0x2F996, + 0x2F997, + 0x2F998, + 0x2F999, + 0x2F99A, + 0x2F99B, + 0x2F99C, + 0x2F99D, + 0x2F99E, + 0x2F99F, + 0x2F9A0, + 0x2F9A1, + 0x2F9A2, + 0x2F9A3, + 0x2F9A4, + 0x2F9A5, + 0x2F9A6, + 0x2F9A7, + 0x2F9A8, + 0x2F9A9, + 0x2F9AA, + 0x2F9AB, + 0x2F9AC, + 0x2F9AD, + 0x2F9AE, + 0x2F9AF, + 0x2F9B0, + 0x2F9B1, + 0x2F9B2, + 0x2F9B3, + 0x2F9B4, + 0x2F9B5, + 0x2F9B6, + 0x2F9B7, + 0x2F9B8, + 0x2F9B9, + 0x2F9BA, + 0x2F9BB, + 0x2F9BC, + 0x2F9BD, + 0x2F9BE, + 0x2F9BF, + 0x2F9C0, + 0x2F9C1, + 0x2F9C2, + 0x2F9C3, + 0x2F9C4, + 0x2F9C5, + 0x2F9C6, + 0x2F9C7, + 0x2F9C8, + 0x2F9C9, + 0x2F9CA, + 0x2F9CB, + 0x2F9CC, + 0x2F9CD, + 0x2F9CE, + 0x2F9CF, + 0x2F9D0, + 0x2F9D1, + 0x2F9D2, + 0x2F9D3, + 0x2F9D4, + 0x2F9D5, + 0x2F9D6, + 0x2F9D7, + 0x2F9D8, + 0x2F9D9, + 0x2F9DA, + 0x2F9DB, + 0x2F9DC, + 0x2F9DD, + 0x2F9DE, + 0x2F9DF, + 0x2F9E0, + 0x2F9E1, + 0x2F9E2, + 0x2F9E3, + 0x2F9E4, + 0x2F9E5, + 0x2F9E6, + 0x2F9E7, + 0x2F9E8, + 0x2F9E9, + 0x2F9EA, + 0x2F9EB, + 0x2F9EC, + 0x2F9ED, + 0x2F9EE, + 0x2F9EF, + 0x2F9F0, + 0x2F9F1, + 0x2F9F2, + 0x2F9F3, + 0x2F9F4, + 0x2F9F5, + 0x2F9F6, + 0x2F9F7, + 0x2F9F8, + 0x2F9F9, + 0x2F9FA, + 0x2F9FB, + 0x2F9FC, + 0x2F9FD, + 0x2F9FE, + 0x2FA00, + 0x2FA01, + 0x2FA02, + 0x2FA03, + 0x2FA04, + 0x2FA05, + 0x2FA06, + 0x2FA07, + 0x2FA08, + 0x2FA09, + 0x2FA0A, + 0x2FA0B, + 0x2FA0C, + 0x2FA0D, + 0x2FA0E, + 0x2FA0F, + 0x2FA10, + 0x2FA11, + 0x2FA12, + 0x2FA13, + 0x2FA14, + 0x2FA15, + 0x2FA16, + 0x2FA17, + 0x2FA18, + 0x2FA19, + 0x2FA1A, + 0x2FA1B, + 0x2FA1C, + 0x2FA1D, + 0x2FA1E, + 0x30000, + 0x3134B, + 0x31350, + 0x3347A, + 0xE0100, + 0xE01F0, + ), +) -def _seg_0() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x0, "V"), - (0x1, "V"), - (0x2, "V"), - (0x3, "V"), - (0x4, "V"), - (0x5, "V"), - (0x6, "V"), - (0x7, "V"), - (0x8, "V"), - (0x9, "V"), - (0xA, "V"), - (0xB, "V"), - (0xC, "V"), - (0xD, "V"), - (0xE, "V"), - (0xF, "V"), - (0x10, "V"), - (0x11, "V"), - (0x12, "V"), - (0x13, "V"), - (0x14, "V"), - (0x15, "V"), - (0x16, "V"), - (0x17, "V"), - (0x18, "V"), - (0x19, "V"), - (0x1A, "V"), - (0x1B, "V"), - (0x1C, "V"), - (0x1D, "V"), - (0x1E, "V"), - (0x1F, "V"), - (0x20, "V"), - (0x21, "V"), - (0x22, "V"), - (0x23, "V"), - (0x24, "V"), - (0x25, "V"), - (0x26, "V"), - (0x27, "V"), - (0x28, "V"), - (0x29, "V"), - (0x2A, "V"), - (0x2B, "V"), - (0x2C, "V"), - (0x2D, "V"), - (0x2E, "V"), - (0x2F, "V"), - (0x30, "V"), - (0x31, "V"), - (0x32, "V"), - (0x33, "V"), - (0x34, "V"), - (0x35, "V"), - (0x36, "V"), - (0x37, "V"), - (0x38, "V"), - (0x39, "V"), - (0x3A, "V"), - (0x3B, "V"), - (0x3C, "V"), - (0x3D, "V"), - (0x3E, "V"), - (0x3F, "V"), - (0x40, "V"), - (0x41, "M", "a"), - (0x42, "M", "b"), - (0x43, "M", "c"), - (0x44, "M", "d"), - (0x45, "M", "e"), - (0x46, "M", "f"), - (0x47, "M", "g"), - (0x48, "M", "h"), - (0x49, "M", "i"), - (0x4A, "M", "j"), - (0x4B, "M", "k"), - (0x4C, "M", "l"), - (0x4D, "M", "m"), - (0x4E, "M", "n"), - (0x4F, "M", "o"), - (0x50, "M", "p"), - (0x51, "M", "q"), - (0x52, "M", "r"), - (0x53, "M", "s"), - (0x54, "M", "t"), - (0x55, "M", "u"), - (0x56, "M", "v"), - (0x57, "M", "w"), - (0x58, "M", "x"), - (0x59, "M", "y"), - (0x5A, "M", "z"), - (0x5B, "V"), - (0x5C, "V"), - (0x5D, "V"), - (0x5E, "V"), - (0x5F, "V"), - (0x60, "V"), - (0x61, "V"), - (0x62, "V"), - (0x63, "V"), - ] +uts46_statuses: bytes = ( + b"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" + b"VMMMMMMMMMMMMMMMMMMMMMMMMMMVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" + b"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXMVVVVVVVMVMVVIVMVVMMMMVVMMMVMMMV" + b"MMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMDVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMVMVMVMVMMV" + b"MVMVMVMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMVMVMVMM" + b"VMVMMVMMMVMMMMVMMVMMMVMMVMMVMVMVMMVMVMVMMVMMMVMVMMVMVMMMMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMMVMMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMMVMVMMMMVMVMVMVMVMMMMMMMMMVMMMMMM" + b"VMMMMMVMMVMMMVIVMVMVMVMVXMVMMXMMMMMMMXMXMMVMMMMMMMMMMMMMMMMMXMMM" + b"MMMMMMVDVMMMMMMMMVMVMVMVMVMVMVMVMVMVMVMVMVMMMVMMVMVMMVMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVXMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMXVMVXVXVXVXVXVXVMMMMVXVXVXVXVXVXVXVXVXVX" + b"VXVXVMMMMMMMMVXVXVXVXVXVXVXVXVXVXVXMMXMVXVXVXVXVXVXVXVMXVMXVXVXV" + b"XVXVXVXMMMVXMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV" + b"XVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV" + b"XVMVXVXVXVXVXVXVXVMVXVXVXVXVXMMVXVMVMVXVMVMVMVMVMVXVMVMMMMMVMVMV" + b"XVMVMVMVMVMVXVXVXVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMXMXVMV" + b"IVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXMMMMMMXVXVXVXVXVXVXVXVXVX" + b"VIVXVXVXVIVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVMMM" + b"MMMMMMVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMVXVXVMMMV" + b"MMMMMMMMMMMVMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMMMMMMMVXMMMMMMXVMMMMMMMMVMMMMMM" + b"MMVXMMMMMMXVXMXMXMXMVMMMMMMMMVMVMVMVMVMVMVMXMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMXVMMMMMMMMMMMMMMXVMMMMMMMMMVMXVM" + b"MMMXMMMVMVMMMMMMMMXMMMXVMMMMMMMMXMIDXVMVMVXVXMVMMVMMVMVMVMMMVMVM" + b"IXIMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMXVMVXVXMMMMVMMMVM" + b"MMMMMVMMVMMMVMMMVMVMVMVMMMMVMMMMMMMMMMVMMMMMVMMMMVMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMVXVMMVMMVMMVXVXMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMMMVMVXVMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMVMMMVMVMVMVMMMMVMVMVMM" + b"MMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVXVXVXVXVXMVXVXVXVXVX" + b"VXVXVXVXVXVXVXVMVMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMVMVMVMMMVXVXVMMVMVMXV" + b"XMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMIMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXVMMMMMMMMMMMMMMVXVMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMXMMMMMMMMMMMMM" + b"MMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXMVMVMVMVMVMVMVM" + b"VMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMVXV" + b"MVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMV" + b"MVMVMVMVMVMVMVMVMVMMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMVMMMMMVM" + b"MMMMVMVMVMVMVMVMVMVMMMMVMVMMVMVMVMVMVMVMVMVMXMMMMMVMMVXVXVXVXVXV" + b"XVXVXVXVXVXVXVXVXVXVXVXVXVXVXVMMMMVMVXMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVXVX" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMVMVMVMMMMMMMMMMVMVMVMMVMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMXMMMMMMXMMMMMXMVMMMMMMMMMMMMMMMMMMMMMMMMXMMMMM" + b"XMXMMXMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXMMMMMMMMMMMMMVIMMXMMM" + b"MMMXVXMMMMMMMMMMMMMMMMMMMVMMMMMMXMMMMMMMMMMMMMMMMMMMXMMMMXMMMVMX" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXIXMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMIMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMXMMMMM" + b"MXMMMMMMXMMMXMMMMMMMXMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV" + b"XVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVMMMMMMMMMMMXMMMMMMMMMMMMMMMXMMM" + b"MMMMXMMXVXVXVXVXVXVXVXVXVMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMXMMMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMXVXVXVXVMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMVXMMMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVIXVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVX" + b"VXVXVXVXVXVXVMMMMMMMVIVMMMMMMVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMXMMXMXMMXMMMMXMMMMMMMMMMMMXMXMMMMMMMXMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMXMMMMMMMMXMMMMM" + b"MMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMXMMMMMXMXMMMMMMMXMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVXVXVXVXVXVXVXMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXVXVXVXVXVXVXVXVXVX" + b"VXVXVXVXVXVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVXVXVXVX" + b"VXMMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMXMMXMXMXMMMMMMMMMMXMMMMXMXMXMX" + b"MXMXMXMMMXMMXMXMXMXMXMXMXMMXMXMMMMXMMMMMMMXMMMMXMMMMXMXMMMMMMMMM" + b"MXMMMMMMMMMMMMMMMMMXMMMXMMMMMXMMMMMMMMMMMMMMMMMXVXVXVXVXVXVXVXMM" + b"MMMMMMMMVMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMVMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMVMMMVMVXVMMMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMXMMMMMMMMMXMMXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVX" + b"VXVMMMMMMMMMMVXVXVXVXVXVXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" + b"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXVXVXIX" +) - -def _seg_1() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x64, "V"), - (0x65, "V"), - (0x66, "V"), - (0x67, "V"), - (0x68, "V"), - (0x69, "V"), - (0x6A, "V"), - (0x6B, "V"), - (0x6C, "V"), - (0x6D, "V"), - (0x6E, "V"), - (0x6F, "V"), - (0x70, "V"), - (0x71, "V"), - (0x72, "V"), - (0x73, "V"), - (0x74, "V"), - (0x75, "V"), - (0x76, "V"), - (0x77, "V"), - (0x78, "V"), - (0x79, "V"), - (0x7A, "V"), - (0x7B, "V"), - (0x7C, "V"), - (0x7D, "V"), - (0x7E, "V"), - (0x7F, "V"), - (0x80, "X"), - (0x81, "X"), - (0x82, "X"), - (0x83, "X"), - (0x84, "X"), - (0x85, "X"), - (0x86, "X"), - (0x87, "X"), - (0x88, "X"), - (0x89, "X"), - (0x8A, "X"), - (0x8B, "X"), - (0x8C, "X"), - (0x8D, "X"), - (0x8E, "X"), - (0x8F, "X"), - (0x90, "X"), - (0x91, "X"), - (0x92, "X"), - (0x93, "X"), - (0x94, "X"), - (0x95, "X"), - (0x96, "X"), - (0x97, "X"), - (0x98, "X"), - (0x99, "X"), - (0x9A, "X"), - (0x9B, "X"), - (0x9C, "X"), - (0x9D, "X"), - (0x9E, "X"), - (0x9F, "X"), - (0xA0, "M", " "), - (0xA1, "V"), - (0xA2, "V"), - (0xA3, "V"), - (0xA4, "V"), - (0xA5, "V"), - (0xA6, "V"), - (0xA7, "V"), - (0xA8, "M", " ̈"), - (0xA9, "V"), - (0xAA, "M", "a"), - (0xAB, "V"), - (0xAC, "V"), - (0xAD, "I"), - (0xAE, "V"), - (0xAF, "M", " ̄"), - (0xB0, "V"), - (0xB1, "V"), - (0xB2, "M", "2"), - (0xB3, "M", "3"), - (0xB4, "M", " ́"), - (0xB5, "M", "μ"), - (0xB6, "V"), - (0xB7, "V"), - (0xB8, "M", " ̧"), - (0xB9, "M", "1"), - (0xBA, "M", "o"), - (0xBB, "V"), - (0xBC, "M", "1⁄4"), - (0xBD, "M", "1⁄2"), - (0xBE, "M", "3⁄4"), - (0xBF, "V"), - (0xC0, "M", "à"), - (0xC1, "M", "á"), - (0xC2, "M", "â"), - (0xC3, "M", "ã"), - (0xC4, "M", "ä"), - (0xC5, "M", "å"), - (0xC6, "M", "æ"), - (0xC7, "M", "ç"), - ] - - -def _seg_2() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xC8, "M", "è"), - (0xC9, "M", "é"), - (0xCA, "M", "ê"), - (0xCB, "M", "ë"), - (0xCC, "M", "ì"), - (0xCD, "M", "í"), - (0xCE, "M", "î"), - (0xCF, "M", "ï"), - (0xD0, "M", "ð"), - (0xD1, "M", "ñ"), - (0xD2, "M", "ò"), - (0xD3, "M", "ó"), - (0xD4, "M", "ô"), - (0xD5, "M", "õ"), - (0xD6, "M", "ö"), - (0xD7, "V"), - (0xD8, "M", "ø"), - (0xD9, "M", "ù"), - (0xDA, "M", "ú"), - (0xDB, "M", "û"), - (0xDC, "M", "ü"), - (0xDD, "M", "ý"), - (0xDE, "M", "þ"), - (0xDF, "D", "ss"), - (0xE0, "V"), - (0xE1, "V"), - (0xE2, "V"), - (0xE3, "V"), - (0xE4, "V"), - (0xE5, "V"), - (0xE6, "V"), - (0xE7, "V"), - (0xE8, "V"), - (0xE9, "V"), - (0xEA, "V"), - (0xEB, "V"), - (0xEC, "V"), - (0xED, "V"), - (0xEE, "V"), - (0xEF, "V"), - (0xF0, "V"), - (0xF1, "V"), - (0xF2, "V"), - (0xF3, "V"), - (0xF4, "V"), - (0xF5, "V"), - (0xF6, "V"), - (0xF7, "V"), - (0xF8, "V"), - (0xF9, "V"), - (0xFA, "V"), - (0xFB, "V"), - (0xFC, "V"), - (0xFD, "V"), - (0xFE, "V"), - (0xFF, "V"), - (0x100, "M", "ā"), - (0x101, "V"), - (0x102, "M", "ă"), - (0x103, "V"), - (0x104, "M", "ą"), - (0x105, "V"), - (0x106, "M", "ć"), - (0x107, "V"), - (0x108, "M", "ĉ"), - (0x109, "V"), - (0x10A, "M", "ċ"), - (0x10B, "V"), - (0x10C, "M", "č"), - (0x10D, "V"), - (0x10E, "M", "ď"), - (0x10F, "V"), - (0x110, "M", "đ"), - (0x111, "V"), - (0x112, "M", "ē"), - (0x113, "V"), - (0x114, "M", "ĕ"), - (0x115, "V"), - (0x116, "M", "ė"), - (0x117, "V"), - (0x118, "M", "ę"), - (0x119, "V"), - (0x11A, "M", "ě"), - (0x11B, "V"), - (0x11C, "M", "ĝ"), - (0x11D, "V"), - (0x11E, "M", "ğ"), - (0x11F, "V"), - (0x120, "M", "ġ"), - (0x121, "V"), - (0x122, "M", "ģ"), - (0x123, "V"), - (0x124, "M", "ĥ"), - (0x125, "V"), - (0x126, "M", "ħ"), - (0x127, "V"), - (0x128, "M", "ĩ"), - (0x129, "V"), - (0x12A, "M", "ī"), - (0x12B, "V"), - ] - - -def _seg_3() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x12C, "M", "ĭ"), - (0x12D, "V"), - (0x12E, "M", "į"), - (0x12F, "V"), - (0x130, "M", "i̇"), - (0x131, "V"), - (0x132, "M", "ij"), - (0x134, "M", "ĵ"), - (0x135, "V"), - (0x136, "M", "ķ"), - (0x137, "V"), - (0x139, "M", "ĺ"), - (0x13A, "V"), - (0x13B, "M", "ļ"), - (0x13C, "V"), - (0x13D, "M", "ľ"), - (0x13E, "V"), - (0x13F, "M", "l·"), - (0x141, "M", "ł"), - (0x142, "V"), - (0x143, "M", "ń"), - (0x144, "V"), - (0x145, "M", "ņ"), - (0x146, "V"), - (0x147, "M", "ň"), - (0x148, "V"), - (0x149, "M", "ʼn"), - (0x14A, "M", "ŋ"), - (0x14B, "V"), - (0x14C, "M", "ō"), - (0x14D, "V"), - (0x14E, "M", "ŏ"), - (0x14F, "V"), - (0x150, "M", "ő"), - (0x151, "V"), - (0x152, "M", "œ"), - (0x153, "V"), - (0x154, "M", "ŕ"), - (0x155, "V"), - (0x156, "M", "ŗ"), - (0x157, "V"), - (0x158, "M", "ř"), - (0x159, "V"), - (0x15A, "M", "ś"), - (0x15B, "V"), - (0x15C, "M", "ŝ"), - (0x15D, "V"), - (0x15E, "M", "ş"), - (0x15F, "V"), - (0x160, "M", "š"), - (0x161, "V"), - (0x162, "M", "ţ"), - (0x163, "V"), - (0x164, "M", "ť"), - (0x165, "V"), - (0x166, "M", "ŧ"), - (0x167, "V"), - (0x168, "M", "ũ"), - (0x169, "V"), - (0x16A, "M", "ū"), - (0x16B, "V"), - (0x16C, "M", "ŭ"), - (0x16D, "V"), - (0x16E, "M", "ů"), - (0x16F, "V"), - (0x170, "M", "ű"), - (0x171, "V"), - (0x172, "M", "ų"), - (0x173, "V"), - (0x174, "M", "ŵ"), - (0x175, "V"), - (0x176, "M", "ŷ"), - (0x177, "V"), - (0x178, "M", "ÿ"), - (0x179, "M", "ź"), - (0x17A, "V"), - (0x17B, "M", "ż"), - (0x17C, "V"), - (0x17D, "M", "ž"), - (0x17E, "V"), - (0x17F, "M", "s"), - (0x180, "V"), - (0x181, "M", "ɓ"), - (0x182, "M", "ƃ"), - (0x183, "V"), - (0x184, "M", "ƅ"), - (0x185, "V"), - (0x186, "M", "ɔ"), - (0x187, "M", "ƈ"), - (0x188, "V"), - (0x189, "M", "ɖ"), - (0x18A, "M", "ɗ"), - (0x18B, "M", "ƌ"), - (0x18C, "V"), - (0x18E, "M", "ǝ"), - (0x18F, "M", "ə"), - (0x190, "M", "ɛ"), - (0x191, "M", "ƒ"), - (0x192, "V"), - (0x193, "M", "ɠ"), - ] - - -def _seg_4() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x194, "M", "ɣ"), - (0x195, "V"), - (0x196, "M", "ɩ"), - (0x197, "M", "ɨ"), - (0x198, "M", "ƙ"), - (0x199, "V"), - (0x19C, "M", "ɯ"), - (0x19D, "M", "ɲ"), - (0x19E, "V"), - (0x19F, "M", "ɵ"), - (0x1A0, "M", "ơ"), - (0x1A1, "V"), - (0x1A2, "M", "ƣ"), - (0x1A3, "V"), - (0x1A4, "M", "ƥ"), - (0x1A5, "V"), - (0x1A6, "M", "ʀ"), - (0x1A7, "M", "ƨ"), - (0x1A8, "V"), - (0x1A9, "M", "ʃ"), - (0x1AA, "V"), - (0x1AC, "M", "ƭ"), - (0x1AD, "V"), - (0x1AE, "M", "ʈ"), - (0x1AF, "M", "ư"), - (0x1B0, "V"), - (0x1B1, "M", "ʊ"), - (0x1B2, "M", "ʋ"), - (0x1B3, "M", "ƴ"), - (0x1B4, "V"), - (0x1B5, "M", "ƶ"), - (0x1B6, "V"), - (0x1B7, "M", "ʒ"), - (0x1B8, "M", "ƹ"), - (0x1B9, "V"), - (0x1BC, "M", "ƽ"), - (0x1BD, "V"), - (0x1C4, "M", "dž"), - (0x1C7, "M", "lj"), - (0x1CA, "M", "nj"), - (0x1CD, "M", "ǎ"), - (0x1CE, "V"), - (0x1CF, "M", "ǐ"), - (0x1D0, "V"), - (0x1D1, "M", "ǒ"), - (0x1D2, "V"), - (0x1D3, "M", "ǔ"), - (0x1D4, "V"), - (0x1D5, "M", "ǖ"), - (0x1D6, "V"), - (0x1D7, "M", "ǘ"), - (0x1D8, "V"), - (0x1D9, "M", "ǚ"), - (0x1DA, "V"), - (0x1DB, "M", "ǜ"), - (0x1DC, "V"), - (0x1DE, "M", "ǟ"), - (0x1DF, "V"), - (0x1E0, "M", "ǡ"), - (0x1E1, "V"), - (0x1E2, "M", "ǣ"), - (0x1E3, "V"), - (0x1E4, "M", "ǥ"), - (0x1E5, "V"), - (0x1E6, "M", "ǧ"), - (0x1E7, "V"), - (0x1E8, "M", "ǩ"), - (0x1E9, "V"), - (0x1EA, "M", "ǫ"), - (0x1EB, "V"), - (0x1EC, "M", "ǭ"), - (0x1ED, "V"), - (0x1EE, "M", "ǯ"), - (0x1EF, "V"), - (0x1F1, "M", "dz"), - (0x1F4, "M", "ǵ"), - (0x1F5, "V"), - (0x1F6, "M", "ƕ"), - (0x1F7, "M", "ƿ"), - (0x1F8, "M", "ǹ"), - (0x1F9, "V"), - (0x1FA, "M", "ǻ"), - (0x1FB, "V"), - (0x1FC, "M", "ǽ"), - (0x1FD, "V"), - (0x1FE, "M", "ǿ"), - (0x1FF, "V"), - (0x200, "M", "ȁ"), - (0x201, "V"), - (0x202, "M", "ȃ"), - (0x203, "V"), - (0x204, "M", "ȅ"), - (0x205, "V"), - (0x206, "M", "ȇ"), - (0x207, "V"), - (0x208, "M", "ȉ"), - (0x209, "V"), - (0x20A, "M", "ȋ"), - (0x20B, "V"), - (0x20C, "M", "ȍ"), - ] - - -def _seg_5() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x20D, "V"), - (0x20E, "M", "ȏ"), - (0x20F, "V"), - (0x210, "M", "ȑ"), - (0x211, "V"), - (0x212, "M", "ȓ"), - (0x213, "V"), - (0x214, "M", "ȕ"), - (0x215, "V"), - (0x216, "M", "ȗ"), - (0x217, "V"), - (0x218, "M", "ș"), - (0x219, "V"), - (0x21A, "M", "ț"), - (0x21B, "V"), - (0x21C, "M", "ȝ"), - (0x21D, "V"), - (0x21E, "M", "ȟ"), - (0x21F, "V"), - (0x220, "M", "ƞ"), - (0x221, "V"), - (0x222, "M", "ȣ"), - (0x223, "V"), - (0x224, "M", "ȥ"), - (0x225, "V"), - (0x226, "M", "ȧ"), - (0x227, "V"), - (0x228, "M", "ȩ"), - (0x229, "V"), - (0x22A, "M", "ȫ"), - (0x22B, "V"), - (0x22C, "M", "ȭ"), - (0x22D, "V"), - (0x22E, "M", "ȯ"), - (0x22F, "V"), - (0x230, "M", "ȱ"), - (0x231, "V"), - (0x232, "M", "ȳ"), - (0x233, "V"), - (0x23A, "M", "ⱥ"), - (0x23B, "M", "ȼ"), - (0x23C, "V"), - (0x23D, "M", "ƚ"), - (0x23E, "M", "ⱦ"), - (0x23F, "V"), - (0x241, "M", "ɂ"), - (0x242, "V"), - (0x243, "M", "ƀ"), - (0x244, "M", "ʉ"), - (0x245, "M", "ʌ"), - (0x246, "M", "ɇ"), - (0x247, "V"), - (0x248, "M", "ɉ"), - (0x249, "V"), - (0x24A, "M", "ɋ"), - (0x24B, "V"), - (0x24C, "M", "ɍ"), - (0x24D, "V"), - (0x24E, "M", "ɏ"), - (0x24F, "V"), - (0x2B0, "M", "h"), - (0x2B1, "M", "ɦ"), - (0x2B2, "M", "j"), - (0x2B3, "M", "r"), - (0x2B4, "M", "ɹ"), - (0x2B5, "M", "ɻ"), - (0x2B6, "M", "ʁ"), - (0x2B7, "M", "w"), - (0x2B8, "M", "y"), - (0x2B9, "V"), - (0x2D8, "M", " ̆"), - (0x2D9, "M", " ̇"), - (0x2DA, "M", " ̊"), - (0x2DB, "M", " ̨"), - (0x2DC, "M", " ̃"), - (0x2DD, "M", " ̋"), - (0x2DE, "V"), - (0x2E0, "M", "ɣ"), - (0x2E1, "M", "l"), - (0x2E2, "M", "s"), - (0x2E3, "M", "x"), - (0x2E4, "M", "ʕ"), - (0x2E5, "V"), - (0x340, "M", "̀"), - (0x341, "M", "́"), - (0x342, "V"), - (0x343, "M", "̓"), - (0x344, "M", "̈́"), - (0x345, "M", "ι"), - (0x346, "V"), - (0x34F, "I"), - (0x350, "V"), - (0x370, "M", "ͱ"), - (0x371, "V"), - (0x372, "M", "ͳ"), - (0x373, "V"), - (0x374, "M", "ʹ"), - (0x375, "V"), - (0x376, "M", "ͷ"), - (0x377, "V"), - ] - - -def _seg_6() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x378, "X"), - (0x37A, "M", " ι"), - (0x37B, "V"), - (0x37E, "M", ";"), - (0x37F, "M", "ϳ"), - (0x380, "X"), - (0x384, "M", " ́"), - (0x385, "M", " ̈́"), - (0x386, "M", "ά"), - (0x387, "M", "·"), - (0x388, "M", "έ"), - (0x389, "M", "ή"), - (0x38A, "M", "ί"), - (0x38B, "X"), - (0x38C, "M", "ό"), - (0x38D, "X"), - (0x38E, "M", "ύ"), - (0x38F, "M", "ώ"), - (0x390, "V"), - (0x391, "M", "α"), - (0x392, "M", "β"), - (0x393, "M", "γ"), - (0x394, "M", "δ"), - (0x395, "M", "ε"), - (0x396, "M", "ζ"), - (0x397, "M", "η"), - (0x398, "M", "θ"), - (0x399, "M", "ι"), - (0x39A, "M", "κ"), - (0x39B, "M", "λ"), - (0x39C, "M", "μ"), - (0x39D, "M", "ν"), - (0x39E, "M", "ξ"), - (0x39F, "M", "ο"), - (0x3A0, "M", "π"), - (0x3A1, "M", "ρ"), - (0x3A2, "X"), - (0x3A3, "M", "σ"), - (0x3A4, "M", "τ"), - (0x3A5, "M", "υ"), - (0x3A6, "M", "φ"), - (0x3A7, "M", "χ"), - (0x3A8, "M", "ψ"), - (0x3A9, "M", "ω"), - (0x3AA, "M", "ϊ"), - (0x3AB, "M", "ϋ"), - (0x3AC, "V"), - (0x3C2, "D", "σ"), - (0x3C3, "V"), - (0x3CF, "M", "ϗ"), - (0x3D0, "M", "β"), - (0x3D1, "M", "θ"), - (0x3D2, "M", "υ"), - (0x3D3, "M", "ύ"), - (0x3D4, "M", "ϋ"), - (0x3D5, "M", "φ"), - (0x3D6, "M", "π"), - (0x3D7, "V"), - (0x3D8, "M", "ϙ"), - (0x3D9, "V"), - (0x3DA, "M", "ϛ"), - (0x3DB, "V"), - (0x3DC, "M", "ϝ"), - (0x3DD, "V"), - (0x3DE, "M", "ϟ"), - (0x3DF, "V"), - (0x3E0, "M", "ϡ"), - (0x3E1, "V"), - (0x3E2, "M", "ϣ"), - (0x3E3, "V"), - (0x3E4, "M", "ϥ"), - (0x3E5, "V"), - (0x3E6, "M", "ϧ"), - (0x3E7, "V"), - (0x3E8, "M", "ϩ"), - (0x3E9, "V"), - (0x3EA, "M", "ϫ"), - (0x3EB, "V"), - (0x3EC, "M", "ϭ"), - (0x3ED, "V"), - (0x3EE, "M", "ϯ"), - (0x3EF, "V"), - (0x3F0, "M", "κ"), - (0x3F1, "M", "ρ"), - (0x3F2, "M", "σ"), - (0x3F3, "V"), - (0x3F4, "M", "θ"), - (0x3F5, "M", "ε"), - (0x3F6, "V"), - (0x3F7, "M", "ϸ"), - (0x3F8, "V"), - (0x3F9, "M", "σ"), - (0x3FA, "M", "ϻ"), - (0x3FB, "V"), - (0x3FD, "M", "ͻ"), - (0x3FE, "M", "ͼ"), - (0x3FF, "M", "ͽ"), - (0x400, "M", "ѐ"), - (0x401, "M", "ё"), - (0x402, "M", "ђ"), - ] - - -def _seg_7() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x403, "M", "ѓ"), - (0x404, "M", "є"), - (0x405, "M", "ѕ"), - (0x406, "M", "і"), - (0x407, "M", "ї"), - (0x408, "M", "ј"), - (0x409, "M", "љ"), - (0x40A, "M", "њ"), - (0x40B, "M", "ћ"), - (0x40C, "M", "ќ"), - (0x40D, "M", "ѝ"), - (0x40E, "M", "ў"), - (0x40F, "M", "џ"), - (0x410, "M", "а"), - (0x411, "M", "б"), - (0x412, "M", "в"), - (0x413, "M", "г"), - (0x414, "M", "д"), - (0x415, "M", "е"), - (0x416, "M", "ж"), - (0x417, "M", "з"), - (0x418, "M", "и"), - (0x419, "M", "й"), - (0x41A, "M", "к"), - (0x41B, "M", "л"), - (0x41C, "M", "м"), - (0x41D, "M", "н"), - (0x41E, "M", "о"), - (0x41F, "M", "п"), - (0x420, "M", "р"), - (0x421, "M", "с"), - (0x422, "M", "т"), - (0x423, "M", "у"), - (0x424, "M", "ф"), - (0x425, "M", "х"), - (0x426, "M", "ц"), - (0x427, "M", "ч"), - (0x428, "M", "ш"), - (0x429, "M", "щ"), - (0x42A, "M", "ъ"), - (0x42B, "M", "ы"), - (0x42C, "M", "ь"), - (0x42D, "M", "э"), - (0x42E, "M", "ю"), - (0x42F, "M", "я"), - (0x430, "V"), - (0x460, "M", "ѡ"), - (0x461, "V"), - (0x462, "M", "ѣ"), - (0x463, "V"), - (0x464, "M", "ѥ"), - (0x465, "V"), - (0x466, "M", "ѧ"), - (0x467, "V"), - (0x468, "M", "ѩ"), - (0x469, "V"), - (0x46A, "M", "ѫ"), - (0x46B, "V"), - (0x46C, "M", "ѭ"), - (0x46D, "V"), - (0x46E, "M", "ѯ"), - (0x46F, "V"), - (0x470, "M", "ѱ"), - (0x471, "V"), - (0x472, "M", "ѳ"), - (0x473, "V"), - (0x474, "M", "ѵ"), - (0x475, "V"), - (0x476, "M", "ѷ"), - (0x477, "V"), - (0x478, "M", "ѹ"), - (0x479, "V"), - (0x47A, "M", "ѻ"), - (0x47B, "V"), - (0x47C, "M", "ѽ"), - (0x47D, "V"), - (0x47E, "M", "ѿ"), - (0x47F, "V"), - (0x480, "M", "ҁ"), - (0x481, "V"), - (0x48A, "M", "ҋ"), - (0x48B, "V"), - (0x48C, "M", "ҍ"), - (0x48D, "V"), - (0x48E, "M", "ҏ"), - (0x48F, "V"), - (0x490, "M", "ґ"), - (0x491, "V"), - (0x492, "M", "ғ"), - (0x493, "V"), - (0x494, "M", "ҕ"), - (0x495, "V"), - (0x496, "M", "җ"), - (0x497, "V"), - (0x498, "M", "ҙ"), - (0x499, "V"), - (0x49A, "M", "қ"), - (0x49B, "V"), - (0x49C, "M", "ҝ"), - (0x49D, "V"), - ] - - -def _seg_8() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x49E, "M", "ҟ"), - (0x49F, "V"), - (0x4A0, "M", "ҡ"), - (0x4A1, "V"), - (0x4A2, "M", "ң"), - (0x4A3, "V"), - (0x4A4, "M", "ҥ"), - (0x4A5, "V"), - (0x4A6, "M", "ҧ"), - (0x4A7, "V"), - (0x4A8, "M", "ҩ"), - (0x4A9, "V"), - (0x4AA, "M", "ҫ"), - (0x4AB, "V"), - (0x4AC, "M", "ҭ"), - (0x4AD, "V"), - (0x4AE, "M", "ү"), - (0x4AF, "V"), - (0x4B0, "M", "ұ"), - (0x4B1, "V"), - (0x4B2, "M", "ҳ"), - (0x4B3, "V"), - (0x4B4, "M", "ҵ"), - (0x4B5, "V"), - (0x4B6, "M", "ҷ"), - (0x4B7, "V"), - (0x4B8, "M", "ҹ"), - (0x4B9, "V"), - (0x4BA, "M", "һ"), - (0x4BB, "V"), - (0x4BC, "M", "ҽ"), - (0x4BD, "V"), - (0x4BE, "M", "ҿ"), - (0x4BF, "V"), - (0x4C0, "M", "ӏ"), - (0x4C1, "M", "ӂ"), - (0x4C2, "V"), - (0x4C3, "M", "ӄ"), - (0x4C4, "V"), - (0x4C5, "M", "ӆ"), - (0x4C6, "V"), - (0x4C7, "M", "ӈ"), - (0x4C8, "V"), - (0x4C9, "M", "ӊ"), - (0x4CA, "V"), - (0x4CB, "M", "ӌ"), - (0x4CC, "V"), - (0x4CD, "M", "ӎ"), - (0x4CE, "V"), - (0x4D0, "M", "ӑ"), - (0x4D1, "V"), - (0x4D2, "M", "ӓ"), - (0x4D3, "V"), - (0x4D4, "M", "ӕ"), - (0x4D5, "V"), - (0x4D6, "M", "ӗ"), - (0x4D7, "V"), - (0x4D8, "M", "ә"), - (0x4D9, "V"), - (0x4DA, "M", "ӛ"), - (0x4DB, "V"), - (0x4DC, "M", "ӝ"), - (0x4DD, "V"), - (0x4DE, "M", "ӟ"), - (0x4DF, "V"), - (0x4E0, "M", "ӡ"), - (0x4E1, "V"), - (0x4E2, "M", "ӣ"), - (0x4E3, "V"), - (0x4E4, "M", "ӥ"), - (0x4E5, "V"), - (0x4E6, "M", "ӧ"), - (0x4E7, "V"), - (0x4E8, "M", "ө"), - (0x4E9, "V"), - (0x4EA, "M", "ӫ"), - (0x4EB, "V"), - (0x4EC, "M", "ӭ"), - (0x4ED, "V"), - (0x4EE, "M", "ӯ"), - (0x4EF, "V"), - (0x4F0, "M", "ӱ"), - (0x4F1, "V"), - (0x4F2, "M", "ӳ"), - (0x4F3, "V"), - (0x4F4, "M", "ӵ"), - (0x4F5, "V"), - (0x4F6, "M", "ӷ"), - (0x4F7, "V"), - (0x4F8, "M", "ӹ"), - (0x4F9, "V"), - (0x4FA, "M", "ӻ"), - (0x4FB, "V"), - (0x4FC, "M", "ӽ"), - (0x4FD, "V"), - (0x4FE, "M", "ӿ"), - (0x4FF, "V"), - (0x500, "M", "ԁ"), - (0x501, "V"), - (0x502, "M", "ԃ"), - ] - - -def _seg_9() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x503, "V"), - (0x504, "M", "ԅ"), - (0x505, "V"), - (0x506, "M", "ԇ"), - (0x507, "V"), - (0x508, "M", "ԉ"), - (0x509, "V"), - (0x50A, "M", "ԋ"), - (0x50B, "V"), - (0x50C, "M", "ԍ"), - (0x50D, "V"), - (0x50E, "M", "ԏ"), - (0x50F, "V"), - (0x510, "M", "ԑ"), - (0x511, "V"), - (0x512, "M", "ԓ"), - (0x513, "V"), - (0x514, "M", "ԕ"), - (0x515, "V"), - (0x516, "M", "ԗ"), - (0x517, "V"), - (0x518, "M", "ԙ"), - (0x519, "V"), - (0x51A, "M", "ԛ"), - (0x51B, "V"), - (0x51C, "M", "ԝ"), - (0x51D, "V"), - (0x51E, "M", "ԟ"), - (0x51F, "V"), - (0x520, "M", "ԡ"), - (0x521, "V"), - (0x522, "M", "ԣ"), - (0x523, "V"), - (0x524, "M", "ԥ"), - (0x525, "V"), - (0x526, "M", "ԧ"), - (0x527, "V"), - (0x528, "M", "ԩ"), - (0x529, "V"), - (0x52A, "M", "ԫ"), - (0x52B, "V"), - (0x52C, "M", "ԭ"), - (0x52D, "V"), - (0x52E, "M", "ԯ"), - (0x52F, "V"), - (0x530, "X"), - (0x531, "M", "ա"), - (0x532, "M", "բ"), - (0x533, "M", "գ"), - (0x534, "M", "դ"), - (0x535, "M", "ե"), - (0x536, "M", "զ"), - (0x537, "M", "է"), - (0x538, "M", "ը"), - (0x539, "M", "թ"), - (0x53A, "M", "ժ"), - (0x53B, "M", "ի"), - (0x53C, "M", "լ"), - (0x53D, "M", "խ"), - (0x53E, "M", "ծ"), - (0x53F, "M", "կ"), - (0x540, "M", "հ"), - (0x541, "M", "ձ"), - (0x542, "M", "ղ"), - (0x543, "M", "ճ"), - (0x544, "M", "մ"), - (0x545, "M", "յ"), - (0x546, "M", "ն"), - (0x547, "M", "շ"), - (0x548, "M", "ո"), - (0x549, "M", "չ"), - (0x54A, "M", "պ"), - (0x54B, "M", "ջ"), - (0x54C, "M", "ռ"), - (0x54D, "M", "ս"), - (0x54E, "M", "վ"), - (0x54F, "M", "տ"), - (0x550, "M", "ր"), - (0x551, "M", "ց"), - (0x552, "M", "ւ"), - (0x553, "M", "փ"), - (0x554, "M", "ք"), - (0x555, "M", "օ"), - (0x556, "M", "ֆ"), - (0x557, "X"), - (0x559, "V"), - (0x587, "M", "եւ"), - (0x588, "V"), - (0x58B, "X"), - (0x58D, "V"), - (0x590, "X"), - (0x591, "V"), - (0x5C8, "X"), - (0x5D0, "V"), - (0x5EB, "X"), - (0x5EF, "V"), - (0x5F5, "X"), - (0x606, "V"), - (0x61C, "X"), - (0x61D, "V"), - ] - - -def _seg_10() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x675, "M", "اٴ"), - (0x676, "M", "وٴ"), - (0x677, "M", "ۇٴ"), - (0x678, "M", "يٴ"), - (0x679, "V"), - (0x6DD, "X"), - (0x6DE, "V"), - (0x70E, "X"), - (0x710, "V"), - (0x74B, "X"), - (0x74D, "V"), - (0x7B2, "X"), - (0x7C0, "V"), - (0x7FB, "X"), - (0x7FD, "V"), - (0x82E, "X"), - (0x830, "V"), - (0x83F, "X"), - (0x840, "V"), - (0x85C, "X"), - (0x85E, "V"), - (0x85F, "X"), - (0x860, "V"), - (0x86B, "X"), - (0x870, "V"), - (0x88F, "X"), - (0x897, "V"), - (0x8E2, "X"), - (0x8E3, "V"), - (0x958, "M", "क़"), - (0x959, "M", "ख़"), - (0x95A, "M", "ग़"), - (0x95B, "M", "ज़"), - (0x95C, "M", "ड़"), - (0x95D, "M", "ढ़"), - (0x95E, "M", "फ़"), - (0x95F, "M", "य़"), - (0x960, "V"), - (0x984, "X"), - (0x985, "V"), - (0x98D, "X"), - (0x98F, "V"), - (0x991, "X"), - (0x993, "V"), - (0x9A9, "X"), - (0x9AA, "V"), - (0x9B1, "X"), - (0x9B2, "V"), - (0x9B3, "X"), - (0x9B6, "V"), - (0x9BA, "X"), - (0x9BC, "V"), - (0x9C5, "X"), - (0x9C7, "V"), - (0x9C9, "X"), - (0x9CB, "V"), - (0x9CF, "X"), - (0x9D7, "V"), - (0x9D8, "X"), - (0x9DC, "M", "ড়"), - (0x9DD, "M", "ঢ়"), - (0x9DE, "X"), - (0x9DF, "M", "য়"), - (0x9E0, "V"), - (0x9E4, "X"), - (0x9E6, "V"), - (0x9FF, "X"), - (0xA01, "V"), - (0xA04, "X"), - (0xA05, "V"), - (0xA0B, "X"), - (0xA0F, "V"), - (0xA11, "X"), - (0xA13, "V"), - (0xA29, "X"), - (0xA2A, "V"), - (0xA31, "X"), - (0xA32, "V"), - (0xA33, "M", "ਲ਼"), - (0xA34, "X"), - (0xA35, "V"), - (0xA36, "M", "ਸ਼"), - (0xA37, "X"), - (0xA38, "V"), - (0xA3A, "X"), - (0xA3C, "V"), - (0xA3D, "X"), - (0xA3E, "V"), - (0xA43, "X"), - (0xA47, "V"), - (0xA49, "X"), - (0xA4B, "V"), - (0xA4E, "X"), - (0xA51, "V"), - (0xA52, "X"), - (0xA59, "M", "ਖ਼"), - (0xA5A, "M", "ਗ਼"), - (0xA5B, "M", "ਜ਼"), - (0xA5C, "V"), - (0xA5D, "X"), - ] - - -def _seg_11() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xA5E, "M", "ਫ਼"), - (0xA5F, "X"), - (0xA66, "V"), - (0xA77, "X"), - (0xA81, "V"), - (0xA84, "X"), - (0xA85, "V"), - (0xA8E, "X"), - (0xA8F, "V"), - (0xA92, "X"), - (0xA93, "V"), - (0xAA9, "X"), - (0xAAA, "V"), - (0xAB1, "X"), - (0xAB2, "V"), - (0xAB4, "X"), - (0xAB5, "V"), - (0xABA, "X"), - (0xABC, "V"), - (0xAC6, "X"), - (0xAC7, "V"), - (0xACA, "X"), - (0xACB, "V"), - (0xACE, "X"), - (0xAD0, "V"), - (0xAD1, "X"), - (0xAE0, "V"), - (0xAE4, "X"), - (0xAE6, "V"), - (0xAF2, "X"), - (0xAF9, "V"), - (0xB00, "X"), - (0xB01, "V"), - (0xB04, "X"), - (0xB05, "V"), - (0xB0D, "X"), - (0xB0F, "V"), - (0xB11, "X"), - (0xB13, "V"), - (0xB29, "X"), - (0xB2A, "V"), - (0xB31, "X"), - (0xB32, "V"), - (0xB34, "X"), - (0xB35, "V"), - (0xB3A, "X"), - (0xB3C, "V"), - (0xB45, "X"), - (0xB47, "V"), - (0xB49, "X"), - (0xB4B, "V"), - (0xB4E, "X"), - (0xB55, "V"), - (0xB58, "X"), - (0xB5C, "M", "ଡ଼"), - (0xB5D, "M", "ଢ଼"), - (0xB5E, "X"), - (0xB5F, "V"), - (0xB64, "X"), - (0xB66, "V"), - (0xB78, "X"), - (0xB82, "V"), - (0xB84, "X"), - (0xB85, "V"), - (0xB8B, "X"), - (0xB8E, "V"), - (0xB91, "X"), - (0xB92, "V"), - (0xB96, "X"), - (0xB99, "V"), - (0xB9B, "X"), - (0xB9C, "V"), - (0xB9D, "X"), - (0xB9E, "V"), - (0xBA0, "X"), - (0xBA3, "V"), - (0xBA5, "X"), - (0xBA8, "V"), - (0xBAB, "X"), - (0xBAE, "V"), - (0xBBA, "X"), - (0xBBE, "V"), - (0xBC3, "X"), - (0xBC6, "V"), - (0xBC9, "X"), - (0xBCA, "V"), - (0xBCE, "X"), - (0xBD0, "V"), - (0xBD1, "X"), - (0xBD7, "V"), - (0xBD8, "X"), - (0xBE6, "V"), - (0xBFB, "X"), - (0xC00, "V"), - (0xC0D, "X"), - (0xC0E, "V"), - (0xC11, "X"), - (0xC12, "V"), - (0xC29, "X"), - (0xC2A, "V"), - ] - - -def _seg_12() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xC3A, "X"), - (0xC3C, "V"), - (0xC45, "X"), - (0xC46, "V"), - (0xC49, "X"), - (0xC4A, "V"), - (0xC4E, "X"), - (0xC55, "V"), - (0xC57, "X"), - (0xC58, "V"), - (0xC5B, "X"), - (0xC5D, "V"), - (0xC5E, "X"), - (0xC60, "V"), - (0xC64, "X"), - (0xC66, "V"), - (0xC70, "X"), - (0xC77, "V"), - (0xC8D, "X"), - (0xC8E, "V"), - (0xC91, "X"), - (0xC92, "V"), - (0xCA9, "X"), - (0xCAA, "V"), - (0xCB4, "X"), - (0xCB5, "V"), - (0xCBA, "X"), - (0xCBC, "V"), - (0xCC5, "X"), - (0xCC6, "V"), - (0xCC9, "X"), - (0xCCA, "V"), - (0xCCE, "X"), - (0xCD5, "V"), - (0xCD7, "X"), - (0xCDD, "V"), - (0xCDF, "X"), - (0xCE0, "V"), - (0xCE4, "X"), - (0xCE6, "V"), - (0xCF0, "X"), - (0xCF1, "V"), - (0xCF4, "X"), - (0xD00, "V"), - (0xD0D, "X"), - (0xD0E, "V"), - (0xD11, "X"), - (0xD12, "V"), - (0xD45, "X"), - (0xD46, "V"), - (0xD49, "X"), - (0xD4A, "V"), - (0xD50, "X"), - (0xD54, "V"), - (0xD64, "X"), - (0xD66, "V"), - (0xD80, "X"), - (0xD81, "V"), - (0xD84, "X"), - (0xD85, "V"), - (0xD97, "X"), - (0xD9A, "V"), - (0xDB2, "X"), - (0xDB3, "V"), - (0xDBC, "X"), - (0xDBD, "V"), - (0xDBE, "X"), - (0xDC0, "V"), - (0xDC7, "X"), - (0xDCA, "V"), - (0xDCB, "X"), - (0xDCF, "V"), - (0xDD5, "X"), - (0xDD6, "V"), - (0xDD7, "X"), - (0xDD8, "V"), - (0xDE0, "X"), - (0xDE6, "V"), - (0xDF0, "X"), - (0xDF2, "V"), - (0xDF5, "X"), - (0xE01, "V"), - (0xE33, "M", "ํา"), - (0xE34, "V"), - (0xE3B, "X"), - (0xE3F, "V"), - (0xE5C, "X"), - (0xE81, "V"), - (0xE83, "X"), - (0xE84, "V"), - (0xE85, "X"), - (0xE86, "V"), - (0xE8B, "X"), - (0xE8C, "V"), - (0xEA4, "X"), - (0xEA5, "V"), - (0xEA6, "X"), - (0xEA7, "V"), - (0xEB3, "M", "ໍາ"), - (0xEB4, "V"), - ] - - -def _seg_13() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xEBE, "X"), - (0xEC0, "V"), - (0xEC5, "X"), - (0xEC6, "V"), - (0xEC7, "X"), - (0xEC8, "V"), - (0xECF, "X"), - (0xED0, "V"), - (0xEDA, "X"), - (0xEDC, "M", "ຫນ"), - (0xEDD, "M", "ຫມ"), - (0xEDE, "V"), - (0xEE0, "X"), - (0xF00, "V"), - (0xF0C, "M", "་"), - (0xF0D, "V"), - (0xF43, "M", "གྷ"), - (0xF44, "V"), - (0xF48, "X"), - (0xF49, "V"), - (0xF4D, "M", "ཌྷ"), - (0xF4E, "V"), - (0xF52, "M", "དྷ"), - (0xF53, "V"), - (0xF57, "M", "བྷ"), - (0xF58, "V"), - (0xF5C, "M", "ཛྷ"), - (0xF5D, "V"), - (0xF69, "M", "ཀྵ"), - (0xF6A, "V"), - (0xF6D, "X"), - (0xF71, "V"), - (0xF73, "M", "ཱི"), - (0xF74, "V"), - (0xF75, "M", "ཱུ"), - (0xF76, "M", "ྲྀ"), - (0xF77, "M", "ྲཱྀ"), - (0xF78, "M", "ླྀ"), - (0xF79, "M", "ླཱྀ"), - (0xF7A, "V"), - (0xF81, "M", "ཱྀ"), - (0xF82, "V"), - (0xF93, "M", "ྒྷ"), - (0xF94, "V"), - (0xF98, "X"), - (0xF99, "V"), - (0xF9D, "M", "ྜྷ"), - (0xF9E, "V"), - (0xFA2, "M", "ྡྷ"), - (0xFA3, "V"), - (0xFA7, "M", "ྦྷ"), - (0xFA8, "V"), - (0xFAC, "M", "ྫྷ"), - (0xFAD, "V"), - (0xFB9, "M", "ྐྵ"), - (0xFBA, "V"), - (0xFBD, "X"), - (0xFBE, "V"), - (0xFCD, "X"), - (0xFCE, "V"), - (0xFDB, "X"), - (0x1000, "V"), - (0x10A0, "M", "ⴀ"), - (0x10A1, "M", "ⴁ"), - (0x10A2, "M", "ⴂ"), - (0x10A3, "M", "ⴃ"), - (0x10A4, "M", "ⴄ"), - (0x10A5, "M", "ⴅ"), - (0x10A6, "M", "ⴆ"), - (0x10A7, "M", "ⴇ"), - (0x10A8, "M", "ⴈ"), - (0x10A9, "M", "ⴉ"), - (0x10AA, "M", "ⴊ"), - (0x10AB, "M", "ⴋ"), - (0x10AC, "M", "ⴌ"), - (0x10AD, "M", "ⴍ"), - (0x10AE, "M", "ⴎ"), - (0x10AF, "M", "ⴏ"), - (0x10B0, "M", "ⴐ"), - (0x10B1, "M", "ⴑ"), - (0x10B2, "M", "ⴒ"), - (0x10B3, "M", "ⴓ"), - (0x10B4, "M", "ⴔ"), - (0x10B5, "M", "ⴕ"), - (0x10B6, "M", "ⴖ"), - (0x10B7, "M", "ⴗ"), - (0x10B8, "M", "ⴘ"), - (0x10B9, "M", "ⴙ"), - (0x10BA, "M", "ⴚ"), - (0x10BB, "M", "ⴛ"), - (0x10BC, "M", "ⴜ"), - (0x10BD, "M", "ⴝ"), - (0x10BE, "M", "ⴞ"), - (0x10BF, "M", "ⴟ"), - (0x10C0, "M", "ⴠ"), - (0x10C1, "M", "ⴡ"), - (0x10C2, "M", "ⴢ"), - (0x10C3, "M", "ⴣ"), - (0x10C4, "M", "ⴤ"), - (0x10C5, "M", "ⴥ"), - ] - - -def _seg_14() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x10C6, "X"), - (0x10C7, "M", "ⴧ"), - (0x10C8, "X"), - (0x10CD, "M", "ⴭ"), - (0x10CE, "X"), - (0x10D0, "V"), - (0x10FC, "M", "ნ"), - (0x10FD, "V"), - (0x115F, "I"), - (0x1161, "V"), - (0x1249, "X"), - (0x124A, "V"), - (0x124E, "X"), - (0x1250, "V"), - (0x1257, "X"), - (0x1258, "V"), - (0x1259, "X"), - (0x125A, "V"), - (0x125E, "X"), - (0x1260, "V"), - (0x1289, "X"), - (0x128A, "V"), - (0x128E, "X"), - (0x1290, "V"), - (0x12B1, "X"), - (0x12B2, "V"), - (0x12B6, "X"), - (0x12B8, "V"), - (0x12BF, "X"), - (0x12C0, "V"), - (0x12C1, "X"), - (0x12C2, "V"), - (0x12C6, "X"), - (0x12C8, "V"), - (0x12D7, "X"), - (0x12D8, "V"), - (0x1311, "X"), - (0x1312, "V"), - (0x1316, "X"), - (0x1318, "V"), - (0x135B, "X"), - (0x135D, "V"), - (0x137D, "X"), - (0x1380, "V"), - (0x139A, "X"), - (0x13A0, "V"), - (0x13F6, "X"), - (0x13F8, "M", "Ᏸ"), - (0x13F9, "M", "Ᏹ"), - (0x13FA, "M", "Ᏺ"), - (0x13FB, "M", "Ᏻ"), - (0x13FC, "M", "Ᏼ"), - (0x13FD, "M", "Ᏽ"), - (0x13FE, "X"), - (0x1400, "V"), - (0x1680, "X"), - (0x1681, "V"), - (0x169D, "X"), - (0x16A0, "V"), - (0x16F9, "X"), - (0x1700, "V"), - (0x1716, "X"), - (0x171F, "V"), - (0x1737, "X"), - (0x1740, "V"), - (0x1754, "X"), - (0x1760, "V"), - (0x176D, "X"), - (0x176E, "V"), - (0x1771, "X"), - (0x1772, "V"), - (0x1774, "X"), - (0x1780, "V"), - (0x17B4, "I"), - (0x17B6, "V"), - (0x17DE, "X"), - (0x17E0, "V"), - (0x17EA, "X"), - (0x17F0, "V"), - (0x17FA, "X"), - (0x1800, "V"), - (0x180B, "I"), - (0x1810, "V"), - (0x181A, "X"), - (0x1820, "V"), - (0x1879, "X"), - (0x1880, "V"), - (0x18AB, "X"), - (0x18B0, "V"), - (0x18F6, "X"), - (0x1900, "V"), - (0x191F, "X"), - (0x1920, "V"), - (0x192C, "X"), - (0x1930, "V"), - (0x193C, "X"), - (0x1940, "V"), - (0x1941, "X"), - (0x1944, "V"), - (0x196E, "X"), - ] - - -def _seg_15() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1970, "V"), - (0x1975, "X"), - (0x1980, "V"), - (0x19AC, "X"), - (0x19B0, "V"), - (0x19CA, "X"), - (0x19D0, "V"), - (0x19DB, "X"), - (0x19DE, "V"), - (0x1A1C, "X"), - (0x1A1E, "V"), - (0x1A5F, "X"), - (0x1A60, "V"), - (0x1A7D, "X"), - (0x1A7F, "V"), - (0x1A8A, "X"), - (0x1A90, "V"), - (0x1A9A, "X"), - (0x1AA0, "V"), - (0x1AAE, "X"), - (0x1AB0, "V"), - (0x1ACF, "X"), - (0x1B00, "V"), - (0x1B4D, "X"), - (0x1B4E, "V"), - (0x1BF4, "X"), - (0x1BFC, "V"), - (0x1C38, "X"), - (0x1C3B, "V"), - (0x1C4A, "X"), - (0x1C4D, "V"), - (0x1C80, "M", "в"), - (0x1C81, "M", "д"), - (0x1C82, "M", "о"), - (0x1C83, "M", "с"), - (0x1C84, "M", "т"), - (0x1C86, "M", "ъ"), - (0x1C87, "M", "ѣ"), - (0x1C88, "M", "ꙋ"), - (0x1C89, "M", "ᲊ"), - (0x1C8A, "V"), - (0x1C8B, "X"), - (0x1C90, "M", "ა"), - (0x1C91, "M", "ბ"), - (0x1C92, "M", "გ"), - (0x1C93, "M", "დ"), - (0x1C94, "M", "ე"), - (0x1C95, "M", "ვ"), - (0x1C96, "M", "ზ"), - (0x1C97, "M", "თ"), - (0x1C98, "M", "ი"), - (0x1C99, "M", "კ"), - (0x1C9A, "M", "ლ"), - (0x1C9B, "M", "მ"), - (0x1C9C, "M", "ნ"), - (0x1C9D, "M", "ო"), - (0x1C9E, "M", "პ"), - (0x1C9F, "M", "ჟ"), - (0x1CA0, "M", "რ"), - (0x1CA1, "M", "ს"), - (0x1CA2, "M", "ტ"), - (0x1CA3, "M", "უ"), - (0x1CA4, "M", "ფ"), - (0x1CA5, "M", "ქ"), - (0x1CA6, "M", "ღ"), - (0x1CA7, "M", "ყ"), - (0x1CA8, "M", "შ"), - (0x1CA9, "M", "ჩ"), - (0x1CAA, "M", "ც"), - (0x1CAB, "M", "ძ"), - (0x1CAC, "M", "წ"), - (0x1CAD, "M", "ჭ"), - (0x1CAE, "M", "ხ"), - (0x1CAF, "M", "ჯ"), - (0x1CB0, "M", "ჰ"), - (0x1CB1, "M", "ჱ"), - (0x1CB2, "M", "ჲ"), - (0x1CB3, "M", "ჳ"), - (0x1CB4, "M", "ჴ"), - (0x1CB5, "M", "ჵ"), - (0x1CB6, "M", "ჶ"), - (0x1CB7, "M", "ჷ"), - (0x1CB8, "M", "ჸ"), - (0x1CB9, "M", "ჹ"), - (0x1CBA, "M", "ჺ"), - (0x1CBB, "X"), - (0x1CBD, "M", "ჽ"), - (0x1CBE, "M", "ჾ"), - (0x1CBF, "M", "ჿ"), - (0x1CC0, "V"), - (0x1CC8, "X"), - (0x1CD0, "V"), - (0x1CFB, "X"), - (0x1D00, "V"), - (0x1D2C, "M", "a"), - (0x1D2D, "M", "æ"), - (0x1D2E, "M", "b"), - (0x1D2F, "V"), - (0x1D30, "M", "d"), - (0x1D31, "M", "e"), - ] - - -def _seg_16() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D32, "M", "ǝ"), - (0x1D33, "M", "g"), - (0x1D34, "M", "h"), - (0x1D35, "M", "i"), - (0x1D36, "M", "j"), - (0x1D37, "M", "k"), - (0x1D38, "M", "l"), - (0x1D39, "M", "m"), - (0x1D3A, "M", "n"), - (0x1D3B, "V"), - (0x1D3C, "M", "o"), - (0x1D3D, "M", "ȣ"), - (0x1D3E, "M", "p"), - (0x1D3F, "M", "r"), - (0x1D40, "M", "t"), - (0x1D41, "M", "u"), - (0x1D42, "M", "w"), - (0x1D43, "M", "a"), - (0x1D44, "M", "ɐ"), - (0x1D45, "M", "ɑ"), - (0x1D46, "M", "ᴂ"), - (0x1D47, "M", "b"), - (0x1D48, "M", "d"), - (0x1D49, "M", "e"), - (0x1D4A, "M", "ə"), - (0x1D4B, "M", "ɛ"), - (0x1D4C, "M", "ɜ"), - (0x1D4D, "M", "g"), - (0x1D4E, "V"), - (0x1D4F, "M", "k"), - (0x1D50, "M", "m"), - (0x1D51, "M", "ŋ"), - (0x1D52, "M", "o"), - (0x1D53, "M", "ɔ"), - (0x1D54, "M", "ᴖ"), - (0x1D55, "M", "ᴗ"), - (0x1D56, "M", "p"), - (0x1D57, "M", "t"), - (0x1D58, "M", "u"), - (0x1D59, "M", "ᴝ"), - (0x1D5A, "M", "ɯ"), - (0x1D5B, "M", "v"), - (0x1D5C, "M", "ᴥ"), - (0x1D5D, "M", "β"), - (0x1D5E, "M", "γ"), - (0x1D5F, "M", "δ"), - (0x1D60, "M", "φ"), - (0x1D61, "M", "χ"), - (0x1D62, "M", "i"), - (0x1D63, "M", "r"), - (0x1D64, "M", "u"), - (0x1D65, "M", "v"), - (0x1D66, "M", "β"), - (0x1D67, "M", "γ"), - (0x1D68, "M", "ρ"), - (0x1D69, "M", "φ"), - (0x1D6A, "M", "χ"), - (0x1D6B, "V"), - (0x1D78, "M", "н"), - (0x1D79, "V"), - (0x1D9B, "M", "ɒ"), - (0x1D9C, "M", "c"), - (0x1D9D, "M", "ɕ"), - (0x1D9E, "M", "ð"), - (0x1D9F, "M", "ɜ"), - (0x1DA0, "M", "f"), - (0x1DA1, "M", "ɟ"), - (0x1DA2, "M", "ɡ"), - (0x1DA3, "M", "ɥ"), - (0x1DA4, "M", "ɨ"), - (0x1DA5, "M", "ɩ"), - (0x1DA6, "M", "ɪ"), - (0x1DA7, "M", "ᵻ"), - (0x1DA8, "M", "ʝ"), - (0x1DA9, "M", "ɭ"), - (0x1DAA, "M", "ᶅ"), - (0x1DAB, "M", "ʟ"), - (0x1DAC, "M", "ɱ"), - (0x1DAD, "M", "ɰ"), - (0x1DAE, "M", "ɲ"), - (0x1DAF, "M", "ɳ"), - (0x1DB0, "M", "ɴ"), - (0x1DB1, "M", "ɵ"), - (0x1DB2, "M", "ɸ"), - (0x1DB3, "M", "ʂ"), - (0x1DB4, "M", "ʃ"), - (0x1DB5, "M", "ƫ"), - (0x1DB6, "M", "ʉ"), - (0x1DB7, "M", "ʊ"), - (0x1DB8, "M", "ᴜ"), - (0x1DB9, "M", "ʋ"), - (0x1DBA, "M", "ʌ"), - (0x1DBB, "M", "z"), - (0x1DBC, "M", "ʐ"), - (0x1DBD, "M", "ʑ"), - (0x1DBE, "M", "ʒ"), - (0x1DBF, "M", "θ"), - (0x1DC0, "V"), - (0x1E00, "M", "ḁ"), - (0x1E01, "V"), - ] - - -def _seg_17() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1E02, "M", "ḃ"), - (0x1E03, "V"), - (0x1E04, "M", "ḅ"), - (0x1E05, "V"), - (0x1E06, "M", "ḇ"), - (0x1E07, "V"), - (0x1E08, "M", "ḉ"), - (0x1E09, "V"), - (0x1E0A, "M", "ḋ"), - (0x1E0B, "V"), - (0x1E0C, "M", "ḍ"), - (0x1E0D, "V"), - (0x1E0E, "M", "ḏ"), - (0x1E0F, "V"), - (0x1E10, "M", "ḑ"), - (0x1E11, "V"), - (0x1E12, "M", "ḓ"), - (0x1E13, "V"), - (0x1E14, "M", "ḕ"), - (0x1E15, "V"), - (0x1E16, "M", "ḗ"), - (0x1E17, "V"), - (0x1E18, "M", "ḙ"), - (0x1E19, "V"), - (0x1E1A, "M", "ḛ"), - (0x1E1B, "V"), - (0x1E1C, "M", "ḝ"), - (0x1E1D, "V"), - (0x1E1E, "M", "ḟ"), - (0x1E1F, "V"), - (0x1E20, "M", "ḡ"), - (0x1E21, "V"), - (0x1E22, "M", "ḣ"), - (0x1E23, "V"), - (0x1E24, "M", "ḥ"), - (0x1E25, "V"), - (0x1E26, "M", "ḧ"), - (0x1E27, "V"), - (0x1E28, "M", "ḩ"), - (0x1E29, "V"), - (0x1E2A, "M", "ḫ"), - (0x1E2B, "V"), - (0x1E2C, "M", "ḭ"), - (0x1E2D, "V"), - (0x1E2E, "M", "ḯ"), - (0x1E2F, "V"), - (0x1E30, "M", "ḱ"), - (0x1E31, "V"), - (0x1E32, "M", "ḳ"), - (0x1E33, "V"), - (0x1E34, "M", "ḵ"), - (0x1E35, "V"), - (0x1E36, "M", "ḷ"), - (0x1E37, "V"), - (0x1E38, "M", "ḹ"), - (0x1E39, "V"), - (0x1E3A, "M", "ḻ"), - (0x1E3B, "V"), - (0x1E3C, "M", "ḽ"), - (0x1E3D, "V"), - (0x1E3E, "M", "ḿ"), - (0x1E3F, "V"), - (0x1E40, "M", "ṁ"), - (0x1E41, "V"), - (0x1E42, "M", "ṃ"), - (0x1E43, "V"), - (0x1E44, "M", "ṅ"), - (0x1E45, "V"), - (0x1E46, "M", "ṇ"), - (0x1E47, "V"), - (0x1E48, "M", "ṉ"), - (0x1E49, "V"), - (0x1E4A, "M", "ṋ"), - (0x1E4B, "V"), - (0x1E4C, "M", "ṍ"), - (0x1E4D, "V"), - (0x1E4E, "M", "ṏ"), - (0x1E4F, "V"), - (0x1E50, "M", "ṑ"), - (0x1E51, "V"), - (0x1E52, "M", "ṓ"), - (0x1E53, "V"), - (0x1E54, "M", "ṕ"), - (0x1E55, "V"), - (0x1E56, "M", "ṗ"), - (0x1E57, "V"), - (0x1E58, "M", "ṙ"), - (0x1E59, "V"), - (0x1E5A, "M", "ṛ"), - (0x1E5B, "V"), - (0x1E5C, "M", "ṝ"), - (0x1E5D, "V"), - (0x1E5E, "M", "ṟ"), - (0x1E5F, "V"), - (0x1E60, "M", "ṡ"), - (0x1E61, "V"), - (0x1E62, "M", "ṣ"), - (0x1E63, "V"), - (0x1E64, "M", "ṥ"), - (0x1E65, "V"), - ] - - -def _seg_18() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1E66, "M", "ṧ"), - (0x1E67, "V"), - (0x1E68, "M", "ṩ"), - (0x1E69, "V"), - (0x1E6A, "M", "ṫ"), - (0x1E6B, "V"), - (0x1E6C, "M", "ṭ"), - (0x1E6D, "V"), - (0x1E6E, "M", "ṯ"), - (0x1E6F, "V"), - (0x1E70, "M", "ṱ"), - (0x1E71, "V"), - (0x1E72, "M", "ṳ"), - (0x1E73, "V"), - (0x1E74, "M", "ṵ"), - (0x1E75, "V"), - (0x1E76, "M", "ṷ"), - (0x1E77, "V"), - (0x1E78, "M", "ṹ"), - (0x1E79, "V"), - (0x1E7A, "M", "ṻ"), - (0x1E7B, "V"), - (0x1E7C, "M", "ṽ"), - (0x1E7D, "V"), - (0x1E7E, "M", "ṿ"), - (0x1E7F, "V"), - (0x1E80, "M", "ẁ"), - (0x1E81, "V"), - (0x1E82, "M", "ẃ"), - (0x1E83, "V"), - (0x1E84, "M", "ẅ"), - (0x1E85, "V"), - (0x1E86, "M", "ẇ"), - (0x1E87, "V"), - (0x1E88, "M", "ẉ"), - (0x1E89, "V"), - (0x1E8A, "M", "ẋ"), - (0x1E8B, "V"), - (0x1E8C, "M", "ẍ"), - (0x1E8D, "V"), - (0x1E8E, "M", "ẏ"), - (0x1E8F, "V"), - (0x1E90, "M", "ẑ"), - (0x1E91, "V"), - (0x1E92, "M", "ẓ"), - (0x1E93, "V"), - (0x1E94, "M", "ẕ"), - (0x1E95, "V"), - (0x1E9A, "M", "aʾ"), - (0x1E9B, "M", "ṡ"), - (0x1E9C, "V"), - (0x1E9E, "M", "ß"), - (0x1E9F, "V"), - (0x1EA0, "M", "ạ"), - (0x1EA1, "V"), - (0x1EA2, "M", "ả"), - (0x1EA3, "V"), - (0x1EA4, "M", "ấ"), - (0x1EA5, "V"), - (0x1EA6, "M", "ầ"), - (0x1EA7, "V"), - (0x1EA8, "M", "ẩ"), - (0x1EA9, "V"), - (0x1EAA, "M", "ẫ"), - (0x1EAB, "V"), - (0x1EAC, "M", "ậ"), - (0x1EAD, "V"), - (0x1EAE, "M", "ắ"), - (0x1EAF, "V"), - (0x1EB0, "M", "ằ"), - (0x1EB1, "V"), - (0x1EB2, "M", "ẳ"), - (0x1EB3, "V"), - (0x1EB4, "M", "ẵ"), - (0x1EB5, "V"), - (0x1EB6, "M", "ặ"), - (0x1EB7, "V"), - (0x1EB8, "M", "ẹ"), - (0x1EB9, "V"), - (0x1EBA, "M", "ẻ"), - (0x1EBB, "V"), - (0x1EBC, "M", "ẽ"), - (0x1EBD, "V"), - (0x1EBE, "M", "ế"), - (0x1EBF, "V"), - (0x1EC0, "M", "ề"), - (0x1EC1, "V"), - (0x1EC2, "M", "ể"), - (0x1EC3, "V"), - (0x1EC4, "M", "ễ"), - (0x1EC5, "V"), - (0x1EC6, "M", "ệ"), - (0x1EC7, "V"), - (0x1EC8, "M", "ỉ"), - (0x1EC9, "V"), - (0x1ECA, "M", "ị"), - (0x1ECB, "V"), - (0x1ECC, "M", "ọ"), - (0x1ECD, "V"), - (0x1ECE, "M", "ỏ"), - ] - - -def _seg_19() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1ECF, "V"), - (0x1ED0, "M", "ố"), - (0x1ED1, "V"), - (0x1ED2, "M", "ồ"), - (0x1ED3, "V"), - (0x1ED4, "M", "ổ"), - (0x1ED5, "V"), - (0x1ED6, "M", "ỗ"), - (0x1ED7, "V"), - (0x1ED8, "M", "ộ"), - (0x1ED9, "V"), - (0x1EDA, "M", "ớ"), - (0x1EDB, "V"), - (0x1EDC, "M", "ờ"), - (0x1EDD, "V"), - (0x1EDE, "M", "ở"), - (0x1EDF, "V"), - (0x1EE0, "M", "ỡ"), - (0x1EE1, "V"), - (0x1EE2, "M", "ợ"), - (0x1EE3, "V"), - (0x1EE4, "M", "ụ"), - (0x1EE5, "V"), - (0x1EE6, "M", "ủ"), - (0x1EE7, "V"), - (0x1EE8, "M", "ứ"), - (0x1EE9, "V"), - (0x1EEA, "M", "ừ"), - (0x1EEB, "V"), - (0x1EEC, "M", "ử"), - (0x1EED, "V"), - (0x1EEE, "M", "ữ"), - (0x1EEF, "V"), - (0x1EF0, "M", "ự"), - (0x1EF1, "V"), - (0x1EF2, "M", "ỳ"), - (0x1EF3, "V"), - (0x1EF4, "M", "ỵ"), - (0x1EF5, "V"), - (0x1EF6, "M", "ỷ"), - (0x1EF7, "V"), - (0x1EF8, "M", "ỹ"), - (0x1EF9, "V"), - (0x1EFA, "M", "ỻ"), - (0x1EFB, "V"), - (0x1EFC, "M", "ỽ"), - (0x1EFD, "V"), - (0x1EFE, "M", "ỿ"), - (0x1EFF, "V"), - (0x1F08, "M", "ἀ"), - (0x1F09, "M", "ἁ"), - (0x1F0A, "M", "ἂ"), - (0x1F0B, "M", "ἃ"), - (0x1F0C, "M", "ἄ"), - (0x1F0D, "M", "ἅ"), - (0x1F0E, "M", "ἆ"), - (0x1F0F, "M", "ἇ"), - (0x1F10, "V"), - (0x1F16, "X"), - (0x1F18, "M", "ἐ"), - (0x1F19, "M", "ἑ"), - (0x1F1A, "M", "ἒ"), - (0x1F1B, "M", "ἓ"), - (0x1F1C, "M", "ἔ"), - (0x1F1D, "M", "ἕ"), - (0x1F1E, "X"), - (0x1F20, "V"), - (0x1F28, "M", "ἠ"), - (0x1F29, "M", "ἡ"), - (0x1F2A, "M", "ἢ"), - (0x1F2B, "M", "ἣ"), - (0x1F2C, "M", "ἤ"), - (0x1F2D, "M", "ἥ"), - (0x1F2E, "M", "ἦ"), - (0x1F2F, "M", "ἧ"), - (0x1F30, "V"), - (0x1F38, "M", "ἰ"), - (0x1F39, "M", "ἱ"), - (0x1F3A, "M", "ἲ"), - (0x1F3B, "M", "ἳ"), - (0x1F3C, "M", "ἴ"), - (0x1F3D, "M", "ἵ"), - (0x1F3E, "M", "ἶ"), - (0x1F3F, "M", "ἷ"), - (0x1F40, "V"), - (0x1F46, "X"), - (0x1F48, "M", "ὀ"), - (0x1F49, "M", "ὁ"), - (0x1F4A, "M", "ὂ"), - (0x1F4B, "M", "ὃ"), - (0x1F4C, "M", "ὄ"), - (0x1F4D, "M", "ὅ"), - (0x1F4E, "X"), - (0x1F50, "V"), - (0x1F58, "X"), - (0x1F59, "M", "ὑ"), - (0x1F5A, "X"), - (0x1F5B, "M", "ὓ"), - (0x1F5C, "X"), - (0x1F5D, "M", "ὕ"), - ] - - -def _seg_20() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1F5E, "X"), - (0x1F5F, "M", "ὗ"), - (0x1F60, "V"), - (0x1F68, "M", "ὠ"), - (0x1F69, "M", "ὡ"), - (0x1F6A, "M", "ὢ"), - (0x1F6B, "M", "ὣ"), - (0x1F6C, "M", "ὤ"), - (0x1F6D, "M", "ὥ"), - (0x1F6E, "M", "ὦ"), - (0x1F6F, "M", "ὧ"), - (0x1F70, "V"), - (0x1F71, "M", "ά"), - (0x1F72, "V"), - (0x1F73, "M", "έ"), - (0x1F74, "V"), - (0x1F75, "M", "ή"), - (0x1F76, "V"), - (0x1F77, "M", "ί"), - (0x1F78, "V"), - (0x1F79, "M", "ό"), - (0x1F7A, "V"), - (0x1F7B, "M", "ύ"), - (0x1F7C, "V"), - (0x1F7D, "M", "ώ"), - (0x1F7E, "X"), - (0x1F80, "M", "ἀι"), - (0x1F81, "M", "ἁι"), - (0x1F82, "M", "ἂι"), - (0x1F83, "M", "ἃι"), - (0x1F84, "M", "ἄι"), - (0x1F85, "M", "ἅι"), - (0x1F86, "M", "ἆι"), - (0x1F87, "M", "ἇι"), - (0x1F88, "M", "ἀι"), - (0x1F89, "M", "ἁι"), - (0x1F8A, "M", "ἂι"), - (0x1F8B, "M", "ἃι"), - (0x1F8C, "M", "ἄι"), - (0x1F8D, "M", "ἅι"), - (0x1F8E, "M", "ἆι"), - (0x1F8F, "M", "ἇι"), - (0x1F90, "M", "ἠι"), - (0x1F91, "M", "ἡι"), - (0x1F92, "M", "ἢι"), - (0x1F93, "M", "ἣι"), - (0x1F94, "M", "ἤι"), - (0x1F95, "M", "ἥι"), - (0x1F96, "M", "ἦι"), - (0x1F97, "M", "ἧι"), - (0x1F98, "M", "ἠι"), - (0x1F99, "M", "ἡι"), - (0x1F9A, "M", "ἢι"), - (0x1F9B, "M", "ἣι"), - (0x1F9C, "M", "ἤι"), - (0x1F9D, "M", "ἥι"), - (0x1F9E, "M", "ἦι"), - (0x1F9F, "M", "ἧι"), - (0x1FA0, "M", "ὠι"), - (0x1FA1, "M", "ὡι"), - (0x1FA2, "M", "ὢι"), - (0x1FA3, "M", "ὣι"), - (0x1FA4, "M", "ὤι"), - (0x1FA5, "M", "ὥι"), - (0x1FA6, "M", "ὦι"), - (0x1FA7, "M", "ὧι"), - (0x1FA8, "M", "ὠι"), - (0x1FA9, "M", "ὡι"), - (0x1FAA, "M", "ὢι"), - (0x1FAB, "M", "ὣι"), - (0x1FAC, "M", "ὤι"), - (0x1FAD, "M", "ὥι"), - (0x1FAE, "M", "ὦι"), - (0x1FAF, "M", "ὧι"), - (0x1FB0, "V"), - (0x1FB2, "M", "ὰι"), - (0x1FB3, "M", "αι"), - (0x1FB4, "M", "άι"), - (0x1FB5, "X"), - (0x1FB6, "V"), - (0x1FB7, "M", "ᾶι"), - (0x1FB8, "M", "ᾰ"), - (0x1FB9, "M", "ᾱ"), - (0x1FBA, "M", "ὰ"), - (0x1FBB, "M", "ά"), - (0x1FBC, "M", "αι"), - (0x1FBD, "M", " ̓"), - (0x1FBE, "M", "ι"), - (0x1FBF, "M", " ̓"), - (0x1FC0, "M", " ͂"), - (0x1FC1, "M", " ̈͂"), - (0x1FC2, "M", "ὴι"), - (0x1FC3, "M", "ηι"), - (0x1FC4, "M", "ήι"), - (0x1FC5, "X"), - (0x1FC6, "V"), - (0x1FC7, "M", "ῆι"), - (0x1FC8, "M", "ὲ"), - (0x1FC9, "M", "έ"), - (0x1FCA, "M", "ὴ"), - ] - - -def _seg_21() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1FCB, "M", "ή"), - (0x1FCC, "M", "ηι"), - (0x1FCD, "M", " ̓̀"), - (0x1FCE, "M", " ̓́"), - (0x1FCF, "M", " ̓͂"), - (0x1FD0, "V"), - (0x1FD3, "M", "ΐ"), - (0x1FD4, "X"), - (0x1FD6, "V"), - (0x1FD8, "M", "ῐ"), - (0x1FD9, "M", "ῑ"), - (0x1FDA, "M", "ὶ"), - (0x1FDB, "M", "ί"), - (0x1FDC, "X"), - (0x1FDD, "M", " ̔̀"), - (0x1FDE, "M", " ̔́"), - (0x1FDF, "M", " ̔͂"), - (0x1FE0, "V"), - (0x1FE3, "M", "ΰ"), - (0x1FE4, "V"), - (0x1FE8, "M", "ῠ"), - (0x1FE9, "M", "ῡ"), - (0x1FEA, "M", "ὺ"), - (0x1FEB, "M", "ύ"), - (0x1FEC, "M", "ῥ"), - (0x1FED, "M", " ̈̀"), - (0x1FEE, "M", " ̈́"), - (0x1FEF, "M", "`"), - (0x1FF0, "X"), - (0x1FF2, "M", "ὼι"), - (0x1FF3, "M", "ωι"), - (0x1FF4, "M", "ώι"), - (0x1FF5, "X"), - (0x1FF6, "V"), - (0x1FF7, "M", "ῶι"), - (0x1FF8, "M", "ὸ"), - (0x1FF9, "M", "ό"), - (0x1FFA, "M", "ὼ"), - (0x1FFB, "M", "ώ"), - (0x1FFC, "M", "ωι"), - (0x1FFD, "M", " ́"), - (0x1FFE, "M", " ̔"), - (0x1FFF, "X"), - (0x2000, "M", " "), - (0x200B, "I"), - (0x200C, "D", ""), - (0x200E, "X"), - (0x2010, "V"), - (0x2011, "M", "‐"), - (0x2012, "V"), - (0x2017, "M", " ̳"), - (0x2018, "V"), - (0x2024, "X"), - (0x2027, "V"), - (0x2028, "X"), - (0x202F, "M", " "), - (0x2030, "V"), - (0x2033, "M", "′′"), - (0x2034, "M", "′′′"), - (0x2035, "V"), - (0x2036, "M", "‵‵"), - (0x2037, "M", "‵‵‵"), - (0x2038, "V"), - (0x203C, "M", "!!"), - (0x203D, "V"), - (0x203E, "M", " ̅"), - (0x203F, "V"), - (0x2047, "M", "??"), - (0x2048, "M", "?!"), - (0x2049, "M", "!?"), - (0x204A, "V"), - (0x2057, "M", "′′′′"), - (0x2058, "V"), - (0x205F, "M", " "), - (0x2060, "I"), - (0x2065, "X"), - (0x206A, "I"), - (0x2070, "M", "0"), - (0x2071, "M", "i"), - (0x2072, "X"), - (0x2074, "M", "4"), - (0x2075, "M", "5"), - (0x2076, "M", "6"), - (0x2077, "M", "7"), - (0x2078, "M", "8"), - (0x2079, "M", "9"), - (0x207A, "M", "+"), - (0x207B, "M", "−"), - (0x207C, "M", "="), - (0x207D, "M", "("), - (0x207E, "M", ")"), - (0x207F, "M", "n"), - (0x2080, "M", "0"), - (0x2081, "M", "1"), - (0x2082, "M", "2"), - (0x2083, "M", "3"), - (0x2084, "M", "4"), - (0x2085, "M", "5"), - (0x2086, "M", "6"), - (0x2087, "M", "7"), - ] - - -def _seg_22() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2088, "M", "8"), - (0x2089, "M", "9"), - (0x208A, "M", "+"), - (0x208B, "M", "−"), - (0x208C, "M", "="), - (0x208D, "M", "("), - (0x208E, "M", ")"), - (0x208F, "X"), - (0x2090, "M", "a"), - (0x2091, "M", "e"), - (0x2092, "M", "o"), - (0x2093, "M", "x"), - (0x2094, "M", "ə"), - (0x2095, "M", "h"), - (0x2096, "M", "k"), - (0x2097, "M", "l"), - (0x2098, "M", "m"), - (0x2099, "M", "n"), - (0x209A, "M", "p"), - (0x209B, "M", "s"), - (0x209C, "M", "t"), - (0x209D, "X"), - (0x20A0, "V"), - (0x20A8, "M", "rs"), - (0x20A9, "V"), - (0x20C1, "X"), - (0x20D0, "V"), - (0x20F1, "X"), - (0x2100, "M", "a/c"), - (0x2101, "M", "a/s"), - (0x2102, "M", "c"), - (0x2103, "M", "°c"), - (0x2104, "V"), - (0x2105, "M", "c/o"), - (0x2106, "M", "c/u"), - (0x2107, "M", "ɛ"), - (0x2108, "V"), - (0x2109, "M", "°f"), - (0x210A, "M", "g"), - (0x210B, "M", "h"), - (0x210F, "M", "ħ"), - (0x2110, "M", "i"), - (0x2112, "M", "l"), - (0x2114, "V"), - (0x2115, "M", "n"), - (0x2116, "M", "no"), - (0x2117, "V"), - (0x2119, "M", "p"), - (0x211A, "M", "q"), - (0x211B, "M", "r"), - (0x211E, "V"), - (0x2120, "M", "sm"), - (0x2121, "M", "tel"), - (0x2122, "M", "tm"), - (0x2123, "V"), - (0x2124, "M", "z"), - (0x2125, "V"), - (0x2126, "M", "ω"), - (0x2127, "V"), - (0x2128, "M", "z"), - (0x2129, "V"), - (0x212A, "M", "k"), - (0x212B, "M", "å"), - (0x212C, "M", "b"), - (0x212D, "M", "c"), - (0x212E, "V"), - (0x212F, "M", "e"), - (0x2131, "M", "f"), - (0x2132, "M", "ⅎ"), - (0x2133, "M", "m"), - (0x2134, "M", "o"), - (0x2135, "M", "א"), - (0x2136, "M", "ב"), - (0x2137, "M", "ג"), - (0x2138, "M", "ד"), - (0x2139, "M", "i"), - (0x213A, "V"), - (0x213B, "M", "fax"), - (0x213C, "M", "π"), - (0x213D, "M", "γ"), - (0x213F, "M", "π"), - (0x2140, "M", "∑"), - (0x2141, "V"), - (0x2145, "M", "d"), - (0x2147, "M", "e"), - (0x2148, "M", "i"), - (0x2149, "M", "j"), - (0x214A, "V"), - (0x2150, "M", "1⁄7"), - (0x2151, "M", "1⁄9"), - (0x2152, "M", "1⁄10"), - (0x2153, "M", "1⁄3"), - (0x2154, "M", "2⁄3"), - (0x2155, "M", "1⁄5"), - (0x2156, "M", "2⁄5"), - (0x2157, "M", "3⁄5"), - (0x2158, "M", "4⁄5"), - (0x2159, "M", "1⁄6"), - (0x215A, "M", "5⁄6"), - (0x215B, "M", "1⁄8"), - ] - - -def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x215C, "M", "3⁄8"), - (0x215D, "M", "5⁄8"), - (0x215E, "M", "7⁄8"), - (0x215F, "M", "1⁄"), - (0x2160, "M", "i"), - (0x2161, "M", "ii"), - (0x2162, "M", "iii"), - (0x2163, "M", "iv"), - (0x2164, "M", "v"), - (0x2165, "M", "vi"), - (0x2166, "M", "vii"), - (0x2167, "M", "viii"), - (0x2168, "M", "ix"), - (0x2169, "M", "x"), - (0x216A, "M", "xi"), - (0x216B, "M", "xii"), - (0x216C, "M", "l"), - (0x216D, "M", "c"), - (0x216E, "M", "d"), - (0x216F, "M", "m"), - (0x2170, "M", "i"), - (0x2171, "M", "ii"), - (0x2172, "M", "iii"), - (0x2173, "M", "iv"), - (0x2174, "M", "v"), - (0x2175, "M", "vi"), - (0x2176, "M", "vii"), - (0x2177, "M", "viii"), - (0x2178, "M", "ix"), - (0x2179, "M", "x"), - (0x217A, "M", "xi"), - (0x217B, "M", "xii"), - (0x217C, "M", "l"), - (0x217D, "M", "c"), - (0x217E, "M", "d"), - (0x217F, "M", "m"), - (0x2180, "V"), - (0x2183, "M", "ↄ"), - (0x2184, "V"), - (0x2189, "M", "0⁄3"), - (0x218A, "V"), - (0x218C, "X"), - (0x2190, "V"), - (0x222C, "M", "∫∫"), - (0x222D, "M", "∫∫∫"), - (0x222E, "V"), - (0x222F, "M", "∮∮"), - (0x2230, "M", "∮∮∮"), - (0x2231, "V"), - (0x2329, "M", "〈"), - (0x232A, "M", "〉"), - (0x232B, "V"), - (0x242A, "X"), - (0x2440, "V"), - (0x244B, "X"), - (0x2460, "M", "1"), - (0x2461, "M", "2"), - (0x2462, "M", "3"), - (0x2463, "M", "4"), - (0x2464, "M", "5"), - (0x2465, "M", "6"), - (0x2466, "M", "7"), - (0x2467, "M", "8"), - (0x2468, "M", "9"), - (0x2469, "M", "10"), - (0x246A, "M", "11"), - (0x246B, "M", "12"), - (0x246C, "M", "13"), - (0x246D, "M", "14"), - (0x246E, "M", "15"), - (0x246F, "M", "16"), - (0x2470, "M", "17"), - (0x2471, "M", "18"), - (0x2472, "M", "19"), - (0x2473, "M", "20"), - (0x2474, "M", "(1)"), - (0x2475, "M", "(2)"), - (0x2476, "M", "(3)"), - (0x2477, "M", "(4)"), - (0x2478, "M", "(5)"), - (0x2479, "M", "(6)"), - (0x247A, "M", "(7)"), - (0x247B, "M", "(8)"), - (0x247C, "M", "(9)"), - (0x247D, "M", "(10)"), - (0x247E, "M", "(11)"), - (0x247F, "M", "(12)"), - (0x2480, "M", "(13)"), - (0x2481, "M", "(14)"), - (0x2482, "M", "(15)"), - (0x2483, "M", "(16)"), - (0x2484, "M", "(17)"), - (0x2485, "M", "(18)"), - (0x2486, "M", "(19)"), - (0x2487, "M", "(20)"), - (0x2488, "X"), - (0x249C, "M", "(a)"), - (0x249D, "M", "(b)"), - (0x249E, "M", "(c)"), - (0x249F, "M", "(d)"), - ] - - -def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x24A0, "M", "(e)"), - (0x24A1, "M", "(f)"), - (0x24A2, "M", "(g)"), - (0x24A3, "M", "(h)"), - (0x24A4, "M", "(i)"), - (0x24A5, "M", "(j)"), - (0x24A6, "M", "(k)"), - (0x24A7, "M", "(l)"), - (0x24A8, "M", "(m)"), - (0x24A9, "M", "(n)"), - (0x24AA, "M", "(o)"), - (0x24AB, "M", "(p)"), - (0x24AC, "M", "(q)"), - (0x24AD, "M", "(r)"), - (0x24AE, "M", "(s)"), - (0x24AF, "M", "(t)"), - (0x24B0, "M", "(u)"), - (0x24B1, "M", "(v)"), - (0x24B2, "M", "(w)"), - (0x24B3, "M", "(x)"), - (0x24B4, "M", "(y)"), - (0x24B5, "M", "(z)"), - (0x24B6, "M", "a"), - (0x24B7, "M", "b"), - (0x24B8, "M", "c"), - (0x24B9, "M", "d"), - (0x24BA, "M", "e"), - (0x24BB, "M", "f"), - (0x24BC, "M", "g"), - (0x24BD, "M", "h"), - (0x24BE, "M", "i"), - (0x24BF, "M", "j"), - (0x24C0, "M", "k"), - (0x24C1, "M", "l"), - (0x24C2, "M", "m"), - (0x24C3, "M", "n"), - (0x24C4, "M", "o"), - (0x24C5, "M", "p"), - (0x24C6, "M", "q"), - (0x24C7, "M", "r"), - (0x24C8, "M", "s"), - (0x24C9, "M", "t"), - (0x24CA, "M", "u"), - (0x24CB, "M", "v"), - (0x24CC, "M", "w"), - (0x24CD, "M", "x"), - (0x24CE, "M", "y"), - (0x24CF, "M", "z"), - (0x24D0, "M", "a"), - (0x24D1, "M", "b"), - (0x24D2, "M", "c"), - (0x24D3, "M", "d"), - (0x24D4, "M", "e"), - (0x24D5, "M", "f"), - (0x24D6, "M", "g"), - (0x24D7, "M", "h"), - (0x24D8, "M", "i"), - (0x24D9, "M", "j"), - (0x24DA, "M", "k"), - (0x24DB, "M", "l"), - (0x24DC, "M", "m"), - (0x24DD, "M", "n"), - (0x24DE, "M", "o"), - (0x24DF, "M", "p"), - (0x24E0, "M", "q"), - (0x24E1, "M", "r"), - (0x24E2, "M", "s"), - (0x24E3, "M", "t"), - (0x24E4, "M", "u"), - (0x24E5, "M", "v"), - (0x24E6, "M", "w"), - (0x24E7, "M", "x"), - (0x24E8, "M", "y"), - (0x24E9, "M", "z"), - (0x24EA, "M", "0"), - (0x24EB, "V"), - (0x2A0C, "M", "∫∫∫∫"), - (0x2A0D, "V"), - (0x2A74, "M", "::="), - (0x2A75, "M", "=="), - (0x2A76, "M", "==="), - (0x2A77, "V"), - (0x2ADC, "M", "⫝̸"), - (0x2ADD, "V"), - (0x2B74, "X"), - (0x2B76, "V"), - (0x2B96, "X"), - (0x2B97, "V"), - (0x2C00, "M", "ⰰ"), - (0x2C01, "M", "ⰱ"), - (0x2C02, "M", "ⰲ"), - (0x2C03, "M", "ⰳ"), - (0x2C04, "M", "ⰴ"), - (0x2C05, "M", "ⰵ"), - (0x2C06, "M", "ⰶ"), - (0x2C07, "M", "ⰷ"), - (0x2C08, "M", "ⰸ"), - (0x2C09, "M", "ⰹ"), - (0x2C0A, "M", "ⰺ"), - (0x2C0B, "M", "ⰻ"), - ] - - -def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2C0C, "M", "ⰼ"), - (0x2C0D, "M", "ⰽ"), - (0x2C0E, "M", "ⰾ"), - (0x2C0F, "M", "ⰿ"), - (0x2C10, "M", "ⱀ"), - (0x2C11, "M", "ⱁ"), - (0x2C12, "M", "ⱂ"), - (0x2C13, "M", "ⱃ"), - (0x2C14, "M", "ⱄ"), - (0x2C15, "M", "ⱅ"), - (0x2C16, "M", "ⱆ"), - (0x2C17, "M", "ⱇ"), - (0x2C18, "M", "ⱈ"), - (0x2C19, "M", "ⱉ"), - (0x2C1A, "M", "ⱊ"), - (0x2C1B, "M", "ⱋ"), - (0x2C1C, "M", "ⱌ"), - (0x2C1D, "M", "ⱍ"), - (0x2C1E, "M", "ⱎ"), - (0x2C1F, "M", "ⱏ"), - (0x2C20, "M", "ⱐ"), - (0x2C21, "M", "ⱑ"), - (0x2C22, "M", "ⱒ"), - (0x2C23, "M", "ⱓ"), - (0x2C24, "M", "ⱔ"), - (0x2C25, "M", "ⱕ"), - (0x2C26, "M", "ⱖ"), - (0x2C27, "M", "ⱗ"), - (0x2C28, "M", "ⱘ"), - (0x2C29, "M", "ⱙ"), - (0x2C2A, "M", "ⱚ"), - (0x2C2B, "M", "ⱛ"), - (0x2C2C, "M", "ⱜ"), - (0x2C2D, "M", "ⱝ"), - (0x2C2E, "M", "ⱞ"), - (0x2C2F, "M", "ⱟ"), - (0x2C30, "V"), - (0x2C60, "M", "ⱡ"), - (0x2C61, "V"), - (0x2C62, "M", "ɫ"), - (0x2C63, "M", "ᵽ"), - (0x2C64, "M", "ɽ"), - (0x2C65, "V"), - (0x2C67, "M", "ⱨ"), - (0x2C68, "V"), - (0x2C69, "M", "ⱪ"), - (0x2C6A, "V"), - (0x2C6B, "M", "ⱬ"), - (0x2C6C, "V"), - (0x2C6D, "M", "ɑ"), - (0x2C6E, "M", "ɱ"), - (0x2C6F, "M", "ɐ"), - (0x2C70, "M", "ɒ"), - (0x2C71, "V"), - (0x2C72, "M", "ⱳ"), - (0x2C73, "V"), - (0x2C75, "M", "ⱶ"), - (0x2C76, "V"), - (0x2C7C, "M", "j"), - (0x2C7D, "M", "v"), - (0x2C7E, "M", "ȿ"), - (0x2C7F, "M", "ɀ"), - (0x2C80, "M", "ⲁ"), - (0x2C81, "V"), - (0x2C82, "M", "ⲃ"), - (0x2C83, "V"), - (0x2C84, "M", "ⲅ"), - (0x2C85, "V"), - (0x2C86, "M", "ⲇ"), - (0x2C87, "V"), - (0x2C88, "M", "ⲉ"), - (0x2C89, "V"), - (0x2C8A, "M", "ⲋ"), - (0x2C8B, "V"), - (0x2C8C, "M", "ⲍ"), - (0x2C8D, "V"), - (0x2C8E, "M", "ⲏ"), - (0x2C8F, "V"), - (0x2C90, "M", "ⲑ"), - (0x2C91, "V"), - (0x2C92, "M", "ⲓ"), - (0x2C93, "V"), - (0x2C94, "M", "ⲕ"), - (0x2C95, "V"), - (0x2C96, "M", "ⲗ"), - (0x2C97, "V"), - (0x2C98, "M", "ⲙ"), - (0x2C99, "V"), - (0x2C9A, "M", "ⲛ"), - (0x2C9B, "V"), - (0x2C9C, "M", "ⲝ"), - (0x2C9D, "V"), - (0x2C9E, "M", "ⲟ"), - (0x2C9F, "V"), - (0x2CA0, "M", "ⲡ"), - (0x2CA1, "V"), - (0x2CA2, "M", "ⲣ"), - (0x2CA3, "V"), - (0x2CA4, "M", "ⲥ"), - (0x2CA5, "V"), - ] - - -def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2CA6, "M", "ⲧ"), - (0x2CA7, "V"), - (0x2CA8, "M", "ⲩ"), - (0x2CA9, "V"), - (0x2CAA, "M", "ⲫ"), - (0x2CAB, "V"), - (0x2CAC, "M", "ⲭ"), - (0x2CAD, "V"), - (0x2CAE, "M", "ⲯ"), - (0x2CAF, "V"), - (0x2CB0, "M", "ⲱ"), - (0x2CB1, "V"), - (0x2CB2, "M", "ⲳ"), - (0x2CB3, "V"), - (0x2CB4, "M", "ⲵ"), - (0x2CB5, "V"), - (0x2CB6, "M", "ⲷ"), - (0x2CB7, "V"), - (0x2CB8, "M", "ⲹ"), - (0x2CB9, "V"), - (0x2CBA, "M", "ⲻ"), - (0x2CBB, "V"), - (0x2CBC, "M", "ⲽ"), - (0x2CBD, "V"), - (0x2CBE, "M", "ⲿ"), - (0x2CBF, "V"), - (0x2CC0, "M", "ⳁ"), - (0x2CC1, "V"), - (0x2CC2, "M", "ⳃ"), - (0x2CC3, "V"), - (0x2CC4, "M", "ⳅ"), - (0x2CC5, "V"), - (0x2CC6, "M", "ⳇ"), - (0x2CC7, "V"), - (0x2CC8, "M", "ⳉ"), - (0x2CC9, "V"), - (0x2CCA, "M", "ⳋ"), - (0x2CCB, "V"), - (0x2CCC, "M", "ⳍ"), - (0x2CCD, "V"), - (0x2CCE, "M", "ⳏ"), - (0x2CCF, "V"), - (0x2CD0, "M", "ⳑ"), - (0x2CD1, "V"), - (0x2CD2, "M", "ⳓ"), - (0x2CD3, "V"), - (0x2CD4, "M", "ⳕ"), - (0x2CD5, "V"), - (0x2CD6, "M", "ⳗ"), - (0x2CD7, "V"), - (0x2CD8, "M", "ⳙ"), - (0x2CD9, "V"), - (0x2CDA, "M", "ⳛ"), - (0x2CDB, "V"), - (0x2CDC, "M", "ⳝ"), - (0x2CDD, "V"), - (0x2CDE, "M", "ⳟ"), - (0x2CDF, "V"), - (0x2CE0, "M", "ⳡ"), - (0x2CE1, "V"), - (0x2CE2, "M", "ⳣ"), - (0x2CE3, "V"), - (0x2CEB, "M", "ⳬ"), - (0x2CEC, "V"), - (0x2CED, "M", "ⳮ"), - (0x2CEE, "V"), - (0x2CF2, "M", "ⳳ"), - (0x2CF3, "V"), - (0x2CF4, "X"), - (0x2CF9, "V"), - (0x2D26, "X"), - (0x2D27, "V"), - (0x2D28, "X"), - (0x2D2D, "V"), - (0x2D2E, "X"), - (0x2D30, "V"), - (0x2D68, "X"), - (0x2D6F, "M", "ⵡ"), - (0x2D70, "V"), - (0x2D71, "X"), - (0x2D7F, "V"), - (0x2D97, "X"), - (0x2DA0, "V"), - (0x2DA7, "X"), - (0x2DA8, "V"), - (0x2DAF, "X"), - (0x2DB0, "V"), - (0x2DB7, "X"), - (0x2DB8, "V"), - (0x2DBF, "X"), - (0x2DC0, "V"), - (0x2DC7, "X"), - (0x2DC8, "V"), - (0x2DCF, "X"), - (0x2DD0, "V"), - (0x2DD7, "X"), - (0x2DD8, "V"), - (0x2DDF, "X"), - (0x2DE0, "V"), - (0x2E5E, "X"), - ] - - -def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2E80, "V"), - (0x2E9A, "X"), - (0x2E9B, "V"), - (0x2E9F, "M", "母"), - (0x2EA0, "V"), - (0x2EF3, "M", "龟"), - (0x2EF4, "X"), - (0x2F00, "M", "一"), - (0x2F01, "M", "丨"), - (0x2F02, "M", "丶"), - (0x2F03, "M", "丿"), - (0x2F04, "M", "乙"), - (0x2F05, "M", "亅"), - (0x2F06, "M", "二"), - (0x2F07, "M", "亠"), - (0x2F08, "M", "人"), - (0x2F09, "M", "儿"), - (0x2F0A, "M", "入"), - (0x2F0B, "M", "八"), - (0x2F0C, "M", "冂"), - (0x2F0D, "M", "冖"), - (0x2F0E, "M", "冫"), - (0x2F0F, "M", "几"), - (0x2F10, "M", "凵"), - (0x2F11, "M", "刀"), - (0x2F12, "M", "力"), - (0x2F13, "M", "勹"), - (0x2F14, "M", "匕"), - (0x2F15, "M", "匚"), - (0x2F16, "M", "匸"), - (0x2F17, "M", "十"), - (0x2F18, "M", "卜"), - (0x2F19, "M", "卩"), - (0x2F1A, "M", "厂"), - (0x2F1B, "M", "厶"), - (0x2F1C, "M", "又"), - (0x2F1D, "M", "口"), - (0x2F1E, "M", "囗"), - (0x2F1F, "M", "土"), - (0x2F20, "M", "士"), - (0x2F21, "M", "夂"), - (0x2F22, "M", "夊"), - (0x2F23, "M", "夕"), - (0x2F24, "M", "大"), - (0x2F25, "M", "女"), - (0x2F26, "M", "子"), - (0x2F27, "M", "宀"), - (0x2F28, "M", "寸"), - (0x2F29, "M", "小"), - (0x2F2A, "M", "尢"), - (0x2F2B, "M", "尸"), - (0x2F2C, "M", "屮"), - (0x2F2D, "M", "山"), - (0x2F2E, "M", "巛"), - (0x2F2F, "M", "工"), - (0x2F30, "M", "己"), - (0x2F31, "M", "巾"), - (0x2F32, "M", "干"), - (0x2F33, "M", "幺"), - (0x2F34, "M", "广"), - (0x2F35, "M", "廴"), - (0x2F36, "M", "廾"), - (0x2F37, "M", "弋"), - (0x2F38, "M", "弓"), - (0x2F39, "M", "彐"), - (0x2F3A, "M", "彡"), - (0x2F3B, "M", "彳"), - (0x2F3C, "M", "心"), - (0x2F3D, "M", "戈"), - (0x2F3E, "M", "戶"), - (0x2F3F, "M", "手"), - (0x2F40, "M", "支"), - (0x2F41, "M", "攴"), - (0x2F42, "M", "文"), - (0x2F43, "M", "斗"), - (0x2F44, "M", "斤"), - (0x2F45, "M", "方"), - (0x2F46, "M", "无"), - (0x2F47, "M", "日"), - (0x2F48, "M", "曰"), - (0x2F49, "M", "月"), - (0x2F4A, "M", "木"), - (0x2F4B, "M", "欠"), - (0x2F4C, "M", "止"), - (0x2F4D, "M", "歹"), - (0x2F4E, "M", "殳"), - (0x2F4F, "M", "毋"), - (0x2F50, "M", "比"), - (0x2F51, "M", "毛"), - (0x2F52, "M", "氏"), - (0x2F53, "M", "气"), - (0x2F54, "M", "水"), - (0x2F55, "M", "火"), - (0x2F56, "M", "爪"), - (0x2F57, "M", "父"), - (0x2F58, "M", "爻"), - (0x2F59, "M", "爿"), - (0x2F5A, "M", "片"), - (0x2F5B, "M", "牙"), - (0x2F5C, "M", "牛"), - ] - - -def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2F5D, "M", "犬"), - (0x2F5E, "M", "玄"), - (0x2F5F, "M", "玉"), - (0x2F60, "M", "瓜"), - (0x2F61, "M", "瓦"), - (0x2F62, "M", "甘"), - (0x2F63, "M", "生"), - (0x2F64, "M", "用"), - (0x2F65, "M", "田"), - (0x2F66, "M", "疋"), - (0x2F67, "M", "疒"), - (0x2F68, "M", "癶"), - (0x2F69, "M", "白"), - (0x2F6A, "M", "皮"), - (0x2F6B, "M", "皿"), - (0x2F6C, "M", "目"), - (0x2F6D, "M", "矛"), - (0x2F6E, "M", "矢"), - (0x2F6F, "M", "石"), - (0x2F70, "M", "示"), - (0x2F71, "M", "禸"), - (0x2F72, "M", "禾"), - (0x2F73, "M", "穴"), - (0x2F74, "M", "立"), - (0x2F75, "M", "竹"), - (0x2F76, "M", "米"), - (0x2F77, "M", "糸"), - (0x2F78, "M", "缶"), - (0x2F79, "M", "网"), - (0x2F7A, "M", "羊"), - (0x2F7B, "M", "羽"), - (0x2F7C, "M", "老"), - (0x2F7D, "M", "而"), - (0x2F7E, "M", "耒"), - (0x2F7F, "M", "耳"), - (0x2F80, "M", "聿"), - (0x2F81, "M", "肉"), - (0x2F82, "M", "臣"), - (0x2F83, "M", "自"), - (0x2F84, "M", "至"), - (0x2F85, "M", "臼"), - (0x2F86, "M", "舌"), - (0x2F87, "M", "舛"), - (0x2F88, "M", "舟"), - (0x2F89, "M", "艮"), - (0x2F8A, "M", "色"), - (0x2F8B, "M", "艸"), - (0x2F8C, "M", "虍"), - (0x2F8D, "M", "虫"), - (0x2F8E, "M", "血"), - (0x2F8F, "M", "行"), - (0x2F90, "M", "衣"), - (0x2F91, "M", "襾"), - (0x2F92, "M", "見"), - (0x2F93, "M", "角"), - (0x2F94, "M", "言"), - (0x2F95, "M", "谷"), - (0x2F96, "M", "豆"), - (0x2F97, "M", "豕"), - (0x2F98, "M", "豸"), - (0x2F99, "M", "貝"), - (0x2F9A, "M", "赤"), - (0x2F9B, "M", "走"), - (0x2F9C, "M", "足"), - (0x2F9D, "M", "身"), - (0x2F9E, "M", "車"), - (0x2F9F, "M", "辛"), - (0x2FA0, "M", "辰"), - (0x2FA1, "M", "辵"), - (0x2FA2, "M", "邑"), - (0x2FA3, "M", "酉"), - (0x2FA4, "M", "釆"), - (0x2FA5, "M", "里"), - (0x2FA6, "M", "金"), - (0x2FA7, "M", "長"), - (0x2FA8, "M", "門"), - (0x2FA9, "M", "阜"), - (0x2FAA, "M", "隶"), - (0x2FAB, "M", "隹"), - (0x2FAC, "M", "雨"), - (0x2FAD, "M", "靑"), - (0x2FAE, "M", "非"), - (0x2FAF, "M", "面"), - (0x2FB0, "M", "革"), - (0x2FB1, "M", "韋"), - (0x2FB2, "M", "韭"), - (0x2FB3, "M", "音"), - (0x2FB4, "M", "頁"), - (0x2FB5, "M", "風"), - (0x2FB6, "M", "飛"), - (0x2FB7, "M", "食"), - (0x2FB8, "M", "首"), - (0x2FB9, "M", "香"), - (0x2FBA, "M", "馬"), - (0x2FBB, "M", "骨"), - (0x2FBC, "M", "高"), - (0x2FBD, "M", "髟"), - (0x2FBE, "M", "鬥"), - (0x2FBF, "M", "鬯"), - (0x2FC0, "M", "鬲"), - ] - - -def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2FC1, "M", "鬼"), - (0x2FC2, "M", "魚"), - (0x2FC3, "M", "鳥"), - (0x2FC4, "M", "鹵"), - (0x2FC5, "M", "鹿"), - (0x2FC6, "M", "麥"), - (0x2FC7, "M", "麻"), - (0x2FC8, "M", "黃"), - (0x2FC9, "M", "黍"), - (0x2FCA, "M", "黑"), - (0x2FCB, "M", "黹"), - (0x2FCC, "M", "黽"), - (0x2FCD, "M", "鼎"), - (0x2FCE, "M", "鼓"), - (0x2FCF, "M", "鼠"), - (0x2FD0, "M", "鼻"), - (0x2FD1, "M", "齊"), - (0x2FD2, "M", "齒"), - (0x2FD3, "M", "龍"), - (0x2FD4, "M", "龜"), - (0x2FD5, "M", "龠"), - (0x2FD6, "X"), - (0x3000, "M", " "), - (0x3001, "V"), - (0x3002, "M", "."), - (0x3003, "V"), - (0x3036, "M", "〒"), - (0x3037, "V"), - (0x3038, "M", "十"), - (0x3039, "M", "卄"), - (0x303A, "M", "卅"), - (0x303B, "V"), - (0x3040, "X"), - (0x3041, "V"), - (0x3097, "X"), - (0x3099, "V"), - (0x309B, "M", " ゙"), - (0x309C, "M", " ゚"), - (0x309D, "V"), - (0x309F, "M", "より"), - (0x30A0, "V"), - (0x30FF, "M", "コト"), - (0x3100, "X"), - (0x3105, "V"), - (0x3130, "X"), - (0x3131, "M", "ᄀ"), - (0x3132, "M", "ᄁ"), - (0x3133, "M", "ᆪ"), - (0x3134, "M", "ᄂ"), - (0x3135, "M", "ᆬ"), - (0x3136, "M", "ᆭ"), - (0x3137, "M", "ᄃ"), - (0x3138, "M", "ᄄ"), - (0x3139, "M", "ᄅ"), - (0x313A, "M", "ᆰ"), - (0x313B, "M", "ᆱ"), - (0x313C, "M", "ᆲ"), - (0x313D, "M", "ᆳ"), - (0x313E, "M", "ᆴ"), - (0x313F, "M", "ᆵ"), - (0x3140, "M", "ᄚ"), - (0x3141, "M", "ᄆ"), - (0x3142, "M", "ᄇ"), - (0x3143, "M", "ᄈ"), - (0x3144, "M", "ᄡ"), - (0x3145, "M", "ᄉ"), - (0x3146, "M", "ᄊ"), - (0x3147, "M", "ᄋ"), - (0x3148, "M", "ᄌ"), - (0x3149, "M", "ᄍ"), - (0x314A, "M", "ᄎ"), - (0x314B, "M", "ᄏ"), - (0x314C, "M", "ᄐ"), - (0x314D, "M", "ᄑ"), - (0x314E, "M", "ᄒ"), - (0x314F, "M", "ᅡ"), - (0x3150, "M", "ᅢ"), - (0x3151, "M", "ᅣ"), - (0x3152, "M", "ᅤ"), - (0x3153, "M", "ᅥ"), - (0x3154, "M", "ᅦ"), - (0x3155, "M", "ᅧ"), - (0x3156, "M", "ᅨ"), - (0x3157, "M", "ᅩ"), - (0x3158, "M", "ᅪ"), - (0x3159, "M", "ᅫ"), - (0x315A, "M", "ᅬ"), - (0x315B, "M", "ᅭ"), - (0x315C, "M", "ᅮ"), - (0x315D, "M", "ᅯ"), - (0x315E, "M", "ᅰ"), - (0x315F, "M", "ᅱ"), - (0x3160, "M", "ᅲ"), - (0x3161, "M", "ᅳ"), - (0x3162, "M", "ᅴ"), - (0x3163, "M", "ᅵ"), - (0x3164, "I"), - (0x3165, "M", "ᄔ"), - (0x3166, "M", "ᄕ"), - (0x3167, "M", "ᇇ"), - ] - - -def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x3168, "M", "ᇈ"), - (0x3169, "M", "ᇌ"), - (0x316A, "M", "ᇎ"), - (0x316B, "M", "ᇓ"), - (0x316C, "M", "ᇗ"), - (0x316D, "M", "ᇙ"), - (0x316E, "M", "ᄜ"), - (0x316F, "M", "ᇝ"), - (0x3170, "M", "ᇟ"), - (0x3171, "M", "ᄝ"), - (0x3172, "M", "ᄞ"), - (0x3173, "M", "ᄠ"), - (0x3174, "M", "ᄢ"), - (0x3175, "M", "ᄣ"), - (0x3176, "M", "ᄧ"), - (0x3177, "M", "ᄩ"), - (0x3178, "M", "ᄫ"), - (0x3179, "M", "ᄬ"), - (0x317A, "M", "ᄭ"), - (0x317B, "M", "ᄮ"), - (0x317C, "M", "ᄯ"), - (0x317D, "M", "ᄲ"), - (0x317E, "M", "ᄶ"), - (0x317F, "M", "ᅀ"), - (0x3180, "M", "ᅇ"), - (0x3181, "M", "ᅌ"), - (0x3182, "M", "ᇱ"), - (0x3183, "M", "ᇲ"), - (0x3184, "M", "ᅗ"), - (0x3185, "M", "ᅘ"), - (0x3186, "M", "ᅙ"), - (0x3187, "M", "ᆄ"), - (0x3188, "M", "ᆅ"), - (0x3189, "M", "ᆈ"), - (0x318A, "M", "ᆑ"), - (0x318B, "M", "ᆒ"), - (0x318C, "M", "ᆔ"), - (0x318D, "M", "ᆞ"), - (0x318E, "M", "ᆡ"), - (0x318F, "X"), - (0x3190, "V"), - (0x3192, "M", "一"), - (0x3193, "M", "二"), - (0x3194, "M", "三"), - (0x3195, "M", "四"), - (0x3196, "M", "上"), - (0x3197, "M", "中"), - (0x3198, "M", "下"), - (0x3199, "M", "甲"), - (0x319A, "M", "乙"), - (0x319B, "M", "丙"), - (0x319C, "M", "丁"), - (0x319D, "M", "天"), - (0x319E, "M", "地"), - (0x319F, "M", "人"), - (0x31A0, "V"), - (0x31E6, "X"), - (0x31F0, "V"), - (0x3200, "M", "(ᄀ)"), - (0x3201, "M", "(ᄂ)"), - (0x3202, "M", "(ᄃ)"), - (0x3203, "M", "(ᄅ)"), - (0x3204, "M", "(ᄆ)"), - (0x3205, "M", "(ᄇ)"), - (0x3206, "M", "(ᄉ)"), - (0x3207, "M", "(ᄋ)"), - (0x3208, "M", "(ᄌ)"), - (0x3209, "M", "(ᄎ)"), - (0x320A, "M", "(ᄏ)"), - (0x320B, "M", "(ᄐ)"), - (0x320C, "M", "(ᄑ)"), - (0x320D, "M", "(ᄒ)"), - (0x320E, "M", "(가)"), - (0x320F, "M", "(나)"), - (0x3210, "M", "(다)"), - (0x3211, "M", "(라)"), - (0x3212, "M", "(마)"), - (0x3213, "M", "(바)"), - (0x3214, "M", "(사)"), - (0x3215, "M", "(아)"), - (0x3216, "M", "(자)"), - (0x3217, "M", "(차)"), - (0x3218, "M", "(카)"), - (0x3219, "M", "(타)"), - (0x321A, "M", "(파)"), - (0x321B, "M", "(하)"), - (0x321C, "M", "(주)"), - (0x321D, "M", "(오전)"), - (0x321E, "M", "(오후)"), - (0x321F, "X"), - (0x3220, "M", "(一)"), - (0x3221, "M", "(二)"), - (0x3222, "M", "(三)"), - (0x3223, "M", "(四)"), - (0x3224, "M", "(五)"), - (0x3225, "M", "(六)"), - (0x3226, "M", "(七)"), - (0x3227, "M", "(八)"), - (0x3228, "M", "(九)"), - (0x3229, "M", "(十)"), - ] - - -def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x322A, "M", "(月)"), - (0x322B, "M", "(火)"), - (0x322C, "M", "(水)"), - (0x322D, "M", "(木)"), - (0x322E, "M", "(金)"), - (0x322F, "M", "(土)"), - (0x3230, "M", "(日)"), - (0x3231, "M", "(株)"), - (0x3232, "M", "(有)"), - (0x3233, "M", "(社)"), - (0x3234, "M", "(名)"), - (0x3235, "M", "(特)"), - (0x3236, "M", "(財)"), - (0x3237, "M", "(祝)"), - (0x3238, "M", "(労)"), - (0x3239, "M", "(代)"), - (0x323A, "M", "(呼)"), - (0x323B, "M", "(学)"), - (0x323C, "M", "(監)"), - (0x323D, "M", "(企)"), - (0x323E, "M", "(資)"), - (0x323F, "M", "(協)"), - (0x3240, "M", "(祭)"), - (0x3241, "M", "(休)"), - (0x3242, "M", "(自)"), - (0x3243, "M", "(至)"), - (0x3244, "M", "問"), - (0x3245, "M", "幼"), - (0x3246, "M", "文"), - (0x3247, "M", "箏"), - (0x3248, "V"), - (0x3250, "M", "pte"), - (0x3251, "M", "21"), - (0x3252, "M", "22"), - (0x3253, "M", "23"), - (0x3254, "M", "24"), - (0x3255, "M", "25"), - (0x3256, "M", "26"), - (0x3257, "M", "27"), - (0x3258, "M", "28"), - (0x3259, "M", "29"), - (0x325A, "M", "30"), - (0x325B, "M", "31"), - (0x325C, "M", "32"), - (0x325D, "M", "33"), - (0x325E, "M", "34"), - (0x325F, "M", "35"), - (0x3260, "M", "ᄀ"), - (0x3261, "M", "ᄂ"), - (0x3262, "M", "ᄃ"), - (0x3263, "M", "ᄅ"), - (0x3264, "M", "ᄆ"), - (0x3265, "M", "ᄇ"), - (0x3266, "M", "ᄉ"), - (0x3267, "M", "ᄋ"), - (0x3268, "M", "ᄌ"), - (0x3269, "M", "ᄎ"), - (0x326A, "M", "ᄏ"), - (0x326B, "M", "ᄐ"), - (0x326C, "M", "ᄑ"), - (0x326D, "M", "ᄒ"), - (0x326E, "M", "가"), - (0x326F, "M", "나"), - (0x3270, "M", "다"), - (0x3271, "M", "라"), - (0x3272, "M", "마"), - (0x3273, "M", "바"), - (0x3274, "M", "사"), - (0x3275, "M", "아"), - (0x3276, "M", "자"), - (0x3277, "M", "차"), - (0x3278, "M", "카"), - (0x3279, "M", "타"), - (0x327A, "M", "파"), - (0x327B, "M", "하"), - (0x327C, "M", "참고"), - (0x327D, "M", "주의"), - (0x327E, "M", "우"), - (0x327F, "V"), - (0x3280, "M", "一"), - (0x3281, "M", "二"), - (0x3282, "M", "三"), - (0x3283, "M", "四"), - (0x3284, "M", "五"), - (0x3285, "M", "六"), - (0x3286, "M", "七"), - (0x3287, "M", "八"), - (0x3288, "M", "九"), - (0x3289, "M", "十"), - (0x328A, "M", "月"), - (0x328B, "M", "火"), - (0x328C, "M", "水"), - (0x328D, "M", "木"), - (0x328E, "M", "金"), - (0x328F, "M", "土"), - (0x3290, "M", "日"), - (0x3291, "M", "株"), - (0x3292, "M", "有"), - (0x3293, "M", "社"), - (0x3294, "M", "名"), - ] - - -def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x3295, "M", "特"), - (0x3296, "M", "財"), - (0x3297, "M", "祝"), - (0x3298, "M", "労"), - (0x3299, "M", "秘"), - (0x329A, "M", "男"), - (0x329B, "M", "女"), - (0x329C, "M", "適"), - (0x329D, "M", "優"), - (0x329E, "M", "印"), - (0x329F, "M", "注"), - (0x32A0, "M", "項"), - (0x32A1, "M", "休"), - (0x32A2, "M", "写"), - (0x32A3, "M", "正"), - (0x32A4, "M", "上"), - (0x32A5, "M", "中"), - (0x32A6, "M", "下"), - (0x32A7, "M", "左"), - (0x32A8, "M", "右"), - (0x32A9, "M", "医"), - (0x32AA, "M", "宗"), - (0x32AB, "M", "学"), - (0x32AC, "M", "監"), - (0x32AD, "M", "企"), - (0x32AE, "M", "資"), - (0x32AF, "M", "協"), - (0x32B0, "M", "夜"), - (0x32B1, "M", "36"), - (0x32B2, "M", "37"), - (0x32B3, "M", "38"), - (0x32B4, "M", "39"), - (0x32B5, "M", "40"), - (0x32B6, "M", "41"), - (0x32B7, "M", "42"), - (0x32B8, "M", "43"), - (0x32B9, "M", "44"), - (0x32BA, "M", "45"), - (0x32BB, "M", "46"), - (0x32BC, "M", "47"), - (0x32BD, "M", "48"), - (0x32BE, "M", "49"), - (0x32BF, "M", "50"), - (0x32C0, "M", "1月"), - (0x32C1, "M", "2月"), - (0x32C2, "M", "3月"), - (0x32C3, "M", "4月"), - (0x32C4, "M", "5月"), - (0x32C5, "M", "6月"), - (0x32C6, "M", "7月"), - (0x32C7, "M", "8月"), - (0x32C8, "M", "9月"), - (0x32C9, "M", "10月"), - (0x32CA, "M", "11月"), - (0x32CB, "M", "12月"), - (0x32CC, "M", "hg"), - (0x32CD, "M", "erg"), - (0x32CE, "M", "ev"), - (0x32CF, "M", "ltd"), - (0x32D0, "M", "ア"), - (0x32D1, "M", "イ"), - (0x32D2, "M", "ウ"), - (0x32D3, "M", "エ"), - (0x32D4, "M", "オ"), - (0x32D5, "M", "カ"), - (0x32D6, "M", "キ"), - (0x32D7, "M", "ク"), - (0x32D8, "M", "ケ"), - (0x32D9, "M", "コ"), - (0x32DA, "M", "サ"), - (0x32DB, "M", "シ"), - (0x32DC, "M", "ス"), - (0x32DD, "M", "セ"), - (0x32DE, "M", "ソ"), - (0x32DF, "M", "タ"), - (0x32E0, "M", "チ"), - (0x32E1, "M", "ツ"), - (0x32E2, "M", "テ"), - (0x32E3, "M", "ト"), - (0x32E4, "M", "ナ"), - (0x32E5, "M", "ニ"), - (0x32E6, "M", "ヌ"), - (0x32E7, "M", "ネ"), - (0x32E8, "M", "ノ"), - (0x32E9, "M", "ハ"), - (0x32EA, "M", "ヒ"), - (0x32EB, "M", "フ"), - (0x32EC, "M", "ヘ"), - (0x32ED, "M", "ホ"), - (0x32EE, "M", "マ"), - (0x32EF, "M", "ミ"), - (0x32F0, "M", "ム"), - (0x32F1, "M", "メ"), - (0x32F2, "M", "モ"), - (0x32F3, "M", "ヤ"), - (0x32F4, "M", "ユ"), - (0x32F5, "M", "ヨ"), - (0x32F6, "M", "ラ"), - (0x32F7, "M", "リ"), - (0x32F8, "M", "ル"), - ] - - -def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x32F9, "M", "レ"), - (0x32FA, "M", "ロ"), - (0x32FB, "M", "ワ"), - (0x32FC, "M", "ヰ"), - (0x32FD, "M", "ヱ"), - (0x32FE, "M", "ヲ"), - (0x32FF, "M", "令和"), - (0x3300, "M", "アパート"), - (0x3301, "M", "アルファ"), - (0x3302, "M", "アンペア"), - (0x3303, "M", "アール"), - (0x3304, "M", "イニング"), - (0x3305, "M", "インチ"), - (0x3306, "M", "ウォン"), - (0x3307, "M", "エスクード"), - (0x3308, "M", "エーカー"), - (0x3309, "M", "オンス"), - (0x330A, "M", "オーム"), - (0x330B, "M", "カイリ"), - (0x330C, "M", "カラット"), - (0x330D, "M", "カロリー"), - (0x330E, "M", "ガロン"), - (0x330F, "M", "ガンマ"), - (0x3310, "M", "ギガ"), - (0x3311, "M", "ギニー"), - (0x3312, "M", "キュリー"), - (0x3313, "M", "ギルダー"), - (0x3314, "M", "キロ"), - (0x3315, "M", "キログラム"), - (0x3316, "M", "キロメートル"), - (0x3317, "M", "キロワット"), - (0x3318, "M", "グラム"), - (0x3319, "M", "グラムトン"), - (0x331A, "M", "クルゼイロ"), - (0x331B, "M", "クローネ"), - (0x331C, "M", "ケース"), - (0x331D, "M", "コルナ"), - (0x331E, "M", "コーポ"), - (0x331F, "M", "サイクル"), - (0x3320, "M", "サンチーム"), - (0x3321, "M", "シリング"), - (0x3322, "M", "センチ"), - (0x3323, "M", "セント"), - (0x3324, "M", "ダース"), - (0x3325, "M", "デシ"), - (0x3326, "M", "ドル"), - (0x3327, "M", "トン"), - (0x3328, "M", "ナノ"), - (0x3329, "M", "ノット"), - (0x332A, "M", "ハイツ"), - (0x332B, "M", "パーセント"), - (0x332C, "M", "パーツ"), - (0x332D, "M", "バーレル"), - (0x332E, "M", "ピアストル"), - (0x332F, "M", "ピクル"), - (0x3330, "M", "ピコ"), - (0x3331, "M", "ビル"), - (0x3332, "M", "ファラッド"), - (0x3333, "M", "フィート"), - (0x3334, "M", "ブッシェル"), - (0x3335, "M", "フラン"), - (0x3336, "M", "ヘクタール"), - (0x3337, "M", "ペソ"), - (0x3338, "M", "ペニヒ"), - (0x3339, "M", "ヘルツ"), - (0x333A, "M", "ペンス"), - (0x333B, "M", "ページ"), - (0x333C, "M", "ベータ"), - (0x333D, "M", "ポイント"), - (0x333E, "M", "ボルト"), - (0x333F, "M", "ホン"), - (0x3340, "M", "ポンド"), - (0x3341, "M", "ホール"), - (0x3342, "M", "ホーン"), - (0x3343, "M", "マイクロ"), - (0x3344, "M", "マイル"), - (0x3345, "M", "マッハ"), - (0x3346, "M", "マルク"), - (0x3347, "M", "マンション"), - (0x3348, "M", "ミクロン"), - (0x3349, "M", "ミリ"), - (0x334A, "M", "ミリバール"), - (0x334B, "M", "メガ"), - (0x334C, "M", "メガトン"), - (0x334D, "M", "メートル"), - (0x334E, "M", "ヤード"), - (0x334F, "M", "ヤール"), - (0x3350, "M", "ユアン"), - (0x3351, "M", "リットル"), - (0x3352, "M", "リラ"), - (0x3353, "M", "ルピー"), - (0x3354, "M", "ルーブル"), - (0x3355, "M", "レム"), - (0x3356, "M", "レントゲン"), - (0x3357, "M", "ワット"), - (0x3358, "M", "0点"), - (0x3359, "M", "1点"), - (0x335A, "M", "2点"), - (0x335B, "M", "3点"), - (0x335C, "M", "4点"), - ] - - -def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x335D, "M", "5点"), - (0x335E, "M", "6点"), - (0x335F, "M", "7点"), - (0x3360, "M", "8点"), - (0x3361, "M", "9点"), - (0x3362, "M", "10点"), - (0x3363, "M", "11点"), - (0x3364, "M", "12点"), - (0x3365, "M", "13点"), - (0x3366, "M", "14点"), - (0x3367, "M", "15点"), - (0x3368, "M", "16点"), - (0x3369, "M", "17点"), - (0x336A, "M", "18点"), - (0x336B, "M", "19点"), - (0x336C, "M", "20点"), - (0x336D, "M", "21点"), - (0x336E, "M", "22点"), - (0x336F, "M", "23点"), - (0x3370, "M", "24点"), - (0x3371, "M", "hpa"), - (0x3372, "M", "da"), - (0x3373, "M", "au"), - (0x3374, "M", "bar"), - (0x3375, "M", "ov"), - (0x3376, "M", "pc"), - (0x3377, "M", "dm"), - (0x3378, "M", "dm2"), - (0x3379, "M", "dm3"), - (0x337A, "M", "iu"), - (0x337B, "M", "平成"), - (0x337C, "M", "昭和"), - (0x337D, "M", "大正"), - (0x337E, "M", "明治"), - (0x337F, "M", "株式会社"), - (0x3380, "M", "pa"), - (0x3381, "M", "na"), - (0x3382, "M", "μa"), - (0x3383, "M", "ma"), - (0x3384, "M", "ka"), - (0x3385, "M", "kb"), - (0x3386, "M", "mb"), - (0x3387, "M", "gb"), - (0x3388, "M", "cal"), - (0x3389, "M", "kcal"), - (0x338A, "M", "pf"), - (0x338B, "M", "nf"), - (0x338C, "M", "μf"), - (0x338D, "M", "μg"), - (0x338E, "M", "mg"), - (0x338F, "M", "kg"), - (0x3390, "M", "hz"), - (0x3391, "M", "khz"), - (0x3392, "M", "mhz"), - (0x3393, "M", "ghz"), - (0x3394, "M", "thz"), - (0x3395, "M", "μl"), - (0x3396, "M", "ml"), - (0x3397, "M", "dl"), - (0x3398, "M", "kl"), - (0x3399, "M", "fm"), - (0x339A, "M", "nm"), - (0x339B, "M", "μm"), - (0x339C, "M", "mm"), - (0x339D, "M", "cm"), - (0x339E, "M", "km"), - (0x339F, "M", "mm2"), - (0x33A0, "M", "cm2"), - (0x33A1, "M", "m2"), - (0x33A2, "M", "km2"), - (0x33A3, "M", "mm3"), - (0x33A4, "M", "cm3"), - (0x33A5, "M", "m3"), - (0x33A6, "M", "km3"), - (0x33A7, "M", "m∕s"), - (0x33A8, "M", "m∕s2"), - (0x33A9, "M", "pa"), - (0x33AA, "M", "kpa"), - (0x33AB, "M", "mpa"), - (0x33AC, "M", "gpa"), - (0x33AD, "M", "rad"), - (0x33AE, "M", "rad∕s"), - (0x33AF, "M", "rad∕s2"), - (0x33B0, "M", "ps"), - (0x33B1, "M", "ns"), - (0x33B2, "M", "μs"), - (0x33B3, "M", "ms"), - (0x33B4, "M", "pv"), - (0x33B5, "M", "nv"), - (0x33B6, "M", "μv"), - (0x33B7, "M", "mv"), - (0x33B8, "M", "kv"), - (0x33B9, "M", "mv"), - (0x33BA, "M", "pw"), - (0x33BB, "M", "nw"), - (0x33BC, "M", "μw"), - (0x33BD, "M", "mw"), - (0x33BE, "M", "kw"), - (0x33BF, "M", "mw"), - (0x33C0, "M", "kω"), - ] - - -def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x33C1, "M", "mω"), - (0x33C2, "X"), - (0x33C3, "M", "bq"), - (0x33C4, "M", "cc"), - (0x33C5, "M", "cd"), - (0x33C6, "M", "c∕kg"), - (0x33C7, "X"), - (0x33C8, "M", "db"), - (0x33C9, "M", "gy"), - (0x33CA, "M", "ha"), - (0x33CB, "M", "hp"), - (0x33CC, "M", "in"), - (0x33CD, "M", "kk"), - (0x33CE, "M", "km"), - (0x33CF, "M", "kt"), - (0x33D0, "M", "lm"), - (0x33D1, "M", "ln"), - (0x33D2, "M", "log"), - (0x33D3, "M", "lx"), - (0x33D4, "M", "mb"), - (0x33D5, "M", "mil"), - (0x33D6, "M", "mol"), - (0x33D7, "M", "ph"), - (0x33D8, "X"), - (0x33D9, "M", "ppm"), - (0x33DA, "M", "pr"), - (0x33DB, "M", "sr"), - (0x33DC, "M", "sv"), - (0x33DD, "M", "wb"), - (0x33DE, "M", "v∕m"), - (0x33DF, "M", "a∕m"), - (0x33E0, "M", "1日"), - (0x33E1, "M", "2日"), - (0x33E2, "M", "3日"), - (0x33E3, "M", "4日"), - (0x33E4, "M", "5日"), - (0x33E5, "M", "6日"), - (0x33E6, "M", "7日"), - (0x33E7, "M", "8日"), - (0x33E8, "M", "9日"), - (0x33E9, "M", "10日"), - (0x33EA, "M", "11日"), - (0x33EB, "M", "12日"), - (0x33EC, "M", "13日"), - (0x33ED, "M", "14日"), - (0x33EE, "M", "15日"), - (0x33EF, "M", "16日"), - (0x33F0, "M", "17日"), - (0x33F1, "M", "18日"), - (0x33F2, "M", "19日"), - (0x33F3, "M", "20日"), - (0x33F4, "M", "21日"), - (0x33F5, "M", "22日"), - (0x33F6, "M", "23日"), - (0x33F7, "M", "24日"), - (0x33F8, "M", "25日"), - (0x33F9, "M", "26日"), - (0x33FA, "M", "27日"), - (0x33FB, "M", "28日"), - (0x33FC, "M", "29日"), - (0x33FD, "M", "30日"), - (0x33FE, "M", "31日"), - (0x33FF, "M", "gal"), - (0x3400, "V"), - (0xA48D, "X"), - (0xA490, "V"), - (0xA4C7, "X"), - (0xA4D0, "V"), - (0xA62C, "X"), - (0xA640, "M", "ꙁ"), - (0xA641, "V"), - (0xA642, "M", "ꙃ"), - (0xA643, "V"), - (0xA644, "M", "ꙅ"), - (0xA645, "V"), - (0xA646, "M", "ꙇ"), - (0xA647, "V"), - (0xA648, "M", "ꙉ"), - (0xA649, "V"), - (0xA64A, "M", "ꙋ"), - (0xA64B, "V"), - (0xA64C, "M", "ꙍ"), - (0xA64D, "V"), - (0xA64E, "M", "ꙏ"), - (0xA64F, "V"), - (0xA650, "M", "ꙑ"), - (0xA651, "V"), - (0xA652, "M", "ꙓ"), - (0xA653, "V"), - (0xA654, "M", "ꙕ"), - (0xA655, "V"), - (0xA656, "M", "ꙗ"), - (0xA657, "V"), - (0xA658, "M", "ꙙ"), - (0xA659, "V"), - (0xA65A, "M", "ꙛ"), - (0xA65B, "V"), - (0xA65C, "M", "ꙝ"), - (0xA65D, "V"), - (0xA65E, "M", "ꙟ"), - ] - - -def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xA65F, "V"), - (0xA660, "M", "ꙡ"), - (0xA661, "V"), - (0xA662, "M", "ꙣ"), - (0xA663, "V"), - (0xA664, "M", "ꙥ"), - (0xA665, "V"), - (0xA666, "M", "ꙧ"), - (0xA667, "V"), - (0xA668, "M", "ꙩ"), - (0xA669, "V"), - (0xA66A, "M", "ꙫ"), - (0xA66B, "V"), - (0xA66C, "M", "ꙭ"), - (0xA66D, "V"), - (0xA680, "M", "ꚁ"), - (0xA681, "V"), - (0xA682, "M", "ꚃ"), - (0xA683, "V"), - (0xA684, "M", "ꚅ"), - (0xA685, "V"), - (0xA686, "M", "ꚇ"), - (0xA687, "V"), - (0xA688, "M", "ꚉ"), - (0xA689, "V"), - (0xA68A, "M", "ꚋ"), - (0xA68B, "V"), - (0xA68C, "M", "ꚍ"), - (0xA68D, "V"), - (0xA68E, "M", "ꚏ"), - (0xA68F, "V"), - (0xA690, "M", "ꚑ"), - (0xA691, "V"), - (0xA692, "M", "ꚓ"), - (0xA693, "V"), - (0xA694, "M", "ꚕ"), - (0xA695, "V"), - (0xA696, "M", "ꚗ"), - (0xA697, "V"), - (0xA698, "M", "ꚙ"), - (0xA699, "V"), - (0xA69A, "M", "ꚛ"), - (0xA69B, "V"), - (0xA69C, "M", "ъ"), - (0xA69D, "M", "ь"), - (0xA69E, "V"), - (0xA6F8, "X"), - (0xA700, "V"), - (0xA722, "M", "ꜣ"), - (0xA723, "V"), - (0xA724, "M", "ꜥ"), - (0xA725, "V"), - (0xA726, "M", "ꜧ"), - (0xA727, "V"), - (0xA728, "M", "ꜩ"), - (0xA729, "V"), - (0xA72A, "M", "ꜫ"), - (0xA72B, "V"), - (0xA72C, "M", "ꜭ"), - (0xA72D, "V"), - (0xA72E, "M", "ꜯ"), - (0xA72F, "V"), - (0xA732, "M", "ꜳ"), - (0xA733, "V"), - (0xA734, "M", "ꜵ"), - (0xA735, "V"), - (0xA736, "M", "ꜷ"), - (0xA737, "V"), - (0xA738, "M", "ꜹ"), - (0xA739, "V"), - (0xA73A, "M", "ꜻ"), - (0xA73B, "V"), - (0xA73C, "M", "ꜽ"), - (0xA73D, "V"), - (0xA73E, "M", "ꜿ"), - (0xA73F, "V"), - (0xA740, "M", "ꝁ"), - (0xA741, "V"), - (0xA742, "M", "ꝃ"), - (0xA743, "V"), - (0xA744, "M", "ꝅ"), - (0xA745, "V"), - (0xA746, "M", "ꝇ"), - (0xA747, "V"), - (0xA748, "M", "ꝉ"), - (0xA749, "V"), - (0xA74A, "M", "ꝋ"), - (0xA74B, "V"), - (0xA74C, "M", "ꝍ"), - (0xA74D, "V"), - (0xA74E, "M", "ꝏ"), - (0xA74F, "V"), - (0xA750, "M", "ꝑ"), - (0xA751, "V"), - (0xA752, "M", "ꝓ"), - (0xA753, "V"), - (0xA754, "M", "ꝕ"), - (0xA755, "V"), - (0xA756, "M", "ꝗ"), - (0xA757, "V"), - ] - - -def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xA758, "M", "ꝙ"), - (0xA759, "V"), - (0xA75A, "M", "ꝛ"), - (0xA75B, "V"), - (0xA75C, "M", "ꝝ"), - (0xA75D, "V"), - (0xA75E, "M", "ꝟ"), - (0xA75F, "V"), - (0xA760, "M", "ꝡ"), - (0xA761, "V"), - (0xA762, "M", "ꝣ"), - (0xA763, "V"), - (0xA764, "M", "ꝥ"), - (0xA765, "V"), - (0xA766, "M", "ꝧ"), - (0xA767, "V"), - (0xA768, "M", "ꝩ"), - (0xA769, "V"), - (0xA76A, "M", "ꝫ"), - (0xA76B, "V"), - (0xA76C, "M", "ꝭ"), - (0xA76D, "V"), - (0xA76E, "M", "ꝯ"), - (0xA76F, "V"), - (0xA770, "M", "ꝯ"), - (0xA771, "V"), - (0xA779, "M", "ꝺ"), - (0xA77A, "V"), - (0xA77B, "M", "ꝼ"), - (0xA77C, "V"), - (0xA77D, "M", "ᵹ"), - (0xA77E, "M", "ꝿ"), - (0xA77F, "V"), - (0xA780, "M", "ꞁ"), - (0xA781, "V"), - (0xA782, "M", "ꞃ"), - (0xA783, "V"), - (0xA784, "M", "ꞅ"), - (0xA785, "V"), - (0xA786, "M", "ꞇ"), - (0xA787, "V"), - (0xA78B, "M", "ꞌ"), - (0xA78C, "V"), - (0xA78D, "M", "ɥ"), - (0xA78E, "V"), - (0xA790, "M", "ꞑ"), - (0xA791, "V"), - (0xA792, "M", "ꞓ"), - (0xA793, "V"), - (0xA796, "M", "ꞗ"), - (0xA797, "V"), - (0xA798, "M", "ꞙ"), - (0xA799, "V"), - (0xA79A, "M", "ꞛ"), - (0xA79B, "V"), - (0xA79C, "M", "ꞝ"), - (0xA79D, "V"), - (0xA79E, "M", "ꞟ"), - (0xA79F, "V"), - (0xA7A0, "M", "ꞡ"), - (0xA7A1, "V"), - (0xA7A2, "M", "ꞣ"), - (0xA7A3, "V"), - (0xA7A4, "M", "ꞥ"), - (0xA7A5, "V"), - (0xA7A6, "M", "ꞧ"), - (0xA7A7, "V"), - (0xA7A8, "M", "ꞩ"), - (0xA7A9, "V"), - (0xA7AA, "M", "ɦ"), - (0xA7AB, "M", "ɜ"), - (0xA7AC, "M", "ɡ"), - (0xA7AD, "M", "ɬ"), - (0xA7AE, "M", "ɪ"), - (0xA7AF, "V"), - (0xA7B0, "M", "ʞ"), - (0xA7B1, "M", "ʇ"), - (0xA7B2, "M", "ʝ"), - (0xA7B3, "M", "ꭓ"), - (0xA7B4, "M", "ꞵ"), - (0xA7B5, "V"), - (0xA7B6, "M", "ꞷ"), - (0xA7B7, "V"), - (0xA7B8, "M", "ꞹ"), - (0xA7B9, "V"), - (0xA7BA, "M", "ꞻ"), - (0xA7BB, "V"), - (0xA7BC, "M", "ꞽ"), - (0xA7BD, "V"), - (0xA7BE, "M", "ꞿ"), - (0xA7BF, "V"), - (0xA7C0, "M", "ꟁ"), - (0xA7C1, "V"), - (0xA7C2, "M", "ꟃ"), - (0xA7C3, "V"), - (0xA7C4, "M", "ꞔ"), - (0xA7C5, "M", "ʂ"), - (0xA7C6, "M", "ᶎ"), - (0xA7C7, "M", "ꟈ"), - (0xA7C8, "V"), - ] - - -def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xA7C9, "M", "ꟊ"), - (0xA7CA, "V"), - (0xA7CB, "M", "ɤ"), - (0xA7CC, "M", "ꟍ"), - (0xA7CD, "V"), - (0xA7CE, "X"), - (0xA7D0, "M", "ꟑ"), - (0xA7D1, "V"), - (0xA7D2, "X"), - (0xA7D3, "V"), - (0xA7D4, "X"), - (0xA7D5, "V"), - (0xA7D6, "M", "ꟗ"), - (0xA7D7, "V"), - (0xA7D8, "M", "ꟙ"), - (0xA7D9, "V"), - (0xA7DA, "M", "ꟛ"), - (0xA7DB, "V"), - (0xA7DC, "M", "ƛ"), - (0xA7DD, "X"), - (0xA7F2, "M", "c"), - (0xA7F3, "M", "f"), - (0xA7F4, "M", "q"), - (0xA7F5, "M", "ꟶ"), - (0xA7F6, "V"), - (0xA7F8, "M", "ħ"), - (0xA7F9, "M", "œ"), - (0xA7FA, "V"), - (0xA82D, "X"), - (0xA830, "V"), - (0xA83A, "X"), - (0xA840, "V"), - (0xA878, "X"), - (0xA880, "V"), - (0xA8C6, "X"), - (0xA8CE, "V"), - (0xA8DA, "X"), - (0xA8E0, "V"), - (0xA954, "X"), - (0xA95F, "V"), - (0xA97D, "X"), - (0xA980, "V"), - (0xA9CE, "X"), - (0xA9CF, "V"), - (0xA9DA, "X"), - (0xA9DE, "V"), - (0xA9FF, "X"), - (0xAA00, "V"), - (0xAA37, "X"), - (0xAA40, "V"), - (0xAA4E, "X"), - (0xAA50, "V"), - (0xAA5A, "X"), - (0xAA5C, "V"), - (0xAAC3, "X"), - (0xAADB, "V"), - (0xAAF7, "X"), - (0xAB01, "V"), - (0xAB07, "X"), - (0xAB09, "V"), - (0xAB0F, "X"), - (0xAB11, "V"), - (0xAB17, "X"), - (0xAB20, "V"), - (0xAB27, "X"), - (0xAB28, "V"), - (0xAB2F, "X"), - (0xAB30, "V"), - (0xAB5C, "M", "ꜧ"), - (0xAB5D, "M", "ꬷ"), - (0xAB5E, "M", "ɫ"), - (0xAB5F, "M", "ꭒ"), - (0xAB60, "V"), - (0xAB69, "M", "ʍ"), - (0xAB6A, "V"), - (0xAB6C, "X"), - (0xAB70, "M", "Ꭰ"), - (0xAB71, "M", "Ꭱ"), - (0xAB72, "M", "Ꭲ"), - (0xAB73, "M", "Ꭳ"), - (0xAB74, "M", "Ꭴ"), - (0xAB75, "M", "Ꭵ"), - (0xAB76, "M", "Ꭶ"), - (0xAB77, "M", "Ꭷ"), - (0xAB78, "M", "Ꭸ"), - (0xAB79, "M", "Ꭹ"), - (0xAB7A, "M", "Ꭺ"), - (0xAB7B, "M", "Ꭻ"), - (0xAB7C, "M", "Ꭼ"), - (0xAB7D, "M", "Ꭽ"), - (0xAB7E, "M", "Ꭾ"), - (0xAB7F, "M", "Ꭿ"), - (0xAB80, "M", "Ꮀ"), - (0xAB81, "M", "Ꮁ"), - (0xAB82, "M", "Ꮂ"), - (0xAB83, "M", "Ꮃ"), - (0xAB84, "M", "Ꮄ"), - (0xAB85, "M", "Ꮅ"), - (0xAB86, "M", "Ꮆ"), - (0xAB87, "M", "Ꮇ"), - ] - - -def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xAB88, "M", "Ꮈ"), - (0xAB89, "M", "Ꮉ"), - (0xAB8A, "M", "Ꮊ"), - (0xAB8B, "M", "Ꮋ"), - (0xAB8C, "M", "Ꮌ"), - (0xAB8D, "M", "Ꮍ"), - (0xAB8E, "M", "Ꮎ"), - (0xAB8F, "M", "Ꮏ"), - (0xAB90, "M", "Ꮐ"), - (0xAB91, "M", "Ꮑ"), - (0xAB92, "M", "Ꮒ"), - (0xAB93, "M", "Ꮓ"), - (0xAB94, "M", "Ꮔ"), - (0xAB95, "M", "Ꮕ"), - (0xAB96, "M", "Ꮖ"), - (0xAB97, "M", "Ꮗ"), - (0xAB98, "M", "Ꮘ"), - (0xAB99, "M", "Ꮙ"), - (0xAB9A, "M", "Ꮚ"), - (0xAB9B, "M", "Ꮛ"), - (0xAB9C, "M", "Ꮜ"), - (0xAB9D, "M", "Ꮝ"), - (0xAB9E, "M", "Ꮞ"), - (0xAB9F, "M", "Ꮟ"), - (0xABA0, "M", "Ꮠ"), - (0xABA1, "M", "Ꮡ"), - (0xABA2, "M", "Ꮢ"), - (0xABA3, "M", "Ꮣ"), - (0xABA4, "M", "Ꮤ"), - (0xABA5, "M", "Ꮥ"), - (0xABA6, "M", "Ꮦ"), - (0xABA7, "M", "Ꮧ"), - (0xABA8, "M", "Ꮨ"), - (0xABA9, "M", "Ꮩ"), - (0xABAA, "M", "Ꮪ"), - (0xABAB, "M", "Ꮫ"), - (0xABAC, "M", "Ꮬ"), - (0xABAD, "M", "Ꮭ"), - (0xABAE, "M", "Ꮮ"), - (0xABAF, "M", "Ꮯ"), - (0xABB0, "M", "Ꮰ"), - (0xABB1, "M", "Ꮱ"), - (0xABB2, "M", "Ꮲ"), - (0xABB3, "M", "Ꮳ"), - (0xABB4, "M", "Ꮴ"), - (0xABB5, "M", "Ꮵ"), - (0xABB6, "M", "Ꮶ"), - (0xABB7, "M", "Ꮷ"), - (0xABB8, "M", "Ꮸ"), - (0xABB9, "M", "Ꮹ"), - (0xABBA, "M", "Ꮺ"), - (0xABBB, "M", "Ꮻ"), - (0xABBC, "M", "Ꮼ"), - (0xABBD, "M", "Ꮽ"), - (0xABBE, "M", "Ꮾ"), - (0xABBF, "M", "Ꮿ"), - (0xABC0, "V"), - (0xABEE, "X"), - (0xABF0, "V"), - (0xABFA, "X"), - (0xAC00, "V"), - (0xD7A4, "X"), - (0xD7B0, "V"), - (0xD7C7, "X"), - (0xD7CB, "V"), - (0xD7FC, "X"), - (0xF900, "M", "豈"), - (0xF901, "M", "更"), - (0xF902, "M", "車"), - (0xF903, "M", "賈"), - (0xF904, "M", "滑"), - (0xF905, "M", "串"), - (0xF906, "M", "句"), - (0xF907, "M", "龜"), - (0xF909, "M", "契"), - (0xF90A, "M", "金"), - (0xF90B, "M", "喇"), - (0xF90C, "M", "奈"), - (0xF90D, "M", "懶"), - (0xF90E, "M", "癩"), - (0xF90F, "M", "羅"), - (0xF910, "M", "蘿"), - (0xF911, "M", "螺"), - (0xF912, "M", "裸"), - (0xF913, "M", "邏"), - (0xF914, "M", "樂"), - (0xF915, "M", "洛"), - (0xF916, "M", "烙"), - (0xF917, "M", "珞"), - (0xF918, "M", "落"), - (0xF919, "M", "酪"), - (0xF91A, "M", "駱"), - (0xF91B, "M", "亂"), - (0xF91C, "M", "卵"), - (0xF91D, "M", "欄"), - (0xF91E, "M", "爛"), - (0xF91F, "M", "蘭"), - (0xF920, "M", "鸞"), - (0xF921, "M", "嵐"), - (0xF922, "M", "濫"), - ] - - -def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xF923, "M", "藍"), - (0xF924, "M", "襤"), - (0xF925, "M", "拉"), - (0xF926, "M", "臘"), - (0xF927, "M", "蠟"), - (0xF928, "M", "廊"), - (0xF929, "M", "朗"), - (0xF92A, "M", "浪"), - (0xF92B, "M", "狼"), - (0xF92C, "M", "郎"), - (0xF92D, "M", "來"), - (0xF92E, "M", "冷"), - (0xF92F, "M", "勞"), - (0xF930, "M", "擄"), - (0xF931, "M", "櫓"), - (0xF932, "M", "爐"), - (0xF933, "M", "盧"), - (0xF934, "M", "老"), - (0xF935, "M", "蘆"), - (0xF936, "M", "虜"), - (0xF937, "M", "路"), - (0xF938, "M", "露"), - (0xF939, "M", "魯"), - (0xF93A, "M", "鷺"), - (0xF93B, "M", "碌"), - (0xF93C, "M", "祿"), - (0xF93D, "M", "綠"), - (0xF93E, "M", "菉"), - (0xF93F, "M", "錄"), - (0xF940, "M", "鹿"), - (0xF941, "M", "論"), - (0xF942, "M", "壟"), - (0xF943, "M", "弄"), - (0xF944, "M", "籠"), - (0xF945, "M", "聾"), - (0xF946, "M", "牢"), - (0xF947, "M", "磊"), - (0xF948, "M", "賂"), - (0xF949, "M", "雷"), - (0xF94A, "M", "壘"), - (0xF94B, "M", "屢"), - (0xF94C, "M", "樓"), - (0xF94D, "M", "淚"), - (0xF94E, "M", "漏"), - (0xF94F, "M", "累"), - (0xF950, "M", "縷"), - (0xF951, "M", "陋"), - (0xF952, "M", "勒"), - (0xF953, "M", "肋"), - (0xF954, "M", "凜"), - (0xF955, "M", "凌"), - (0xF956, "M", "稜"), - (0xF957, "M", "綾"), - (0xF958, "M", "菱"), - (0xF959, "M", "陵"), - (0xF95A, "M", "讀"), - (0xF95B, "M", "拏"), - (0xF95C, "M", "樂"), - (0xF95D, "M", "諾"), - (0xF95E, "M", "丹"), - (0xF95F, "M", "寧"), - (0xF960, "M", "怒"), - (0xF961, "M", "率"), - (0xF962, "M", "異"), - (0xF963, "M", "北"), - (0xF964, "M", "磻"), - (0xF965, "M", "便"), - (0xF966, "M", "復"), - (0xF967, "M", "不"), - (0xF968, "M", "泌"), - (0xF969, "M", "數"), - (0xF96A, "M", "索"), - (0xF96B, "M", "參"), - (0xF96C, "M", "塞"), - (0xF96D, "M", "省"), - (0xF96E, "M", "葉"), - (0xF96F, "M", "說"), - (0xF970, "M", "殺"), - (0xF971, "M", "辰"), - (0xF972, "M", "沈"), - (0xF973, "M", "拾"), - (0xF974, "M", "若"), - (0xF975, "M", "掠"), - (0xF976, "M", "略"), - (0xF977, "M", "亮"), - (0xF978, "M", "兩"), - (0xF979, "M", "凉"), - (0xF97A, "M", "梁"), - (0xF97B, "M", "糧"), - (0xF97C, "M", "良"), - (0xF97D, "M", "諒"), - (0xF97E, "M", "量"), - (0xF97F, "M", "勵"), - (0xF980, "M", "呂"), - (0xF981, "M", "女"), - (0xF982, "M", "廬"), - (0xF983, "M", "旅"), - (0xF984, "M", "濾"), - (0xF985, "M", "礪"), - (0xF986, "M", "閭"), - ] - - -def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xF987, "M", "驪"), - (0xF988, "M", "麗"), - (0xF989, "M", "黎"), - (0xF98A, "M", "力"), - (0xF98B, "M", "曆"), - (0xF98C, "M", "歷"), - (0xF98D, "M", "轢"), - (0xF98E, "M", "年"), - (0xF98F, "M", "憐"), - (0xF990, "M", "戀"), - (0xF991, "M", "撚"), - (0xF992, "M", "漣"), - (0xF993, "M", "煉"), - (0xF994, "M", "璉"), - (0xF995, "M", "秊"), - (0xF996, "M", "練"), - (0xF997, "M", "聯"), - (0xF998, "M", "輦"), - (0xF999, "M", "蓮"), - (0xF99A, "M", "連"), - (0xF99B, "M", "鍊"), - (0xF99C, "M", "列"), - (0xF99D, "M", "劣"), - (0xF99E, "M", "咽"), - (0xF99F, "M", "烈"), - (0xF9A0, "M", "裂"), - (0xF9A1, "M", "說"), - (0xF9A2, "M", "廉"), - (0xF9A3, "M", "念"), - (0xF9A4, "M", "捻"), - (0xF9A5, "M", "殮"), - (0xF9A6, "M", "簾"), - (0xF9A7, "M", "獵"), - (0xF9A8, "M", "令"), - (0xF9A9, "M", "囹"), - (0xF9AA, "M", "寧"), - (0xF9AB, "M", "嶺"), - (0xF9AC, "M", "怜"), - (0xF9AD, "M", "玲"), - (0xF9AE, "M", "瑩"), - (0xF9AF, "M", "羚"), - (0xF9B0, "M", "聆"), - (0xF9B1, "M", "鈴"), - (0xF9B2, "M", "零"), - (0xF9B3, "M", "靈"), - (0xF9B4, "M", "領"), - (0xF9B5, "M", "例"), - (0xF9B6, "M", "禮"), - (0xF9B7, "M", "醴"), - (0xF9B8, "M", "隸"), - (0xF9B9, "M", "惡"), - (0xF9BA, "M", "了"), - (0xF9BB, "M", "僚"), - (0xF9BC, "M", "寮"), - (0xF9BD, "M", "尿"), - (0xF9BE, "M", "料"), - (0xF9BF, "M", "樂"), - (0xF9C0, "M", "燎"), - (0xF9C1, "M", "療"), - (0xF9C2, "M", "蓼"), - (0xF9C3, "M", "遼"), - (0xF9C4, "M", "龍"), - (0xF9C5, "M", "暈"), - (0xF9C6, "M", "阮"), - (0xF9C7, "M", "劉"), - (0xF9C8, "M", "杻"), - (0xF9C9, "M", "柳"), - (0xF9CA, "M", "流"), - (0xF9CB, "M", "溜"), - (0xF9CC, "M", "琉"), - (0xF9CD, "M", "留"), - (0xF9CE, "M", "硫"), - (0xF9CF, "M", "紐"), - (0xF9D0, "M", "類"), - (0xF9D1, "M", "六"), - (0xF9D2, "M", "戮"), - (0xF9D3, "M", "陸"), - (0xF9D4, "M", "倫"), - (0xF9D5, "M", "崙"), - (0xF9D6, "M", "淪"), - (0xF9D7, "M", "輪"), - (0xF9D8, "M", "律"), - (0xF9D9, "M", "慄"), - (0xF9DA, "M", "栗"), - (0xF9DB, "M", "率"), - (0xF9DC, "M", "隆"), - (0xF9DD, "M", "利"), - (0xF9DE, "M", "吏"), - (0xF9DF, "M", "履"), - (0xF9E0, "M", "易"), - (0xF9E1, "M", "李"), - (0xF9E2, "M", "梨"), - (0xF9E3, "M", "泥"), - (0xF9E4, "M", "理"), - (0xF9E5, "M", "痢"), - (0xF9E6, "M", "罹"), - (0xF9E7, "M", "裏"), - (0xF9E8, "M", "裡"), - (0xF9E9, "M", "里"), - (0xF9EA, "M", "離"), - ] - - -def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xF9EB, "M", "匿"), - (0xF9EC, "M", "溺"), - (0xF9ED, "M", "吝"), - (0xF9EE, "M", "燐"), - (0xF9EF, "M", "璘"), - (0xF9F0, "M", "藺"), - (0xF9F1, "M", "隣"), - (0xF9F2, "M", "鱗"), - (0xF9F3, "M", "麟"), - (0xF9F4, "M", "林"), - (0xF9F5, "M", "淋"), - (0xF9F6, "M", "臨"), - (0xF9F7, "M", "立"), - (0xF9F8, "M", "笠"), - (0xF9F9, "M", "粒"), - (0xF9FA, "M", "狀"), - (0xF9FB, "M", "炙"), - (0xF9FC, "M", "識"), - (0xF9FD, "M", "什"), - (0xF9FE, "M", "茶"), - (0xF9FF, "M", "刺"), - (0xFA00, "M", "切"), - (0xFA01, "M", "度"), - (0xFA02, "M", "拓"), - (0xFA03, "M", "糖"), - (0xFA04, "M", "宅"), - (0xFA05, "M", "洞"), - (0xFA06, "M", "暴"), - (0xFA07, "M", "輻"), - (0xFA08, "M", "行"), - (0xFA09, "M", "降"), - (0xFA0A, "M", "見"), - (0xFA0B, "M", "廓"), - (0xFA0C, "M", "兀"), - (0xFA0D, "M", "嗀"), - (0xFA0E, "V"), - (0xFA10, "M", "塚"), - (0xFA11, "V"), - (0xFA12, "M", "晴"), - (0xFA13, "V"), - (0xFA15, "M", "凞"), - (0xFA16, "M", "猪"), - (0xFA17, "M", "益"), - (0xFA18, "M", "礼"), - (0xFA19, "M", "神"), - (0xFA1A, "M", "祥"), - (0xFA1B, "M", "福"), - (0xFA1C, "M", "靖"), - (0xFA1D, "M", "精"), - (0xFA1E, "M", "羽"), - (0xFA1F, "V"), - (0xFA20, "M", "蘒"), - (0xFA21, "V"), - (0xFA22, "M", "諸"), - (0xFA23, "V"), - (0xFA25, "M", "逸"), - (0xFA26, "M", "都"), - (0xFA27, "V"), - (0xFA2A, "M", "飯"), - (0xFA2B, "M", "飼"), - (0xFA2C, "M", "館"), - (0xFA2D, "M", "鶴"), - (0xFA2E, "M", "郞"), - (0xFA2F, "M", "隷"), - (0xFA30, "M", "侮"), - (0xFA31, "M", "僧"), - (0xFA32, "M", "免"), - (0xFA33, "M", "勉"), - (0xFA34, "M", "勤"), - (0xFA35, "M", "卑"), - (0xFA36, "M", "喝"), - (0xFA37, "M", "嘆"), - (0xFA38, "M", "器"), - (0xFA39, "M", "塀"), - (0xFA3A, "M", "墨"), - (0xFA3B, "M", "層"), - (0xFA3C, "M", "屮"), - (0xFA3D, "M", "悔"), - (0xFA3E, "M", "慨"), - (0xFA3F, "M", "憎"), - (0xFA40, "M", "懲"), - (0xFA41, "M", "敏"), - (0xFA42, "M", "既"), - (0xFA43, "M", "暑"), - (0xFA44, "M", "梅"), - (0xFA45, "M", "海"), - (0xFA46, "M", "渚"), - (0xFA47, "M", "漢"), - (0xFA48, "M", "煮"), - (0xFA49, "M", "爫"), - (0xFA4A, "M", "琢"), - (0xFA4B, "M", "碑"), - (0xFA4C, "M", "社"), - (0xFA4D, "M", "祉"), - (0xFA4E, "M", "祈"), - (0xFA4F, "M", "祐"), - (0xFA50, "M", "祖"), - (0xFA51, "M", "祝"), - (0xFA52, "M", "禍"), - (0xFA53, "M", "禎"), - ] - - -def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFA54, "M", "穀"), - (0xFA55, "M", "突"), - (0xFA56, "M", "節"), - (0xFA57, "M", "練"), - (0xFA58, "M", "縉"), - (0xFA59, "M", "繁"), - (0xFA5A, "M", "署"), - (0xFA5B, "M", "者"), - (0xFA5C, "M", "臭"), - (0xFA5D, "M", "艹"), - (0xFA5F, "M", "著"), - (0xFA60, "M", "褐"), - (0xFA61, "M", "視"), - (0xFA62, "M", "謁"), - (0xFA63, "M", "謹"), - (0xFA64, "M", "賓"), - (0xFA65, "M", "贈"), - (0xFA66, "M", "辶"), - (0xFA67, "M", "逸"), - (0xFA68, "M", "難"), - (0xFA69, "M", "響"), - (0xFA6A, "M", "頻"), - (0xFA6B, "M", "恵"), - (0xFA6C, "M", "𤋮"), - (0xFA6D, "M", "舘"), - (0xFA6E, "X"), - (0xFA70, "M", "並"), - (0xFA71, "M", "况"), - (0xFA72, "M", "全"), - (0xFA73, "M", "侀"), - (0xFA74, "M", "充"), - (0xFA75, "M", "冀"), - (0xFA76, "M", "勇"), - (0xFA77, "M", "勺"), - (0xFA78, "M", "喝"), - (0xFA79, "M", "啕"), - (0xFA7A, "M", "喙"), - (0xFA7B, "M", "嗢"), - (0xFA7C, "M", "塚"), - (0xFA7D, "M", "墳"), - (0xFA7E, "M", "奄"), - (0xFA7F, "M", "奔"), - (0xFA80, "M", "婢"), - (0xFA81, "M", "嬨"), - (0xFA82, "M", "廒"), - (0xFA83, "M", "廙"), - (0xFA84, "M", "彩"), - (0xFA85, "M", "徭"), - (0xFA86, "M", "惘"), - (0xFA87, "M", "慎"), - (0xFA88, "M", "愈"), - (0xFA89, "M", "憎"), - (0xFA8A, "M", "慠"), - (0xFA8B, "M", "懲"), - (0xFA8C, "M", "戴"), - (0xFA8D, "M", "揄"), - (0xFA8E, "M", "搜"), - (0xFA8F, "M", "摒"), - (0xFA90, "M", "敖"), - (0xFA91, "M", "晴"), - (0xFA92, "M", "朗"), - (0xFA93, "M", "望"), - (0xFA94, "M", "杖"), - (0xFA95, "M", "歹"), - (0xFA96, "M", "殺"), - (0xFA97, "M", "流"), - (0xFA98, "M", "滛"), - (0xFA99, "M", "滋"), - (0xFA9A, "M", "漢"), - (0xFA9B, "M", "瀞"), - (0xFA9C, "M", "煮"), - (0xFA9D, "M", "瞧"), - (0xFA9E, "M", "爵"), - (0xFA9F, "M", "犯"), - (0xFAA0, "M", "猪"), - (0xFAA1, "M", "瑱"), - (0xFAA2, "M", "甆"), - (0xFAA3, "M", "画"), - (0xFAA4, "M", "瘝"), - (0xFAA5, "M", "瘟"), - (0xFAA6, "M", "益"), - (0xFAA7, "M", "盛"), - (0xFAA8, "M", "直"), - (0xFAA9, "M", "睊"), - (0xFAAA, "M", "着"), - (0xFAAB, "M", "磌"), - (0xFAAC, "M", "窱"), - (0xFAAD, "M", "節"), - (0xFAAE, "M", "类"), - (0xFAAF, "M", "絛"), - (0xFAB0, "M", "練"), - (0xFAB1, "M", "缾"), - (0xFAB2, "M", "者"), - (0xFAB3, "M", "荒"), - (0xFAB4, "M", "華"), - (0xFAB5, "M", "蝹"), - (0xFAB6, "M", "襁"), - (0xFAB7, "M", "覆"), - (0xFAB8, "M", "視"), - (0xFAB9, "M", "調"), - ] - - -def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFABA, "M", "諸"), - (0xFABB, "M", "請"), - (0xFABC, "M", "謁"), - (0xFABD, "M", "諾"), - (0xFABE, "M", "諭"), - (0xFABF, "M", "謹"), - (0xFAC0, "M", "變"), - (0xFAC1, "M", "贈"), - (0xFAC2, "M", "輸"), - (0xFAC3, "M", "遲"), - (0xFAC4, "M", "醙"), - (0xFAC5, "M", "鉶"), - (0xFAC6, "M", "陼"), - (0xFAC7, "M", "難"), - (0xFAC8, "M", "靖"), - (0xFAC9, "M", "韛"), - (0xFACA, "M", "響"), - (0xFACB, "M", "頋"), - (0xFACC, "M", "頻"), - (0xFACD, "M", "鬒"), - (0xFACE, "M", "龜"), - (0xFACF, "M", "𢡊"), - (0xFAD0, "M", "𢡄"), - (0xFAD1, "M", "𣏕"), - (0xFAD2, "M", "㮝"), - (0xFAD3, "M", "䀘"), - (0xFAD4, "M", "䀹"), - (0xFAD5, "M", "𥉉"), - (0xFAD6, "M", "𥳐"), - (0xFAD7, "M", "𧻓"), - (0xFAD8, "M", "齃"), - (0xFAD9, "M", "龎"), - (0xFADA, "X"), - (0xFB00, "M", "ff"), - (0xFB01, "M", "fi"), - (0xFB02, "M", "fl"), - (0xFB03, "M", "ffi"), - (0xFB04, "M", "ffl"), - (0xFB05, "M", "st"), - (0xFB07, "X"), - (0xFB13, "M", "մն"), - (0xFB14, "M", "մե"), - (0xFB15, "M", "մի"), - (0xFB16, "M", "վն"), - (0xFB17, "M", "մխ"), - (0xFB18, "X"), - (0xFB1D, "M", "יִ"), - (0xFB1E, "V"), - (0xFB1F, "M", "ײַ"), - (0xFB20, "M", "ע"), - (0xFB21, "M", "א"), - (0xFB22, "M", "ד"), - (0xFB23, "M", "ה"), - (0xFB24, "M", "כ"), - (0xFB25, "M", "ל"), - (0xFB26, "M", "ם"), - (0xFB27, "M", "ר"), - (0xFB28, "M", "ת"), - (0xFB29, "M", "+"), - (0xFB2A, "M", "שׁ"), - (0xFB2B, "M", "שׂ"), - (0xFB2C, "M", "שּׁ"), - (0xFB2D, "M", "שּׂ"), - (0xFB2E, "M", "אַ"), - (0xFB2F, "M", "אָ"), - (0xFB30, "M", "אּ"), - (0xFB31, "M", "בּ"), - (0xFB32, "M", "גּ"), - (0xFB33, "M", "דּ"), - (0xFB34, "M", "הּ"), - (0xFB35, "M", "וּ"), - (0xFB36, "M", "זּ"), - (0xFB37, "X"), - (0xFB38, "M", "טּ"), - (0xFB39, "M", "יּ"), - (0xFB3A, "M", "ךּ"), - (0xFB3B, "M", "כּ"), - (0xFB3C, "M", "לּ"), - (0xFB3D, "X"), - (0xFB3E, "M", "מּ"), - (0xFB3F, "X"), - (0xFB40, "M", "נּ"), - (0xFB41, "M", "סּ"), - (0xFB42, "X"), - (0xFB43, "M", "ףּ"), - (0xFB44, "M", "פּ"), - (0xFB45, "X"), - (0xFB46, "M", "צּ"), - (0xFB47, "M", "קּ"), - (0xFB48, "M", "רּ"), - (0xFB49, "M", "שּ"), - (0xFB4A, "M", "תּ"), - (0xFB4B, "M", "וֹ"), - (0xFB4C, "M", "בֿ"), - (0xFB4D, "M", "כֿ"), - (0xFB4E, "M", "פֿ"), - (0xFB4F, "M", "אל"), - (0xFB50, "M", "ٱ"), - (0xFB52, "M", "ٻ"), - (0xFB56, "M", "پ"), - ] - - -def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFB5A, "M", "ڀ"), - (0xFB5E, "M", "ٺ"), - (0xFB62, "M", "ٿ"), - (0xFB66, "M", "ٹ"), - (0xFB6A, "M", "ڤ"), - (0xFB6E, "M", "ڦ"), - (0xFB72, "M", "ڄ"), - (0xFB76, "M", "ڃ"), - (0xFB7A, "M", "چ"), - (0xFB7E, "M", "ڇ"), - (0xFB82, "M", "ڍ"), - (0xFB84, "M", "ڌ"), - (0xFB86, "M", "ڎ"), - (0xFB88, "M", "ڈ"), - (0xFB8A, "M", "ژ"), - (0xFB8C, "M", "ڑ"), - (0xFB8E, "M", "ک"), - (0xFB92, "M", "گ"), - (0xFB96, "M", "ڳ"), - (0xFB9A, "M", "ڱ"), - (0xFB9E, "M", "ں"), - (0xFBA0, "M", "ڻ"), - (0xFBA4, "M", "ۀ"), - (0xFBA6, "M", "ہ"), - (0xFBAA, "M", "ھ"), - (0xFBAE, "M", "ے"), - (0xFBB0, "M", "ۓ"), - (0xFBB2, "V"), - (0xFBC3, "X"), - (0xFBD3, "M", "ڭ"), - (0xFBD7, "M", "ۇ"), - (0xFBD9, "M", "ۆ"), - (0xFBDB, "M", "ۈ"), - (0xFBDD, "M", "ۇٴ"), - (0xFBDE, "M", "ۋ"), - (0xFBE0, "M", "ۅ"), - (0xFBE2, "M", "ۉ"), - (0xFBE4, "M", "ې"), - (0xFBE8, "M", "ى"), - (0xFBEA, "M", "ئا"), - (0xFBEC, "M", "ئە"), - (0xFBEE, "M", "ئو"), - (0xFBF0, "M", "ئۇ"), - (0xFBF2, "M", "ئۆ"), - (0xFBF4, "M", "ئۈ"), - (0xFBF6, "M", "ئې"), - (0xFBF9, "M", "ئى"), - (0xFBFC, "M", "ی"), - (0xFC00, "M", "ئج"), - (0xFC01, "M", "ئح"), - (0xFC02, "M", "ئم"), - (0xFC03, "M", "ئى"), - (0xFC04, "M", "ئي"), - (0xFC05, "M", "بج"), - (0xFC06, "M", "بح"), - (0xFC07, "M", "بخ"), - (0xFC08, "M", "بم"), - (0xFC09, "M", "بى"), - (0xFC0A, "M", "بي"), - (0xFC0B, "M", "تج"), - (0xFC0C, "M", "تح"), - (0xFC0D, "M", "تخ"), - (0xFC0E, "M", "تم"), - (0xFC0F, "M", "تى"), - (0xFC10, "M", "تي"), - (0xFC11, "M", "ثج"), - (0xFC12, "M", "ثم"), - (0xFC13, "M", "ثى"), - (0xFC14, "M", "ثي"), - (0xFC15, "M", "جح"), - (0xFC16, "M", "جم"), - (0xFC17, "M", "حج"), - (0xFC18, "M", "حم"), - (0xFC19, "M", "خج"), - (0xFC1A, "M", "خح"), - (0xFC1B, "M", "خم"), - (0xFC1C, "M", "سج"), - (0xFC1D, "M", "سح"), - (0xFC1E, "M", "سخ"), - (0xFC1F, "M", "سم"), - (0xFC20, "M", "صح"), - (0xFC21, "M", "صم"), - (0xFC22, "M", "ضج"), - (0xFC23, "M", "ضح"), - (0xFC24, "M", "ضخ"), - (0xFC25, "M", "ضم"), - (0xFC26, "M", "طح"), - (0xFC27, "M", "طم"), - (0xFC28, "M", "ظم"), - (0xFC29, "M", "عج"), - (0xFC2A, "M", "عم"), - (0xFC2B, "M", "غج"), - (0xFC2C, "M", "غم"), - (0xFC2D, "M", "فج"), - (0xFC2E, "M", "فح"), - (0xFC2F, "M", "فخ"), - (0xFC30, "M", "فم"), - (0xFC31, "M", "فى"), - (0xFC32, "M", "في"), - (0xFC33, "M", "قح"), - ] - - -def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFC34, "M", "قم"), - (0xFC35, "M", "قى"), - (0xFC36, "M", "قي"), - (0xFC37, "M", "كا"), - (0xFC38, "M", "كج"), - (0xFC39, "M", "كح"), - (0xFC3A, "M", "كخ"), - (0xFC3B, "M", "كل"), - (0xFC3C, "M", "كم"), - (0xFC3D, "M", "كى"), - (0xFC3E, "M", "كي"), - (0xFC3F, "M", "لج"), - (0xFC40, "M", "لح"), - (0xFC41, "M", "لخ"), - (0xFC42, "M", "لم"), - (0xFC43, "M", "لى"), - (0xFC44, "M", "لي"), - (0xFC45, "M", "مج"), - (0xFC46, "M", "مح"), - (0xFC47, "M", "مخ"), - (0xFC48, "M", "مم"), - (0xFC49, "M", "مى"), - (0xFC4A, "M", "مي"), - (0xFC4B, "M", "نج"), - (0xFC4C, "M", "نح"), - (0xFC4D, "M", "نخ"), - (0xFC4E, "M", "نم"), - (0xFC4F, "M", "نى"), - (0xFC50, "M", "ني"), - (0xFC51, "M", "هج"), - (0xFC52, "M", "هم"), - (0xFC53, "M", "هى"), - (0xFC54, "M", "هي"), - (0xFC55, "M", "يج"), - (0xFC56, "M", "يح"), - (0xFC57, "M", "يخ"), - (0xFC58, "M", "يم"), - (0xFC59, "M", "يى"), - (0xFC5A, "M", "يي"), - (0xFC5B, "M", "ذٰ"), - (0xFC5C, "M", "رٰ"), - (0xFC5D, "M", "ىٰ"), - (0xFC5E, "M", " ٌّ"), - (0xFC5F, "M", " ٍّ"), - (0xFC60, "M", " َّ"), - (0xFC61, "M", " ُّ"), - (0xFC62, "M", " ِّ"), - (0xFC63, "M", " ّٰ"), - (0xFC64, "M", "ئر"), - (0xFC65, "M", "ئز"), - (0xFC66, "M", "ئم"), - (0xFC67, "M", "ئن"), - (0xFC68, "M", "ئى"), - (0xFC69, "M", "ئي"), - (0xFC6A, "M", "بر"), - (0xFC6B, "M", "بز"), - (0xFC6C, "M", "بم"), - (0xFC6D, "M", "بن"), - (0xFC6E, "M", "بى"), - (0xFC6F, "M", "بي"), - (0xFC70, "M", "تر"), - (0xFC71, "M", "تز"), - (0xFC72, "M", "تم"), - (0xFC73, "M", "تن"), - (0xFC74, "M", "تى"), - (0xFC75, "M", "تي"), - (0xFC76, "M", "ثر"), - (0xFC77, "M", "ثز"), - (0xFC78, "M", "ثم"), - (0xFC79, "M", "ثن"), - (0xFC7A, "M", "ثى"), - (0xFC7B, "M", "ثي"), - (0xFC7C, "M", "فى"), - (0xFC7D, "M", "في"), - (0xFC7E, "M", "قى"), - (0xFC7F, "M", "قي"), - (0xFC80, "M", "كا"), - (0xFC81, "M", "كل"), - (0xFC82, "M", "كم"), - (0xFC83, "M", "كى"), - (0xFC84, "M", "كي"), - (0xFC85, "M", "لم"), - (0xFC86, "M", "لى"), - (0xFC87, "M", "لي"), - (0xFC88, "M", "ما"), - (0xFC89, "M", "مم"), - (0xFC8A, "M", "نر"), - (0xFC8B, "M", "نز"), - (0xFC8C, "M", "نم"), - (0xFC8D, "M", "نن"), - (0xFC8E, "M", "نى"), - (0xFC8F, "M", "ني"), - (0xFC90, "M", "ىٰ"), - (0xFC91, "M", "ير"), - (0xFC92, "M", "يز"), - (0xFC93, "M", "يم"), - (0xFC94, "M", "ين"), - (0xFC95, "M", "يى"), - (0xFC96, "M", "يي"), - (0xFC97, "M", "ئج"), - ] - - -def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFC98, "M", "ئح"), - (0xFC99, "M", "ئخ"), - (0xFC9A, "M", "ئم"), - (0xFC9B, "M", "ئه"), - (0xFC9C, "M", "بج"), - (0xFC9D, "M", "بح"), - (0xFC9E, "M", "بخ"), - (0xFC9F, "M", "بم"), - (0xFCA0, "M", "به"), - (0xFCA1, "M", "تج"), - (0xFCA2, "M", "تح"), - (0xFCA3, "M", "تخ"), - (0xFCA4, "M", "تم"), - (0xFCA5, "M", "ته"), - (0xFCA6, "M", "ثم"), - (0xFCA7, "M", "جح"), - (0xFCA8, "M", "جم"), - (0xFCA9, "M", "حج"), - (0xFCAA, "M", "حم"), - (0xFCAB, "M", "خج"), - (0xFCAC, "M", "خم"), - (0xFCAD, "M", "سج"), - (0xFCAE, "M", "سح"), - (0xFCAF, "M", "سخ"), - (0xFCB0, "M", "سم"), - (0xFCB1, "M", "صح"), - (0xFCB2, "M", "صخ"), - (0xFCB3, "M", "صم"), - (0xFCB4, "M", "ضج"), - (0xFCB5, "M", "ضح"), - (0xFCB6, "M", "ضخ"), - (0xFCB7, "M", "ضم"), - (0xFCB8, "M", "طح"), - (0xFCB9, "M", "ظم"), - (0xFCBA, "M", "عج"), - (0xFCBB, "M", "عم"), - (0xFCBC, "M", "غج"), - (0xFCBD, "M", "غم"), - (0xFCBE, "M", "فج"), - (0xFCBF, "M", "فح"), - (0xFCC0, "M", "فخ"), - (0xFCC1, "M", "فم"), - (0xFCC2, "M", "قح"), - (0xFCC3, "M", "قم"), - (0xFCC4, "M", "كج"), - (0xFCC5, "M", "كح"), - (0xFCC6, "M", "كخ"), - (0xFCC7, "M", "كل"), - (0xFCC8, "M", "كم"), - (0xFCC9, "M", "لج"), - (0xFCCA, "M", "لح"), - (0xFCCB, "M", "لخ"), - (0xFCCC, "M", "لم"), - (0xFCCD, "M", "له"), - (0xFCCE, "M", "مج"), - (0xFCCF, "M", "مح"), - (0xFCD0, "M", "مخ"), - (0xFCD1, "M", "مم"), - (0xFCD2, "M", "نج"), - (0xFCD3, "M", "نح"), - (0xFCD4, "M", "نخ"), - (0xFCD5, "M", "نم"), - (0xFCD6, "M", "نه"), - (0xFCD7, "M", "هج"), - (0xFCD8, "M", "هم"), - (0xFCD9, "M", "هٰ"), - (0xFCDA, "M", "يج"), - (0xFCDB, "M", "يح"), - (0xFCDC, "M", "يخ"), - (0xFCDD, "M", "يم"), - (0xFCDE, "M", "يه"), - (0xFCDF, "M", "ئم"), - (0xFCE0, "M", "ئه"), - (0xFCE1, "M", "بم"), - (0xFCE2, "M", "به"), - (0xFCE3, "M", "تم"), - (0xFCE4, "M", "ته"), - (0xFCE5, "M", "ثم"), - (0xFCE6, "M", "ثه"), - (0xFCE7, "M", "سم"), - (0xFCE8, "M", "سه"), - (0xFCE9, "M", "شم"), - (0xFCEA, "M", "شه"), - (0xFCEB, "M", "كل"), - (0xFCEC, "M", "كم"), - (0xFCED, "M", "لم"), - (0xFCEE, "M", "نم"), - (0xFCEF, "M", "نه"), - (0xFCF0, "M", "يم"), - (0xFCF1, "M", "يه"), - (0xFCF2, "M", "ـَّ"), - (0xFCF3, "M", "ـُّ"), - (0xFCF4, "M", "ـِّ"), - (0xFCF5, "M", "طى"), - (0xFCF6, "M", "طي"), - (0xFCF7, "M", "عى"), - (0xFCF8, "M", "عي"), - (0xFCF9, "M", "غى"), - (0xFCFA, "M", "غي"), - (0xFCFB, "M", "سى"), - ] - - -def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFCFC, "M", "سي"), - (0xFCFD, "M", "شى"), - (0xFCFE, "M", "شي"), - (0xFCFF, "M", "حى"), - (0xFD00, "M", "حي"), - (0xFD01, "M", "جى"), - (0xFD02, "M", "جي"), - (0xFD03, "M", "خى"), - (0xFD04, "M", "خي"), - (0xFD05, "M", "صى"), - (0xFD06, "M", "صي"), - (0xFD07, "M", "ضى"), - (0xFD08, "M", "ضي"), - (0xFD09, "M", "شج"), - (0xFD0A, "M", "شح"), - (0xFD0B, "M", "شخ"), - (0xFD0C, "M", "شم"), - (0xFD0D, "M", "شر"), - (0xFD0E, "M", "سر"), - (0xFD0F, "M", "صر"), - (0xFD10, "M", "ضر"), - (0xFD11, "M", "طى"), - (0xFD12, "M", "طي"), - (0xFD13, "M", "عى"), - (0xFD14, "M", "عي"), - (0xFD15, "M", "غى"), - (0xFD16, "M", "غي"), - (0xFD17, "M", "سى"), - (0xFD18, "M", "سي"), - (0xFD19, "M", "شى"), - (0xFD1A, "M", "شي"), - (0xFD1B, "M", "حى"), - (0xFD1C, "M", "حي"), - (0xFD1D, "M", "جى"), - (0xFD1E, "M", "جي"), - (0xFD1F, "M", "خى"), - (0xFD20, "M", "خي"), - (0xFD21, "M", "صى"), - (0xFD22, "M", "صي"), - (0xFD23, "M", "ضى"), - (0xFD24, "M", "ضي"), - (0xFD25, "M", "شج"), - (0xFD26, "M", "شح"), - (0xFD27, "M", "شخ"), - (0xFD28, "M", "شم"), - (0xFD29, "M", "شر"), - (0xFD2A, "M", "سر"), - (0xFD2B, "M", "صر"), - (0xFD2C, "M", "ضر"), - (0xFD2D, "M", "شج"), - (0xFD2E, "M", "شح"), - (0xFD2F, "M", "شخ"), - (0xFD30, "M", "شم"), - (0xFD31, "M", "سه"), - (0xFD32, "M", "شه"), - (0xFD33, "M", "طم"), - (0xFD34, "M", "سج"), - (0xFD35, "M", "سح"), - (0xFD36, "M", "سخ"), - (0xFD37, "M", "شج"), - (0xFD38, "M", "شح"), - (0xFD39, "M", "شخ"), - (0xFD3A, "M", "طم"), - (0xFD3B, "M", "ظم"), - (0xFD3C, "M", "اً"), - (0xFD3E, "V"), - (0xFD50, "M", "تجم"), - (0xFD51, "M", "تحج"), - (0xFD53, "M", "تحم"), - (0xFD54, "M", "تخم"), - (0xFD55, "M", "تمج"), - (0xFD56, "M", "تمح"), - (0xFD57, "M", "تمخ"), - (0xFD58, "M", "جمح"), - (0xFD5A, "M", "حمي"), - (0xFD5B, "M", "حمى"), - (0xFD5C, "M", "سحج"), - (0xFD5D, "M", "سجح"), - (0xFD5E, "M", "سجى"), - (0xFD5F, "M", "سمح"), - (0xFD61, "M", "سمج"), - (0xFD62, "M", "سمم"), - (0xFD64, "M", "صحح"), - (0xFD66, "M", "صمم"), - (0xFD67, "M", "شحم"), - (0xFD69, "M", "شجي"), - (0xFD6A, "M", "شمخ"), - (0xFD6C, "M", "شمم"), - (0xFD6E, "M", "ضحى"), - (0xFD6F, "M", "ضخم"), - (0xFD71, "M", "طمح"), - (0xFD73, "M", "طمم"), - (0xFD74, "M", "طمي"), - (0xFD75, "M", "عجم"), - (0xFD76, "M", "عمم"), - (0xFD78, "M", "عمى"), - (0xFD79, "M", "غمم"), - (0xFD7A, "M", "غمي"), - (0xFD7B, "M", "غمى"), - (0xFD7C, "M", "فخم"), - ] - - -def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFD7E, "M", "قمح"), - (0xFD7F, "M", "قمم"), - (0xFD80, "M", "لحم"), - (0xFD81, "M", "لحي"), - (0xFD82, "M", "لحى"), - (0xFD83, "M", "لجج"), - (0xFD85, "M", "لخم"), - (0xFD87, "M", "لمح"), - (0xFD89, "M", "محج"), - (0xFD8A, "M", "محم"), - (0xFD8B, "M", "محي"), - (0xFD8C, "M", "مجح"), - (0xFD8D, "M", "مجم"), - (0xFD8E, "M", "مخج"), - (0xFD8F, "M", "مخم"), - (0xFD90, "X"), - (0xFD92, "M", "مجخ"), - (0xFD93, "M", "همج"), - (0xFD94, "M", "همم"), - (0xFD95, "M", "نحم"), - (0xFD96, "M", "نحى"), - (0xFD97, "M", "نجم"), - (0xFD99, "M", "نجى"), - (0xFD9A, "M", "نمي"), - (0xFD9B, "M", "نمى"), - (0xFD9C, "M", "يمم"), - (0xFD9E, "M", "بخي"), - (0xFD9F, "M", "تجي"), - (0xFDA0, "M", "تجى"), - (0xFDA1, "M", "تخي"), - (0xFDA2, "M", "تخى"), - (0xFDA3, "M", "تمي"), - (0xFDA4, "M", "تمى"), - (0xFDA5, "M", "جمي"), - (0xFDA6, "M", "جحى"), - (0xFDA7, "M", "جمى"), - (0xFDA8, "M", "سخى"), - (0xFDA9, "M", "صحي"), - (0xFDAA, "M", "شحي"), - (0xFDAB, "M", "ضحي"), - (0xFDAC, "M", "لجي"), - (0xFDAD, "M", "لمي"), - (0xFDAE, "M", "يحي"), - (0xFDAF, "M", "يجي"), - (0xFDB0, "M", "يمي"), - (0xFDB1, "M", "ممي"), - (0xFDB2, "M", "قمي"), - (0xFDB3, "M", "نحي"), - (0xFDB4, "M", "قمح"), - (0xFDB5, "M", "لحم"), - (0xFDB6, "M", "عمي"), - (0xFDB7, "M", "كمي"), - (0xFDB8, "M", "نجح"), - (0xFDB9, "M", "مخي"), - (0xFDBA, "M", "لجم"), - (0xFDBB, "M", "كمم"), - (0xFDBC, "M", "لجم"), - (0xFDBD, "M", "نجح"), - (0xFDBE, "M", "جحي"), - (0xFDBF, "M", "حجي"), - (0xFDC0, "M", "مجي"), - (0xFDC1, "M", "فمي"), - (0xFDC2, "M", "بحي"), - (0xFDC3, "M", "كمم"), - (0xFDC4, "M", "عجم"), - (0xFDC5, "M", "صمم"), - (0xFDC6, "M", "سخي"), - (0xFDC7, "M", "نجي"), - (0xFDC8, "X"), - (0xFDCF, "V"), - (0xFDD0, "X"), - (0xFDF0, "M", "صلے"), - (0xFDF1, "M", "قلے"), - (0xFDF2, "M", "الله"), - (0xFDF3, "M", "اكبر"), - (0xFDF4, "M", "محمد"), - (0xFDF5, "M", "صلعم"), - (0xFDF6, "M", "رسول"), - (0xFDF7, "M", "عليه"), - (0xFDF8, "M", "وسلم"), - (0xFDF9, "M", "صلى"), - (0xFDFA, "M", "صلى الله عليه وسلم"), - (0xFDFB, "M", "جل جلاله"), - (0xFDFC, "M", "ریال"), - (0xFDFD, "V"), - (0xFE00, "I"), - (0xFE10, "M", ","), - (0xFE11, "M", "、"), - (0xFE12, "X"), - (0xFE13, "M", ":"), - (0xFE14, "M", ";"), - (0xFE15, "M", "!"), - (0xFE16, "M", "?"), - (0xFE17, "M", "〖"), - (0xFE18, "M", "〗"), - (0xFE19, "X"), - (0xFE20, "V"), - (0xFE30, "X"), - (0xFE31, "M", "—"), - (0xFE32, "M", "–"), - ] - - -def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFE33, "M", "_"), - (0xFE35, "M", "("), - (0xFE36, "M", ")"), - (0xFE37, "M", "{"), - (0xFE38, "M", "}"), - (0xFE39, "M", "〔"), - (0xFE3A, "M", "〕"), - (0xFE3B, "M", "【"), - (0xFE3C, "M", "】"), - (0xFE3D, "M", "《"), - (0xFE3E, "M", "》"), - (0xFE3F, "M", "〈"), - (0xFE40, "M", "〉"), - (0xFE41, "M", "「"), - (0xFE42, "M", "」"), - (0xFE43, "M", "『"), - (0xFE44, "M", "』"), - (0xFE45, "V"), - (0xFE47, "M", "["), - (0xFE48, "M", "]"), - (0xFE49, "M", " ̅"), - (0xFE4D, "M", "_"), - (0xFE50, "M", ","), - (0xFE51, "M", "、"), - (0xFE52, "X"), - (0xFE54, "M", ";"), - (0xFE55, "M", ":"), - (0xFE56, "M", "?"), - (0xFE57, "M", "!"), - (0xFE58, "M", "—"), - (0xFE59, "M", "("), - (0xFE5A, "M", ")"), - (0xFE5B, "M", "{"), - (0xFE5C, "M", "}"), - (0xFE5D, "M", "〔"), - (0xFE5E, "M", "〕"), - (0xFE5F, "M", "#"), - (0xFE60, "M", "&"), - (0xFE61, "M", "*"), - (0xFE62, "M", "+"), - (0xFE63, "M", "-"), - (0xFE64, "M", "<"), - (0xFE65, "M", ">"), - (0xFE66, "M", "="), - (0xFE67, "X"), - (0xFE68, "M", "\\"), - (0xFE69, "M", "$"), - (0xFE6A, "M", "%"), - (0xFE6B, "M", "@"), - (0xFE6C, "X"), - (0xFE70, "M", " ً"), - (0xFE71, "M", "ـً"), - (0xFE72, "M", " ٌ"), - (0xFE73, "V"), - (0xFE74, "M", " ٍ"), - (0xFE75, "X"), - (0xFE76, "M", " َ"), - (0xFE77, "M", "ـَ"), - (0xFE78, "M", " ُ"), - (0xFE79, "M", "ـُ"), - (0xFE7A, "M", " ِ"), - (0xFE7B, "M", "ـِ"), - (0xFE7C, "M", " ّ"), - (0xFE7D, "M", "ـّ"), - (0xFE7E, "M", " ْ"), - (0xFE7F, "M", "ـْ"), - (0xFE80, "M", "ء"), - (0xFE81, "M", "آ"), - (0xFE83, "M", "أ"), - (0xFE85, "M", "ؤ"), - (0xFE87, "M", "إ"), - (0xFE89, "M", "ئ"), - (0xFE8D, "M", "ا"), - (0xFE8F, "M", "ب"), - (0xFE93, "M", "ة"), - (0xFE95, "M", "ت"), - (0xFE99, "M", "ث"), - (0xFE9D, "M", "ج"), - (0xFEA1, "M", "ح"), - (0xFEA5, "M", "خ"), - (0xFEA9, "M", "د"), - (0xFEAB, "M", "ذ"), - (0xFEAD, "M", "ر"), - (0xFEAF, "M", "ز"), - (0xFEB1, "M", "س"), - (0xFEB5, "M", "ش"), - (0xFEB9, "M", "ص"), - (0xFEBD, "M", "ض"), - (0xFEC1, "M", "ط"), - (0xFEC5, "M", "ظ"), - (0xFEC9, "M", "ع"), - (0xFECD, "M", "غ"), - (0xFED1, "M", "ف"), - (0xFED5, "M", "ق"), - (0xFED9, "M", "ك"), - (0xFEDD, "M", "ل"), - (0xFEE1, "M", "م"), - (0xFEE5, "M", "ن"), - (0xFEE9, "M", "ه"), - (0xFEED, "M", "و"), - ] - - -def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFEEF, "M", "ى"), - (0xFEF1, "M", "ي"), - (0xFEF5, "M", "لآ"), - (0xFEF7, "M", "لأ"), - (0xFEF9, "M", "لإ"), - (0xFEFB, "M", "لا"), - (0xFEFD, "X"), - (0xFEFF, "I"), - (0xFF00, "X"), - (0xFF01, "M", "!"), - (0xFF02, "M", '"'), - (0xFF03, "M", "#"), - (0xFF04, "M", "$"), - (0xFF05, "M", "%"), - (0xFF06, "M", "&"), - (0xFF07, "M", "'"), - (0xFF08, "M", "("), - (0xFF09, "M", ")"), - (0xFF0A, "M", "*"), - (0xFF0B, "M", "+"), - (0xFF0C, "M", ","), - (0xFF0D, "M", "-"), - (0xFF0E, "M", "."), - (0xFF0F, "M", "/"), - (0xFF10, "M", "0"), - (0xFF11, "M", "1"), - (0xFF12, "M", "2"), - (0xFF13, "M", "3"), - (0xFF14, "M", "4"), - (0xFF15, "M", "5"), - (0xFF16, "M", "6"), - (0xFF17, "M", "7"), - (0xFF18, "M", "8"), - (0xFF19, "M", "9"), - (0xFF1A, "M", ":"), - (0xFF1B, "M", ";"), - (0xFF1C, "M", "<"), - (0xFF1D, "M", "="), - (0xFF1E, "M", ">"), - (0xFF1F, "M", "?"), - (0xFF20, "M", "@"), - (0xFF21, "M", "a"), - (0xFF22, "M", "b"), - (0xFF23, "M", "c"), - (0xFF24, "M", "d"), - (0xFF25, "M", "e"), - (0xFF26, "M", "f"), - (0xFF27, "M", "g"), - (0xFF28, "M", "h"), - (0xFF29, "M", "i"), - (0xFF2A, "M", "j"), - (0xFF2B, "M", "k"), - (0xFF2C, "M", "l"), - (0xFF2D, "M", "m"), - (0xFF2E, "M", "n"), - (0xFF2F, "M", "o"), - (0xFF30, "M", "p"), - (0xFF31, "M", "q"), - (0xFF32, "M", "r"), - (0xFF33, "M", "s"), - (0xFF34, "M", "t"), - (0xFF35, "M", "u"), - (0xFF36, "M", "v"), - (0xFF37, "M", "w"), - (0xFF38, "M", "x"), - (0xFF39, "M", "y"), - (0xFF3A, "M", "z"), - (0xFF3B, "M", "["), - (0xFF3C, "M", "\\"), - (0xFF3D, "M", "]"), - (0xFF3E, "M", "^"), - (0xFF3F, "M", "_"), - (0xFF40, "M", "`"), - (0xFF41, "M", "a"), - (0xFF42, "M", "b"), - (0xFF43, "M", "c"), - (0xFF44, "M", "d"), - (0xFF45, "M", "e"), - (0xFF46, "M", "f"), - (0xFF47, "M", "g"), - (0xFF48, "M", "h"), - (0xFF49, "M", "i"), - (0xFF4A, "M", "j"), - (0xFF4B, "M", "k"), - (0xFF4C, "M", "l"), - (0xFF4D, "M", "m"), - (0xFF4E, "M", "n"), - (0xFF4F, "M", "o"), - (0xFF50, "M", "p"), - (0xFF51, "M", "q"), - (0xFF52, "M", "r"), - (0xFF53, "M", "s"), - (0xFF54, "M", "t"), - (0xFF55, "M", "u"), - (0xFF56, "M", "v"), - (0xFF57, "M", "w"), - (0xFF58, "M", "x"), - (0xFF59, "M", "y"), - (0xFF5A, "M", "z"), - (0xFF5B, "M", "{"), - ] - - -def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFF5C, "M", "|"), - (0xFF5D, "M", "}"), - (0xFF5E, "M", "~"), - (0xFF5F, "M", "⦅"), - (0xFF60, "M", "⦆"), - (0xFF61, "M", "."), - (0xFF62, "M", "「"), - (0xFF63, "M", "」"), - (0xFF64, "M", "、"), - (0xFF65, "M", "・"), - (0xFF66, "M", "ヲ"), - (0xFF67, "M", "ァ"), - (0xFF68, "M", "ィ"), - (0xFF69, "M", "ゥ"), - (0xFF6A, "M", "ェ"), - (0xFF6B, "M", "ォ"), - (0xFF6C, "M", "ャ"), - (0xFF6D, "M", "ュ"), - (0xFF6E, "M", "ョ"), - (0xFF6F, "M", "ッ"), - (0xFF70, "M", "ー"), - (0xFF71, "M", "ア"), - (0xFF72, "M", "イ"), - (0xFF73, "M", "ウ"), - (0xFF74, "M", "エ"), - (0xFF75, "M", "オ"), - (0xFF76, "M", "カ"), - (0xFF77, "M", "キ"), - (0xFF78, "M", "ク"), - (0xFF79, "M", "ケ"), - (0xFF7A, "M", "コ"), - (0xFF7B, "M", "サ"), - (0xFF7C, "M", "シ"), - (0xFF7D, "M", "ス"), - (0xFF7E, "M", "セ"), - (0xFF7F, "M", "ソ"), - (0xFF80, "M", "タ"), - (0xFF81, "M", "チ"), - (0xFF82, "M", "ツ"), - (0xFF83, "M", "テ"), - (0xFF84, "M", "ト"), - (0xFF85, "M", "ナ"), - (0xFF86, "M", "ニ"), - (0xFF87, "M", "ヌ"), - (0xFF88, "M", "ネ"), - (0xFF89, "M", "ノ"), - (0xFF8A, "M", "ハ"), - (0xFF8B, "M", "ヒ"), - (0xFF8C, "M", "フ"), - (0xFF8D, "M", "ヘ"), - (0xFF8E, "M", "ホ"), - (0xFF8F, "M", "マ"), - (0xFF90, "M", "ミ"), - (0xFF91, "M", "ム"), - (0xFF92, "M", "メ"), - (0xFF93, "M", "モ"), - (0xFF94, "M", "ヤ"), - (0xFF95, "M", "ユ"), - (0xFF96, "M", "ヨ"), - (0xFF97, "M", "ラ"), - (0xFF98, "M", "リ"), - (0xFF99, "M", "ル"), - (0xFF9A, "M", "レ"), - (0xFF9B, "M", "ロ"), - (0xFF9C, "M", "ワ"), - (0xFF9D, "M", "ン"), - (0xFF9E, "M", "゙"), - (0xFF9F, "M", "゚"), - (0xFFA0, "I"), - (0xFFA1, "M", "ᄀ"), - (0xFFA2, "M", "ᄁ"), - (0xFFA3, "M", "ᆪ"), - (0xFFA4, "M", "ᄂ"), - (0xFFA5, "M", "ᆬ"), - (0xFFA6, "M", "ᆭ"), - (0xFFA7, "M", "ᄃ"), - (0xFFA8, "M", "ᄄ"), - (0xFFA9, "M", "ᄅ"), - (0xFFAA, "M", "ᆰ"), - (0xFFAB, "M", "ᆱ"), - (0xFFAC, "M", "ᆲ"), - (0xFFAD, "M", "ᆳ"), - (0xFFAE, "M", "ᆴ"), - (0xFFAF, "M", "ᆵ"), - (0xFFB0, "M", "ᄚ"), - (0xFFB1, "M", "ᄆ"), - (0xFFB2, "M", "ᄇ"), - (0xFFB3, "M", "ᄈ"), - (0xFFB4, "M", "ᄡ"), - (0xFFB5, "M", "ᄉ"), - (0xFFB6, "M", "ᄊ"), - (0xFFB7, "M", "ᄋ"), - (0xFFB8, "M", "ᄌ"), - (0xFFB9, "M", "ᄍ"), - (0xFFBA, "M", "ᄎ"), - (0xFFBB, "M", "ᄏ"), - (0xFFBC, "M", "ᄐ"), - (0xFFBD, "M", "ᄑ"), - (0xFFBE, "M", "ᄒ"), - (0xFFBF, "X"), - ] - - -def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0xFFC2, "M", "ᅡ"), - (0xFFC3, "M", "ᅢ"), - (0xFFC4, "M", "ᅣ"), - (0xFFC5, "M", "ᅤ"), - (0xFFC6, "M", "ᅥ"), - (0xFFC7, "M", "ᅦ"), - (0xFFC8, "X"), - (0xFFCA, "M", "ᅧ"), - (0xFFCB, "M", "ᅨ"), - (0xFFCC, "M", "ᅩ"), - (0xFFCD, "M", "ᅪ"), - (0xFFCE, "M", "ᅫ"), - (0xFFCF, "M", "ᅬ"), - (0xFFD0, "X"), - (0xFFD2, "M", "ᅭ"), - (0xFFD3, "M", "ᅮ"), - (0xFFD4, "M", "ᅯ"), - (0xFFD5, "M", "ᅰ"), - (0xFFD6, "M", "ᅱ"), - (0xFFD7, "M", "ᅲ"), - (0xFFD8, "X"), - (0xFFDA, "M", "ᅳ"), - (0xFFDB, "M", "ᅴ"), - (0xFFDC, "M", "ᅵ"), - (0xFFDD, "X"), - (0xFFE0, "M", "¢"), - (0xFFE1, "M", "£"), - (0xFFE2, "M", "¬"), - (0xFFE3, "M", " ̄"), - (0xFFE4, "M", "¦"), - (0xFFE5, "M", "¥"), - (0xFFE6, "M", "₩"), - (0xFFE7, "X"), - (0xFFE8, "M", "│"), - (0xFFE9, "M", "←"), - (0xFFEA, "M", "↑"), - (0xFFEB, "M", "→"), - (0xFFEC, "M", "↓"), - (0xFFED, "M", "■"), - (0xFFEE, "M", "○"), - (0xFFEF, "X"), - (0x10000, "V"), - (0x1000C, "X"), - (0x1000D, "V"), - (0x10027, "X"), - (0x10028, "V"), - (0x1003B, "X"), - (0x1003C, "V"), - (0x1003E, "X"), - (0x1003F, "V"), - (0x1004E, "X"), - (0x10050, "V"), - (0x1005E, "X"), - (0x10080, "V"), - (0x100FB, "X"), - (0x10100, "V"), - (0x10103, "X"), - (0x10107, "V"), - (0x10134, "X"), - (0x10137, "V"), - (0x1018F, "X"), - (0x10190, "V"), - (0x1019D, "X"), - (0x101A0, "V"), - (0x101A1, "X"), - (0x101D0, "V"), - (0x101FE, "X"), - (0x10280, "V"), - (0x1029D, "X"), - (0x102A0, "V"), - (0x102D1, "X"), - (0x102E0, "V"), - (0x102FC, "X"), - (0x10300, "V"), - (0x10324, "X"), - (0x1032D, "V"), - (0x1034B, "X"), - (0x10350, "V"), - (0x1037B, "X"), - (0x10380, "V"), - (0x1039E, "X"), - (0x1039F, "V"), - (0x103C4, "X"), - (0x103C8, "V"), - (0x103D6, "X"), - (0x10400, "M", "𐐨"), - (0x10401, "M", "𐐩"), - (0x10402, "M", "𐐪"), - (0x10403, "M", "𐐫"), - (0x10404, "M", "𐐬"), - (0x10405, "M", "𐐭"), - (0x10406, "M", "𐐮"), - (0x10407, "M", "𐐯"), - (0x10408, "M", "𐐰"), - (0x10409, "M", "𐐱"), - (0x1040A, "M", "𐐲"), - (0x1040B, "M", "𐐳"), - (0x1040C, "M", "𐐴"), - (0x1040D, "M", "𐐵"), - (0x1040E, "M", "𐐶"), - ] - - -def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1040F, "M", "𐐷"), - (0x10410, "M", "𐐸"), - (0x10411, "M", "𐐹"), - (0x10412, "M", "𐐺"), - (0x10413, "M", "𐐻"), - (0x10414, "M", "𐐼"), - (0x10415, "M", "𐐽"), - (0x10416, "M", "𐐾"), - (0x10417, "M", "𐐿"), - (0x10418, "M", "𐑀"), - (0x10419, "M", "𐑁"), - (0x1041A, "M", "𐑂"), - (0x1041B, "M", "𐑃"), - (0x1041C, "M", "𐑄"), - (0x1041D, "M", "𐑅"), - (0x1041E, "M", "𐑆"), - (0x1041F, "M", "𐑇"), - (0x10420, "M", "𐑈"), - (0x10421, "M", "𐑉"), - (0x10422, "M", "𐑊"), - (0x10423, "M", "𐑋"), - (0x10424, "M", "𐑌"), - (0x10425, "M", "𐑍"), - (0x10426, "M", "𐑎"), - (0x10427, "M", "𐑏"), - (0x10428, "V"), - (0x1049E, "X"), - (0x104A0, "V"), - (0x104AA, "X"), - (0x104B0, "M", "𐓘"), - (0x104B1, "M", "𐓙"), - (0x104B2, "M", "𐓚"), - (0x104B3, "M", "𐓛"), - (0x104B4, "M", "𐓜"), - (0x104B5, "M", "𐓝"), - (0x104B6, "M", "𐓞"), - (0x104B7, "M", "𐓟"), - (0x104B8, "M", "𐓠"), - (0x104B9, "M", "𐓡"), - (0x104BA, "M", "𐓢"), - (0x104BB, "M", "𐓣"), - (0x104BC, "M", "𐓤"), - (0x104BD, "M", "𐓥"), - (0x104BE, "M", "𐓦"), - (0x104BF, "M", "𐓧"), - (0x104C0, "M", "𐓨"), - (0x104C1, "M", "𐓩"), - (0x104C2, "M", "𐓪"), - (0x104C3, "M", "𐓫"), - (0x104C4, "M", "𐓬"), - (0x104C5, "M", "𐓭"), - (0x104C6, "M", "𐓮"), - (0x104C7, "M", "𐓯"), - (0x104C8, "M", "𐓰"), - (0x104C9, "M", "𐓱"), - (0x104CA, "M", "𐓲"), - (0x104CB, "M", "𐓳"), - (0x104CC, "M", "𐓴"), - (0x104CD, "M", "𐓵"), - (0x104CE, "M", "𐓶"), - (0x104CF, "M", "𐓷"), - (0x104D0, "M", "𐓸"), - (0x104D1, "M", "𐓹"), - (0x104D2, "M", "𐓺"), - (0x104D3, "M", "𐓻"), - (0x104D4, "X"), - (0x104D8, "V"), - (0x104FC, "X"), - (0x10500, "V"), - (0x10528, "X"), - (0x10530, "V"), - (0x10564, "X"), - (0x1056F, "V"), - (0x10570, "M", "𐖗"), - (0x10571, "M", "𐖘"), - (0x10572, "M", "𐖙"), - (0x10573, "M", "𐖚"), - (0x10574, "M", "𐖛"), - (0x10575, "M", "𐖜"), - (0x10576, "M", "𐖝"), - (0x10577, "M", "𐖞"), - (0x10578, "M", "𐖟"), - (0x10579, "M", "𐖠"), - (0x1057A, "M", "𐖡"), - (0x1057B, "X"), - (0x1057C, "M", "𐖣"), - (0x1057D, "M", "𐖤"), - (0x1057E, "M", "𐖥"), - (0x1057F, "M", "𐖦"), - (0x10580, "M", "𐖧"), - (0x10581, "M", "𐖨"), - (0x10582, "M", "𐖩"), - (0x10583, "M", "𐖪"), - (0x10584, "M", "𐖫"), - (0x10585, "M", "𐖬"), - (0x10586, "M", "𐖭"), - (0x10587, "M", "𐖮"), - (0x10588, "M", "𐖯"), - (0x10589, "M", "𐖰"), - (0x1058A, "M", "𐖱"), - ] - - -def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1058B, "X"), - (0x1058C, "M", "𐖳"), - (0x1058D, "M", "𐖴"), - (0x1058E, "M", "𐖵"), - (0x1058F, "M", "𐖶"), - (0x10590, "M", "𐖷"), - (0x10591, "M", "𐖸"), - (0x10592, "M", "𐖹"), - (0x10593, "X"), - (0x10594, "M", "𐖻"), - (0x10595, "M", "𐖼"), - (0x10596, "X"), - (0x10597, "V"), - (0x105A2, "X"), - (0x105A3, "V"), - (0x105B2, "X"), - (0x105B3, "V"), - (0x105BA, "X"), - (0x105BB, "V"), - (0x105BD, "X"), - (0x105C0, "V"), - (0x105F4, "X"), - (0x10600, "V"), - (0x10737, "X"), - (0x10740, "V"), - (0x10756, "X"), - (0x10760, "V"), - (0x10768, "X"), - (0x10780, "V"), - (0x10781, "M", "ː"), - (0x10782, "M", "ˑ"), - (0x10783, "M", "æ"), - (0x10784, "M", "ʙ"), - (0x10785, "M", "ɓ"), - (0x10786, "X"), - (0x10787, "M", "ʣ"), - (0x10788, "M", "ꭦ"), - (0x10789, "M", "ʥ"), - (0x1078A, "M", "ʤ"), - (0x1078B, "M", "ɖ"), - (0x1078C, "M", "ɗ"), - (0x1078D, "M", "ᶑ"), - (0x1078E, "M", "ɘ"), - (0x1078F, "M", "ɞ"), - (0x10790, "M", "ʩ"), - (0x10791, "M", "ɤ"), - (0x10792, "M", "ɢ"), - (0x10793, "M", "ɠ"), - (0x10794, "M", "ʛ"), - (0x10795, "M", "ħ"), - (0x10796, "M", "ʜ"), - (0x10797, "M", "ɧ"), - (0x10798, "M", "ʄ"), - (0x10799, "M", "ʪ"), - (0x1079A, "M", "ʫ"), - (0x1079B, "M", "ɬ"), - (0x1079C, "M", "𝼄"), - (0x1079D, "M", "ꞎ"), - (0x1079E, "M", "ɮ"), - (0x1079F, "M", "𝼅"), - (0x107A0, "M", "ʎ"), - (0x107A1, "M", "𝼆"), - (0x107A2, "M", "ø"), - (0x107A3, "M", "ɶ"), - (0x107A4, "M", "ɷ"), - (0x107A5, "M", "q"), - (0x107A6, "M", "ɺ"), - (0x107A7, "M", "𝼈"), - (0x107A8, "M", "ɽ"), - (0x107A9, "M", "ɾ"), - (0x107AA, "M", "ʀ"), - (0x107AB, "M", "ʨ"), - (0x107AC, "M", "ʦ"), - (0x107AD, "M", "ꭧ"), - (0x107AE, "M", "ʧ"), - (0x107AF, "M", "ʈ"), - (0x107B0, "M", "ⱱ"), - (0x107B1, "X"), - (0x107B2, "M", "ʏ"), - (0x107B3, "M", "ʡ"), - (0x107B4, "M", "ʢ"), - (0x107B5, "M", "ʘ"), - (0x107B6, "M", "ǀ"), - (0x107B7, "M", "ǁ"), - (0x107B8, "M", "ǂ"), - (0x107B9, "M", "𝼊"), - (0x107BA, "M", "𝼞"), - (0x107BB, "X"), - (0x10800, "V"), - (0x10806, "X"), - (0x10808, "V"), - (0x10809, "X"), - (0x1080A, "V"), - (0x10836, "X"), - (0x10837, "V"), - (0x10839, "X"), - (0x1083C, "V"), - (0x1083D, "X"), - (0x1083F, "V"), - (0x10856, "X"), - ] - - -def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x10857, "V"), - (0x1089F, "X"), - (0x108A7, "V"), - (0x108B0, "X"), - (0x108E0, "V"), - (0x108F3, "X"), - (0x108F4, "V"), - (0x108F6, "X"), - (0x108FB, "V"), - (0x1091C, "X"), - (0x1091F, "V"), - (0x1093A, "X"), - (0x1093F, "V"), - (0x10940, "X"), - (0x10980, "V"), - (0x109B8, "X"), - (0x109BC, "V"), - (0x109D0, "X"), - (0x109D2, "V"), - (0x10A04, "X"), - (0x10A05, "V"), - (0x10A07, "X"), - (0x10A0C, "V"), - (0x10A14, "X"), - (0x10A15, "V"), - (0x10A18, "X"), - (0x10A19, "V"), - (0x10A36, "X"), - (0x10A38, "V"), - (0x10A3B, "X"), - (0x10A3F, "V"), - (0x10A49, "X"), - (0x10A50, "V"), - (0x10A59, "X"), - (0x10A60, "V"), - (0x10AA0, "X"), - (0x10AC0, "V"), - (0x10AE7, "X"), - (0x10AEB, "V"), - (0x10AF7, "X"), - (0x10B00, "V"), - (0x10B36, "X"), - (0x10B39, "V"), - (0x10B56, "X"), - (0x10B58, "V"), - (0x10B73, "X"), - (0x10B78, "V"), - (0x10B92, "X"), - (0x10B99, "V"), - (0x10B9D, "X"), - (0x10BA9, "V"), - (0x10BB0, "X"), - (0x10C00, "V"), - (0x10C49, "X"), - (0x10C80, "M", "𐳀"), - (0x10C81, "M", "𐳁"), - (0x10C82, "M", "𐳂"), - (0x10C83, "M", "𐳃"), - (0x10C84, "M", "𐳄"), - (0x10C85, "M", "𐳅"), - (0x10C86, "M", "𐳆"), - (0x10C87, "M", "𐳇"), - (0x10C88, "M", "𐳈"), - (0x10C89, "M", "𐳉"), - (0x10C8A, "M", "𐳊"), - (0x10C8B, "M", "𐳋"), - (0x10C8C, "M", "𐳌"), - (0x10C8D, "M", "𐳍"), - (0x10C8E, "M", "𐳎"), - (0x10C8F, "M", "𐳏"), - (0x10C90, "M", "𐳐"), - (0x10C91, "M", "𐳑"), - (0x10C92, "M", "𐳒"), - (0x10C93, "M", "𐳓"), - (0x10C94, "M", "𐳔"), - (0x10C95, "M", "𐳕"), - (0x10C96, "M", "𐳖"), - (0x10C97, "M", "𐳗"), - (0x10C98, "M", "𐳘"), - (0x10C99, "M", "𐳙"), - (0x10C9A, "M", "𐳚"), - (0x10C9B, "M", "𐳛"), - (0x10C9C, "M", "𐳜"), - (0x10C9D, "M", "𐳝"), - (0x10C9E, "M", "𐳞"), - (0x10C9F, "M", "𐳟"), - (0x10CA0, "M", "𐳠"), - (0x10CA1, "M", "𐳡"), - (0x10CA2, "M", "𐳢"), - (0x10CA3, "M", "𐳣"), - (0x10CA4, "M", "𐳤"), - (0x10CA5, "M", "𐳥"), - (0x10CA6, "M", "𐳦"), - (0x10CA7, "M", "𐳧"), - (0x10CA8, "M", "𐳨"), - (0x10CA9, "M", "𐳩"), - (0x10CAA, "M", "𐳪"), - (0x10CAB, "M", "𐳫"), - (0x10CAC, "M", "𐳬"), - (0x10CAD, "M", "𐳭"), - ] - - -def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x10CAE, "M", "𐳮"), - (0x10CAF, "M", "𐳯"), - (0x10CB0, "M", "𐳰"), - (0x10CB1, "M", "𐳱"), - (0x10CB2, "M", "𐳲"), - (0x10CB3, "X"), - (0x10CC0, "V"), - (0x10CF3, "X"), - (0x10CFA, "V"), - (0x10D28, "X"), - (0x10D30, "V"), - (0x10D3A, "X"), - (0x10D40, "V"), - (0x10D50, "M", "𐵰"), - (0x10D51, "M", "𐵱"), - (0x10D52, "M", "𐵲"), - (0x10D53, "M", "𐵳"), - (0x10D54, "M", "𐵴"), - (0x10D55, "M", "𐵵"), - (0x10D56, "M", "𐵶"), - (0x10D57, "M", "𐵷"), - (0x10D58, "M", "𐵸"), - (0x10D59, "M", "𐵹"), - (0x10D5A, "M", "𐵺"), - (0x10D5B, "M", "𐵻"), - (0x10D5C, "M", "𐵼"), - (0x10D5D, "M", "𐵽"), - (0x10D5E, "M", "𐵾"), - (0x10D5F, "M", "𐵿"), - (0x10D60, "M", "𐶀"), - (0x10D61, "M", "𐶁"), - (0x10D62, "M", "𐶂"), - (0x10D63, "M", "𐶃"), - (0x10D64, "M", "𐶄"), - (0x10D65, "M", "𐶅"), - (0x10D66, "X"), - (0x10D69, "V"), - (0x10D86, "X"), - (0x10D8E, "V"), - (0x10D90, "X"), - (0x10E60, "V"), - (0x10E7F, "X"), - (0x10E80, "V"), - (0x10EAA, "X"), - (0x10EAB, "V"), - (0x10EAE, "X"), - (0x10EB0, "V"), - (0x10EB2, "X"), - (0x10EC2, "V"), - (0x10EC5, "X"), - (0x10EFC, "V"), - (0x10F28, "X"), - (0x10F30, "V"), - (0x10F5A, "X"), - (0x10F70, "V"), - (0x10F8A, "X"), - (0x10FB0, "V"), - (0x10FCC, "X"), - (0x10FE0, "V"), - (0x10FF7, "X"), - (0x11000, "V"), - (0x1104E, "X"), - (0x11052, "V"), - (0x11076, "X"), - (0x1107F, "V"), - (0x110BD, "X"), - (0x110BE, "V"), - (0x110C3, "X"), - (0x110D0, "V"), - (0x110E9, "X"), - (0x110F0, "V"), - (0x110FA, "X"), - (0x11100, "V"), - (0x11135, "X"), - (0x11136, "V"), - (0x11148, "X"), - (0x11150, "V"), - (0x11177, "X"), - (0x11180, "V"), - (0x111E0, "X"), - (0x111E1, "V"), - (0x111F5, "X"), - (0x11200, "V"), - (0x11212, "X"), - (0x11213, "V"), - (0x11242, "X"), - (0x11280, "V"), - (0x11287, "X"), - (0x11288, "V"), - (0x11289, "X"), - (0x1128A, "V"), - (0x1128E, "X"), - (0x1128F, "V"), - (0x1129E, "X"), - (0x1129F, "V"), - (0x112AA, "X"), - (0x112B0, "V"), - (0x112EB, "X"), - (0x112F0, "V"), - (0x112FA, "X"), - ] - - -def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x11300, "V"), - (0x11304, "X"), - (0x11305, "V"), - (0x1130D, "X"), - (0x1130F, "V"), - (0x11311, "X"), - (0x11313, "V"), - (0x11329, "X"), - (0x1132A, "V"), - (0x11331, "X"), - (0x11332, "V"), - (0x11334, "X"), - (0x11335, "V"), - (0x1133A, "X"), - (0x1133B, "V"), - (0x11345, "X"), - (0x11347, "V"), - (0x11349, "X"), - (0x1134B, "V"), - (0x1134E, "X"), - (0x11350, "V"), - (0x11351, "X"), - (0x11357, "V"), - (0x11358, "X"), - (0x1135D, "V"), - (0x11364, "X"), - (0x11366, "V"), - (0x1136D, "X"), - (0x11370, "V"), - (0x11375, "X"), - (0x11380, "V"), - (0x1138A, "X"), - (0x1138B, "V"), - (0x1138C, "X"), - (0x1138E, "V"), - (0x1138F, "X"), - (0x11390, "V"), - (0x113B6, "X"), - (0x113B7, "V"), - (0x113C1, "X"), - (0x113C2, "V"), - (0x113C3, "X"), - (0x113C5, "V"), - (0x113C6, "X"), - (0x113C7, "V"), - (0x113CB, "X"), - (0x113CC, "V"), - (0x113D6, "X"), - (0x113D7, "V"), - (0x113D9, "X"), - (0x113E1, "V"), - (0x113E3, "X"), - (0x11400, "V"), - (0x1145C, "X"), - (0x1145D, "V"), - (0x11462, "X"), - (0x11480, "V"), - (0x114C8, "X"), - (0x114D0, "V"), - (0x114DA, "X"), - (0x11580, "V"), - (0x115B6, "X"), - (0x115B8, "V"), - (0x115DE, "X"), - (0x11600, "V"), - (0x11645, "X"), - (0x11650, "V"), - (0x1165A, "X"), - (0x11660, "V"), - (0x1166D, "X"), - (0x11680, "V"), - (0x116BA, "X"), - (0x116C0, "V"), - (0x116CA, "X"), - (0x116D0, "V"), - (0x116E4, "X"), - (0x11700, "V"), - (0x1171B, "X"), - (0x1171D, "V"), - (0x1172C, "X"), - (0x11730, "V"), - (0x11747, "X"), - (0x11800, "V"), - (0x1183C, "X"), - (0x118A0, "M", "𑣀"), - (0x118A1, "M", "𑣁"), - (0x118A2, "M", "𑣂"), - (0x118A3, "M", "𑣃"), - (0x118A4, "M", "𑣄"), - (0x118A5, "M", "𑣅"), - (0x118A6, "M", "𑣆"), - (0x118A7, "M", "𑣇"), - (0x118A8, "M", "𑣈"), - (0x118A9, "M", "𑣉"), - (0x118AA, "M", "𑣊"), - (0x118AB, "M", "𑣋"), - (0x118AC, "M", "𑣌"), - (0x118AD, "M", "𑣍"), - (0x118AE, "M", "𑣎"), - (0x118AF, "M", "𑣏"), - ] - - -def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x118B0, "M", "𑣐"), - (0x118B1, "M", "𑣑"), - (0x118B2, "M", "𑣒"), - (0x118B3, "M", "𑣓"), - (0x118B4, "M", "𑣔"), - (0x118B5, "M", "𑣕"), - (0x118B6, "M", "𑣖"), - (0x118B7, "M", "𑣗"), - (0x118B8, "M", "𑣘"), - (0x118B9, "M", "𑣙"), - (0x118BA, "M", "𑣚"), - (0x118BB, "M", "𑣛"), - (0x118BC, "M", "𑣜"), - (0x118BD, "M", "𑣝"), - (0x118BE, "M", "𑣞"), - (0x118BF, "M", "𑣟"), - (0x118C0, "V"), - (0x118F3, "X"), - (0x118FF, "V"), - (0x11907, "X"), - (0x11909, "V"), - (0x1190A, "X"), - (0x1190C, "V"), - (0x11914, "X"), - (0x11915, "V"), - (0x11917, "X"), - (0x11918, "V"), - (0x11936, "X"), - (0x11937, "V"), - (0x11939, "X"), - (0x1193B, "V"), - (0x11947, "X"), - (0x11950, "V"), - (0x1195A, "X"), - (0x119A0, "V"), - (0x119A8, "X"), - (0x119AA, "V"), - (0x119D8, "X"), - (0x119DA, "V"), - (0x119E5, "X"), - (0x11A00, "V"), - (0x11A48, "X"), - (0x11A50, "V"), - (0x11AA3, "X"), - (0x11AB0, "V"), - (0x11AF9, "X"), - (0x11B00, "V"), - (0x11B0A, "X"), - (0x11BC0, "V"), - (0x11BE2, "X"), - (0x11BF0, "V"), - (0x11BFA, "X"), - (0x11C00, "V"), - (0x11C09, "X"), - (0x11C0A, "V"), - (0x11C37, "X"), - (0x11C38, "V"), - (0x11C46, "X"), - (0x11C50, "V"), - (0x11C6D, "X"), - (0x11C70, "V"), - (0x11C90, "X"), - (0x11C92, "V"), - (0x11CA8, "X"), - (0x11CA9, "V"), - (0x11CB7, "X"), - (0x11D00, "V"), - (0x11D07, "X"), - (0x11D08, "V"), - (0x11D0A, "X"), - (0x11D0B, "V"), - (0x11D37, "X"), - (0x11D3A, "V"), - (0x11D3B, "X"), - (0x11D3C, "V"), - (0x11D3E, "X"), - (0x11D3F, "V"), - (0x11D48, "X"), - (0x11D50, "V"), - (0x11D5A, "X"), - (0x11D60, "V"), - (0x11D66, "X"), - (0x11D67, "V"), - (0x11D69, "X"), - (0x11D6A, "V"), - (0x11D8F, "X"), - (0x11D90, "V"), - (0x11D92, "X"), - (0x11D93, "V"), - (0x11D99, "X"), - (0x11DA0, "V"), - (0x11DAA, "X"), - (0x11EE0, "V"), - (0x11EF9, "X"), - (0x11F00, "V"), - (0x11F11, "X"), - (0x11F12, "V"), - (0x11F3B, "X"), - (0x11F3E, "V"), - (0x11F5B, "X"), - ] - - -def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x11FB0, "V"), - (0x11FB1, "X"), - (0x11FC0, "V"), - (0x11FF2, "X"), - (0x11FFF, "V"), - (0x1239A, "X"), - (0x12400, "V"), - (0x1246F, "X"), - (0x12470, "V"), - (0x12475, "X"), - (0x12480, "V"), - (0x12544, "X"), - (0x12F90, "V"), - (0x12FF3, "X"), - (0x13000, "V"), - (0x13430, "X"), - (0x13440, "V"), - (0x13456, "X"), - (0x13460, "V"), - (0x143FB, "X"), - (0x14400, "V"), - (0x14647, "X"), - (0x16100, "V"), - (0x1613A, "X"), - (0x16800, "V"), - (0x16A39, "X"), - (0x16A40, "V"), - (0x16A5F, "X"), - (0x16A60, "V"), - (0x16A6A, "X"), - (0x16A6E, "V"), - (0x16ABF, "X"), - (0x16AC0, "V"), - (0x16ACA, "X"), - (0x16AD0, "V"), - (0x16AEE, "X"), - (0x16AF0, "V"), - (0x16AF6, "X"), - (0x16B00, "V"), - (0x16B46, "X"), - (0x16B50, "V"), - (0x16B5A, "X"), - (0x16B5B, "V"), - (0x16B62, "X"), - (0x16B63, "V"), - (0x16B78, "X"), - (0x16B7D, "V"), - (0x16B90, "X"), - (0x16D40, "V"), - (0x16D7A, "X"), - (0x16E40, "M", "𖹠"), - (0x16E41, "M", "𖹡"), - (0x16E42, "M", "𖹢"), - (0x16E43, "M", "𖹣"), - (0x16E44, "M", "𖹤"), - (0x16E45, "M", "𖹥"), - (0x16E46, "M", "𖹦"), - (0x16E47, "M", "𖹧"), - (0x16E48, "M", "𖹨"), - (0x16E49, "M", "𖹩"), - (0x16E4A, "M", "𖹪"), - (0x16E4B, "M", "𖹫"), - (0x16E4C, "M", "𖹬"), - (0x16E4D, "M", "𖹭"), - (0x16E4E, "M", "𖹮"), - (0x16E4F, "M", "𖹯"), - (0x16E50, "M", "𖹰"), - (0x16E51, "M", "𖹱"), - (0x16E52, "M", "𖹲"), - (0x16E53, "M", "𖹳"), - (0x16E54, "M", "𖹴"), - (0x16E55, "M", "𖹵"), - (0x16E56, "M", "𖹶"), - (0x16E57, "M", "𖹷"), - (0x16E58, "M", "𖹸"), - (0x16E59, "M", "𖹹"), - (0x16E5A, "M", "𖹺"), - (0x16E5B, "M", "𖹻"), - (0x16E5C, "M", "𖹼"), - (0x16E5D, "M", "𖹽"), - (0x16E5E, "M", "𖹾"), - (0x16E5F, "M", "𖹿"), - (0x16E60, "V"), - (0x16E9B, "X"), - (0x16F00, "V"), - (0x16F4B, "X"), - (0x16F4F, "V"), - (0x16F88, "X"), - (0x16F8F, "V"), - (0x16FA0, "X"), - (0x16FE0, "V"), - (0x16FE5, "X"), - (0x16FF0, "V"), - (0x16FF2, "X"), - (0x17000, "V"), - (0x187F8, "X"), - (0x18800, "V"), - (0x18CD6, "X"), - (0x18CFF, "V"), - (0x18D09, "X"), - ] - - -def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1AFF0, "V"), - (0x1AFF4, "X"), - (0x1AFF5, "V"), - (0x1AFFC, "X"), - (0x1AFFD, "V"), - (0x1AFFF, "X"), - (0x1B000, "V"), - (0x1B123, "X"), - (0x1B132, "V"), - (0x1B133, "X"), - (0x1B150, "V"), - (0x1B153, "X"), - (0x1B155, "V"), - (0x1B156, "X"), - (0x1B164, "V"), - (0x1B168, "X"), - (0x1B170, "V"), - (0x1B2FC, "X"), - (0x1BC00, "V"), - (0x1BC6B, "X"), - (0x1BC70, "V"), - (0x1BC7D, "X"), - (0x1BC80, "V"), - (0x1BC89, "X"), - (0x1BC90, "V"), - (0x1BC9A, "X"), - (0x1BC9C, "V"), - (0x1BCA0, "I"), - (0x1BCA4, "X"), - (0x1CC00, "V"), - (0x1CCD6, "M", "a"), - (0x1CCD7, "M", "b"), - (0x1CCD8, "M", "c"), - (0x1CCD9, "M", "d"), - (0x1CCDA, "M", "e"), - (0x1CCDB, "M", "f"), - (0x1CCDC, "M", "g"), - (0x1CCDD, "M", "h"), - (0x1CCDE, "M", "i"), - (0x1CCDF, "M", "j"), - (0x1CCE0, "M", "k"), - (0x1CCE1, "M", "l"), - (0x1CCE2, "M", "m"), - (0x1CCE3, "M", "n"), - (0x1CCE4, "M", "o"), - (0x1CCE5, "M", "p"), - (0x1CCE6, "M", "q"), - (0x1CCE7, "M", "r"), - (0x1CCE8, "M", "s"), - (0x1CCE9, "M", "t"), - (0x1CCEA, "M", "u"), - (0x1CCEB, "M", "v"), - (0x1CCEC, "M", "w"), - (0x1CCED, "M", "x"), - (0x1CCEE, "M", "y"), - (0x1CCEF, "M", "z"), - (0x1CCF0, "M", "0"), - (0x1CCF1, "M", "1"), - (0x1CCF2, "M", "2"), - (0x1CCF3, "M", "3"), - (0x1CCF4, "M", "4"), - (0x1CCF5, "M", "5"), - (0x1CCF6, "M", "6"), - (0x1CCF7, "M", "7"), - (0x1CCF8, "M", "8"), - (0x1CCF9, "M", "9"), - (0x1CCFA, "X"), - (0x1CD00, "V"), - (0x1CEB4, "X"), - (0x1CF00, "V"), - (0x1CF2E, "X"), - (0x1CF30, "V"), - (0x1CF47, "X"), - (0x1CF50, "V"), - (0x1CFC4, "X"), - (0x1D000, "V"), - (0x1D0F6, "X"), - (0x1D100, "V"), - (0x1D127, "X"), - (0x1D129, "V"), - (0x1D15E, "M", "𝅗𝅥"), - (0x1D15F, "M", "𝅘𝅥"), - (0x1D160, "M", "𝅘𝅥𝅮"), - (0x1D161, "M", "𝅘𝅥𝅯"), - (0x1D162, "M", "𝅘𝅥𝅰"), - (0x1D163, "M", "𝅘𝅥𝅱"), - (0x1D164, "M", "𝅘𝅥𝅲"), - (0x1D165, "V"), - (0x1D173, "I"), - (0x1D17B, "V"), - (0x1D1BB, "M", "𝆹𝅥"), - (0x1D1BC, "M", "𝆺𝅥"), - (0x1D1BD, "M", "𝆹𝅥𝅮"), - (0x1D1BE, "M", "𝆺𝅥𝅮"), - (0x1D1BF, "M", "𝆹𝅥𝅯"), - (0x1D1C0, "M", "𝆺𝅥𝅯"), - (0x1D1C1, "V"), - (0x1D1EB, "X"), - (0x1D200, "V"), - (0x1D246, "X"), - ] - - -def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D2C0, "V"), - (0x1D2D4, "X"), - (0x1D2E0, "V"), - (0x1D2F4, "X"), - (0x1D300, "V"), - (0x1D357, "X"), - (0x1D360, "V"), - (0x1D379, "X"), - (0x1D400, "M", "a"), - (0x1D401, "M", "b"), - (0x1D402, "M", "c"), - (0x1D403, "M", "d"), - (0x1D404, "M", "e"), - (0x1D405, "M", "f"), - (0x1D406, "M", "g"), - (0x1D407, "M", "h"), - (0x1D408, "M", "i"), - (0x1D409, "M", "j"), - (0x1D40A, "M", "k"), - (0x1D40B, "M", "l"), - (0x1D40C, "M", "m"), - (0x1D40D, "M", "n"), - (0x1D40E, "M", "o"), - (0x1D40F, "M", "p"), - (0x1D410, "M", "q"), - (0x1D411, "M", "r"), - (0x1D412, "M", "s"), - (0x1D413, "M", "t"), - (0x1D414, "M", "u"), - (0x1D415, "M", "v"), - (0x1D416, "M", "w"), - (0x1D417, "M", "x"), - (0x1D418, "M", "y"), - (0x1D419, "M", "z"), - (0x1D41A, "M", "a"), - (0x1D41B, "M", "b"), - (0x1D41C, "M", "c"), - (0x1D41D, "M", "d"), - (0x1D41E, "M", "e"), - (0x1D41F, "M", "f"), - (0x1D420, "M", "g"), - (0x1D421, "M", "h"), - (0x1D422, "M", "i"), - (0x1D423, "M", "j"), - (0x1D424, "M", "k"), - (0x1D425, "M", "l"), - (0x1D426, "M", "m"), - (0x1D427, "M", "n"), - (0x1D428, "M", "o"), - (0x1D429, "M", "p"), - (0x1D42A, "M", "q"), - (0x1D42B, "M", "r"), - (0x1D42C, "M", "s"), - (0x1D42D, "M", "t"), - (0x1D42E, "M", "u"), - (0x1D42F, "M", "v"), - (0x1D430, "M", "w"), - (0x1D431, "M", "x"), - (0x1D432, "M", "y"), - (0x1D433, "M", "z"), - (0x1D434, "M", "a"), - (0x1D435, "M", "b"), - (0x1D436, "M", "c"), - (0x1D437, "M", "d"), - (0x1D438, "M", "e"), - (0x1D439, "M", "f"), - (0x1D43A, "M", "g"), - (0x1D43B, "M", "h"), - (0x1D43C, "M", "i"), - (0x1D43D, "M", "j"), - (0x1D43E, "M", "k"), - (0x1D43F, "M", "l"), - (0x1D440, "M", "m"), - (0x1D441, "M", "n"), - (0x1D442, "M", "o"), - (0x1D443, "M", "p"), - (0x1D444, "M", "q"), - (0x1D445, "M", "r"), - (0x1D446, "M", "s"), - (0x1D447, "M", "t"), - (0x1D448, "M", "u"), - (0x1D449, "M", "v"), - (0x1D44A, "M", "w"), - (0x1D44B, "M", "x"), - (0x1D44C, "M", "y"), - (0x1D44D, "M", "z"), - (0x1D44E, "M", "a"), - (0x1D44F, "M", "b"), - (0x1D450, "M", "c"), - (0x1D451, "M", "d"), - (0x1D452, "M", "e"), - (0x1D453, "M", "f"), - (0x1D454, "M", "g"), - (0x1D455, "X"), - (0x1D456, "M", "i"), - (0x1D457, "M", "j"), - (0x1D458, "M", "k"), - (0x1D459, "M", "l"), - (0x1D45A, "M", "m"), - (0x1D45B, "M", "n"), - ] - - -def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D45C, "M", "o"), - (0x1D45D, "M", "p"), - (0x1D45E, "M", "q"), - (0x1D45F, "M", "r"), - (0x1D460, "M", "s"), - (0x1D461, "M", "t"), - (0x1D462, "M", "u"), - (0x1D463, "M", "v"), - (0x1D464, "M", "w"), - (0x1D465, "M", "x"), - (0x1D466, "M", "y"), - (0x1D467, "M", "z"), - (0x1D468, "M", "a"), - (0x1D469, "M", "b"), - (0x1D46A, "M", "c"), - (0x1D46B, "M", "d"), - (0x1D46C, "M", "e"), - (0x1D46D, "M", "f"), - (0x1D46E, "M", "g"), - (0x1D46F, "M", "h"), - (0x1D470, "M", "i"), - (0x1D471, "M", "j"), - (0x1D472, "M", "k"), - (0x1D473, "M", "l"), - (0x1D474, "M", "m"), - (0x1D475, "M", "n"), - (0x1D476, "M", "o"), - (0x1D477, "M", "p"), - (0x1D478, "M", "q"), - (0x1D479, "M", "r"), - (0x1D47A, "M", "s"), - (0x1D47B, "M", "t"), - (0x1D47C, "M", "u"), - (0x1D47D, "M", "v"), - (0x1D47E, "M", "w"), - (0x1D47F, "M", "x"), - (0x1D480, "M", "y"), - (0x1D481, "M", "z"), - (0x1D482, "M", "a"), - (0x1D483, "M", "b"), - (0x1D484, "M", "c"), - (0x1D485, "M", "d"), - (0x1D486, "M", "e"), - (0x1D487, "M", "f"), - (0x1D488, "M", "g"), - (0x1D489, "M", "h"), - (0x1D48A, "M", "i"), - (0x1D48B, "M", "j"), - (0x1D48C, "M", "k"), - (0x1D48D, "M", "l"), - (0x1D48E, "M", "m"), - (0x1D48F, "M", "n"), - (0x1D490, "M", "o"), - (0x1D491, "M", "p"), - (0x1D492, "M", "q"), - (0x1D493, "M", "r"), - (0x1D494, "M", "s"), - (0x1D495, "M", "t"), - (0x1D496, "M", "u"), - (0x1D497, "M", "v"), - (0x1D498, "M", "w"), - (0x1D499, "M", "x"), - (0x1D49A, "M", "y"), - (0x1D49B, "M", "z"), - (0x1D49C, "M", "a"), - (0x1D49D, "X"), - (0x1D49E, "M", "c"), - (0x1D49F, "M", "d"), - (0x1D4A0, "X"), - (0x1D4A2, "M", "g"), - (0x1D4A3, "X"), - (0x1D4A5, "M", "j"), - (0x1D4A6, "M", "k"), - (0x1D4A7, "X"), - (0x1D4A9, "M", "n"), - (0x1D4AA, "M", "o"), - (0x1D4AB, "M", "p"), - (0x1D4AC, "M", "q"), - (0x1D4AD, "X"), - (0x1D4AE, "M", "s"), - (0x1D4AF, "M", "t"), - (0x1D4B0, "M", "u"), - (0x1D4B1, "M", "v"), - (0x1D4B2, "M", "w"), - (0x1D4B3, "M", "x"), - (0x1D4B4, "M", "y"), - (0x1D4B5, "M", "z"), - (0x1D4B6, "M", "a"), - (0x1D4B7, "M", "b"), - (0x1D4B8, "M", "c"), - (0x1D4B9, "M", "d"), - (0x1D4BA, "X"), - (0x1D4BB, "M", "f"), - (0x1D4BC, "X"), - (0x1D4BD, "M", "h"), - (0x1D4BE, "M", "i"), - (0x1D4BF, "M", "j"), - (0x1D4C0, "M", "k"), - (0x1D4C1, "M", "l"), - (0x1D4C2, "M", "m"), - ] - - -def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D4C3, "M", "n"), - (0x1D4C4, "X"), - (0x1D4C5, "M", "p"), - (0x1D4C6, "M", "q"), - (0x1D4C7, "M", "r"), - (0x1D4C8, "M", "s"), - (0x1D4C9, "M", "t"), - (0x1D4CA, "M", "u"), - (0x1D4CB, "M", "v"), - (0x1D4CC, "M", "w"), - (0x1D4CD, "M", "x"), - (0x1D4CE, "M", "y"), - (0x1D4CF, "M", "z"), - (0x1D4D0, "M", "a"), - (0x1D4D1, "M", "b"), - (0x1D4D2, "M", "c"), - (0x1D4D3, "M", "d"), - (0x1D4D4, "M", "e"), - (0x1D4D5, "M", "f"), - (0x1D4D6, "M", "g"), - (0x1D4D7, "M", "h"), - (0x1D4D8, "M", "i"), - (0x1D4D9, "M", "j"), - (0x1D4DA, "M", "k"), - (0x1D4DB, "M", "l"), - (0x1D4DC, "M", "m"), - (0x1D4DD, "M", "n"), - (0x1D4DE, "M", "o"), - (0x1D4DF, "M", "p"), - (0x1D4E0, "M", "q"), - (0x1D4E1, "M", "r"), - (0x1D4E2, "M", "s"), - (0x1D4E3, "M", "t"), - (0x1D4E4, "M", "u"), - (0x1D4E5, "M", "v"), - (0x1D4E6, "M", "w"), - (0x1D4E7, "M", "x"), - (0x1D4E8, "M", "y"), - (0x1D4E9, "M", "z"), - (0x1D4EA, "M", "a"), - (0x1D4EB, "M", "b"), - (0x1D4EC, "M", "c"), - (0x1D4ED, "M", "d"), - (0x1D4EE, "M", "e"), - (0x1D4EF, "M", "f"), - (0x1D4F0, "M", "g"), - (0x1D4F1, "M", "h"), - (0x1D4F2, "M", "i"), - (0x1D4F3, "M", "j"), - (0x1D4F4, "M", "k"), - (0x1D4F5, "M", "l"), - (0x1D4F6, "M", "m"), - (0x1D4F7, "M", "n"), - (0x1D4F8, "M", "o"), - (0x1D4F9, "M", "p"), - (0x1D4FA, "M", "q"), - (0x1D4FB, "M", "r"), - (0x1D4FC, "M", "s"), - (0x1D4FD, "M", "t"), - (0x1D4FE, "M", "u"), - (0x1D4FF, "M", "v"), - (0x1D500, "M", "w"), - (0x1D501, "M", "x"), - (0x1D502, "M", "y"), - (0x1D503, "M", "z"), - (0x1D504, "M", "a"), - (0x1D505, "M", "b"), - (0x1D506, "X"), - (0x1D507, "M", "d"), - (0x1D508, "M", "e"), - (0x1D509, "M", "f"), - (0x1D50A, "M", "g"), - (0x1D50B, "X"), - (0x1D50D, "M", "j"), - (0x1D50E, "M", "k"), - (0x1D50F, "M", "l"), - (0x1D510, "M", "m"), - (0x1D511, "M", "n"), - (0x1D512, "M", "o"), - (0x1D513, "M", "p"), - (0x1D514, "M", "q"), - (0x1D515, "X"), - (0x1D516, "M", "s"), - (0x1D517, "M", "t"), - (0x1D518, "M", "u"), - (0x1D519, "M", "v"), - (0x1D51A, "M", "w"), - (0x1D51B, "M", "x"), - (0x1D51C, "M", "y"), - (0x1D51D, "X"), - (0x1D51E, "M", "a"), - (0x1D51F, "M", "b"), - (0x1D520, "M", "c"), - (0x1D521, "M", "d"), - (0x1D522, "M", "e"), - (0x1D523, "M", "f"), - (0x1D524, "M", "g"), - (0x1D525, "M", "h"), - (0x1D526, "M", "i"), - (0x1D527, "M", "j"), - ] - - -def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D528, "M", "k"), - (0x1D529, "M", "l"), - (0x1D52A, "M", "m"), - (0x1D52B, "M", "n"), - (0x1D52C, "M", "o"), - (0x1D52D, "M", "p"), - (0x1D52E, "M", "q"), - (0x1D52F, "M", "r"), - (0x1D530, "M", "s"), - (0x1D531, "M", "t"), - (0x1D532, "M", "u"), - (0x1D533, "M", "v"), - (0x1D534, "M", "w"), - (0x1D535, "M", "x"), - (0x1D536, "M", "y"), - (0x1D537, "M", "z"), - (0x1D538, "M", "a"), - (0x1D539, "M", "b"), - (0x1D53A, "X"), - (0x1D53B, "M", "d"), - (0x1D53C, "M", "e"), - (0x1D53D, "M", "f"), - (0x1D53E, "M", "g"), - (0x1D53F, "X"), - (0x1D540, "M", "i"), - (0x1D541, "M", "j"), - (0x1D542, "M", "k"), - (0x1D543, "M", "l"), - (0x1D544, "M", "m"), - (0x1D545, "X"), - (0x1D546, "M", "o"), - (0x1D547, "X"), - (0x1D54A, "M", "s"), - (0x1D54B, "M", "t"), - (0x1D54C, "M", "u"), - (0x1D54D, "M", "v"), - (0x1D54E, "M", "w"), - (0x1D54F, "M", "x"), - (0x1D550, "M", "y"), - (0x1D551, "X"), - (0x1D552, "M", "a"), - (0x1D553, "M", "b"), - (0x1D554, "M", "c"), - (0x1D555, "M", "d"), - (0x1D556, "M", "e"), - (0x1D557, "M", "f"), - (0x1D558, "M", "g"), - (0x1D559, "M", "h"), - (0x1D55A, "M", "i"), - (0x1D55B, "M", "j"), - (0x1D55C, "M", "k"), - (0x1D55D, "M", "l"), - (0x1D55E, "M", "m"), - (0x1D55F, "M", "n"), - (0x1D560, "M", "o"), - (0x1D561, "M", "p"), - (0x1D562, "M", "q"), - (0x1D563, "M", "r"), - (0x1D564, "M", "s"), - (0x1D565, "M", "t"), - (0x1D566, "M", "u"), - (0x1D567, "M", "v"), - (0x1D568, "M", "w"), - (0x1D569, "M", "x"), - (0x1D56A, "M", "y"), - (0x1D56B, "M", "z"), - (0x1D56C, "M", "a"), - (0x1D56D, "M", "b"), - (0x1D56E, "M", "c"), - (0x1D56F, "M", "d"), - (0x1D570, "M", "e"), - (0x1D571, "M", "f"), - (0x1D572, "M", "g"), - (0x1D573, "M", "h"), - (0x1D574, "M", "i"), - (0x1D575, "M", "j"), - (0x1D576, "M", "k"), - (0x1D577, "M", "l"), - (0x1D578, "M", "m"), - (0x1D579, "M", "n"), - (0x1D57A, "M", "o"), - (0x1D57B, "M", "p"), - (0x1D57C, "M", "q"), - (0x1D57D, "M", "r"), - (0x1D57E, "M", "s"), - (0x1D57F, "M", "t"), - (0x1D580, "M", "u"), - (0x1D581, "M", "v"), - (0x1D582, "M", "w"), - (0x1D583, "M", "x"), - (0x1D584, "M", "y"), - (0x1D585, "M", "z"), - (0x1D586, "M", "a"), - (0x1D587, "M", "b"), - (0x1D588, "M", "c"), - (0x1D589, "M", "d"), - (0x1D58A, "M", "e"), - (0x1D58B, "M", "f"), - (0x1D58C, "M", "g"), - (0x1D58D, "M", "h"), - ] - - -def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D58E, "M", "i"), - (0x1D58F, "M", "j"), - (0x1D590, "M", "k"), - (0x1D591, "M", "l"), - (0x1D592, "M", "m"), - (0x1D593, "M", "n"), - (0x1D594, "M", "o"), - (0x1D595, "M", "p"), - (0x1D596, "M", "q"), - (0x1D597, "M", "r"), - (0x1D598, "M", "s"), - (0x1D599, "M", "t"), - (0x1D59A, "M", "u"), - (0x1D59B, "M", "v"), - (0x1D59C, "M", "w"), - (0x1D59D, "M", "x"), - (0x1D59E, "M", "y"), - (0x1D59F, "M", "z"), - (0x1D5A0, "M", "a"), - (0x1D5A1, "M", "b"), - (0x1D5A2, "M", "c"), - (0x1D5A3, "M", "d"), - (0x1D5A4, "M", "e"), - (0x1D5A5, "M", "f"), - (0x1D5A6, "M", "g"), - (0x1D5A7, "M", "h"), - (0x1D5A8, "M", "i"), - (0x1D5A9, "M", "j"), - (0x1D5AA, "M", "k"), - (0x1D5AB, "M", "l"), - (0x1D5AC, "M", "m"), - (0x1D5AD, "M", "n"), - (0x1D5AE, "M", "o"), - (0x1D5AF, "M", "p"), - (0x1D5B0, "M", "q"), - (0x1D5B1, "M", "r"), - (0x1D5B2, "M", "s"), - (0x1D5B3, "M", "t"), - (0x1D5B4, "M", "u"), - (0x1D5B5, "M", "v"), - (0x1D5B6, "M", "w"), - (0x1D5B7, "M", "x"), - (0x1D5B8, "M", "y"), - (0x1D5B9, "M", "z"), - (0x1D5BA, "M", "a"), - (0x1D5BB, "M", "b"), - (0x1D5BC, "M", "c"), - (0x1D5BD, "M", "d"), - (0x1D5BE, "M", "e"), - (0x1D5BF, "M", "f"), - (0x1D5C0, "M", "g"), - (0x1D5C1, "M", "h"), - (0x1D5C2, "M", "i"), - (0x1D5C3, "M", "j"), - (0x1D5C4, "M", "k"), - (0x1D5C5, "M", "l"), - (0x1D5C6, "M", "m"), - (0x1D5C7, "M", "n"), - (0x1D5C8, "M", "o"), - (0x1D5C9, "M", "p"), - (0x1D5CA, "M", "q"), - (0x1D5CB, "M", "r"), - (0x1D5CC, "M", "s"), - (0x1D5CD, "M", "t"), - (0x1D5CE, "M", "u"), - (0x1D5CF, "M", "v"), - (0x1D5D0, "M", "w"), - (0x1D5D1, "M", "x"), - (0x1D5D2, "M", "y"), - (0x1D5D3, "M", "z"), - (0x1D5D4, "M", "a"), - (0x1D5D5, "M", "b"), - (0x1D5D6, "M", "c"), - (0x1D5D7, "M", "d"), - (0x1D5D8, "M", "e"), - (0x1D5D9, "M", "f"), - (0x1D5DA, "M", "g"), - (0x1D5DB, "M", "h"), - (0x1D5DC, "M", "i"), - (0x1D5DD, "M", "j"), - (0x1D5DE, "M", "k"), - (0x1D5DF, "M", "l"), - (0x1D5E0, "M", "m"), - (0x1D5E1, "M", "n"), - (0x1D5E2, "M", "o"), - (0x1D5E3, "M", "p"), - (0x1D5E4, "M", "q"), - (0x1D5E5, "M", "r"), - (0x1D5E6, "M", "s"), - (0x1D5E7, "M", "t"), - (0x1D5E8, "M", "u"), - (0x1D5E9, "M", "v"), - (0x1D5EA, "M", "w"), - (0x1D5EB, "M", "x"), - (0x1D5EC, "M", "y"), - (0x1D5ED, "M", "z"), - (0x1D5EE, "M", "a"), - (0x1D5EF, "M", "b"), - (0x1D5F0, "M", "c"), - (0x1D5F1, "M", "d"), - ] - - -def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D5F2, "M", "e"), - (0x1D5F3, "M", "f"), - (0x1D5F4, "M", "g"), - (0x1D5F5, "M", "h"), - (0x1D5F6, "M", "i"), - (0x1D5F7, "M", "j"), - (0x1D5F8, "M", "k"), - (0x1D5F9, "M", "l"), - (0x1D5FA, "M", "m"), - (0x1D5FB, "M", "n"), - (0x1D5FC, "M", "o"), - (0x1D5FD, "M", "p"), - (0x1D5FE, "M", "q"), - (0x1D5FF, "M", "r"), - (0x1D600, "M", "s"), - (0x1D601, "M", "t"), - (0x1D602, "M", "u"), - (0x1D603, "M", "v"), - (0x1D604, "M", "w"), - (0x1D605, "M", "x"), - (0x1D606, "M", "y"), - (0x1D607, "M", "z"), - (0x1D608, "M", "a"), - (0x1D609, "M", "b"), - (0x1D60A, "M", "c"), - (0x1D60B, "M", "d"), - (0x1D60C, "M", "e"), - (0x1D60D, "M", "f"), - (0x1D60E, "M", "g"), - (0x1D60F, "M", "h"), - (0x1D610, "M", "i"), - (0x1D611, "M", "j"), - (0x1D612, "M", "k"), - (0x1D613, "M", "l"), - (0x1D614, "M", "m"), - (0x1D615, "M", "n"), - (0x1D616, "M", "o"), - (0x1D617, "M", "p"), - (0x1D618, "M", "q"), - (0x1D619, "M", "r"), - (0x1D61A, "M", "s"), - (0x1D61B, "M", "t"), - (0x1D61C, "M", "u"), - (0x1D61D, "M", "v"), - (0x1D61E, "M", "w"), - (0x1D61F, "M", "x"), - (0x1D620, "M", "y"), - (0x1D621, "M", "z"), - (0x1D622, "M", "a"), - (0x1D623, "M", "b"), - (0x1D624, "M", "c"), - (0x1D625, "M", "d"), - (0x1D626, "M", "e"), - (0x1D627, "M", "f"), - (0x1D628, "M", "g"), - (0x1D629, "M", "h"), - (0x1D62A, "M", "i"), - (0x1D62B, "M", "j"), - (0x1D62C, "M", "k"), - (0x1D62D, "M", "l"), - (0x1D62E, "M", "m"), - (0x1D62F, "M", "n"), - (0x1D630, "M", "o"), - (0x1D631, "M", "p"), - (0x1D632, "M", "q"), - (0x1D633, "M", "r"), - (0x1D634, "M", "s"), - (0x1D635, "M", "t"), - (0x1D636, "M", "u"), - (0x1D637, "M", "v"), - (0x1D638, "M", "w"), - (0x1D639, "M", "x"), - (0x1D63A, "M", "y"), - (0x1D63B, "M", "z"), - (0x1D63C, "M", "a"), - (0x1D63D, "M", "b"), - (0x1D63E, "M", "c"), - (0x1D63F, "M", "d"), - (0x1D640, "M", "e"), - (0x1D641, "M", "f"), - (0x1D642, "M", "g"), - (0x1D643, "M", "h"), - (0x1D644, "M", "i"), - (0x1D645, "M", "j"), - (0x1D646, "M", "k"), - (0x1D647, "M", "l"), - (0x1D648, "M", "m"), - (0x1D649, "M", "n"), - (0x1D64A, "M", "o"), - (0x1D64B, "M", "p"), - (0x1D64C, "M", "q"), - (0x1D64D, "M", "r"), - (0x1D64E, "M", "s"), - (0x1D64F, "M", "t"), - (0x1D650, "M", "u"), - (0x1D651, "M", "v"), - (0x1D652, "M", "w"), - (0x1D653, "M", "x"), - (0x1D654, "M", "y"), - (0x1D655, "M", "z"), - ] - - -def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D656, "M", "a"), - (0x1D657, "M", "b"), - (0x1D658, "M", "c"), - (0x1D659, "M", "d"), - (0x1D65A, "M", "e"), - (0x1D65B, "M", "f"), - (0x1D65C, "M", "g"), - (0x1D65D, "M", "h"), - (0x1D65E, "M", "i"), - (0x1D65F, "M", "j"), - (0x1D660, "M", "k"), - (0x1D661, "M", "l"), - (0x1D662, "M", "m"), - (0x1D663, "M", "n"), - (0x1D664, "M", "o"), - (0x1D665, "M", "p"), - (0x1D666, "M", "q"), - (0x1D667, "M", "r"), - (0x1D668, "M", "s"), - (0x1D669, "M", "t"), - (0x1D66A, "M", "u"), - (0x1D66B, "M", "v"), - (0x1D66C, "M", "w"), - (0x1D66D, "M", "x"), - (0x1D66E, "M", "y"), - (0x1D66F, "M", "z"), - (0x1D670, "M", "a"), - (0x1D671, "M", "b"), - (0x1D672, "M", "c"), - (0x1D673, "M", "d"), - (0x1D674, "M", "e"), - (0x1D675, "M", "f"), - (0x1D676, "M", "g"), - (0x1D677, "M", "h"), - (0x1D678, "M", "i"), - (0x1D679, "M", "j"), - (0x1D67A, "M", "k"), - (0x1D67B, "M", "l"), - (0x1D67C, "M", "m"), - (0x1D67D, "M", "n"), - (0x1D67E, "M", "o"), - (0x1D67F, "M", "p"), - (0x1D680, "M", "q"), - (0x1D681, "M", "r"), - (0x1D682, "M", "s"), - (0x1D683, "M", "t"), - (0x1D684, "M", "u"), - (0x1D685, "M", "v"), - (0x1D686, "M", "w"), - (0x1D687, "M", "x"), - (0x1D688, "M", "y"), - (0x1D689, "M", "z"), - (0x1D68A, "M", "a"), - (0x1D68B, "M", "b"), - (0x1D68C, "M", "c"), - (0x1D68D, "M", "d"), - (0x1D68E, "M", "e"), - (0x1D68F, "M", "f"), - (0x1D690, "M", "g"), - (0x1D691, "M", "h"), - (0x1D692, "M", "i"), - (0x1D693, "M", "j"), - (0x1D694, "M", "k"), - (0x1D695, "M", "l"), - (0x1D696, "M", "m"), - (0x1D697, "M", "n"), - (0x1D698, "M", "o"), - (0x1D699, "M", "p"), - (0x1D69A, "M", "q"), - (0x1D69B, "M", "r"), - (0x1D69C, "M", "s"), - (0x1D69D, "M", "t"), - (0x1D69E, "M", "u"), - (0x1D69F, "M", "v"), - (0x1D6A0, "M", "w"), - (0x1D6A1, "M", "x"), - (0x1D6A2, "M", "y"), - (0x1D6A3, "M", "z"), - (0x1D6A4, "M", "ı"), - (0x1D6A5, "M", "ȷ"), - (0x1D6A6, "X"), - (0x1D6A8, "M", "α"), - (0x1D6A9, "M", "β"), - (0x1D6AA, "M", "γ"), - (0x1D6AB, "M", "δ"), - (0x1D6AC, "M", "ε"), - (0x1D6AD, "M", "ζ"), - (0x1D6AE, "M", "η"), - (0x1D6AF, "M", "θ"), - (0x1D6B0, "M", "ι"), - (0x1D6B1, "M", "κ"), - (0x1D6B2, "M", "λ"), - (0x1D6B3, "M", "μ"), - (0x1D6B4, "M", "ν"), - (0x1D6B5, "M", "ξ"), - (0x1D6B6, "M", "ο"), - (0x1D6B7, "M", "π"), - (0x1D6B8, "M", "ρ"), - (0x1D6B9, "M", "θ"), - (0x1D6BA, "M", "σ"), - ] - - -def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D6BB, "M", "τ"), - (0x1D6BC, "M", "υ"), - (0x1D6BD, "M", "φ"), - (0x1D6BE, "M", "χ"), - (0x1D6BF, "M", "ψ"), - (0x1D6C0, "M", "ω"), - (0x1D6C1, "M", "∇"), - (0x1D6C2, "M", "α"), - (0x1D6C3, "M", "β"), - (0x1D6C4, "M", "γ"), - (0x1D6C5, "M", "δ"), - (0x1D6C6, "M", "ε"), - (0x1D6C7, "M", "ζ"), - (0x1D6C8, "M", "η"), - (0x1D6C9, "M", "θ"), - (0x1D6CA, "M", "ι"), - (0x1D6CB, "M", "κ"), - (0x1D6CC, "M", "λ"), - (0x1D6CD, "M", "μ"), - (0x1D6CE, "M", "ν"), - (0x1D6CF, "M", "ξ"), - (0x1D6D0, "M", "ο"), - (0x1D6D1, "M", "π"), - (0x1D6D2, "M", "ρ"), - (0x1D6D3, "M", "σ"), - (0x1D6D5, "M", "τ"), - (0x1D6D6, "M", "υ"), - (0x1D6D7, "M", "φ"), - (0x1D6D8, "M", "χ"), - (0x1D6D9, "M", "ψ"), - (0x1D6DA, "M", "ω"), - (0x1D6DB, "M", "∂"), - (0x1D6DC, "M", "ε"), - (0x1D6DD, "M", "θ"), - (0x1D6DE, "M", "κ"), - (0x1D6DF, "M", "φ"), - (0x1D6E0, "M", "ρ"), - (0x1D6E1, "M", "π"), - (0x1D6E2, "M", "α"), - (0x1D6E3, "M", "β"), - (0x1D6E4, "M", "γ"), - (0x1D6E5, "M", "δ"), - (0x1D6E6, "M", "ε"), - (0x1D6E7, "M", "ζ"), - (0x1D6E8, "M", "η"), - (0x1D6E9, "M", "θ"), - (0x1D6EA, "M", "ι"), - (0x1D6EB, "M", "κ"), - (0x1D6EC, "M", "λ"), - (0x1D6ED, "M", "μ"), - (0x1D6EE, "M", "ν"), - (0x1D6EF, "M", "ξ"), - (0x1D6F0, "M", "ο"), - (0x1D6F1, "M", "π"), - (0x1D6F2, "M", "ρ"), - (0x1D6F3, "M", "θ"), - (0x1D6F4, "M", "σ"), - (0x1D6F5, "M", "τ"), - (0x1D6F6, "M", "υ"), - (0x1D6F7, "M", "φ"), - (0x1D6F8, "M", "χ"), - (0x1D6F9, "M", "ψ"), - (0x1D6FA, "M", "ω"), - (0x1D6FB, "M", "∇"), - (0x1D6FC, "M", "α"), - (0x1D6FD, "M", "β"), - (0x1D6FE, "M", "γ"), - (0x1D6FF, "M", "δ"), - (0x1D700, "M", "ε"), - (0x1D701, "M", "ζ"), - (0x1D702, "M", "η"), - (0x1D703, "M", "θ"), - (0x1D704, "M", "ι"), - (0x1D705, "M", "κ"), - (0x1D706, "M", "λ"), - (0x1D707, "M", "μ"), - (0x1D708, "M", "ν"), - (0x1D709, "M", "ξ"), - (0x1D70A, "M", "ο"), - (0x1D70B, "M", "π"), - (0x1D70C, "M", "ρ"), - (0x1D70D, "M", "σ"), - (0x1D70F, "M", "τ"), - (0x1D710, "M", "υ"), - (0x1D711, "M", "φ"), - (0x1D712, "M", "χ"), - (0x1D713, "M", "ψ"), - (0x1D714, "M", "ω"), - (0x1D715, "M", "∂"), - (0x1D716, "M", "ε"), - (0x1D717, "M", "θ"), - (0x1D718, "M", "κ"), - (0x1D719, "M", "φ"), - (0x1D71A, "M", "ρ"), - (0x1D71B, "M", "π"), - (0x1D71C, "M", "α"), - (0x1D71D, "M", "β"), - (0x1D71E, "M", "γ"), - (0x1D71F, "M", "δ"), - (0x1D720, "M", "ε"), - ] - - -def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D721, "M", "ζ"), - (0x1D722, "M", "η"), - (0x1D723, "M", "θ"), - (0x1D724, "M", "ι"), - (0x1D725, "M", "κ"), - (0x1D726, "M", "λ"), - (0x1D727, "M", "μ"), - (0x1D728, "M", "ν"), - (0x1D729, "M", "ξ"), - (0x1D72A, "M", "ο"), - (0x1D72B, "M", "π"), - (0x1D72C, "M", "ρ"), - (0x1D72D, "M", "θ"), - (0x1D72E, "M", "σ"), - (0x1D72F, "M", "τ"), - (0x1D730, "M", "υ"), - (0x1D731, "M", "φ"), - (0x1D732, "M", "χ"), - (0x1D733, "M", "ψ"), - (0x1D734, "M", "ω"), - (0x1D735, "M", "∇"), - (0x1D736, "M", "α"), - (0x1D737, "M", "β"), - (0x1D738, "M", "γ"), - (0x1D739, "M", "δ"), - (0x1D73A, "M", "ε"), - (0x1D73B, "M", "ζ"), - (0x1D73C, "M", "η"), - (0x1D73D, "M", "θ"), - (0x1D73E, "M", "ι"), - (0x1D73F, "M", "κ"), - (0x1D740, "M", "λ"), - (0x1D741, "M", "μ"), - (0x1D742, "M", "ν"), - (0x1D743, "M", "ξ"), - (0x1D744, "M", "ο"), - (0x1D745, "M", "π"), - (0x1D746, "M", "ρ"), - (0x1D747, "M", "σ"), - (0x1D749, "M", "τ"), - (0x1D74A, "M", "υ"), - (0x1D74B, "M", "φ"), - (0x1D74C, "M", "χ"), - (0x1D74D, "M", "ψ"), - (0x1D74E, "M", "ω"), - (0x1D74F, "M", "∂"), - (0x1D750, "M", "ε"), - (0x1D751, "M", "θ"), - (0x1D752, "M", "κ"), - (0x1D753, "M", "φ"), - (0x1D754, "M", "ρ"), - (0x1D755, "M", "π"), - (0x1D756, "M", "α"), - (0x1D757, "M", "β"), - (0x1D758, "M", "γ"), - (0x1D759, "M", "δ"), - (0x1D75A, "M", "ε"), - (0x1D75B, "M", "ζ"), - (0x1D75C, "M", "η"), - (0x1D75D, "M", "θ"), - (0x1D75E, "M", "ι"), - (0x1D75F, "M", "κ"), - (0x1D760, "M", "λ"), - (0x1D761, "M", "μ"), - (0x1D762, "M", "ν"), - (0x1D763, "M", "ξ"), - (0x1D764, "M", "ο"), - (0x1D765, "M", "π"), - (0x1D766, "M", "ρ"), - (0x1D767, "M", "θ"), - (0x1D768, "M", "σ"), - (0x1D769, "M", "τ"), - (0x1D76A, "M", "υ"), - (0x1D76B, "M", "φ"), - (0x1D76C, "M", "χ"), - (0x1D76D, "M", "ψ"), - (0x1D76E, "M", "ω"), - (0x1D76F, "M", "∇"), - (0x1D770, "M", "α"), - (0x1D771, "M", "β"), - (0x1D772, "M", "γ"), - (0x1D773, "M", "δ"), - (0x1D774, "M", "ε"), - (0x1D775, "M", "ζ"), - (0x1D776, "M", "η"), - (0x1D777, "M", "θ"), - (0x1D778, "M", "ι"), - (0x1D779, "M", "κ"), - (0x1D77A, "M", "λ"), - (0x1D77B, "M", "μ"), - (0x1D77C, "M", "ν"), - (0x1D77D, "M", "ξ"), - (0x1D77E, "M", "ο"), - (0x1D77F, "M", "π"), - (0x1D780, "M", "ρ"), - (0x1D781, "M", "σ"), - (0x1D783, "M", "τ"), - (0x1D784, "M", "υ"), - (0x1D785, "M", "φ"), - (0x1D786, "M", "χ"), - ] - - -def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D787, "M", "ψ"), - (0x1D788, "M", "ω"), - (0x1D789, "M", "∂"), - (0x1D78A, "M", "ε"), - (0x1D78B, "M", "θ"), - (0x1D78C, "M", "κ"), - (0x1D78D, "M", "φ"), - (0x1D78E, "M", "ρ"), - (0x1D78F, "M", "π"), - (0x1D790, "M", "α"), - (0x1D791, "M", "β"), - (0x1D792, "M", "γ"), - (0x1D793, "M", "δ"), - (0x1D794, "M", "ε"), - (0x1D795, "M", "ζ"), - (0x1D796, "M", "η"), - (0x1D797, "M", "θ"), - (0x1D798, "M", "ι"), - (0x1D799, "M", "κ"), - (0x1D79A, "M", "λ"), - (0x1D79B, "M", "μ"), - (0x1D79C, "M", "ν"), - (0x1D79D, "M", "ξ"), - (0x1D79E, "M", "ο"), - (0x1D79F, "M", "π"), - (0x1D7A0, "M", "ρ"), - (0x1D7A1, "M", "θ"), - (0x1D7A2, "M", "σ"), - (0x1D7A3, "M", "τ"), - (0x1D7A4, "M", "υ"), - (0x1D7A5, "M", "φ"), - (0x1D7A6, "M", "χ"), - (0x1D7A7, "M", "ψ"), - (0x1D7A8, "M", "ω"), - (0x1D7A9, "M", "∇"), - (0x1D7AA, "M", "α"), - (0x1D7AB, "M", "β"), - (0x1D7AC, "M", "γ"), - (0x1D7AD, "M", "δ"), - (0x1D7AE, "M", "ε"), - (0x1D7AF, "M", "ζ"), - (0x1D7B0, "M", "η"), - (0x1D7B1, "M", "θ"), - (0x1D7B2, "M", "ι"), - (0x1D7B3, "M", "κ"), - (0x1D7B4, "M", "λ"), - (0x1D7B5, "M", "μ"), - (0x1D7B6, "M", "ν"), - (0x1D7B7, "M", "ξ"), - (0x1D7B8, "M", "ο"), - (0x1D7B9, "M", "π"), - (0x1D7BA, "M", "ρ"), - (0x1D7BB, "M", "σ"), - (0x1D7BD, "M", "τ"), - (0x1D7BE, "M", "υ"), - (0x1D7BF, "M", "φ"), - (0x1D7C0, "M", "χ"), - (0x1D7C1, "M", "ψ"), - (0x1D7C2, "M", "ω"), - (0x1D7C3, "M", "∂"), - (0x1D7C4, "M", "ε"), - (0x1D7C5, "M", "θ"), - (0x1D7C6, "M", "κ"), - (0x1D7C7, "M", "φ"), - (0x1D7C8, "M", "ρ"), - (0x1D7C9, "M", "π"), - (0x1D7CA, "M", "ϝ"), - (0x1D7CC, "X"), - (0x1D7CE, "M", "0"), - (0x1D7CF, "M", "1"), - (0x1D7D0, "M", "2"), - (0x1D7D1, "M", "3"), - (0x1D7D2, "M", "4"), - (0x1D7D3, "M", "5"), - (0x1D7D4, "M", "6"), - (0x1D7D5, "M", "7"), - (0x1D7D6, "M", "8"), - (0x1D7D7, "M", "9"), - (0x1D7D8, "M", "0"), - (0x1D7D9, "M", "1"), - (0x1D7DA, "M", "2"), - (0x1D7DB, "M", "3"), - (0x1D7DC, "M", "4"), - (0x1D7DD, "M", "5"), - (0x1D7DE, "M", "6"), - (0x1D7DF, "M", "7"), - (0x1D7E0, "M", "8"), - (0x1D7E1, "M", "9"), - (0x1D7E2, "M", "0"), - (0x1D7E3, "M", "1"), - (0x1D7E4, "M", "2"), - (0x1D7E5, "M", "3"), - (0x1D7E6, "M", "4"), - (0x1D7E7, "M", "5"), - (0x1D7E8, "M", "6"), - (0x1D7E9, "M", "7"), - (0x1D7EA, "M", "8"), - (0x1D7EB, "M", "9"), - (0x1D7EC, "M", "0"), - (0x1D7ED, "M", "1"), - ] - - -def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1D7EE, "M", "2"), - (0x1D7EF, "M", "3"), - (0x1D7F0, "M", "4"), - (0x1D7F1, "M", "5"), - (0x1D7F2, "M", "6"), - (0x1D7F3, "M", "7"), - (0x1D7F4, "M", "8"), - (0x1D7F5, "M", "9"), - (0x1D7F6, "M", "0"), - (0x1D7F7, "M", "1"), - (0x1D7F8, "M", "2"), - (0x1D7F9, "M", "3"), - (0x1D7FA, "M", "4"), - (0x1D7FB, "M", "5"), - (0x1D7FC, "M", "6"), - (0x1D7FD, "M", "7"), - (0x1D7FE, "M", "8"), - (0x1D7FF, "M", "9"), - (0x1D800, "V"), - (0x1DA8C, "X"), - (0x1DA9B, "V"), - (0x1DAA0, "X"), - (0x1DAA1, "V"), - (0x1DAB0, "X"), - (0x1DF00, "V"), - (0x1DF1F, "X"), - (0x1DF25, "V"), - (0x1DF2B, "X"), - (0x1E000, "V"), - (0x1E007, "X"), - (0x1E008, "V"), - (0x1E019, "X"), - (0x1E01B, "V"), - (0x1E022, "X"), - (0x1E023, "V"), - (0x1E025, "X"), - (0x1E026, "V"), - (0x1E02B, "X"), - (0x1E030, "M", "а"), - (0x1E031, "M", "б"), - (0x1E032, "M", "в"), - (0x1E033, "M", "г"), - (0x1E034, "M", "д"), - (0x1E035, "M", "е"), - (0x1E036, "M", "ж"), - (0x1E037, "M", "з"), - (0x1E038, "M", "и"), - (0x1E039, "M", "к"), - (0x1E03A, "M", "л"), - (0x1E03B, "M", "м"), - (0x1E03C, "M", "о"), - (0x1E03D, "M", "п"), - (0x1E03E, "M", "р"), - (0x1E03F, "M", "с"), - (0x1E040, "M", "т"), - (0x1E041, "M", "у"), - (0x1E042, "M", "ф"), - (0x1E043, "M", "х"), - (0x1E044, "M", "ц"), - (0x1E045, "M", "ч"), - (0x1E046, "M", "ш"), - (0x1E047, "M", "ы"), - (0x1E048, "M", "э"), - (0x1E049, "M", "ю"), - (0x1E04A, "M", "ꚉ"), - (0x1E04B, "M", "ә"), - (0x1E04C, "M", "і"), - (0x1E04D, "M", "ј"), - (0x1E04E, "M", "ө"), - (0x1E04F, "M", "ү"), - (0x1E050, "M", "ӏ"), - (0x1E051, "M", "а"), - (0x1E052, "M", "б"), - (0x1E053, "M", "в"), - (0x1E054, "M", "г"), - (0x1E055, "M", "д"), - (0x1E056, "M", "е"), - (0x1E057, "M", "ж"), - (0x1E058, "M", "з"), - (0x1E059, "M", "и"), - (0x1E05A, "M", "к"), - (0x1E05B, "M", "л"), - (0x1E05C, "M", "о"), - (0x1E05D, "M", "п"), - (0x1E05E, "M", "с"), - (0x1E05F, "M", "у"), - (0x1E060, "M", "ф"), - (0x1E061, "M", "х"), - (0x1E062, "M", "ц"), - (0x1E063, "M", "ч"), - (0x1E064, "M", "ш"), - (0x1E065, "M", "ъ"), - (0x1E066, "M", "ы"), - (0x1E067, "M", "ґ"), - (0x1E068, "M", "і"), - (0x1E069, "M", "ѕ"), - (0x1E06A, "M", "џ"), - (0x1E06B, "M", "ҫ"), - (0x1E06C, "M", "ꙑ"), - (0x1E06D, "M", "ұ"), - ] - - -def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1E06E, "X"), - (0x1E08F, "V"), - (0x1E090, "X"), - (0x1E100, "V"), - (0x1E12D, "X"), - (0x1E130, "V"), - (0x1E13E, "X"), - (0x1E140, "V"), - (0x1E14A, "X"), - (0x1E14E, "V"), - (0x1E150, "X"), - (0x1E290, "V"), - (0x1E2AF, "X"), - (0x1E2C0, "V"), - (0x1E2FA, "X"), - (0x1E2FF, "V"), - (0x1E300, "X"), - (0x1E4D0, "V"), - (0x1E4FA, "X"), - (0x1E5D0, "V"), - (0x1E5FB, "X"), - (0x1E5FF, "V"), - (0x1E600, "X"), - (0x1E7E0, "V"), - (0x1E7E7, "X"), - (0x1E7E8, "V"), - (0x1E7EC, "X"), - (0x1E7ED, "V"), - (0x1E7EF, "X"), - (0x1E7F0, "V"), - (0x1E7FF, "X"), - (0x1E800, "V"), - (0x1E8C5, "X"), - (0x1E8C7, "V"), - (0x1E8D7, "X"), - (0x1E900, "M", "𞤢"), - (0x1E901, "M", "𞤣"), - (0x1E902, "M", "𞤤"), - (0x1E903, "M", "𞤥"), - (0x1E904, "M", "𞤦"), - (0x1E905, "M", "𞤧"), - (0x1E906, "M", "𞤨"), - (0x1E907, "M", "𞤩"), - (0x1E908, "M", "𞤪"), - (0x1E909, "M", "𞤫"), - (0x1E90A, "M", "𞤬"), - (0x1E90B, "M", "𞤭"), - (0x1E90C, "M", "𞤮"), - (0x1E90D, "M", "𞤯"), - (0x1E90E, "M", "𞤰"), - (0x1E90F, "M", "𞤱"), - (0x1E910, "M", "𞤲"), - (0x1E911, "M", "𞤳"), - (0x1E912, "M", "𞤴"), - (0x1E913, "M", "𞤵"), - (0x1E914, "M", "𞤶"), - (0x1E915, "M", "𞤷"), - (0x1E916, "M", "𞤸"), - (0x1E917, "M", "𞤹"), - (0x1E918, "M", "𞤺"), - (0x1E919, "M", "𞤻"), - (0x1E91A, "M", "𞤼"), - (0x1E91B, "M", "𞤽"), - (0x1E91C, "M", "𞤾"), - (0x1E91D, "M", "𞤿"), - (0x1E91E, "M", "𞥀"), - (0x1E91F, "M", "𞥁"), - (0x1E920, "M", "𞥂"), - (0x1E921, "M", "𞥃"), - (0x1E922, "V"), - (0x1E94C, "X"), - (0x1E950, "V"), - (0x1E95A, "X"), - (0x1E95E, "V"), - (0x1E960, "X"), - (0x1EC71, "V"), - (0x1ECB5, "X"), - (0x1ED01, "V"), - (0x1ED3E, "X"), - (0x1EE00, "M", "ا"), - (0x1EE01, "M", "ب"), - (0x1EE02, "M", "ج"), - (0x1EE03, "M", "د"), - (0x1EE04, "X"), - (0x1EE05, "M", "و"), - (0x1EE06, "M", "ز"), - (0x1EE07, "M", "ح"), - (0x1EE08, "M", "ط"), - (0x1EE09, "M", "ي"), - (0x1EE0A, "M", "ك"), - (0x1EE0B, "M", "ل"), - (0x1EE0C, "M", "م"), - (0x1EE0D, "M", "ن"), - (0x1EE0E, "M", "س"), - (0x1EE0F, "M", "ع"), - (0x1EE10, "M", "ف"), - (0x1EE11, "M", "ص"), - (0x1EE12, "M", "ق"), - (0x1EE13, "M", "ر"), - (0x1EE14, "M", "ش"), - ] - - -def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1EE15, "M", "ت"), - (0x1EE16, "M", "ث"), - (0x1EE17, "M", "خ"), - (0x1EE18, "M", "ذ"), - (0x1EE19, "M", "ض"), - (0x1EE1A, "M", "ظ"), - (0x1EE1B, "M", "غ"), - (0x1EE1C, "M", "ٮ"), - (0x1EE1D, "M", "ں"), - (0x1EE1E, "M", "ڡ"), - (0x1EE1F, "M", "ٯ"), - (0x1EE20, "X"), - (0x1EE21, "M", "ب"), - (0x1EE22, "M", "ج"), - (0x1EE23, "X"), - (0x1EE24, "M", "ه"), - (0x1EE25, "X"), - (0x1EE27, "M", "ح"), - (0x1EE28, "X"), - (0x1EE29, "M", "ي"), - (0x1EE2A, "M", "ك"), - (0x1EE2B, "M", "ل"), - (0x1EE2C, "M", "م"), - (0x1EE2D, "M", "ن"), - (0x1EE2E, "M", "س"), - (0x1EE2F, "M", "ع"), - (0x1EE30, "M", "ف"), - (0x1EE31, "M", "ص"), - (0x1EE32, "M", "ق"), - (0x1EE33, "X"), - (0x1EE34, "M", "ش"), - (0x1EE35, "M", "ت"), - (0x1EE36, "M", "ث"), - (0x1EE37, "M", "خ"), - (0x1EE38, "X"), - (0x1EE39, "M", "ض"), - (0x1EE3A, "X"), - (0x1EE3B, "M", "غ"), - (0x1EE3C, "X"), - (0x1EE42, "M", "ج"), - (0x1EE43, "X"), - (0x1EE47, "M", "ح"), - (0x1EE48, "X"), - (0x1EE49, "M", "ي"), - (0x1EE4A, "X"), - (0x1EE4B, "M", "ل"), - (0x1EE4C, "X"), - (0x1EE4D, "M", "ن"), - (0x1EE4E, "M", "س"), - (0x1EE4F, "M", "ع"), - (0x1EE50, "X"), - (0x1EE51, "M", "ص"), - (0x1EE52, "M", "ق"), - (0x1EE53, "X"), - (0x1EE54, "M", "ش"), - (0x1EE55, "X"), - (0x1EE57, "M", "خ"), - (0x1EE58, "X"), - (0x1EE59, "M", "ض"), - (0x1EE5A, "X"), - (0x1EE5B, "M", "غ"), - (0x1EE5C, "X"), - (0x1EE5D, "M", "ں"), - (0x1EE5E, "X"), - (0x1EE5F, "M", "ٯ"), - (0x1EE60, "X"), - (0x1EE61, "M", "ب"), - (0x1EE62, "M", "ج"), - (0x1EE63, "X"), - (0x1EE64, "M", "ه"), - (0x1EE65, "X"), - (0x1EE67, "M", "ح"), - (0x1EE68, "M", "ط"), - (0x1EE69, "M", "ي"), - (0x1EE6A, "M", "ك"), - (0x1EE6B, "X"), - (0x1EE6C, "M", "م"), - (0x1EE6D, "M", "ن"), - (0x1EE6E, "M", "س"), - (0x1EE6F, "M", "ع"), - (0x1EE70, "M", "ف"), - (0x1EE71, "M", "ص"), - (0x1EE72, "M", "ق"), - (0x1EE73, "X"), - (0x1EE74, "M", "ش"), - (0x1EE75, "M", "ت"), - (0x1EE76, "M", "ث"), - (0x1EE77, "M", "خ"), - (0x1EE78, "X"), - (0x1EE79, "M", "ض"), - (0x1EE7A, "M", "ظ"), - (0x1EE7B, "M", "غ"), - (0x1EE7C, "M", "ٮ"), - (0x1EE7D, "X"), - (0x1EE7E, "M", "ڡ"), - (0x1EE7F, "X"), - (0x1EE80, "M", "ا"), - (0x1EE81, "M", "ب"), - (0x1EE82, "M", "ج"), - (0x1EE83, "M", "د"), - ] - - -def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1EE84, "M", "ه"), - (0x1EE85, "M", "و"), - (0x1EE86, "M", "ز"), - (0x1EE87, "M", "ح"), - (0x1EE88, "M", "ط"), - (0x1EE89, "M", "ي"), - (0x1EE8A, "X"), - (0x1EE8B, "M", "ل"), - (0x1EE8C, "M", "م"), - (0x1EE8D, "M", "ن"), - (0x1EE8E, "M", "س"), - (0x1EE8F, "M", "ع"), - (0x1EE90, "M", "ف"), - (0x1EE91, "M", "ص"), - (0x1EE92, "M", "ق"), - (0x1EE93, "M", "ر"), - (0x1EE94, "M", "ش"), - (0x1EE95, "M", "ت"), - (0x1EE96, "M", "ث"), - (0x1EE97, "M", "خ"), - (0x1EE98, "M", "ذ"), - (0x1EE99, "M", "ض"), - (0x1EE9A, "M", "ظ"), - (0x1EE9B, "M", "غ"), - (0x1EE9C, "X"), - (0x1EEA1, "M", "ب"), - (0x1EEA2, "M", "ج"), - (0x1EEA3, "M", "د"), - (0x1EEA4, "X"), - (0x1EEA5, "M", "و"), - (0x1EEA6, "M", "ز"), - (0x1EEA7, "M", "ح"), - (0x1EEA8, "M", "ط"), - (0x1EEA9, "M", "ي"), - (0x1EEAA, "X"), - (0x1EEAB, "M", "ل"), - (0x1EEAC, "M", "م"), - (0x1EEAD, "M", "ن"), - (0x1EEAE, "M", "س"), - (0x1EEAF, "M", "ع"), - (0x1EEB0, "M", "ف"), - (0x1EEB1, "M", "ص"), - (0x1EEB2, "M", "ق"), - (0x1EEB3, "M", "ر"), - (0x1EEB4, "M", "ش"), - (0x1EEB5, "M", "ت"), - (0x1EEB6, "M", "ث"), - (0x1EEB7, "M", "خ"), - (0x1EEB8, "M", "ذ"), - (0x1EEB9, "M", "ض"), - (0x1EEBA, "M", "ظ"), - (0x1EEBB, "M", "غ"), - (0x1EEBC, "X"), - (0x1EEF0, "V"), - (0x1EEF2, "X"), - (0x1F000, "V"), - (0x1F02C, "X"), - (0x1F030, "V"), - (0x1F094, "X"), - (0x1F0A0, "V"), - (0x1F0AF, "X"), - (0x1F0B1, "V"), - (0x1F0C0, "X"), - (0x1F0C1, "V"), - (0x1F0D0, "X"), - (0x1F0D1, "V"), - (0x1F0F6, "X"), - (0x1F101, "M", "0,"), - (0x1F102, "M", "1,"), - (0x1F103, "M", "2,"), - (0x1F104, "M", "3,"), - (0x1F105, "M", "4,"), - (0x1F106, "M", "5,"), - (0x1F107, "M", "6,"), - (0x1F108, "M", "7,"), - (0x1F109, "M", "8,"), - (0x1F10A, "M", "9,"), - (0x1F10B, "V"), - (0x1F110, "M", "(a)"), - (0x1F111, "M", "(b)"), - (0x1F112, "M", "(c)"), - (0x1F113, "M", "(d)"), - (0x1F114, "M", "(e)"), - (0x1F115, "M", "(f)"), - (0x1F116, "M", "(g)"), - (0x1F117, "M", "(h)"), - (0x1F118, "M", "(i)"), - (0x1F119, "M", "(j)"), - (0x1F11A, "M", "(k)"), - (0x1F11B, "M", "(l)"), - (0x1F11C, "M", "(m)"), - (0x1F11D, "M", "(n)"), - (0x1F11E, "M", "(o)"), - (0x1F11F, "M", "(p)"), - (0x1F120, "M", "(q)"), - (0x1F121, "M", "(r)"), - (0x1F122, "M", "(s)"), - (0x1F123, "M", "(t)"), - (0x1F124, "M", "(u)"), - (0x1F125, "M", "(v)"), - ] - - -def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1F126, "M", "(w)"), - (0x1F127, "M", "(x)"), - (0x1F128, "M", "(y)"), - (0x1F129, "M", "(z)"), - (0x1F12A, "M", "〔s〕"), - (0x1F12B, "M", "c"), - (0x1F12C, "M", "r"), - (0x1F12D, "M", "cd"), - (0x1F12E, "M", "wz"), - (0x1F12F, "V"), - (0x1F130, "M", "a"), - (0x1F131, "M", "b"), - (0x1F132, "M", "c"), - (0x1F133, "M", "d"), - (0x1F134, "M", "e"), - (0x1F135, "M", "f"), - (0x1F136, "M", "g"), - (0x1F137, "M", "h"), - (0x1F138, "M", "i"), - (0x1F139, "M", "j"), - (0x1F13A, "M", "k"), - (0x1F13B, "M", "l"), - (0x1F13C, "M", "m"), - (0x1F13D, "M", "n"), - (0x1F13E, "M", "o"), - (0x1F13F, "M", "p"), - (0x1F140, "M", "q"), - (0x1F141, "M", "r"), - (0x1F142, "M", "s"), - (0x1F143, "M", "t"), - (0x1F144, "M", "u"), - (0x1F145, "M", "v"), - (0x1F146, "M", "w"), - (0x1F147, "M", "x"), - (0x1F148, "M", "y"), - (0x1F149, "M", "z"), - (0x1F14A, "M", "hv"), - (0x1F14B, "M", "mv"), - (0x1F14C, "M", "sd"), - (0x1F14D, "M", "ss"), - (0x1F14E, "M", "ppv"), - (0x1F14F, "M", "wc"), - (0x1F150, "V"), - (0x1F16A, "M", "mc"), - (0x1F16B, "M", "md"), - (0x1F16C, "M", "mr"), - (0x1F16D, "V"), - (0x1F190, "M", "dj"), - (0x1F191, "V"), - (0x1F1AE, "X"), - (0x1F1E6, "V"), - (0x1F200, "M", "ほか"), - (0x1F201, "M", "ココ"), - (0x1F202, "M", "サ"), - (0x1F203, "X"), - (0x1F210, "M", "手"), - (0x1F211, "M", "字"), - (0x1F212, "M", "双"), - (0x1F213, "M", "デ"), - (0x1F214, "M", "二"), - (0x1F215, "M", "多"), - (0x1F216, "M", "解"), - (0x1F217, "M", "天"), - (0x1F218, "M", "交"), - (0x1F219, "M", "映"), - (0x1F21A, "M", "無"), - (0x1F21B, "M", "料"), - (0x1F21C, "M", "前"), - (0x1F21D, "M", "後"), - (0x1F21E, "M", "再"), - (0x1F21F, "M", "新"), - (0x1F220, "M", "初"), - (0x1F221, "M", "終"), - (0x1F222, "M", "生"), - (0x1F223, "M", "販"), - (0x1F224, "M", "声"), - (0x1F225, "M", "吹"), - (0x1F226, "M", "演"), - (0x1F227, "M", "投"), - (0x1F228, "M", "捕"), - (0x1F229, "M", "一"), - (0x1F22A, "M", "三"), - (0x1F22B, "M", "遊"), - (0x1F22C, "M", "左"), - (0x1F22D, "M", "中"), - (0x1F22E, "M", "右"), - (0x1F22F, "M", "指"), - (0x1F230, "M", "走"), - (0x1F231, "M", "打"), - (0x1F232, "M", "禁"), - (0x1F233, "M", "空"), - (0x1F234, "M", "合"), - (0x1F235, "M", "満"), - (0x1F236, "M", "有"), - (0x1F237, "M", "月"), - (0x1F238, "M", "申"), - (0x1F239, "M", "割"), - (0x1F23A, "M", "営"), - (0x1F23B, "M", "配"), - (0x1F23C, "X"), - ] - - -def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x1F240, "M", "〔本〕"), - (0x1F241, "M", "〔三〕"), - (0x1F242, "M", "〔二〕"), - (0x1F243, "M", "〔安〕"), - (0x1F244, "M", "〔点〕"), - (0x1F245, "M", "〔打〕"), - (0x1F246, "M", "〔盗〕"), - (0x1F247, "M", "〔勝〕"), - (0x1F248, "M", "〔敗〕"), - (0x1F249, "X"), - (0x1F250, "M", "得"), - (0x1F251, "M", "可"), - (0x1F252, "X"), - (0x1F260, "V"), - (0x1F266, "X"), - (0x1F300, "V"), - (0x1F6D8, "X"), - (0x1F6DC, "V"), - (0x1F6ED, "X"), - (0x1F6F0, "V"), - (0x1F6FD, "X"), - (0x1F700, "V"), - (0x1F777, "X"), - (0x1F77B, "V"), - (0x1F7DA, "X"), - (0x1F7E0, "V"), - (0x1F7EC, "X"), - (0x1F7F0, "V"), - (0x1F7F1, "X"), - (0x1F800, "V"), - (0x1F80C, "X"), - (0x1F810, "V"), - (0x1F848, "X"), - (0x1F850, "V"), - (0x1F85A, "X"), - (0x1F860, "V"), - (0x1F888, "X"), - (0x1F890, "V"), - (0x1F8AE, "X"), - (0x1F8B0, "V"), - (0x1F8BC, "X"), - (0x1F8C0, "V"), - (0x1F8C2, "X"), - (0x1F900, "V"), - (0x1FA54, "X"), - (0x1FA60, "V"), - (0x1FA6E, "X"), - (0x1FA70, "V"), - (0x1FA7D, "X"), - (0x1FA80, "V"), - (0x1FA8A, "X"), - (0x1FA8F, "V"), - (0x1FAC7, "X"), - (0x1FACE, "V"), - (0x1FADD, "X"), - (0x1FADF, "V"), - (0x1FAEA, "X"), - (0x1FAF0, "V"), - (0x1FAF9, "X"), - (0x1FB00, "V"), - (0x1FB93, "X"), - (0x1FB94, "V"), - (0x1FBF0, "M", "0"), - (0x1FBF1, "M", "1"), - (0x1FBF2, "M", "2"), - (0x1FBF3, "M", "3"), - (0x1FBF4, "M", "4"), - (0x1FBF5, "M", "5"), - (0x1FBF6, "M", "6"), - (0x1FBF7, "M", "7"), - (0x1FBF8, "M", "8"), - (0x1FBF9, "M", "9"), - (0x1FBFA, "X"), - (0x20000, "V"), - (0x2A6E0, "X"), - (0x2A700, "V"), - (0x2B73A, "X"), - (0x2B740, "V"), - (0x2B81E, "X"), - (0x2B820, "V"), - (0x2CEA2, "X"), - (0x2CEB0, "V"), - (0x2EBE1, "X"), - (0x2EBF0, "V"), - (0x2EE5E, "X"), - (0x2F800, "M", "丽"), - (0x2F801, "M", "丸"), - (0x2F802, "M", "乁"), - (0x2F803, "M", "𠄢"), - (0x2F804, "M", "你"), - (0x2F805, "M", "侮"), - (0x2F806, "M", "侻"), - (0x2F807, "M", "倂"), - (0x2F808, "M", "偺"), - (0x2F809, "M", "備"), - (0x2F80A, "M", "僧"), - (0x2F80B, "M", "像"), - (0x2F80C, "M", "㒞"), - (0x2F80D, "M", "𠘺"), - (0x2F80E, "M", "免"), - ] - - -def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2F80F, "M", "兔"), - (0x2F810, "M", "兤"), - (0x2F811, "M", "具"), - (0x2F812, "M", "𠔜"), - (0x2F813, "M", "㒹"), - (0x2F814, "M", "內"), - (0x2F815, "M", "再"), - (0x2F816, "M", "𠕋"), - (0x2F817, "M", "冗"), - (0x2F818, "M", "冤"), - (0x2F819, "M", "仌"), - (0x2F81A, "M", "冬"), - (0x2F81B, "M", "况"), - (0x2F81C, "M", "𩇟"), - (0x2F81D, "M", "凵"), - (0x2F81E, "M", "刃"), - (0x2F81F, "M", "㓟"), - (0x2F820, "M", "刻"), - (0x2F821, "M", "剆"), - (0x2F822, "M", "割"), - (0x2F823, "M", "剷"), - (0x2F824, "M", "㔕"), - (0x2F825, "M", "勇"), - (0x2F826, "M", "勉"), - (0x2F827, "M", "勤"), - (0x2F828, "M", "勺"), - (0x2F829, "M", "包"), - (0x2F82A, "M", "匆"), - (0x2F82B, "M", "北"), - (0x2F82C, "M", "卉"), - (0x2F82D, "M", "卑"), - (0x2F82E, "M", "博"), - (0x2F82F, "M", "即"), - (0x2F830, "M", "卽"), - (0x2F831, "M", "卿"), - (0x2F834, "M", "𠨬"), - (0x2F835, "M", "灰"), - (0x2F836, "M", "及"), - (0x2F837, "M", "叟"), - (0x2F838, "M", "𠭣"), - (0x2F839, "M", "叫"), - (0x2F83A, "M", "叱"), - (0x2F83B, "M", "吆"), - (0x2F83C, "M", "咞"), - (0x2F83D, "M", "吸"), - (0x2F83E, "M", "呈"), - (0x2F83F, "M", "周"), - (0x2F840, "M", "咢"), - (0x2F841, "M", "哶"), - (0x2F842, "M", "唐"), - (0x2F843, "M", "啓"), - (0x2F844, "M", "啣"), - (0x2F845, "M", "善"), - (0x2F847, "M", "喙"), - (0x2F848, "M", "喫"), - (0x2F849, "M", "喳"), - (0x2F84A, "M", "嗂"), - (0x2F84B, "M", "圖"), - (0x2F84C, "M", "嘆"), - (0x2F84D, "M", "圗"), - (0x2F84E, "M", "噑"), - (0x2F84F, "M", "噴"), - (0x2F850, "M", "切"), - (0x2F851, "M", "壮"), - (0x2F852, "M", "城"), - (0x2F853, "M", "埴"), - (0x2F854, "M", "堍"), - (0x2F855, "M", "型"), - (0x2F856, "M", "堲"), - (0x2F857, "M", "報"), - (0x2F858, "M", "墬"), - (0x2F859, "M", "𡓤"), - (0x2F85A, "M", "売"), - (0x2F85B, "M", "壷"), - (0x2F85C, "M", "夆"), - (0x2F85D, "M", "多"), - (0x2F85E, "M", "夢"), - (0x2F85F, "M", "奢"), - (0x2F860, "M", "𡚨"), - (0x2F861, "M", "𡛪"), - (0x2F862, "M", "姬"), - (0x2F863, "M", "娛"), - (0x2F864, "M", "娧"), - (0x2F865, "M", "姘"), - (0x2F866, "M", "婦"), - (0x2F867, "M", "㛮"), - (0x2F868, "M", "㛼"), - (0x2F869, "M", "嬈"), - (0x2F86A, "M", "嬾"), - (0x2F86C, "M", "𡧈"), - (0x2F86D, "M", "寃"), - (0x2F86E, "M", "寘"), - (0x2F86F, "M", "寧"), - (0x2F870, "M", "寳"), - (0x2F871, "M", "𡬘"), - (0x2F872, "M", "寿"), - (0x2F873, "M", "将"), - (0x2F874, "M", "当"), - (0x2F875, "M", "尢"), - (0x2F876, "M", "㞁"), - ] - - -def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2F877, "M", "屠"), - (0x2F878, "M", "屮"), - (0x2F879, "M", "峀"), - (0x2F87A, "M", "岍"), - (0x2F87B, "M", "𡷤"), - (0x2F87C, "M", "嵃"), - (0x2F87D, "M", "𡷦"), - (0x2F87E, "M", "嵮"), - (0x2F87F, "M", "嵫"), - (0x2F880, "M", "嵼"), - (0x2F881, "M", "巡"), - (0x2F882, "M", "巢"), - (0x2F883, "M", "㠯"), - (0x2F884, "M", "巽"), - (0x2F885, "M", "帨"), - (0x2F886, "M", "帽"), - (0x2F887, "M", "幩"), - (0x2F888, "M", "㡢"), - (0x2F889, "M", "𢆃"), - (0x2F88A, "M", "㡼"), - (0x2F88B, "M", "庰"), - (0x2F88C, "M", "庳"), - (0x2F88D, "M", "庶"), - (0x2F88E, "M", "廊"), - (0x2F88F, "M", "𪎒"), - (0x2F890, "M", "廾"), - (0x2F891, "M", "𢌱"), - (0x2F893, "M", "舁"), - (0x2F894, "M", "弢"), - (0x2F896, "M", "㣇"), - (0x2F897, "M", "𣊸"), - (0x2F898, "M", "𦇚"), - (0x2F899, "M", "形"), - (0x2F89A, "M", "彫"), - (0x2F89B, "M", "㣣"), - (0x2F89C, "M", "徚"), - (0x2F89D, "M", "忍"), - (0x2F89E, "M", "志"), - (0x2F89F, "M", "忹"), - (0x2F8A0, "M", "悁"), - (0x2F8A1, "M", "㤺"), - (0x2F8A2, "M", "㤜"), - (0x2F8A3, "M", "悔"), - (0x2F8A4, "M", "𢛔"), - (0x2F8A5, "M", "惇"), - (0x2F8A6, "M", "慈"), - (0x2F8A7, "M", "慌"), - (0x2F8A8, "M", "慎"), - (0x2F8A9, "M", "慌"), - (0x2F8AA, "M", "慺"), - (0x2F8AB, "M", "憎"), - (0x2F8AC, "M", "憲"), - (0x2F8AD, "M", "憤"), - (0x2F8AE, "M", "憯"), - (0x2F8AF, "M", "懞"), - (0x2F8B0, "M", "懲"), - (0x2F8B1, "M", "懶"), - (0x2F8B2, "M", "成"), - (0x2F8B3, "M", "戛"), - (0x2F8B4, "M", "扝"), - (0x2F8B5, "M", "抱"), - (0x2F8B6, "M", "拔"), - (0x2F8B7, "M", "捐"), - (0x2F8B8, "M", "𢬌"), - (0x2F8B9, "M", "挽"), - (0x2F8BA, "M", "拼"), - (0x2F8BB, "M", "捨"), - (0x2F8BC, "M", "掃"), - (0x2F8BD, "M", "揤"), - (0x2F8BE, "M", "𢯱"), - (0x2F8BF, "M", "搢"), - (0x2F8C0, "M", "揅"), - (0x2F8C1, "M", "掩"), - (0x2F8C2, "M", "㨮"), - (0x2F8C3, "M", "摩"), - (0x2F8C4, "M", "摾"), - (0x2F8C5, "M", "撝"), - (0x2F8C6, "M", "摷"), - (0x2F8C7, "M", "㩬"), - (0x2F8C8, "M", "敏"), - (0x2F8C9, "M", "敬"), - (0x2F8CA, "M", "𣀊"), - (0x2F8CB, "M", "旣"), - (0x2F8CC, "M", "書"), - (0x2F8CD, "M", "晉"), - (0x2F8CE, "M", "㬙"), - (0x2F8CF, "M", "暑"), - (0x2F8D0, "M", "㬈"), - (0x2F8D1, "M", "㫤"), - (0x2F8D2, "M", "冒"), - (0x2F8D3, "M", "冕"), - (0x2F8D4, "M", "最"), - (0x2F8D5, "M", "暜"), - (0x2F8D6, "M", "肭"), - (0x2F8D7, "M", "䏙"), - (0x2F8D8, "M", "朗"), - (0x2F8D9, "M", "望"), - (0x2F8DA, "M", "朡"), - (0x2F8DB, "M", "杞"), - (0x2F8DC, "M", "杓"), - ] - - -def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2F8DD, "M", "𣏃"), - (0x2F8DE, "M", "㭉"), - (0x2F8DF, "M", "柺"), - (0x2F8E0, "M", "枅"), - (0x2F8E1, "M", "桒"), - (0x2F8E2, "M", "梅"), - (0x2F8E3, "M", "𣑭"), - (0x2F8E4, "M", "梎"), - (0x2F8E5, "M", "栟"), - (0x2F8E6, "M", "椔"), - (0x2F8E7, "M", "㮝"), - (0x2F8E8, "M", "楂"), - (0x2F8E9, "M", "榣"), - (0x2F8EA, "M", "槪"), - (0x2F8EB, "M", "檨"), - (0x2F8EC, "M", "𣚣"), - (0x2F8ED, "M", "櫛"), - (0x2F8EE, "M", "㰘"), - (0x2F8EF, "M", "次"), - (0x2F8F0, "M", "𣢧"), - (0x2F8F1, "M", "歔"), - (0x2F8F2, "M", "㱎"), - (0x2F8F3, "M", "歲"), - (0x2F8F4, "M", "殟"), - (0x2F8F5, "M", "殺"), - (0x2F8F6, "M", "殻"), - (0x2F8F7, "M", "𣪍"), - (0x2F8F8, "M", "𡴋"), - (0x2F8F9, "M", "𣫺"), - (0x2F8FA, "M", "汎"), - (0x2F8FB, "M", "𣲼"), - (0x2F8FC, "M", "沿"), - (0x2F8FD, "M", "泍"), - (0x2F8FE, "M", "汧"), - (0x2F8FF, "M", "洖"), - (0x2F900, "M", "派"), - (0x2F901, "M", "海"), - (0x2F902, "M", "流"), - (0x2F903, "M", "浩"), - (0x2F904, "M", "浸"), - (0x2F905, "M", "涅"), - (0x2F906, "M", "𣴞"), - (0x2F907, "M", "洴"), - (0x2F908, "M", "港"), - (0x2F909, "M", "湮"), - (0x2F90A, "M", "㴳"), - (0x2F90B, "M", "滋"), - (0x2F90C, "M", "滇"), - (0x2F90D, "M", "𣻑"), - (0x2F90E, "M", "淹"), - (0x2F90F, "M", "潮"), - (0x2F910, "M", "𣽞"), - (0x2F911, "M", "𣾎"), - (0x2F912, "M", "濆"), - (0x2F913, "M", "瀹"), - (0x2F914, "M", "瀞"), - (0x2F915, "M", "瀛"), - (0x2F916, "M", "㶖"), - (0x2F917, "M", "灊"), - (0x2F918, "M", "災"), - (0x2F919, "M", "灷"), - (0x2F91A, "M", "炭"), - (0x2F91B, "M", "𠔥"), - (0x2F91C, "M", "煅"), - (0x2F91D, "M", "𤉣"), - (0x2F91E, "M", "熜"), - (0x2F91F, "M", "𤎫"), - (0x2F920, "M", "爨"), - (0x2F921, "M", "爵"), - (0x2F922, "M", "牐"), - (0x2F923, "M", "𤘈"), - (0x2F924, "M", "犀"), - (0x2F925, "M", "犕"), - (0x2F926, "M", "𤜵"), - (0x2F927, "M", "𤠔"), - (0x2F928, "M", "獺"), - (0x2F929, "M", "王"), - (0x2F92A, "M", "㺬"), - (0x2F92B, "M", "玥"), - (0x2F92C, "M", "㺸"), - (0x2F92E, "M", "瑇"), - (0x2F92F, "M", "瑜"), - (0x2F930, "M", "瑱"), - (0x2F931, "M", "璅"), - (0x2F932, "M", "瓊"), - (0x2F933, "M", "㼛"), - (0x2F934, "M", "甤"), - (0x2F935, "M", "𤰶"), - (0x2F936, "M", "甾"), - (0x2F937, "M", "𤲒"), - (0x2F938, "M", "異"), - (0x2F939, "M", "𢆟"), - (0x2F93A, "M", "瘐"), - (0x2F93B, "M", "𤾡"), - (0x2F93C, "M", "𤾸"), - (0x2F93D, "M", "𥁄"), - (0x2F93E, "M", "㿼"), - (0x2F93F, "M", "䀈"), - (0x2F940, "M", "直"), - (0x2F941, "M", "𥃳"), - ] - - -def _seg_81() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2F942, "M", "𥃲"), - (0x2F943, "M", "𥄙"), - (0x2F944, "M", "𥄳"), - (0x2F945, "M", "眞"), - (0x2F946, "M", "真"), - (0x2F948, "M", "睊"), - (0x2F949, "M", "䀹"), - (0x2F94A, "M", "瞋"), - (0x2F94B, "M", "䁆"), - (0x2F94C, "M", "䂖"), - (0x2F94D, "M", "𥐝"), - (0x2F94E, "M", "硎"), - (0x2F94F, "M", "碌"), - (0x2F950, "M", "磌"), - (0x2F951, "M", "䃣"), - (0x2F952, "M", "𥘦"), - (0x2F953, "M", "祖"), - (0x2F954, "M", "𥚚"), - (0x2F955, "M", "𥛅"), - (0x2F956, "M", "福"), - (0x2F957, "M", "秫"), - (0x2F958, "M", "䄯"), - (0x2F959, "M", "穀"), - (0x2F95A, "M", "穊"), - (0x2F95B, "M", "穏"), - (0x2F95C, "M", "𥥼"), - (0x2F95D, "M", "𥪧"), - (0x2F95F, "M", "竮"), - (0x2F960, "M", "䈂"), - (0x2F961, "M", "𥮫"), - (0x2F962, "M", "篆"), - (0x2F963, "M", "築"), - (0x2F964, "M", "䈧"), - (0x2F965, "M", "𥲀"), - (0x2F966, "M", "糒"), - (0x2F967, "M", "䊠"), - (0x2F968, "M", "糨"), - (0x2F969, "M", "糣"), - (0x2F96A, "M", "紀"), - (0x2F96B, "M", "𥾆"), - (0x2F96C, "M", "絣"), - (0x2F96D, "M", "䌁"), - (0x2F96E, "M", "緇"), - (0x2F96F, "M", "縂"), - (0x2F970, "M", "繅"), - (0x2F971, "M", "䌴"), - (0x2F972, "M", "𦈨"), - (0x2F973, "M", "𦉇"), - (0x2F974, "M", "䍙"), - (0x2F975, "M", "𦋙"), - (0x2F976, "M", "罺"), - (0x2F977, "M", "𦌾"), - (0x2F978, "M", "羕"), - (0x2F979, "M", "翺"), - (0x2F97A, "M", "者"), - (0x2F97B, "M", "𦓚"), - (0x2F97C, "M", "𦔣"), - (0x2F97D, "M", "聠"), - (0x2F97E, "M", "𦖨"), - (0x2F97F, "M", "聰"), - (0x2F980, "M", "𣍟"), - (0x2F981, "M", "䏕"), - (0x2F982, "M", "育"), - (0x2F983, "M", "脃"), - (0x2F984, "M", "䐋"), - (0x2F985, "M", "脾"), - (0x2F986, "M", "媵"), - (0x2F987, "M", "𦞧"), - (0x2F988, "M", "𦞵"), - (0x2F989, "M", "𣎓"), - (0x2F98A, "M", "𣎜"), - (0x2F98B, "M", "舁"), - (0x2F98C, "M", "舄"), - (0x2F98D, "M", "辞"), - (0x2F98E, "M", "䑫"), - (0x2F98F, "M", "芑"), - (0x2F990, "M", "芋"), - (0x2F991, "M", "芝"), - (0x2F992, "M", "劳"), - (0x2F993, "M", "花"), - (0x2F994, "M", "芳"), - (0x2F995, "M", "芽"), - (0x2F996, "M", "苦"), - (0x2F997, "M", "𦬼"), - (0x2F998, "M", "若"), - (0x2F999, "M", "茝"), - (0x2F99A, "M", "荣"), - (0x2F99B, "M", "莭"), - (0x2F99C, "M", "茣"), - (0x2F99D, "M", "莽"), - (0x2F99E, "M", "菧"), - (0x2F99F, "M", "著"), - (0x2F9A0, "M", "荓"), - (0x2F9A1, "M", "菊"), - (0x2F9A2, "M", "菌"), - (0x2F9A3, "M", "菜"), - (0x2F9A4, "M", "𦰶"), - (0x2F9A5, "M", "𦵫"), - (0x2F9A6, "M", "𦳕"), - (0x2F9A7, "M", "䔫"), - ] - - -def _seg_82() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2F9A8, "M", "蓱"), - (0x2F9A9, "M", "蓳"), - (0x2F9AA, "M", "蔖"), - (0x2F9AB, "M", "𧏊"), - (0x2F9AC, "M", "蕤"), - (0x2F9AD, "M", "𦼬"), - (0x2F9AE, "M", "䕝"), - (0x2F9AF, "M", "䕡"), - (0x2F9B0, "M", "𦾱"), - (0x2F9B1, "M", "𧃒"), - (0x2F9B2, "M", "䕫"), - (0x2F9B3, "M", "虐"), - (0x2F9B4, "M", "虜"), - (0x2F9B5, "M", "虧"), - (0x2F9B6, "M", "虩"), - (0x2F9B7, "M", "蚩"), - (0x2F9B8, "M", "蚈"), - (0x2F9B9, "M", "蜎"), - (0x2F9BA, "M", "蛢"), - (0x2F9BB, "M", "蝹"), - (0x2F9BC, "M", "蜨"), - (0x2F9BD, "M", "蝫"), - (0x2F9BE, "M", "螆"), - (0x2F9BF, "M", "䗗"), - (0x2F9C0, "M", "蟡"), - (0x2F9C1, "M", "蠁"), - (0x2F9C2, "M", "䗹"), - (0x2F9C3, "M", "衠"), - (0x2F9C4, "M", "衣"), - (0x2F9C5, "M", "𧙧"), - (0x2F9C6, "M", "裗"), - (0x2F9C7, "M", "裞"), - (0x2F9C8, "M", "䘵"), - (0x2F9C9, "M", "裺"), - (0x2F9CA, "M", "㒻"), - (0x2F9CB, "M", "𧢮"), - (0x2F9CC, "M", "𧥦"), - (0x2F9CD, "M", "䚾"), - (0x2F9CE, "M", "䛇"), - (0x2F9CF, "M", "誠"), - (0x2F9D0, "M", "諭"), - (0x2F9D1, "M", "變"), - (0x2F9D2, "M", "豕"), - (0x2F9D3, "M", "𧲨"), - (0x2F9D4, "M", "貫"), - (0x2F9D5, "M", "賁"), - (0x2F9D6, "M", "贛"), - (0x2F9D7, "M", "起"), - (0x2F9D8, "M", "𧼯"), - (0x2F9D9, "M", "𠠄"), - (0x2F9DA, "M", "跋"), - (0x2F9DB, "M", "趼"), - (0x2F9DC, "M", "跰"), - (0x2F9DD, "M", "𠣞"), - (0x2F9DE, "M", "軔"), - (0x2F9DF, "M", "輸"), - (0x2F9E0, "M", "𨗒"), - (0x2F9E1, "M", "𨗭"), - (0x2F9E2, "M", "邔"), - (0x2F9E3, "M", "郱"), - (0x2F9E4, "M", "鄑"), - (0x2F9E5, "M", "𨜮"), - (0x2F9E6, "M", "鄛"), - (0x2F9E7, "M", "鈸"), - (0x2F9E8, "M", "鋗"), - (0x2F9E9, "M", "鋘"), - (0x2F9EA, "M", "鉼"), - (0x2F9EB, "M", "鏹"), - (0x2F9EC, "M", "鐕"), - (0x2F9ED, "M", "𨯺"), - (0x2F9EE, "M", "開"), - (0x2F9EF, "M", "䦕"), - (0x2F9F0, "M", "閷"), - (0x2F9F1, "M", "𨵷"), - (0x2F9F2, "M", "䧦"), - (0x2F9F3, "M", "雃"), - (0x2F9F4, "M", "嶲"), - (0x2F9F5, "M", "霣"), - (0x2F9F6, "M", "𩅅"), - (0x2F9F7, "M", "𩈚"), - (0x2F9F8, "M", "䩮"), - (0x2F9F9, "M", "䩶"), - (0x2F9FA, "M", "韠"), - (0x2F9FB, "M", "𩐊"), - (0x2F9FC, "M", "䪲"), - (0x2F9FD, "M", "𩒖"), - (0x2F9FE, "M", "頋"), - (0x2FA00, "M", "頩"), - (0x2FA01, "M", "𩖶"), - (0x2FA02, "M", "飢"), - (0x2FA03, "M", "䬳"), - (0x2FA04, "M", "餩"), - (0x2FA05, "M", "馧"), - (0x2FA06, "M", "駂"), - (0x2FA07, "M", "駾"), - (0x2FA08, "M", "䯎"), - (0x2FA09, "M", "𩬰"), - (0x2FA0A, "M", "鬒"), - (0x2FA0B, "M", "鱀"), - (0x2FA0C, "M", "鳽"), - ] - - -def _seg_83() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: - return [ - (0x2FA0D, "M", "䳎"), - (0x2FA0E, "M", "䳭"), - (0x2FA0F, "M", "鵧"), - (0x2FA10, "M", "𪃎"), - (0x2FA11, "M", "䳸"), - (0x2FA12, "M", "𪄅"), - (0x2FA13, "M", "𪈎"), - (0x2FA14, "M", "𪊑"), - (0x2FA15, "M", "麻"), - (0x2FA16, "M", "䵖"), - (0x2FA17, "M", "黹"), - (0x2FA18, "M", "黾"), - (0x2FA19, "M", "鼅"), - (0x2FA1A, "M", "鼏"), - (0x2FA1B, "M", "鼖"), - (0x2FA1C, "M", "鼻"), - (0x2FA1D, "M", "𪘀"), - (0x2FA1E, "X"), - (0x30000, "V"), - (0x3134B, "X"), - (0x31350, "V"), - (0x323B0, "X"), - (0xE0100, "I"), - (0xE01F0, "X"), - ] - - -uts46data = tuple( - _seg_0() - + _seg_1() - + _seg_2() - + _seg_3() - + _seg_4() - + _seg_5() - + _seg_6() - + _seg_7() - + _seg_8() - + _seg_9() - + _seg_10() - + _seg_11() - + _seg_12() - + _seg_13() - + _seg_14() - + _seg_15() - + _seg_16() - + _seg_17() - + _seg_18() - + _seg_19() - + _seg_20() - + _seg_21() - + _seg_22() - + _seg_23() - + _seg_24() - + _seg_25() - + _seg_26() - + _seg_27() - + _seg_28() - + _seg_29() - + _seg_30() - + _seg_31() - + _seg_32() - + _seg_33() - + _seg_34() - + _seg_35() - + _seg_36() - + _seg_37() - + _seg_38() - + _seg_39() - + _seg_40() - + _seg_41() - + _seg_42() - + _seg_43() - + _seg_44() - + _seg_45() - + _seg_46() - + _seg_47() - + _seg_48() - + _seg_49() - + _seg_50() - + _seg_51() - + _seg_52() - + _seg_53() - + _seg_54() - + _seg_55() - + _seg_56() - + _seg_57() - + _seg_58() - + _seg_59() - + _seg_60() - + _seg_61() - + _seg_62() - + _seg_63() - + _seg_64() - + _seg_65() - + _seg_66() - + _seg_67() - + _seg_68() - + _seg_69() - + _seg_70() - + _seg_71() - + _seg_72() - + _seg_73() - + _seg_74() - + _seg_75() - + _seg_76() - + _seg_77() - + _seg_78() - + _seg_79() - + _seg_80() - + _seg_81() - + _seg_82() - + _seg_83() -) # type: Tuple[Union[Tuple[int, str], Tuple[int, str, str]], ...] +uts46_replacements: tuple[Optional[str], ...] = ( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + " ", + None, + None, + None, + None, + None, + None, + None, + " ̈", + None, + "a", + None, + None, + None, + None, + " ̄", + None, + None, + "2", + "3", + " ́", + "μ", + None, + None, + " ̧", + "1", + "o", + None, + "1⁄4", + "1⁄2", + "3⁄4", + None, + "à", + "á", + "â", + "ã", + "ä", + "å", + "æ", + "ç", + "è", + "é", + "ê", + "ë", + "ì", + "í", + "î", + "ï", + "ð", + "ñ", + "ò", + "ó", + "ô", + "õ", + "ö", + None, + "ø", + "ù", + "ú", + "û", + "ü", + "ý", + "þ", + "ss", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ā", + None, + "ă", + None, + "ą", + None, + "ć", + None, + "ĉ", + None, + "ċ", + None, + "č", + None, + "ď", + None, + "đ", + None, + "ē", + None, + "ĕ", + None, + "ė", + None, + "ę", + None, + "ě", + None, + "ĝ", + None, + "ğ", + None, + "ġ", + None, + "ģ", + None, + "ĥ", + None, + "ħ", + None, + "ĩ", + None, + "ī", + None, + "ĭ", + None, + "į", + None, + "i̇", + None, + "ij", + "ĵ", + None, + "ķ", + None, + "ĺ", + None, + "ļ", + None, + "ľ", + None, + "l·", + "ł", + None, + "ń", + None, + "ņ", + None, + "ň", + None, + "ʼn", + "ŋ", + None, + "ō", + None, + "ŏ", + None, + "ő", + None, + "œ", + None, + "ŕ", + None, + "ŗ", + None, + "ř", + None, + "ś", + None, + "ŝ", + None, + "ş", + None, + "š", + None, + "ţ", + None, + "ť", + None, + "ŧ", + None, + "ũ", + None, + "ū", + None, + "ŭ", + None, + "ů", + None, + "ű", + None, + "ų", + None, + "ŵ", + None, + "ŷ", + None, + "ÿ", + "ź", + None, + "ż", + None, + "ž", + None, + "s", + None, + "ɓ", + "ƃ", + None, + "ƅ", + None, + "ɔ", + "ƈ", + None, + "ɖ", + "ɗ", + "ƌ", + None, + "ǝ", + "ə", + "ɛ", + "ƒ", + None, + "ɠ", + "ɣ", + None, + "ɩ", + "ɨ", + "ƙ", + None, + "ɯ", + "ɲ", + None, + "ɵ", + "ơ", + None, + "ƣ", + None, + "ƥ", + None, + "ʀ", + "ƨ", + None, + "ʃ", + None, + "ƭ", + None, + "ʈ", + "ư", + None, + "ʊ", + "ʋ", + "ƴ", + None, + "ƶ", + None, + "ʒ", + "ƹ", + None, + "ƽ", + None, + "dž", + "lj", + "nj", + "ǎ", + None, + "ǐ", + None, + "ǒ", + None, + "ǔ", + None, + "ǖ", + None, + "ǘ", + None, + "ǚ", + None, + "ǜ", + None, + "ǟ", + None, + "ǡ", + None, + "ǣ", + None, + "ǥ", + None, + "ǧ", + None, + "ǩ", + None, + "ǫ", + None, + "ǭ", + None, + "ǯ", + None, + "dz", + "ǵ", + None, + "ƕ", + "ƿ", + "ǹ", + None, + "ǻ", + None, + "ǽ", + None, + "ǿ", + None, + "ȁ", + None, + "ȃ", + None, + "ȅ", + None, + "ȇ", + None, + "ȉ", + None, + "ȋ", + None, + "ȍ", + None, + "ȏ", + None, + "ȑ", + None, + "ȓ", + None, + "ȕ", + None, + "ȗ", + None, + "ș", + None, + "ț", + None, + "ȝ", + None, + "ȟ", + None, + "ƞ", + None, + "ȣ", + None, + "ȥ", + None, + "ȧ", + None, + "ȩ", + None, + "ȫ", + None, + "ȭ", + None, + "ȯ", + None, + "ȱ", + None, + "ȳ", + None, + "ⱥ", + "ȼ", + None, + "ƚ", + "ⱦ", + None, + "ɂ", + None, + "ƀ", + "ʉ", + "ʌ", + "ɇ", + None, + "ɉ", + None, + "ɋ", + None, + "ɍ", + None, + "ɏ", + None, + "h", + "ɦ", + "j", + "r", + "ɹ", + "ɻ", + "ʁ", + "w", + "y", + None, + " ̆", + " ̇", + " ̊", + " ̨", + " ̃", + " ̋", + None, + "ɣ", + "l", + "s", + "x", + "ʕ", + None, + "̀", + "́", + None, + "̓", + "̈́", + "ι", + None, + None, + None, + "ͱ", + None, + "ͳ", + None, + "ʹ", + None, + "ͷ", + None, + None, + " ι", + None, + ";", + "ϳ", + None, + " ́", + " ̈́", + "ά", + "·", + "έ", + "ή", + "ί", + None, + "ό", + None, + "ύ", + "ώ", + None, + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + None, + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "ϊ", + "ϋ", + None, + "σ", + None, + "ϗ", + "β", + "θ", + "υ", + "ύ", + "ϋ", + "φ", + "π", + None, + "ϙ", + None, + "ϛ", + None, + "ϝ", + None, + "ϟ", + None, + "ϡ", + None, + "ϣ", + None, + "ϥ", + None, + "ϧ", + None, + "ϩ", + None, + "ϫ", + None, + "ϭ", + None, + "ϯ", + None, + "κ", + "ρ", + "σ", + None, + "θ", + "ε", + None, + "ϸ", + None, + "σ", + "ϻ", + None, + "ͻ", + "ͼ", + "ͽ", + "ѐ", + "ё", + "ђ", + "ѓ", + "є", + "ѕ", + "і", + "ї", + "ј", + "љ", + "њ", + "ћ", + "ќ", + "ѝ", + "ў", + "џ", + "а", + "б", + "в", + "г", + "д", + "е", + "ж", + "з", + "и", + "й", + "к", + "л", + "м", + "н", + "о", + "п", + "р", + "с", + "т", + "у", + "ф", + "х", + "ц", + "ч", + "ш", + "щ", + "ъ", + "ы", + "ь", + "э", + "ю", + "я", + None, + "ѡ", + None, + "ѣ", + None, + "ѥ", + None, + "ѧ", + None, + "ѩ", + None, + "ѫ", + None, + "ѭ", + None, + "ѯ", + None, + "ѱ", + None, + "ѳ", + None, + "ѵ", + None, + "ѷ", + None, + "ѹ", + None, + "ѻ", + None, + "ѽ", + None, + "ѿ", + None, + "ҁ", + None, + "ҋ", + None, + "ҍ", + None, + "ҏ", + None, + "ґ", + None, + "ғ", + None, + "ҕ", + None, + "җ", + None, + "ҙ", + None, + "қ", + None, + "ҝ", + None, + "ҟ", + None, + "ҡ", + None, + "ң", + None, + "ҥ", + None, + "ҧ", + None, + "ҩ", + None, + "ҫ", + None, + "ҭ", + None, + "ү", + None, + "ұ", + None, + "ҳ", + None, + "ҵ", + None, + "ҷ", + None, + "ҹ", + None, + "һ", + None, + "ҽ", + None, + "ҿ", + None, + "ӏ", + "ӂ", + None, + "ӄ", + None, + "ӆ", + None, + "ӈ", + None, + "ӊ", + None, + "ӌ", + None, + "ӎ", + None, + "ӑ", + None, + "ӓ", + None, + "ӕ", + None, + "ӗ", + None, + "ә", + None, + "ӛ", + None, + "ӝ", + None, + "ӟ", + None, + "ӡ", + None, + "ӣ", + None, + "ӥ", + None, + "ӧ", + None, + "ө", + None, + "ӫ", + None, + "ӭ", + None, + "ӯ", + None, + "ӱ", + None, + "ӳ", + None, + "ӵ", + None, + "ӷ", + None, + "ӹ", + None, + "ӻ", + None, + "ӽ", + None, + "ӿ", + None, + "ԁ", + None, + "ԃ", + None, + "ԅ", + None, + "ԇ", + None, + "ԉ", + None, + "ԋ", + None, + "ԍ", + None, + "ԏ", + None, + "ԑ", + None, + "ԓ", + None, + "ԕ", + None, + "ԗ", + None, + "ԙ", + None, + "ԛ", + None, + "ԝ", + None, + "ԟ", + None, + "ԡ", + None, + "ԣ", + None, + "ԥ", + None, + "ԧ", + None, + "ԩ", + None, + "ԫ", + None, + "ԭ", + None, + "ԯ", + None, + None, + "ա", + "բ", + "գ", + "դ", + "ե", + "զ", + "է", + "ը", + "թ", + "ժ", + "ի", + "լ", + "խ", + "ծ", + "կ", + "հ", + "ձ", + "ղ", + "ճ", + "մ", + "յ", + "ն", + "շ", + "ո", + "չ", + "պ", + "ջ", + "ռ", + "ս", + "վ", + "տ", + "ր", + "ց", + "ւ", + "փ", + "ք", + "օ", + "ֆ", + None, + None, + "եւ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "اٴ", + "وٴ", + "ۇٴ", + "يٴ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "क़", + "ख़", + "ग़", + "ज़", + "ड़", + "ढ़", + "फ़", + "य़", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ড়", + "ঢ়", + None, + "য়", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ਲ਼", + None, + None, + "ਸ਼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ਖ਼", + "ਗ਼", + "ਜ਼", + None, + None, + "ਫ਼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ଡ଼", + "ଢ଼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ํา", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ໍາ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ຫນ", + "ຫມ", + None, + None, + None, + "་", + None, + "གྷ", + None, + None, + None, + "ཌྷ", + None, + "དྷ", + None, + "བྷ", + None, + "ཛྷ", + None, + "ཀྵ", + None, + None, + None, + "ཱི", + None, + "ཱུ", + "ྲྀ", + "ྲཱྀ", + "ླྀ", + "ླཱྀ", + None, + "ཱྀ", + None, + "ྒྷ", + None, + None, + None, + "ྜྷ", + None, + "ྡྷ", + None, + "ྦྷ", + None, + "ྫྷ", + None, + "ྐྵ", + None, + None, + None, + None, + None, + None, + None, + "ⴀ", + "ⴁ", + "ⴂ", + "ⴃ", + "ⴄ", + "ⴅ", + "ⴆ", + "ⴇ", + "ⴈ", + "ⴉ", + "ⴊ", + "ⴋ", + "ⴌ", + "ⴍ", + "ⴎ", + "ⴏ", + "ⴐ", + "ⴑ", + "ⴒ", + "ⴓ", + "ⴔ", + "ⴕ", + "ⴖ", + "ⴗ", + "ⴘ", + "ⴙ", + "ⴚ", + "ⴛ", + "ⴜ", + "ⴝ", + "ⴞ", + "ⴟ", + "ⴠ", + "ⴡ", + "ⴢ", + "ⴣ", + "ⴤ", + "ⴥ", + None, + "ⴧ", + None, + "ⴭ", + None, + None, + "ნ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "Ᏸ", + "Ᏹ", + "Ᏺ", + "Ᏻ", + "Ᏼ", + "Ᏽ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "в", + "д", + "о", + "с", + "т", + "ъ", + "ѣ", + "ꙋ", + "\u1c8a", + None, + None, + "ა", + "ბ", + "გ", + "დ", + "ე", + "ვ", + "ზ", + "თ", + "ი", + "კ", + "ლ", + "მ", + "ნ", + "ო", + "პ", + "ჟ", + "რ", + "ს", + "ტ", + "უ", + "ფ", + "ქ", + "ღ", + "ყ", + "შ", + "ჩ", + "ც", + "ძ", + "წ", + "ჭ", + "ხ", + "ჯ", + "ჰ", + "ჱ", + "ჲ", + "ჳ", + "ჴ", + "ჵ", + "ჶ", + "ჷ", + "ჸ", + "ჹ", + "ჺ", + None, + "ჽ", + "ჾ", + "ჿ", + None, + None, + None, + None, + None, + "a", + "æ", + "b", + None, + "d", + "e", + "ǝ", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + None, + "o", + "ȣ", + "p", + "r", + "t", + "u", + "w", + "a", + "ɐ", + "ɑ", + "ᴂ", + "b", + "d", + "e", + "ə", + "ɛ", + "ɜ", + "g", + None, + "k", + "m", + "ŋ", + "o", + "ɔ", + "ᴖ", + "ᴗ", + "p", + "t", + "u", + "ᴝ", + "ɯ", + "v", + "ᴥ", + "β", + "γ", + "δ", + "φ", + "χ", + "i", + "r", + "u", + "v", + "β", + "γ", + "ρ", + "φ", + "χ", + None, + "н", + None, + "ɒ", + "c", + "ɕ", + "ð", + "ɜ", + "f", + "ɟ", + "ɡ", + "ɥ", + "ɨ", + "ɩ", + "ɪ", + "ᵻ", + "ʝ", + "ɭ", + "ᶅ", + "ʟ", + "ɱ", + "ɰ", + "ɲ", + "ɳ", + "ɴ", + "ɵ", + "ɸ", + "ʂ", + "ʃ", + "ƫ", + "ʉ", + "ʊ", + "ᴜ", + "ʋ", + "ʌ", + "z", + "ʐ", + "ʑ", + "ʒ", + "θ", + None, + "ḁ", + None, + "ḃ", + None, + "ḅ", + None, + "ḇ", + None, + "ḉ", + None, + "ḋ", + None, + "ḍ", + None, + "ḏ", + None, + "ḑ", + None, + "ḓ", + None, + "ḕ", + None, + "ḗ", + None, + "ḙ", + None, + "ḛ", + None, + "ḝ", + None, + "ḟ", + None, + "ḡ", + None, + "ḣ", + None, + "ḥ", + None, + "ḧ", + None, + "ḩ", + None, + "ḫ", + None, + "ḭ", + None, + "ḯ", + None, + "ḱ", + None, + "ḳ", + None, + "ḵ", + None, + "ḷ", + None, + "ḹ", + None, + "ḻ", + None, + "ḽ", + None, + "ḿ", + None, + "ṁ", + None, + "ṃ", + None, + "ṅ", + None, + "ṇ", + None, + "ṉ", + None, + "ṋ", + None, + "ṍ", + None, + "ṏ", + None, + "ṑ", + None, + "ṓ", + None, + "ṕ", + None, + "ṗ", + None, + "ṙ", + None, + "ṛ", + None, + "ṝ", + None, + "ṟ", + None, + "ṡ", + None, + "ṣ", + None, + "ṥ", + None, + "ṧ", + None, + "ṩ", + None, + "ṫ", + None, + "ṭ", + None, + "ṯ", + None, + "ṱ", + None, + "ṳ", + None, + "ṵ", + None, + "ṷ", + None, + "ṹ", + None, + "ṻ", + None, + "ṽ", + None, + "ṿ", + None, + "ẁ", + None, + "ẃ", + None, + "ẅ", + None, + "ẇ", + None, + "ẉ", + None, + "ẋ", + None, + "ẍ", + None, + "ẏ", + None, + "ẑ", + None, + "ẓ", + None, + "ẕ", + None, + "aʾ", + "ṡ", + None, + "ß", + None, + "ạ", + None, + "ả", + None, + "ấ", + None, + "ầ", + None, + "ẩ", + None, + "ẫ", + None, + "ậ", + None, + "ắ", + None, + "ằ", + None, + "ẳ", + None, + "ẵ", + None, + "ặ", + None, + "ẹ", + None, + "ẻ", + None, + "ẽ", + None, + "ế", + None, + "ề", + None, + "ể", + None, + "ễ", + None, + "ệ", + None, + "ỉ", + None, + "ị", + None, + "ọ", + None, + "ỏ", + None, + "ố", + None, + "ồ", + None, + "ổ", + None, + "ỗ", + None, + "ộ", + None, + "ớ", + None, + "ờ", + None, + "ở", + None, + "ỡ", + None, + "ợ", + None, + "ụ", + None, + "ủ", + None, + "ứ", + None, + "ừ", + None, + "ử", + None, + "ữ", + None, + "ự", + None, + "ỳ", + None, + "ỵ", + None, + "ỷ", + None, + "ỹ", + None, + "ỻ", + None, + "ỽ", + None, + "ỿ", + None, + "ἀ", + "ἁ", + "ἂ", + "ἃ", + "ἄ", + "ἅ", + "ἆ", + "ἇ", + None, + None, + "ἐ", + "ἑ", + "ἒ", + "ἓ", + "ἔ", + "ἕ", + None, + None, + "ἠ", + "ἡ", + "ἢ", + "ἣ", + "ἤ", + "ἥ", + "ἦ", + "ἧ", + None, + "ἰ", + "ἱ", + "ἲ", + "ἳ", + "ἴ", + "ἵ", + "ἶ", + "ἷ", + None, + None, + "ὀ", + "ὁ", + "ὂ", + "ὃ", + "ὄ", + "ὅ", + None, + None, + None, + "ὑ", + None, + "ὓ", + None, + "ὕ", + None, + "ὗ", + None, + "ὠ", + "ὡ", + "ὢ", + "ὣ", + "ὤ", + "ὥ", + "ὦ", + "ὧ", + None, + "ά", + None, + "έ", + None, + "ή", + None, + "ί", + None, + "ό", + None, + "ύ", + None, + "ώ", + None, + "ἀι", + "ἁι", + "ἂι", + "ἃι", + "ἄι", + "ἅι", + "ἆι", + "ἇι", + "ἀι", + "ἁι", + "ἂι", + "ἃι", + "ἄι", + "ἅι", + "ἆι", + "ἇι", + "ἠι", + "ἡι", + "ἢι", + "ἣι", + "ἤι", + "ἥι", + "ἦι", + "ἧι", + "ἠι", + "ἡι", + "ἢι", + "ἣι", + "ἤι", + "ἥι", + "ἦι", + "ἧι", + "ὠι", + "ὡι", + "ὢι", + "ὣι", + "ὤι", + "ὥι", + "ὦι", + "ὧι", + "ὠι", + "ὡι", + "ὢι", + "ὣι", + "ὤι", + "ὥι", + "ὦι", + "ὧι", + None, + "ὰι", + "αι", + "άι", + None, + None, + "ᾶι", + "ᾰ", + "ᾱ", + "ὰ", + "ά", + "αι", + " ̓", + "ι", + " ̓", + " ͂", + " ̈͂", + "ὴι", + "ηι", + "ήι", + None, + None, + "ῆι", + "ὲ", + "έ", + "ὴ", + "ή", + "ηι", + " ̓̀", + " ̓́", + " ̓͂", + None, + "ΐ", + None, + None, + "ῐ", + "ῑ", + "ὶ", + "ί", + None, + " ̔̀", + " ̔́", + " ̔͂", + None, + "ΰ", + None, + "ῠ", + "ῡ", + "ὺ", + "ύ", + "ῥ", + " ̈̀", + " ̈́", + "`", + None, + "ὼι", + "ωι", + "ώι", + None, + None, + "ῶι", + "ὸ", + "ό", + "ὼ", + "ώ", + "ωι", + " ́", + " ̔", + None, + " ", + None, + "", + None, + None, + "‐", + None, + " ̳", + None, + None, + None, + None, + " ", + None, + "′′", + "′′′", + None, + "‵‵", + "‵‵‵", + None, + "!!", + None, + " ̅", + None, + "??", + "?!", + "!?", + None, + "′′′′", + None, + " ", + None, + None, + None, + "0", + "i", + None, + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "−", + "=", + "(", + ")", + "n", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "−", + "=", + "(", + ")", + None, + "a", + "e", + "o", + "x", + "ə", + "h", + "k", + "l", + "m", + "n", + "p", + "s", + "t", + None, + None, + "rs", + None, + None, + None, + None, + "a/c", + "a/s", + "c", + "°c", + None, + "c/o", + "c/u", + "ɛ", + None, + "°f", + "g", + "h", + "ħ", + "i", + "l", + None, + "n", + "no", + None, + "p", + "q", + "r", + None, + "sm", + "tel", + "tm", + None, + "z", + None, + "ω", + None, + "z", + None, + "k", + "å", + "b", + "c", + None, + "e", + "f", + "ⅎ", + "m", + "o", + "א", + "ב", + "ג", + "ד", + "i", + None, + "fax", + "π", + "γ", + "π", + "∑", + None, + "d", + "e", + "i", + "j", + None, + "1⁄7", + "1⁄9", + "1⁄10", + "1⁄3", + "2⁄3", + "1⁄5", + "2⁄5", + "3⁄5", + "4⁄5", + "1⁄6", + "5⁄6", + "1⁄8", + "3⁄8", + "5⁄8", + "7⁄8", + "1⁄", + "i", + "ii", + "iii", + "iv", + "v", + "vi", + "vii", + "viii", + "ix", + "x", + "xi", + "xii", + "l", + "c", + "d", + "m", + "i", + "ii", + "iii", + "iv", + "v", + "vi", + "vii", + "viii", + "ix", + "x", + "xi", + "xii", + "l", + "c", + "d", + "m", + None, + "ↄ", + None, + "0⁄3", + None, + None, + None, + "∫∫", + "∫∫∫", + None, + "∮∮", + "∮∮∮", + None, + "〈", + "〉", + None, + None, + None, + None, + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "(1)", + "(2)", + "(3)", + "(4)", + "(5)", + "(6)", + "(7)", + "(8)", + "(9)", + "(10)", + "(11)", + "(12)", + "(13)", + "(14)", + "(15)", + "(16)", + "(17)", + "(18)", + "(19)", + "(20)", + None, + "(a)", + "(b)", + "(c)", + "(d)", + "(e)", + "(f)", + "(g)", + "(h)", + "(i)", + "(j)", + "(k)", + "(l)", + "(m)", + "(n)", + "(o)", + "(p)", + "(q)", + "(r)", + "(s)", + "(t)", + "(u)", + "(v)", + "(w)", + "(x)", + "(y)", + "(z)", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + None, + "∫∫∫∫", + None, + "::=", + "==", + "===", + None, + "⫝̸", + None, + None, + None, + "ⰰ", + "ⰱ", + "ⰲ", + "ⰳ", + "ⰴ", + "ⰵ", + "ⰶ", + "ⰷ", + "ⰸ", + "ⰹ", + "ⰺ", + "ⰻ", + "ⰼ", + "ⰽ", + "ⰾ", + "ⰿ", + "ⱀ", + "ⱁ", + "ⱂ", + "ⱃ", + "ⱄ", + "ⱅ", + "ⱆ", + "ⱇ", + "ⱈ", + "ⱉ", + "ⱊ", + "ⱋ", + "ⱌ", + "ⱍ", + "ⱎ", + "ⱏ", + "ⱐ", + "ⱑ", + "ⱒ", + "ⱓ", + "ⱔ", + "ⱕ", + "ⱖ", + "ⱗ", + "ⱘ", + "ⱙ", + "ⱚ", + "ⱛ", + "ⱜ", + "ⱝ", + "ⱞ", + "ⱟ", + None, + "ⱡ", + None, + "ɫ", + "ᵽ", + "ɽ", + None, + "ⱨ", + None, + "ⱪ", + None, + "ⱬ", + None, + "ɑ", + "ɱ", + "ɐ", + "ɒ", + None, + "ⱳ", + None, + "ⱶ", + None, + "j", + "v", + "ȿ", + "ɀ", + "ⲁ", + None, + "ⲃ", + None, + "ⲅ", + None, + "ⲇ", + None, + "ⲉ", + None, + "ⲋ", + None, + "ⲍ", + None, + "ⲏ", + None, + "ⲑ", + None, + "ⲓ", + None, + "ⲕ", + None, + "ⲗ", + None, + "ⲙ", + None, + "ⲛ", + None, + "ⲝ", + None, + "ⲟ", + None, + "ⲡ", + None, + "ⲣ", + None, + "ⲥ", + None, + "ⲧ", + None, + "ⲩ", + None, + "ⲫ", + None, + "ⲭ", + None, + "ⲯ", + None, + "ⲱ", + None, + "ⲳ", + None, + "ⲵ", + None, + "ⲷ", + None, + "ⲹ", + None, + "ⲻ", + None, + "ⲽ", + None, + "ⲿ", + None, + "ⳁ", + None, + "ⳃ", + None, + "ⳅ", + None, + "ⳇ", + None, + "ⳉ", + None, + "ⳋ", + None, + "ⳍ", + None, + "ⳏ", + None, + "ⳑ", + None, + "ⳓ", + None, + "ⳕ", + None, + "ⳗ", + None, + "ⳙ", + None, + "ⳛ", + None, + "ⳝ", + None, + "ⳟ", + None, + "ⳡ", + None, + "ⳣ", + None, + "ⳬ", + None, + "ⳮ", + None, + "ⳳ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ⵡ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "母", + None, + "龟", + None, + "一", + "丨", + "丶", + "丿", + "乙", + "亅", + "二", + "亠", + "人", + "儿", + "入", + "八", + "冂", + "冖", + "冫", + "几", + "凵", + "刀", + "力", + "勹", + "匕", + "匚", + "匸", + "十", + "卜", + "卩", + "厂", + "厶", + "又", + "口", + "囗", + "土", + "士", + "夂", + "夊", + "夕", + "大", + "女", + "子", + "宀", + "寸", + "小", + "尢", + "尸", + "屮", + "山", + "巛", + "工", + "己", + "巾", + "干", + "幺", + "广", + "廴", + "廾", + "弋", + "弓", + "彐", + "彡", + "彳", + "心", + "戈", + "戶", + "手", + "支", + "攴", + "文", + "斗", + "斤", + "方", + "无", + "日", + "曰", + "月", + "木", + "欠", + "止", + "歹", + "殳", + "毋", + "比", + "毛", + "氏", + "气", + "水", + "火", + "爪", + "父", + "爻", + "爿", + "片", + "牙", + "牛", + "犬", + "玄", + "玉", + "瓜", + "瓦", + "甘", + "生", + "用", + "田", + "疋", + "疒", + "癶", + "白", + "皮", + "皿", + "目", + "矛", + "矢", + "石", + "示", + "禸", + "禾", + "穴", + "立", + "竹", + "米", + "糸", + "缶", + "网", + "羊", + "羽", + "老", + "而", + "耒", + "耳", + "聿", + "肉", + "臣", + "自", + "至", + "臼", + "舌", + "舛", + "舟", + "艮", + "色", + "艸", + "虍", + "虫", + "血", + "行", + "衣", + "襾", + "見", + "角", + "言", + "谷", + "豆", + "豕", + "豸", + "貝", + "赤", + "走", + "足", + "身", + "車", + "辛", + "辰", + "辵", + "邑", + "酉", + "釆", + "里", + "金", + "長", + "門", + "阜", + "隶", + "隹", + "雨", + "靑", + "非", + "面", + "革", + "韋", + "韭", + "音", + "頁", + "風", + "飛", + "食", + "首", + "香", + "馬", + "骨", + "高", + "髟", + "鬥", + "鬯", + "鬲", + "鬼", + "魚", + "鳥", + "鹵", + "鹿", + "麥", + "麻", + "黃", + "黍", + "黑", + "黹", + "黽", + "鼎", + "鼓", + "鼠", + "鼻", + "齊", + "齒", + "龍", + "龜", + "龠", + None, + " ", + None, + ".", + None, + "〒", + None, + "十", + "卄", + "卅", + None, + None, + None, + None, + None, + " ゙", + " ゚", + None, + "より", + None, + "コト", + None, + None, + None, + "ᄀ", + "ᄁ", + "ᆪ", + "ᄂ", + "ᆬ", + "ᆭ", + "ᄃ", + "ᄄ", + "ᄅ", + "ᆰ", + "ᆱ", + "ᆲ", + "ᆳ", + "ᆴ", + "ᆵ", + "ᄚ", + "ᄆ", + "ᄇ", + "ᄈ", + "ᄡ", + "ᄉ", + "ᄊ", + "ᄋ", + "ᄌ", + "ᄍ", + "ᄎ", + "ᄏ", + "ᄐ", + "ᄑ", + "ᄒ", + "ᅡ", + "ᅢ", + "ᅣ", + "ᅤ", + "ᅥ", + "ᅦ", + "ᅧ", + "ᅨ", + "ᅩ", + "ᅪ", + "ᅫ", + "ᅬ", + "ᅭ", + "ᅮ", + "ᅯ", + "ᅰ", + "ᅱ", + "ᅲ", + "ᅳ", + "ᅴ", + "ᅵ", + None, + "ᄔ", + "ᄕ", + "ᇇ", + "ᇈ", + "ᇌ", + "ᇎ", + "ᇓ", + "ᇗ", + "ᇙ", + "ᄜ", + "ᇝ", + "ᇟ", + "ᄝ", + "ᄞ", + "ᄠ", + "ᄢ", + "ᄣ", + "ᄧ", + "ᄩ", + "ᄫ", + "ᄬ", + "ᄭ", + "ᄮ", + "ᄯ", + "ᄲ", + "ᄶ", + "ᅀ", + "ᅇ", + "ᅌ", + "ᇱ", + "ᇲ", + "ᅗ", + "ᅘ", + "ᅙ", + "ᆄ", + "ᆅ", + "ᆈ", + "ᆑ", + "ᆒ", + "ᆔ", + "ᆞ", + "ᆡ", + None, + None, + "一", + "二", + "三", + "四", + "上", + "中", + "下", + "甲", + "乙", + "丙", + "丁", + "天", + "地", + "人", + None, + None, + None, + "(ᄀ)", + "(ᄂ)", + "(ᄃ)", + "(ᄅ)", + "(ᄆ)", + "(ᄇ)", + "(ᄉ)", + "(ᄋ)", + "(ᄌ)", + "(ᄎ)", + "(ᄏ)", + "(ᄐ)", + "(ᄑ)", + "(ᄒ)", + "(가)", + "(나)", + "(다)", + "(라)", + "(마)", + "(바)", + "(사)", + "(아)", + "(자)", + "(차)", + "(카)", + "(타)", + "(파)", + "(하)", + "(주)", + "(오전)", + "(오후)", + None, + "(一)", + "(二)", + "(三)", + "(四)", + "(五)", + "(六)", + "(七)", + "(八)", + "(九)", + "(十)", + "(月)", + "(火)", + "(水)", + "(木)", + "(金)", + "(土)", + "(日)", + "(株)", + "(有)", + "(社)", + "(名)", + "(特)", + "(財)", + "(祝)", + "(労)", + "(代)", + "(呼)", + "(学)", + "(監)", + "(企)", + "(資)", + "(協)", + "(祭)", + "(休)", + "(自)", + "(至)", + "問", + "幼", + "文", + "箏", + None, + "pte", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "ᄀ", + "ᄂ", + "ᄃ", + "ᄅ", + "ᄆ", + "ᄇ", + "ᄉ", + "ᄋ", + "ᄌ", + "ᄎ", + "ᄏ", + "ᄐ", + "ᄑ", + "ᄒ", + "가", + "나", + "다", + "라", + "마", + "바", + "사", + "아", + "자", + "차", + "카", + "타", + "파", + "하", + "참고", + "주의", + "우", + None, + "一", + "二", + "三", + "四", + "五", + "六", + "七", + "八", + "九", + "十", + "月", + "火", + "水", + "木", + "金", + "土", + "日", + "株", + "有", + "社", + "名", + "特", + "財", + "祝", + "労", + "秘", + "男", + "女", + "適", + "優", + "印", + "注", + "項", + "休", + "写", + "正", + "上", + "中", + "下", + "左", + "右", + "医", + "宗", + "学", + "監", + "企", + "資", + "協", + "夜", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + "hg", + "erg", + "ev", + "ltd", + "ア", + "イ", + "ウ", + "エ", + "オ", + "カ", + "キ", + "ク", + "ケ", + "コ", + "サ", + "シ", + "ス", + "セ", + "ソ", + "タ", + "チ", + "ツ", + "テ", + "ト", + "ナ", + "ニ", + "ヌ", + "ネ", + "ノ", + "ハ", + "ヒ", + "フ", + "ヘ", + "ホ", + "マ", + "ミ", + "ム", + "メ", + "モ", + "ヤ", + "ユ", + "ヨ", + "ラ", + "リ", + "ル", + "レ", + "ロ", + "ワ", + "ヰ", + "ヱ", + "ヲ", + "令和", + "アパート", + "アルファ", + "アンペア", + "アール", + "イニング", + "インチ", + "ウォン", + "エスクード", + "エーカー", + "オンス", + "オーム", + "カイリ", + "カラット", + "カロリー", + "ガロン", + "ガンマ", + "ギガ", + "ギニー", + "キュリー", + "ギルダー", + "キロ", + "キログラム", + "キロメートル", + "キロワット", + "グラム", + "グラムトン", + "クルゼイロ", + "クローネ", + "ケース", + "コルナ", + "コーポ", + "サイクル", + "サンチーム", + "シリング", + "センチ", + "セント", + "ダース", + "デシ", + "ドル", + "トン", + "ナノ", + "ノット", + "ハイツ", + "パーセント", + "パーツ", + "バーレル", + "ピアストル", + "ピクル", + "ピコ", + "ビル", + "ファラッド", + "フィート", + "ブッシェル", + "フラン", + "ヘクタール", + "ペソ", + "ペニヒ", + "ヘルツ", + "ペンス", + "ページ", + "ベータ", + "ポイント", + "ボルト", + "ホン", + "ポンド", + "ホール", + "ホーン", + "マイクロ", + "マイル", + "マッハ", + "マルク", + "マンション", + "ミクロン", + "ミリ", + "ミリバール", + "メガ", + "メガトン", + "メートル", + "ヤード", + "ヤール", + "ユアン", + "リットル", + "リラ", + "ルピー", + "ルーブル", + "レム", + "レントゲン", + "ワット", + "0点", + "1点", + "2点", + "3点", + "4点", + "5点", + "6点", + "7点", + "8点", + "9点", + "10点", + "11点", + "12点", + "13点", + "14点", + "15点", + "16点", + "17点", + "18点", + "19点", + "20点", + "21点", + "22点", + "23点", + "24点", + "hpa", + "da", + "au", + "bar", + "ov", + "pc", + "dm", + "dm2", + "dm3", + "iu", + "平成", + "昭和", + "大正", + "明治", + "株式会社", + "pa", + "na", + "μa", + "ma", + "ka", + "kb", + "mb", + "gb", + "cal", + "kcal", + "pf", + "nf", + "μf", + "μg", + "mg", + "kg", + "hz", + "khz", + "mhz", + "ghz", + "thz", + "μl", + "ml", + "dl", + "kl", + "fm", + "nm", + "μm", + "mm", + "cm", + "km", + "mm2", + "cm2", + "m2", + "km2", + "mm3", + "cm3", + "m3", + "km3", + "m∕s", + "m∕s2", + "pa", + "kpa", + "mpa", + "gpa", + "rad", + "rad∕s", + "rad∕s2", + "ps", + "ns", + "μs", + "ms", + "pv", + "nv", + "μv", + "mv", + "kv", + "mv", + "pw", + "nw", + "μw", + "mw", + "kw", + "mw", + "kω", + "mω", + None, + "bq", + "cc", + "cd", + "c∕kg", + None, + "db", + "gy", + "ha", + "hp", + "in", + "kk", + "km", + "kt", + "lm", + "ln", + "log", + "lx", + "mb", + "mil", + "mol", + "ph", + None, + "ppm", + "pr", + "sr", + "sv", + "wb", + "v∕m", + "a∕m", + "1日", + "2日", + "3日", + "4日", + "5日", + "6日", + "7日", + "8日", + "9日", + "10日", + "11日", + "12日", + "13日", + "14日", + "15日", + "16日", + "17日", + "18日", + "19日", + "20日", + "21日", + "22日", + "23日", + "24日", + "25日", + "26日", + "27日", + "28日", + "29日", + "30日", + "31日", + "gal", + None, + None, + None, + None, + None, + None, + "ꙁ", + None, + "ꙃ", + None, + "ꙅ", + None, + "ꙇ", + None, + "ꙉ", + None, + "ꙋ", + None, + "ꙍ", + None, + "ꙏ", + None, + "ꙑ", + None, + "ꙓ", + None, + "ꙕ", + None, + "ꙗ", + None, + "ꙙ", + None, + "ꙛ", + None, + "ꙝ", + None, + "ꙟ", + None, + "ꙡ", + None, + "ꙣ", + None, + "ꙥ", + None, + "ꙧ", + None, + "ꙩ", + None, + "ꙫ", + None, + "ꙭ", + None, + "ꚁ", + None, + "ꚃ", + None, + "ꚅ", + None, + "ꚇ", + None, + "ꚉ", + None, + "ꚋ", + None, + "ꚍ", + None, + "ꚏ", + None, + "ꚑ", + None, + "ꚓ", + None, + "ꚕ", + None, + "ꚗ", + None, + "ꚙ", + None, + "ꚛ", + None, + "ъ", + "ь", + None, + None, + None, + "ꜣ", + None, + "ꜥ", + None, + "ꜧ", + None, + "ꜩ", + None, + "ꜫ", + None, + "ꜭ", + None, + "ꜯ", + None, + "ꜳ", + None, + "ꜵ", + None, + "ꜷ", + None, + "ꜹ", + None, + "ꜻ", + None, + "ꜽ", + None, + "ꜿ", + None, + "ꝁ", + None, + "ꝃ", + None, + "ꝅ", + None, + "ꝇ", + None, + "ꝉ", + None, + "ꝋ", + None, + "ꝍ", + None, + "ꝏ", + None, + "ꝑ", + None, + "ꝓ", + None, + "ꝕ", + None, + "ꝗ", + None, + "ꝙ", + None, + "ꝛ", + None, + "ꝝ", + None, + "ꝟ", + None, + "ꝡ", + None, + "ꝣ", + None, + "ꝥ", + None, + "ꝧ", + None, + "ꝩ", + None, + "ꝫ", + None, + "ꝭ", + None, + "ꝯ", + None, + "ꝯ", + None, + "ꝺ", + None, + "ꝼ", + None, + "ᵹ", + "ꝿ", + None, + "ꞁ", + None, + "ꞃ", + None, + "ꞅ", + None, + "ꞇ", + None, + "ꞌ", + None, + "ɥ", + None, + "ꞑ", + None, + "ꞓ", + None, + "ꞗ", + None, + "ꞙ", + None, + "ꞛ", + None, + "ꞝ", + None, + "ꞟ", + None, + "ꞡ", + None, + "ꞣ", + None, + "ꞥ", + None, + "ꞧ", + None, + "ꞩ", + None, + "ɦ", + "ɜ", + "ɡ", + "ɬ", + "ɪ", + None, + "ʞ", + "ʇ", + "ʝ", + "ꭓ", + "ꞵ", + None, + "ꞷ", + None, + "ꞹ", + None, + "ꞻ", + None, + "ꞽ", + None, + "ꞿ", + None, + "ꟁ", + None, + "ꟃ", + None, + "ꞔ", + "ʂ", + "ᶎ", + "ꟈ", + None, + "ꟊ", + None, + "ɤ", + "\ua7cd", + None, + "\ua7cf", + None, + "ꟑ", + None, + "ꟓ", + None, + "ꟕ", + None, + "ꟗ", + None, + "ꟙ", + None, + "\ua7db", + None, + "ƛ", + None, + "s", + "c", + "f", + "q", + "ꟶ", + None, + "ħ", + "œ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ꜧ", + "ꬷ", + "ɫ", + "ꭒ", + None, + "ʍ", + None, + None, + "Ꭰ", + "Ꭱ", + "Ꭲ", + "Ꭳ", + "Ꭴ", + "Ꭵ", + "Ꭶ", + "Ꭷ", + "Ꭸ", + "Ꭹ", + "Ꭺ", + "Ꭻ", + "Ꭼ", + "Ꭽ", + "Ꭾ", + "Ꭿ", + "Ꮀ", + "Ꮁ", + "Ꮂ", + "Ꮃ", + "Ꮄ", + "Ꮅ", + "Ꮆ", + "Ꮇ", + "Ꮈ", + "Ꮉ", + "Ꮊ", + "Ꮋ", + "Ꮌ", + "Ꮍ", + "Ꮎ", + "Ꮏ", + "Ꮐ", + "Ꮑ", + "Ꮒ", + "Ꮓ", + "Ꮔ", + "Ꮕ", + "Ꮖ", + "Ꮗ", + "Ꮘ", + "Ꮙ", + "Ꮚ", + "Ꮛ", + "Ꮜ", + "Ꮝ", + "Ꮞ", + "Ꮟ", + "Ꮠ", + "Ꮡ", + "Ꮢ", + "Ꮣ", + "Ꮤ", + "Ꮥ", + "Ꮦ", + "Ꮧ", + "Ꮨ", + "Ꮩ", + "Ꮪ", + "Ꮫ", + "Ꮬ", + "Ꮭ", + "Ꮮ", + "Ꮯ", + "Ꮰ", + "Ꮱ", + "Ꮲ", + "Ꮳ", + "Ꮴ", + "Ꮵ", + "Ꮶ", + "Ꮷ", + "Ꮸ", + "Ꮹ", + "Ꮺ", + "Ꮻ", + "Ꮼ", + "Ꮽ", + "Ꮾ", + "Ꮿ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "豈", + "更", + "車", + "賈", + "滑", + "串", + "句", + "龜", + "契", + "金", + "喇", + "奈", + "懶", + "癩", + "羅", + "蘿", + "螺", + "裸", + "邏", + "樂", + "洛", + "烙", + "珞", + "落", + "酪", + "駱", + "亂", + "卵", + "欄", + "爛", + "蘭", + "鸞", + "嵐", + "濫", + "藍", + "襤", + "拉", + "臘", + "蠟", + "廊", + "朗", + "浪", + "狼", + "郎", + "來", + "冷", + "勞", + "擄", + "櫓", + "爐", + "盧", + "老", + "蘆", + "虜", + "路", + "露", + "魯", + "鷺", + "碌", + "祿", + "綠", + "菉", + "錄", + "鹿", + "論", + "壟", + "弄", + "籠", + "聾", + "牢", + "磊", + "賂", + "雷", + "壘", + "屢", + "樓", + "淚", + "漏", + "累", + "縷", + "陋", + "勒", + "肋", + "凜", + "凌", + "稜", + "綾", + "菱", + "陵", + "讀", + "拏", + "樂", + "諾", + "丹", + "寧", + "怒", + "率", + "異", + "北", + "磻", + "便", + "復", + "不", + "泌", + "數", + "索", + "參", + "塞", + "省", + "葉", + "說", + "殺", + "辰", + "沈", + "拾", + "若", + "掠", + "略", + "亮", + "兩", + "凉", + "梁", + "糧", + "良", + "諒", + "量", + "勵", + "呂", + "女", + "廬", + "旅", + "濾", + "礪", + "閭", + "驪", + "麗", + "黎", + "力", + "曆", + "歷", + "轢", + "年", + "憐", + "戀", + "撚", + "漣", + "煉", + "璉", + "秊", + "練", + "聯", + "輦", + "蓮", + "連", + "鍊", + "列", + "劣", + "咽", + "烈", + "裂", + "說", + "廉", + "念", + "捻", + "殮", + "簾", + "獵", + "令", + "囹", + "寧", + "嶺", + "怜", + "玲", + "瑩", + "羚", + "聆", + "鈴", + "零", + "靈", + "領", + "例", + "禮", + "醴", + "隸", + "惡", + "了", + "僚", + "寮", + "尿", + "料", + "樂", + "燎", + "療", + "蓼", + "遼", + "龍", + "暈", + "阮", + "劉", + "杻", + "柳", + "流", + "溜", + "琉", + "留", + "硫", + "紐", + "類", + "六", + "戮", + "陸", + "倫", + "崙", + "淪", + "輪", + "律", + "慄", + "栗", + "率", + "隆", + "利", + "吏", + "履", + "易", + "李", + "梨", + "泥", + "理", + "痢", + "罹", + "裏", + "裡", + "里", + "離", + "匿", + "溺", + "吝", + "燐", + "璘", + "藺", + "隣", + "鱗", + "麟", + "林", + "淋", + "臨", + "立", + "笠", + "粒", + "狀", + "炙", + "識", + "什", + "茶", + "刺", + "切", + "度", + "拓", + "糖", + "宅", + "洞", + "暴", + "輻", + "行", + "降", + "見", + "廓", + "兀", + "嗀", + None, + "塚", + None, + "晴", + None, + "凞", + "猪", + "益", + "礼", + "神", + "祥", + "福", + "靖", + "精", + "羽", + None, + "蘒", + None, + "諸", + None, + "逸", + "都", + None, + "飯", + "飼", + "館", + "鶴", + "郞", + "隷", + "侮", + "僧", + "免", + "勉", + "勤", + "卑", + "喝", + "嘆", + "器", + "塀", + "墨", + "層", + "屮", + "悔", + "慨", + "憎", + "懲", + "敏", + "既", + "暑", + "梅", + "海", + "渚", + "漢", + "煮", + "爫", + "琢", + "碑", + "社", + "祉", + "祈", + "祐", + "祖", + "祝", + "禍", + "禎", + "穀", + "突", + "節", + "練", + "縉", + "繁", + "署", + "者", + "臭", + "艹", + "著", + "褐", + "視", + "謁", + "謹", + "賓", + "贈", + "辶", + "逸", + "難", + "響", + "頻", + "恵", + "𤋮", + "舘", + None, + "並", + "况", + "全", + "侀", + "充", + "冀", + "勇", + "勺", + "喝", + "啕", + "喙", + "嗢", + "塚", + "墳", + "奄", + "奔", + "婢", + "嬨", + "廒", + "廙", + "彩", + "徭", + "惘", + "慎", + "愈", + "憎", + "慠", + "懲", + "戴", + "揄", + "搜", + "摒", + "敖", + "晴", + "朗", + "望", + "杖", + "歹", + "殺", + "流", + "滛", + "滋", + "漢", + "瀞", + "煮", + "瞧", + "爵", + "犯", + "猪", + "瑱", + "甆", + "画", + "瘝", + "瘟", + "益", + "盛", + "直", + "睊", + "着", + "磌", + "窱", + "節", + "类", + "絛", + "練", + "缾", + "者", + "荒", + "華", + "蝹", + "襁", + "覆", + "視", + "調", + "諸", + "請", + "謁", + "諾", + "諭", + "謹", + "變", + "贈", + "輸", + "遲", + "醙", + "鉶", + "陼", + "難", + "靖", + "韛", + "響", + "頋", + "頻", + "鬒", + "龜", + "𢡊", + "𢡄", + "𣏕", + "㮝", + "䀘", + "䀹", + "𥉉", + "𥳐", + "𧻓", + "齃", + "龎", + None, + "ff", + "fi", + "fl", + "ffi", + "ffl", + "st", + None, + "մն", + "մե", + "մի", + "վն", + "մխ", + None, + "יִ", + None, + "ײַ", + "ע", + "א", + "ד", + "ה", + "כ", + "ל", + "ם", + "ר", + "ת", + "+", + "שׁ", + "שׂ", + "שּׁ", + "שּׂ", + "אַ", + "אָ", + "אּ", + "בּ", + "גּ", + "דּ", + "הּ", + "וּ", + "זּ", + None, + "טּ", + "יּ", + "ךּ", + "כּ", + "לּ", + None, + "מּ", + None, + "נּ", + "סּ", + None, + "ףּ", + "פּ", + None, + "צּ", + "קּ", + "רּ", + "שּ", + "תּ", + "וֹ", + "בֿ", + "כֿ", + "פֿ", + "אל", + "ٱ", + "ٻ", + "پ", + "ڀ", + "ٺ", + "ٿ", + "ٹ", + "ڤ", + "ڦ", + "ڄ", + "ڃ", + "چ", + "ڇ", + "ڍ", + "ڌ", + "ڎ", + "ڈ", + "ژ", + "ڑ", + "ک", + "گ", + "ڳ", + "ڱ", + "ں", + "ڻ", + "ۀ", + "ہ", + "ھ", + "ے", + "ۓ", + None, + "ڭ", + "ۇ", + "ۆ", + "ۈ", + "ۇٴ", + "ۋ", + "ۅ", + "ۉ", + "ې", + "ى", + "ئا", + "ئە", + "ئو", + "ئۇ", + "ئۆ", + "ئۈ", + "ئې", + "ئى", + "ی", + "ئج", + "ئح", + "ئم", + "ئى", + "ئي", + "بج", + "بح", + "بخ", + "بم", + "بى", + "بي", + "تج", + "تح", + "تخ", + "تم", + "تى", + "تي", + "ثج", + "ثم", + "ثى", + "ثي", + "جح", + "جم", + "حج", + "حم", + "خج", + "خح", + "خم", + "سج", + "سح", + "سخ", + "سم", + "صح", + "صم", + "ضج", + "ضح", + "ضخ", + "ضم", + "طح", + "طم", + "ظم", + "عج", + "عم", + "غج", + "غم", + "فج", + "فح", + "فخ", + "فم", + "فى", + "في", + "قح", + "قم", + "قى", + "قي", + "كا", + "كج", + "كح", + "كخ", + "كل", + "كم", + "كى", + "كي", + "لج", + "لح", + "لخ", + "لم", + "لى", + "لي", + "مج", + "مح", + "مخ", + "مم", + "مى", + "مي", + "نج", + "نح", + "نخ", + "نم", + "نى", + "ني", + "هج", + "هم", + "هى", + "هي", + "يج", + "يح", + "يخ", + "يم", + "يى", + "يي", + "ذٰ", + "رٰ", + "ىٰ", + " ٌّ", + " ٍّ", + " َّ", + " ُّ", + " ِّ", + " ّٰ", + "ئر", + "ئز", + "ئم", + "ئن", + "ئى", + "ئي", + "بر", + "بز", + "بم", + "بن", + "بى", + "بي", + "تر", + "تز", + "تم", + "تن", + "تى", + "تي", + "ثر", + "ثز", + "ثم", + "ثن", + "ثى", + "ثي", + "فى", + "في", + "قى", + "قي", + "كا", + "كل", + "كم", + "كى", + "كي", + "لم", + "لى", + "لي", + "ما", + "مم", + "نر", + "نز", + "نم", + "نن", + "نى", + "ني", + "ىٰ", + "ير", + "يز", + "يم", + "ين", + "يى", + "يي", + "ئج", + "ئح", + "ئخ", + "ئم", + "ئه", + "بج", + "بح", + "بخ", + "بم", + "به", + "تج", + "تح", + "تخ", + "تم", + "ته", + "ثم", + "جح", + "جم", + "حج", + "حم", + "خج", + "خم", + "سج", + "سح", + "سخ", + "سم", + "صح", + "صخ", + "صم", + "ضج", + "ضح", + "ضخ", + "ضم", + "طح", + "ظم", + "عج", + "عم", + "غج", + "غم", + "فج", + "فح", + "فخ", + "فم", + "قح", + "قم", + "كج", + "كح", + "كخ", + "كل", + "كم", + "لج", + "لح", + "لخ", + "لم", + "له", + "مج", + "مح", + "مخ", + "مم", + "نج", + "نح", + "نخ", + "نم", + "نه", + "هج", + "هم", + "هٰ", + "يج", + "يح", + "يخ", + "يم", + "يه", + "ئم", + "ئه", + "بم", + "به", + "تم", + "ته", + "ثم", + "ثه", + "سم", + "سه", + "شم", + "شه", + "كل", + "كم", + "لم", + "نم", + "نه", + "يم", + "يه", + "ـَّ", + "ـُّ", + "ـِّ", + "طى", + "طي", + "عى", + "عي", + "غى", + "غي", + "سى", + "سي", + "شى", + "شي", + "حى", + "حي", + "جى", + "جي", + "خى", + "خي", + "صى", + "صي", + "ضى", + "ضي", + "شج", + "شح", + "شخ", + "شم", + "شر", + "سر", + "صر", + "ضر", + "طى", + "طي", + "عى", + "عي", + "غى", + "غي", + "سى", + "سي", + "شى", + "شي", + "حى", + "حي", + "جى", + "جي", + "خى", + "خي", + "صى", + "صي", + "ضى", + "ضي", + "شج", + "شح", + "شخ", + "شم", + "شر", + "سر", + "صر", + "ضر", + "شج", + "شح", + "شخ", + "شم", + "سه", + "شه", + "طم", + "سج", + "سح", + "سخ", + "شج", + "شح", + "شخ", + "طم", + "ظم", + "اً", + None, + "تجم", + "تحج", + "تحم", + "تخم", + "تمج", + "تمح", + "تمخ", + "جمح", + "حمي", + "حمى", + "سحج", + "سجح", + "سجى", + "سمح", + "سمج", + "سمم", + "صحح", + "صمم", + "شحم", + "شجي", + "شمخ", + "شمم", + "ضحى", + "ضخم", + "طمح", + "طمم", + "طمي", + "عجم", + "عمم", + "عمى", + "غمم", + "غمي", + "غمى", + "فخم", + "قمح", + "قمم", + "لحم", + "لحي", + "لحى", + "لجج", + "لخم", + "لمح", + "محج", + "محم", + "محي", + "مجح", + "مجم", + "مخج", + "مخم", + None, + "مجخ", + "همج", + "همم", + "نحم", + "نحى", + "نجم", + "نجى", + "نمي", + "نمى", + "يمم", + "بخي", + "تجي", + "تجى", + "تخي", + "تخى", + "تمي", + "تمى", + "جمي", + "جحى", + "جمى", + "سخى", + "صحي", + "شحي", + "ضحي", + "لجي", + "لمي", + "يحي", + "يجي", + "يمي", + "ممي", + "قمي", + "نحي", + "قمح", + "لحم", + "عمي", + "كمي", + "نجح", + "مخي", + "لجم", + "كمم", + "لجم", + "نجح", + "جحي", + "حجي", + "مجي", + "فمي", + "بحي", + "كمم", + "عجم", + "صمم", + "سخي", + "نجي", + None, + None, + "صلے", + "قلے", + "الله", + "اكبر", + "محمد", + "صلعم", + "رسول", + "عليه", + "وسلم", + "صلى", + "صلى الله عليه وسلم", + "جل جلاله", + "ریال", + None, + None, + ",", + "、", + None, + ":", + ";", + "!", + "?", + "〖", + "〗", + None, + None, + None, + "—", + "–", + "_", + "(", + ")", + "{", + "}", + "〔", + "〕", + "【", + "】", + "《", + "》", + "〈", + "〉", + "「", + "」", + "『", + "』", + None, + "[", + "]", + " ̅", + "_", + ",", + "、", + None, + ";", + ":", + "?", + "!", + "—", + "(", + ")", + "{", + "}", + "〔", + "〕", + "#", + "&", + "*", + "+", + "-", + "<", + ">", + "=", + None, + "\\", + "$", + "%", + "@", + None, + " ً", + "ـً", + " ٌ", + None, + " ٍ", + None, + " َ", + "ـَ", + " ُ", + "ـُ", + " ِ", + "ـِ", + " ّ", + "ـّ", + " ْ", + "ـْ", + "ء", + "آ", + "أ", + "ؤ", + "إ", + "ئ", + "ا", + "ب", + "ة", + "ت", + "ث", + "ج", + "ح", + "خ", + "د", + "ذ", + "ر", + "ز", + "س", + "ش", + "ص", + "ض", + "ط", + "ظ", + "ع", + "غ", + "ف", + "ق", + "ك", + "ل", + "م", + "ن", + "ه", + "و", + "ى", + "ي", + "لآ", + "لأ", + "لإ", + "لا", + None, + None, + None, + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "[", + "\\", + "]", + "^", + "_", + "`", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "⦅", + "⦆", + ".", + "「", + "」", + "、", + "・", + "ヲ", + "ァ", + "ィ", + "ゥ", + "ェ", + "ォ", + "ャ", + "ュ", + "ョ", + "ッ", + "ー", + "ア", + "イ", + "ウ", + "エ", + "オ", + "カ", + "キ", + "ク", + "ケ", + "コ", + "サ", + "シ", + "ス", + "セ", + "ソ", + "タ", + "チ", + "ツ", + "テ", + "ト", + "ナ", + "ニ", + "ヌ", + "ネ", + "ノ", + "ハ", + "ヒ", + "フ", + "ヘ", + "ホ", + "マ", + "ミ", + "ム", + "メ", + "モ", + "ヤ", + "ユ", + "ヨ", + "ラ", + "リ", + "ル", + "レ", + "ロ", + "ワ", + "ン", + "゙", + "゚", + None, + "ᄀ", + "ᄁ", + "ᆪ", + "ᄂ", + "ᆬ", + "ᆭ", + "ᄃ", + "ᄄ", + "ᄅ", + "ᆰ", + "ᆱ", + "ᆲ", + "ᆳ", + "ᆴ", + "ᆵ", + "ᄚ", + "ᄆ", + "ᄇ", + "ᄈ", + "ᄡ", + "ᄉ", + "ᄊ", + "ᄋ", + "ᄌ", + "ᄍ", + "ᄎ", + "ᄏ", + "ᄐ", + "ᄑ", + "ᄒ", + None, + "ᅡ", + "ᅢ", + "ᅣ", + "ᅤ", + "ᅥ", + "ᅦ", + None, + "ᅧ", + "ᅨ", + "ᅩ", + "ᅪ", + "ᅫ", + "ᅬ", + None, + "ᅭ", + "ᅮ", + "ᅯ", + "ᅰ", + "ᅱ", + "ᅲ", + None, + "ᅳ", + "ᅴ", + "ᅵ", + None, + "¢", + "£", + "¬", + " ̄", + "¦", + "¥", + "₩", + None, + "│", + "←", + "↑", + "→", + "↓", + "■", + "○", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𐐨", + "𐐩", + "𐐪", + "𐐫", + "𐐬", + "𐐭", + "𐐮", + "𐐯", + "𐐰", + "𐐱", + "𐐲", + "𐐳", + "𐐴", + "𐐵", + "𐐶", + "𐐷", + "𐐸", + "𐐹", + "𐐺", + "𐐻", + "𐐼", + "𐐽", + "𐐾", + "𐐿", + "𐑀", + "𐑁", + "𐑂", + "𐑃", + "𐑄", + "𐑅", + "𐑆", + "𐑇", + "𐑈", + "𐑉", + "𐑊", + "𐑋", + "𐑌", + "𐑍", + "𐑎", + "𐑏", + None, + None, + None, + None, + "𐓘", + "𐓙", + "𐓚", + "𐓛", + "𐓜", + "𐓝", + "𐓞", + "𐓟", + "𐓠", + "𐓡", + "𐓢", + "𐓣", + "𐓤", + "𐓥", + "𐓦", + "𐓧", + "𐓨", + "𐓩", + "𐓪", + "𐓫", + "𐓬", + "𐓭", + "𐓮", + "𐓯", + "𐓰", + "𐓱", + "𐓲", + "𐓳", + "𐓴", + "𐓵", + "𐓶", + "𐓷", + "𐓸", + "𐓹", + "𐓺", + "𐓻", + None, + None, + None, + None, + None, + None, + None, + None, + "𐖗", + "𐖘", + "𐖙", + "𐖚", + "𐖛", + "𐖜", + "𐖝", + "𐖞", + "𐖟", + "𐖠", + "𐖡", + None, + "𐖣", + "𐖤", + "𐖥", + "𐖦", + "𐖧", + "𐖨", + "𐖩", + "𐖪", + "𐖫", + "𐖬", + "𐖭", + "𐖮", + "𐖯", + "𐖰", + "𐖱", + None, + "𐖳", + "𐖴", + "𐖵", + "𐖶", + "𐖷", + "𐖸", + "𐖹", + None, + "𐖻", + "𐖼", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ː", + "ˑ", + "æ", + "ʙ", + "ɓ", + None, + "ʣ", + "ꭦ", + "ʥ", + "ʤ", + "ɖ", + "ɗ", + "ᶑ", + "ɘ", + "ɞ", + "ʩ", + "ɤ", + "ɢ", + "ɠ", + "ʛ", + "ħ", + "ʜ", + "ɧ", + "ʄ", + "ʪ", + "ʫ", + "ɬ", + "𝼄", + "ꞎ", + "ɮ", + "𝼅", + "ʎ", + "𝼆", + "ø", + "ɶ", + "ɷ", + "q", + "ɺ", + "𝼈", + "ɽ", + "ɾ", + "ʀ", + "ʨ", + "ʦ", + "ꭧ", + "ʧ", + "ʈ", + "ⱱ", + None, + "ʏ", + "ʡ", + "ʢ", + "ʘ", + "ǀ", + "ǁ", + "ǂ", + "𝼊", + "𝼞", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𐳀", + "𐳁", + "𐳂", + "𐳃", + "𐳄", + "𐳅", + "𐳆", + "𐳇", + "𐳈", + "𐳉", + "𐳊", + "𐳋", + "𐳌", + "𐳍", + "𐳎", + "𐳏", + "𐳐", + "𐳑", + "𐳒", + "𐳓", + "𐳔", + "𐳕", + "𐳖", + "𐳗", + "𐳘", + "𐳙", + "𐳚", + "𐳛", + "𐳜", + "𐳝", + "𐳞", + "𐳟", + "𐳠", + "𐳡", + "𐳢", + "𐳣", + "𐳤", + "𐳥", + "𐳦", + "𐳧", + "𐳨", + "𐳩", + "𐳪", + "𐳫", + "𐳬", + "𐳭", + "𐳮", + "𐳯", + "𐳰", + "𐳱", + "𐳲", + None, + None, + None, + None, + None, + None, + None, + None, + "\U00010d70", + "\U00010d71", + "\U00010d72", + "\U00010d73", + "\U00010d74", + "\U00010d75", + "\U00010d76", + "\U00010d77", + "\U00010d78", + "\U00010d79", + "\U00010d7a", + "\U00010d7b", + "\U00010d7c", + "\U00010d7d", + "\U00010d7e", + "\U00010d7f", + "\U00010d80", + "\U00010d81", + "\U00010d82", + "\U00010d83", + "\U00010d84", + "\U00010d85", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𑣀", + "𑣁", + "𑣂", + "𑣃", + "𑣄", + "𑣅", + "𑣆", + "𑣇", + "𑣈", + "𑣉", + "𑣊", + "𑣋", + "𑣌", + "𑣍", + "𑣎", + "𑣏", + "𑣐", + "𑣑", + "𑣒", + "𑣓", + "𑣔", + "𑣕", + "𑣖", + "𑣗", + "𑣘", + "𑣙", + "𑣚", + "𑣛", + "𑣜", + "𑣝", + "𑣞", + "𑣟", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𖹠", + "𖹡", + "𖹢", + "𖹣", + "𖹤", + "𖹥", + "𖹦", + "𖹧", + "𖹨", + "𖹩", + "𖹪", + "𖹫", + "𖹬", + "𖹭", + "𖹮", + "𖹯", + "𖹰", + "𖹱", + "𖹲", + "𖹳", + "𖹴", + "𖹵", + "𖹶", + "𖹷", + "𖹸", + "𖹹", + "𖹺", + "𖹻", + "𖹼", + "𖹽", + "𖹾", + "𖹿", + None, + None, + "\U00016ebb", + "\U00016ebc", + "\U00016ebd", + "\U00016ebe", + "\U00016ebf", + "\U00016ec0", + "\U00016ec1", + "\U00016ec2", + "\U00016ec3", + "\U00016ec4", + "\U00016ec5", + "\U00016ec6", + "\U00016ec7", + "\U00016ec8", + "\U00016ec9", + "\U00016eca", + "\U00016ecb", + "\U00016ecc", + "\U00016ecd", + "\U00016ece", + "\U00016ecf", + "\U00016ed0", + "\U00016ed1", + "\U00016ed2", + "\U00016ed3", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𝅗𝅥", + "𝅘𝅥", + "𝅘𝅥𝅮", + "𝅘𝅥𝅯", + "𝅘𝅥𝅰", + "𝅘𝅥𝅱", + "𝅘𝅥𝅲", + None, + None, + None, + "𝆹𝅥", + "𝆺𝅥", + "𝆹𝅥𝅮", + "𝆺𝅥𝅮", + "𝆹𝅥𝅯", + "𝆺𝅥𝅯", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + None, + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + None, + "c", + "d", + None, + "g", + None, + "j", + "k", + None, + "n", + "o", + "p", + "q", + None, + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + None, + "f", + None, + "h", + "i", + "j", + "k", + "l", + "m", + "n", + None, + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + None, + "d", + "e", + "f", + "g", + None, + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + None, + "s", + "t", + "u", + "v", + "w", + "x", + "y", + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + None, + "d", + "e", + "f", + "g", + None, + "i", + "j", + "k", + "l", + "m", + None, + "o", + None, + "s", + "t", + "u", + "v", + "w", + "x", + "y", + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "ı", + "ȷ", + None, + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "θ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∇", + "α", + "β", + "γ", + "δ", + "ε", + "ζ", + "η", + "θ", + "ι", + "κ", + "λ", + "μ", + "ν", + "ξ", + "ο", + "π", + "ρ", + "σ", + "τ", + "υ", + "φ", + "χ", + "ψ", + "ω", + "∂", + "ε", + "θ", + "κ", + "φ", + "ρ", + "π", + "ϝ", + None, + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "а", + "б", + "в", + "г", + "д", + "е", + "ж", + "з", + "и", + "к", + "л", + "м", + "о", + "п", + "р", + "с", + "т", + "у", + "ф", + "х", + "ц", + "ч", + "ш", + "ы", + "э", + "ю", + "ꚉ", + "ә", + "і", + "ј", + "ө", + "ү", + "ӏ", + "а", + "б", + "в", + "г", + "д", + "е", + "ж", + "з", + "и", + "к", + "л", + "о", + "п", + "с", + "у", + "ф", + "х", + "ц", + "ч", + "ш", + "ъ", + "ы", + "ґ", + "і", + "ѕ", + "џ", + "ҫ", + "ꙑ", + "ұ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "𞤢", + "𞤣", + "𞤤", + "𞤥", + "𞤦", + "𞤧", + "𞤨", + "𞤩", + "𞤪", + "𞤫", + "𞤬", + "𞤭", + "𞤮", + "𞤯", + "𞤰", + "𞤱", + "𞤲", + "𞤳", + "𞤴", + "𞤵", + "𞤶", + "𞤷", + "𞤸", + "𞤹", + "𞤺", + "𞤻", + "𞤼", + "𞤽", + "𞤾", + "𞤿", + "𞥀", + "𞥁", + "𞥂", + "𞥃", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "ا", + "ب", + "ج", + "د", + None, + "و", + "ز", + "ح", + "ط", + "ي", + "ك", + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + "ر", + "ش", + "ت", + "ث", + "خ", + "ذ", + "ض", + "ظ", + "غ", + "ٮ", + "ں", + "ڡ", + "ٯ", + None, + "ب", + "ج", + None, + "ه", + None, + "ح", + None, + "ي", + "ك", + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + None, + "ش", + "ت", + "ث", + "خ", + None, + "ض", + None, + "غ", + None, + "ج", + None, + "ح", + None, + "ي", + None, + "ل", + None, + "ن", + "س", + "ع", + None, + "ص", + "ق", + None, + "ش", + None, + "خ", + None, + "ض", + None, + "غ", + None, + "ں", + None, + "ٯ", + None, + "ب", + "ج", + None, + "ه", + None, + "ح", + "ط", + "ي", + "ك", + None, + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + None, + "ش", + "ت", + "ث", + "خ", + None, + "ض", + "ظ", + "غ", + "ٮ", + None, + "ڡ", + None, + "ا", + "ب", + "ج", + "د", + "ه", + "و", + "ز", + "ح", + "ط", + "ي", + None, + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + "ر", + "ش", + "ت", + "ث", + "خ", + "ذ", + "ض", + "ظ", + "غ", + None, + "ب", + "ج", + "د", + None, + "و", + "ز", + "ح", + "ط", + "ي", + None, + "ل", + "م", + "ن", + "س", + "ع", + "ف", + "ص", + "ق", + "ر", + "ش", + "ت", + "ث", + "خ", + "ذ", + "ض", + "ظ", + "غ", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "0,", + "1,", + "2,", + "3,", + "4,", + "5,", + "6,", + "7,", + "8,", + "9,", + None, + "(a)", + "(b)", + "(c)", + "(d)", + "(e)", + "(f)", + "(g)", + "(h)", + "(i)", + "(j)", + "(k)", + "(l)", + "(m)", + "(n)", + "(o)", + "(p)", + "(q)", + "(r)", + "(s)", + "(t)", + "(u)", + "(v)", + "(w)", + "(x)", + "(y)", + "(z)", + "〔s〕", + "c", + "r", + "cd", + "wz", + None, + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "hv", + "mv", + "sd", + "ss", + "ppv", + "wc", + None, + "mc", + "md", + "mr", + None, + "dj", + None, + None, + None, + "ほか", + "ココ", + "サ", + None, + "手", + "字", + "双", + "デ", + "二", + "多", + "解", + "天", + "交", + "映", + "無", + "料", + "前", + "後", + "再", + "新", + "初", + "終", + "生", + "販", + "声", + "吹", + "演", + "投", + "捕", + "一", + "三", + "遊", + "左", + "中", + "右", + "指", + "走", + "打", + "禁", + "空", + "合", + "満", + "有", + "月", + "申", + "割", + "営", + "配", + None, + "〔本〕", + "〔三〕", + "〔二〕", + "〔安〕", + "〔点〕", + "〔打〕", + "〔盗〕", + "〔勝〕", + "〔敗〕", + None, + "得", + "可", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + "丽", + "丸", + "乁", + "𠄢", + "你", + "侮", + "侻", + "倂", + "偺", + "備", + "僧", + "像", + "㒞", + "𠘺", + "免", + "兔", + "兤", + "具", + "𠔜", + "㒹", + "內", + "再", + "𠕋", + "冗", + "冤", + "仌", + "冬", + "况", + "𩇟", + "凵", + "刃", + "㓟", + "刻", + "剆", + "割", + "剷", + "㔕", + "勇", + "勉", + "勤", + "勺", + "包", + "匆", + "北", + "卉", + "卑", + "博", + "即", + "卽", + "卿", + "𠨬", + "灰", + "及", + "叟", + "𠭣", + "叫", + "叱", + "吆", + "咞", + "吸", + "呈", + "周", + "咢", + "哶", + "唐", + "啓", + "啣", + "善", + "喙", + "喫", + "喳", + "嗂", + "圖", + "嘆", + "圗", + "噑", + "噴", + "切", + "壮", + "城", + "埴", + "堍", + "型", + "堲", + "報", + "墬", + "𡓤", + "売", + "壷", + "夆", + "多", + "夢", + "奢", + "𡚨", + "𡛪", + "姬", + "娛", + "娧", + "姘", + "婦", + "㛮", + "㛼", + "嬈", + "嬾", + "𡧈", + "寃", + "寘", + "寧", + "寳", + "𡬘", + "寿", + "将", + "当", + "尢", + "㞁", + "屠", + "屮", + "峀", + "岍", + "𡷤", + "嵃", + "𡷦", + "嵮", + "嵫", + "嵼", + "巡", + "巢", + "㠯", + "巽", + "帨", + "帽", + "幩", + "㡢", + "𢆃", + "㡼", + "庰", + "庳", + "庶", + "廊", + "𪎒", + "廾", + "𢌱", + "舁", + "弢", + "㣇", + "𣊸", + "𦇚", + "形", + "彫", + "㣣", + "徚", + "忍", + "志", + "忹", + "悁", + "㤺", + "㤜", + "悔", + "𢛔", + "惇", + "慈", + "慌", + "慎", + "慌", + "慺", + "憎", + "憲", + "憤", + "憯", + "懞", + "懲", + "懶", + "成", + "戛", + "扝", + "抱", + "拔", + "捐", + "𢬌", + "挽", + "拼", + "捨", + "掃", + "揤", + "𢯱", + "搢", + "揅", + "掩", + "㨮", + "摩", + "摾", + "撝", + "摷", + "㩬", + "敏", + "敬", + "𣀊", + "旣", + "書", + "晉", + "㬙", + "暑", + "㬈", + "㫤", + "冒", + "冕", + "最", + "暜", + "肭", + "䏙", + "朗", + "望", + "朡", + "杞", + "杓", + "𣏃", + "㭉", + "柺", + "枅", + "桒", + "梅", + "𣑭", + "梎", + "栟", + "椔", + "㮝", + "楂", + "榣", + "槪", + "檨", + "𣚣", + "櫛", + "㰘", + "次", + "𣢧", + "歔", + "㱎", + "歲", + "殟", + "殺", + "殻", + "𣪍", + "𡴋", + "𣫺", + "汎", + "𣲼", + "沿", + "泍", + "汧", + "洖", + "派", + "海", + "流", + "浩", + "浸", + "涅", + "𣴞", + "洴", + "港", + "湮", + "㴳", + "滋", + "滇", + "𣻑", + "淹", + "潮", + "𣽞", + "𣾎", + "濆", + "瀹", + "瀞", + "瀛", + "㶖", + "灊", + "災", + "灷", + "炭", + "𠔥", + "煅", + "𤉣", + "熜", + "𤎫", + "爨", + "爵", + "牐", + "𤘈", + "犀", + "犕", + "𤜵", + "𤠔", + "獺", + "王", + "㺬", + "玥", + "㺸", + "瑇", + "瑜", + "瑱", + "璅", + "瓊", + "㼛", + "甤", + "𤰶", + "甾", + "𤲒", + "異", + "𢆟", + "瘐", + "𤾡", + "𤾸", + "𥁄", + "㿼", + "䀈", + "直", + "𥃳", + "𥃲", + "𥄙", + "𥄳", + "眞", + "真", + "睊", + "䀹", + "瞋", + "䁆", + "䂖", + "𥐝", + "硎", + "碌", + "磌", + "䃣", + "𥘦", + "祖", + "𥚚", + "𥛅", + "福", + "秫", + "䄯", + "穀", + "穊", + "穏", + "𥥼", + "𥪧", + "竮", + "䈂", + "𥮫", + "篆", + "築", + "䈧", + "𥲀", + "糒", + "䊠", + "糨", + "糣", + "紀", + "𥾆", + "絣", + "䌁", + "緇", + "縂", + "繅", + "䌴", + "𦈨", + "𦉇", + "䍙", + "𦋙", + "罺", + "𦌾", + "羕", + "翺", + "者", + "𦓚", + "𦔣", + "聠", + "𦖨", + "聰", + "𣍟", + "䏕", + "育", + "脃", + "䐋", + "脾", + "媵", + "𦞧", + "𦞵", + "𣎓", + "𣎜", + "舁", + "舄", + "辞", + "䑫", + "芑", + "芋", + "芝", + "劳", + "花", + "芳", + "芽", + "苦", + "𦬼", + "若", + "茝", + "荣", + "莭", + "茣", + "莽", + "菧", + "著", + "荓", + "菊", + "菌", + "菜", + "𦰶", + "𦵫", + "𦳕", + "䔫", + "蓱", + "蓳", + "蔖", + "𧏊", + "蕤", + "𦼬", + "䕝", + "䕡", + "𦾱", + "𧃒", + "䕫", + "虐", + "虜", + "虧", + "虩", + "蚩", + "蚈", + "蜎", + "蛢", + "蝹", + "蜨", + "蝫", + "螆", + "䗗", + "蟡", + "蠁", + "䗹", + "衠", + "衣", + "𧙧", + "裗", + "裞", + "䘵", + "裺", + "㒻", + "𧢮", + "𧥦", + "䚾", + "䛇", + "誠", + "諭", + "變", + "豕", + "𧲨", + "貫", + "賁", + "贛", + "起", + "𧼯", + "𠠄", + "跋", + "趼", + "跰", + "𠣞", + "軔", + "輸", + "𨗒", + "𨗭", + "邔", + "郱", + "鄑", + "𨜮", + "鄛", + "鈸", + "鋗", + "鋘", + "鉼", + "鏹", + "鐕", + "𨯺", + "開", + "䦕", + "閷", + "𨵷", + "䧦", + "雃", + "嶲", + "霣", + "𩅅", + "𩈚", + "䩮", + "䩶", + "韠", + "𩐊", + "䪲", + "𩒖", + "頋", + "頩", + "𩖶", + "飢", + "䬳", + "餩", + "馧", + "駂", + "駾", + "䯎", + "𩬰", + "鬒", + "鱀", + "鳽", + "䳎", + "䳭", + "鵧", + "𪃎", + "䳸", + "𪄅", + "𪈎", + "𪊑", + "麻", + "䵖", + "黹", + "黾", + "鼅", + "鼏", + "鼖", + "鼻", + "𪘀", + None, + None, + None, + None, + None, + None, + None, +) diff --git a/libs/importlib_resources-6.5.2.dist-info/INSTALLER b/libs/importlib_resources-7.1.0.dist-info/INSTALLER similarity index 100% rename from libs/importlib_resources-6.5.2.dist-info/INSTALLER rename to libs/importlib_resources-7.1.0.dist-info/INSTALLER diff --git a/libs/importlib_resources-6.5.2.dist-info/METADATA b/libs/importlib_resources-7.1.0.dist-info/METADATA similarity index 91% rename from libs/importlib_resources-6.5.2.dist-info/METADATA rename to libs/importlib_resources-7.1.0.dist-info/METADATA index 2952ea3f9..fea7349dd 100644 --- a/libs/importlib_resources-6.5.2.dist-info/METADATA +++ b/libs/importlib_resources-7.1.0.dist-info/METADATA @@ -1,16 +1,16 @@ Metadata-Version: 2.4 Name: importlib_resources -Version: 6.5.2 +Version: 7.1.0 Summary: Read resources from Python packages Author-email: Barry Warsaw Maintainer-email: "Jason R. Coombs" +License-Expression: Apache-2.0 Project-URL: Source, https://github.com/python/importlib_resources Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.9 +Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: zipp>=3.1.0; python_version < "3.10" @@ -26,14 +26,14 @@ Requires-Dist: furo; extra == "doc" Requires-Dist: sphinx-lint; extra == "doc" Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" Provides-Extra: check -Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-checkdocs>=2.14; extra == "check" Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" Provides-Extra: cover Requires-Dist: pytest-cov; extra == "cover" Provides-Extra: enabler -Requires-Dist: pytest-enabler>=2.2; extra == "enabler" +Requires-Dist: pytest-enabler>=3.4; extra == "enabler" Provides-Extra: type -Requires-Dist: pytest-mypy; extra == "type" +Requires-Dist: pytest-mypy>=1.0.1; platform_python_implementation != "PyPy" and extra == "type" Dynamic: license-file .. image:: https://img.shields.io/pypi/v/importlib_resources.svg @@ -45,7 +45,7 @@ Dynamic: license-file :target: https://github.com/python/importlib_resources/actions?query=workflow%3A%22tests%22 :alt: tests -.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff diff --git a/libs/importlib_resources-6.5.2.dist-info/RECORD b/libs/importlib_resources-7.1.0.dist-info/RECORD similarity index 50% rename from libs/importlib_resources-6.5.2.dist-info/RECORD rename to libs/importlib_resources-7.1.0.dist-info/RECORD index c88b47418..501fe02f7 100644 --- a/libs/importlib_resources-6.5.2.dist-info/RECORD +++ b/libs/importlib_resources-7.1.0.dist-info/RECORD @@ -1,38 +1,38 @@ -importlib_resources-6.5.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -importlib_resources-6.5.2.dist-info/METADATA,sha256=snGqWHAR2n2pnsjqoCMZsTjVhV5B3kqZ9z4cHa31Bzo,3969 -importlib_resources-6.5.2.dist-info/RECORD,, -importlib_resources-6.5.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources-6.5.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -importlib_resources-6.5.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 -importlib_resources-6.5.2.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20 +importlib_resources-7.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +importlib_resources-7.1.0.dist-info/METADATA,sha256=q0fMmH8UBUwTiLUYBlbSH-n_IaLc2Nejm9dQPA5m1kI,3987 +importlib_resources-7.1.0.dist-info/RECORD,, +importlib_resources-7.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources-7.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +importlib_resources-7.1.0.dist-info/licenses/LICENSE,sha256=4ve7BVfno4FJnxW6p8HXdtg9ZhZZkXnXHTEJuOOfeyU,10278 +importlib_resources-7.1.0.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20 importlib_resources/__init__.py,sha256=F6SOMZILiez96lK3h9duE_c5kRTEydzHvI6D7TRcOJE,700 importlib_resources/_adapters.py,sha256=vprJGbUeHbajX6XCuMP6J3lMrqCi-P_MTlziJUR7jfk,4482 -importlib_resources/_common.py,sha256=Vfmmdk5ICrZ8odgfWEuUHG-P7ZbJ-5x3pRLCVwVk8Tg,5624 +importlib_resources/_common.py,sha256=_RxoOvtqCPulSmK_2gVD6__guE85JDg8EEUZQwv1VLE,5178 importlib_resources/_functional.py,sha256=nMop9Zc7g1Yhiyk5oqr9ukvfJcOx4vnvqD7nh24hV7Y,2743 importlib_resources/_itertools.py,sha256=eDisV6RqiNZOogLSXf6LOGHOYc79FGgPrKNLzFLmCrU,1277 -importlib_resources/abc.py,sha256=inhYsYj6LrYHn4kCraZq8zqXFQnDX4stNnLTruhf6NU,5549 +importlib_resources/abc.py,sha256=kQb57_sqzkfduUn1EGqrwrhlrfvgr7vnq2-fkYF3Bo4,5531 importlib_resources/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 importlib_resources/compat/py39.py,sha256=gx5NE25XH1pQrTRJDY7QJUngZpD7OotsK9F6pZy5KjM,151 importlib_resources/future/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 importlib_resources/future/adapters.py,sha256=UFIMxEuLo4PqcVbtoV3e_k9BcxCS-mmG22f8U2Vtkx4,3285 importlib_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 importlib_resources/readers.py,sha256=uHf9X-smA3CtxdB9IHWH0z-O53379wlcEMmzA2UXzqE,6250 -importlib_resources/simple.py,sha256=wJm2qGZ9EMPFhRLiJBa9Em5tVKbD7Q8ibWtt4ZNgWBU,2590 +importlib_resources/simple.py,sha256=twuYiz7U18iqoFC-49ogFDniaDGA2BG4HJ9x60cLguM,2584 importlib_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_resources/tests/_path.py,sha256=IK3o6J_4UaN-4DPuwKr0lOb2KWpoUjwIyQkOtGqZWzs,2250 +importlib_resources/tests/_path.py,sha256=ph8DOLJEvvWBe3eWMgE3tqg27wgOa2g0AbMAVFQKGaY,2227 importlib_resources/tests/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 importlib_resources/tests/compat/py312.py,sha256=qcWjpZhQo2oEsdwIlRRQHrsMGDltkFTnETeG7fLdUS8,364 importlib_resources/tests/compat/py39.py,sha256=nOjut4CZDtRgZEblX9hWhupo9vli_CA1270_JwdQzRo,441 -importlib_resources/tests/test_compatibilty_files.py,sha256=cOLwa5kU6ucxNfiNsAQSpLHCE8asuqAHui6Aw9mxKX8,3313 +importlib_resources/tests/test_compatibilty_files.py,sha256=niKlQ7-bqC37EeLpUQmuf_zpBVm-gMjtTXMndItilYQ,3081 importlib_resources/tests/test_contents.py,sha256=r8kVH1k7DHrco2JVTUMCbsWK0JvvjKt0zgrmH56c4Vg,838 importlib_resources/tests/test_custom.py,sha256=ep5mGT6uT6O1gPdM-b3mXo9HWDnCLD90uc4H1bU68oI,1222 -importlib_resources/tests/test_files.py,sha256=u4vMrXPpaWGJY8DyFJBkgg78lr_l9sfjzatP_7mkn88,5741 -importlib_resources/tests/test_functional.py,sha256=B3cFWFztSkMh4Y58MyOex_lcEooIPR-ezTaA9xzI6Y4,9070 -importlib_resources/tests/test_open.py,sha256=dT-cZmJoGbMMIVVWkNXt8KUxWcQn9Cd0emb85U4CiBA,2682 -importlib_resources/tests/test_path.py,sha256=orETYbmGyYNgXA8Dslj9MM3XjGUDRW8u4dJhIBy0qdU,1986 -importlib_resources/tests/test_read.py,sha256=o3uXJCUTYmUZeaFoQyXeRxTPpwhbjgRz-aALAg_btz8,3046 -importlib_resources/tests/test_reader.py,sha256=1RFB-rJs1hCDSy6qgv2z3LDm42gbQmj2n718CCdn1JY,4655 -importlib_resources/tests/test_resource.py,sha256=ZgpXCuYs4GZpSCElJyhZlMkb8oOfMZX1cnPLoinzis0,7687 +importlib_resources/tests/test_files.py,sha256=YzmwkxC8Qgdf8jNj9Q5Rgp5gmJUxFTHEBf2NNpRJuxM,5471 +importlib_resources/tests/test_functional.py,sha256=xFKw0VzJWaLNklbl2PwYurIvIl4O3Av4V0bZNd6iusA,8989 +importlib_resources/tests/test_open.py,sha256=ceMNlQZKNGtl1Sc-mr32ecvOLFLyDfeX4QRqDq6teKY,2635 +importlib_resources/tests/test_path.py,sha256=HnZqYaRds6V6znDwWlmFZkgGaVXbzCbjTlgnx6HiUx4,1963 +importlib_resources/tests/test_read.py,sha256=5MB7JNG1bMUXqMAwsBbPH8vLAeHOxTp9BLCyaDvmdgc,3002 +importlib_resources/tests/test_reader.py,sha256=di6Yury4x6Y4fatM6qNlpWIYy6CdNXHInLahtqSFvkI,4380 +importlib_resources/tests/test_resource.py,sha256=x6eeB-S4ZSolz0DqaCl2WVvjQTFpfVlQ1-6TSBYQtpI,7983 importlib_resources/tests/test_util.py,sha256=VotLNbngOyFg03FKVTzIZKd4Gz4UO-dMEr0G-vHMiuI,1105 -importlib_resources/tests/util.py,sha256=PKybGEyCo53mMu7A-5vC6n0e3nc9R8DS2NtvO-JCyxY,9804 +importlib_resources/tests/util.py,sha256=NaGtI-csCx6rBA67UOpDchPlL6MrVuQkO_PEPv7u9l0,9786 importlib_resources/tests/zip.py,sha256=nG9D6u_4nu67NrEUFfMt7vzba0qO_-QPyAAWc6W4gP4,577 diff --git a/libs/importlib_resources-6.5.2.dist-info/REQUESTED b/libs/importlib_resources-7.1.0.dist-info/REQUESTED similarity index 100% rename from libs/importlib_resources-6.5.2.dist-info/REQUESTED rename to libs/importlib_resources-7.1.0.dist-info/REQUESTED diff --git a/libs/guess_language_spirit-0.5.3.dist-info/WHEEL b/libs/importlib_resources-7.1.0.dist-info/WHEEL similarity index 100% rename from libs/guess_language_spirit-0.5.3.dist-info/WHEEL rename to libs/importlib_resources-7.1.0.dist-info/WHEEL diff --git a/libs/importlib_resources-7.1.0.dist-info/licenses/LICENSE b/libs/importlib_resources-7.1.0.dist-info/licenses/LICENSE new file mode 100644 index 000000000..7b25cfb85 --- /dev/null +++ b/libs/importlib_resources-7.1.0.dist-info/licenses/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2026 [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/libs/importlib_resources-6.5.2.dist-info/top_level.txt b/libs/importlib_resources-7.1.0.dist-info/top_level.txt similarity index 100% rename from libs/importlib_resources-6.5.2.dist-info/top_level.txt rename to libs/importlib_resources-7.1.0.dist-info/top_level.txt diff --git a/libs/importlib_resources/_common.py b/libs/importlib_resources/_common.py index 5f41c2652..4e7c91374 100644 --- a/libs/importlib_resources/_common.py +++ b/libs/importlib_resources/_common.py @@ -7,48 +7,14 @@ import os import pathlib import tempfile import types -import warnings -from typing import Optional, Union, cast +from typing import Optional, cast from .abc import ResourceReader, Traversable -Package = Union[types.ModuleType, str] +Package = types.ModuleType | str Anchor = Package -def package_to_anchor(func): - """ - Replace 'package' parameter as 'anchor' and warn about the change. - - Other errors should fall through. - - >>> files('a', 'b') - Traceback (most recent call last): - TypeError: files() takes from 0 to 1 positional arguments but 2 were given - - Remove this compatibility in Python 3.14. - """ - undefined = object() - - @functools.wraps(func) - def wrapper(anchor=undefined, package=undefined): - if package is not undefined: - if anchor is not undefined: - return func(anchor, package) - warnings.warn( - "First parameter to files is renamed to 'anchor'", - DeprecationWarning, - stacklevel=2, - ) - return func(package) - elif anchor is undefined: - return func() - return func(anchor) - - return wrapper - - -@package_to_anchor def files(anchor: Optional[Anchor] = None) -> Traversable: """ Get a Traversable resource for an anchor. @@ -105,6 +71,19 @@ def _infer_caller(): return next(callers).frame +def _assert_spec(package: types.ModuleType) -> None: + """ + Provide a nicer error message when package is ``__main__`` + and its ``__spec__`` is ``None`` + (https://docs.python.org/3/reference/import.html#main-spec). + """ + if package.__spec__ is None: + raise TypeError( + f"Cannot access resources for '{package.__name__}' " + "as it does not appear to correspond to an importable module (its __spec__ is None)." + ) + + def from_package(package: types.ModuleType): """ Return a Traversable object for the given package. @@ -113,6 +92,7 @@ def from_package(package: types.ModuleType): # deferred for performance (python/cpython#109829) from .future.adapters import wrap_spec + _assert_spec(package) spec = wrap_spec(package) reader = spec.loader.get_resource_reader(spec.name) return reader.files() diff --git a/libs/importlib_resources/abc.py b/libs/importlib_resources/abc.py index 883d33283..0b5fdee80 100644 --- a/libs/importlib_resources/abc.py +++ b/libs/importlib_resources/abc.py @@ -2,23 +2,21 @@ import abc import itertools import os import pathlib +from collections.abc import Iterable, Iterator from typing import ( Any, BinaryIO, - Iterable, - Iterator, - NoReturn, Literal, + NoReturn, Optional, Protocol, Text, TextIO, - Union, overload, runtime_checkable, ) -StrPath = Union[str, os.PathLike[str]] +StrPath = str | os.PathLike[str] __all__ = ["ResourceReader", "Traversable", "TraversableResources"] @@ -151,9 +149,7 @@ class Traversable(Protocol): def open(self, mode: Literal['rb'], *args: Any, **kwargs: Any) -> BinaryIO: ... @abc.abstractmethod - def open( - self, mode: str = 'r', *args: Any, **kwargs: Any - ) -> Union[TextIO, BinaryIO]: + def open(self, mode: str = 'r', *args: Any, **kwargs: Any) -> TextIO | BinaryIO: """ mode may be 'r' or 'rb' to open as text or binary. Return a handle suitable for reading (same as pathlib.Path.open). diff --git a/libs/importlib_resources/simple.py b/libs/importlib_resources/simple.py index 2e75299b1..5e182d126 100644 --- a/libs/importlib_resources/simple.py +++ b/libs/importlib_resources/simple.py @@ -5,7 +5,7 @@ Interface adapters for low-level readers. import abc import io import itertools -from typing import BinaryIO, List +from typing import BinaryIO from .abc import Traversable, TraversableResources @@ -24,14 +24,14 @@ class SimpleReader(abc.ABC): """ @abc.abstractmethod - def children(self) -> List['SimpleReader']: + def children(self) -> list['SimpleReader']: """ Obtain an iterable of SimpleReader for available child containers (e.g. directories). """ @abc.abstractmethod - def resources(self) -> List[str]: + def resources(self) -> list[str]: """ Obtain available named resources for this virtual package. """ diff --git a/libs/importlib_resources/tests/_path.py b/libs/importlib_resources/tests/_path.py index 0033983dc..3720af7c5 100644 --- a/libs/importlib_resources/tests/_path.py +++ b/libs/importlib_resources/tests/_path.py @@ -1,6 +1,6 @@ import functools import pathlib -from typing import Dict, Protocol, Union, runtime_checkable +from typing import Protocol, Union, runtime_checkable #### # from jaraco.path 3.7.1 @@ -12,7 +12,7 @@ class Symlink(str): """ -FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] +FilesSpec = dict[str, Union[str, bytes, Symlink, 'FilesSpec']] @runtime_checkable @@ -28,13 +28,13 @@ class TreeMaker(Protocol): def symlink_to(self, target): ... # pragma: no cover -def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: +def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker: return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] + prefix: str | TreeMaker = pathlib.Path(), # type: ignore[assignment] ): """ Build a set of files/directories, as described by the spec. @@ -66,7 +66,7 @@ def build( @functools.singledispatch -def create(content: Union[str, bytes, FilesSpec], path): +def create(content: str | bytes | FilesSpec, path): path.mkdir(exist_ok=True) build(content, prefix=path) # type: ignore[arg-type] diff --git a/libs/importlib_resources/tests/test_compatibilty_files.py b/libs/importlib_resources/tests/test_compatibilty_files.py index e8aac2841..6b3b1696c 100644 --- a/libs/importlib_resources/tests/test_compatibilty_files.py +++ b/libs/importlib_resources/tests/test_compatibilty_files.py @@ -25,51 +25,46 @@ class CompatibilityFilesTests(unittest.TestCase): return resources.files(self.package) def test_spec_path_iter(self): - self.assertEqual( - sorted(path.name for path in self.files.iterdir()), - ['a', 'b', 'c'], - ) + assert sorted(path.name for path in self.files.iterdir()) == ['a', 'b', 'c'] def test_child_path_iter(self): - self.assertEqual(list((self.files / 'a').iterdir()), []) + assert list((self.files / 'a').iterdir()) == [] def test_orphan_path_iter(self): - self.assertEqual(list((self.files / 'a' / 'a').iterdir()), []) - self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), []) + assert list((self.files / 'a' / 'a').iterdir()) == [] + assert list((self.files / 'a' / 'a' / 'a').iterdir()) == [] def test_spec_path_is(self): - self.assertFalse(self.files.is_file()) - self.assertFalse(self.files.is_dir()) + assert not self.files.is_file() + assert not self.files.is_dir() def test_child_path_is(self): - self.assertTrue((self.files / 'a').is_file()) - self.assertFalse((self.files / 'a').is_dir()) + assert (self.files / 'a').is_file() + assert not (self.files / 'a').is_dir() def test_orphan_path_is(self): - self.assertFalse((self.files / 'a' / 'a').is_file()) - self.assertFalse((self.files / 'a' / 'a').is_dir()) - self.assertFalse((self.files / 'a' / 'a' / 'a').is_file()) - self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir()) + assert not (self.files / 'a' / 'a').is_file() + assert not (self.files / 'a' / 'a').is_dir() + assert not (self.files / 'a' / 'a' / 'a').is_file() + assert not (self.files / 'a' / 'a' / 'a').is_dir() def test_spec_path_name(self): - self.assertEqual(self.files.name, 'testingpackage') + assert self.files.name == 'testingpackage' def test_child_path_name(self): - self.assertEqual((self.files / 'a').name, 'a') + assert (self.files / 'a').name == 'a' def test_orphan_path_name(self): - self.assertEqual((self.files / 'a' / 'b').name, 'b') - self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c') + assert (self.files / 'a' / 'b').name == 'b' + assert (self.files / 'a' / 'b' / 'c').name == 'c' def test_spec_path_open(self): - self.assertEqual(self.files.read_bytes(), b'Hello, world!') - self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!') + assert self.files.read_bytes() == b'Hello, world!' + assert self.files.read_text(encoding='utf-8') == 'Hello, world!' def test_child_path_open(self): - self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!') - self.assertEqual( - (self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!' - ) + assert (self.files / 'a').read_bytes() == b'Hello, world!' + assert (self.files / 'a').read_text(encoding='utf-8') == 'Hello, world!' def test_orphan_path_open(self): with self.assertRaises(FileNotFoundError): @@ -87,7 +82,7 @@ class CompatibilityFilesTests(unittest.TestCase): def test_wrap_spec(self): spec = wrap_spec(self.package) - self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles) + assert isinstance(spec.loader.get_resource_reader(None), CompatibilityFiles) class CompatibilityFilesNoReaderTests(unittest.TestCase): @@ -100,4 +95,4 @@ class CompatibilityFilesNoReaderTests(unittest.TestCase): return resources.files(self.package) def test_spec_path_joinpath(self): - self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath) + assert isinstance(self.files / 'a', CompatibilityFiles.OrphanPath) diff --git a/libs/importlib_resources/tests/test_files.py b/libs/importlib_resources/tests/test_files.py index be2066031..06237d7b3 100644 --- a/libs/importlib_resources/tests/test_files.py +++ b/libs/importlib_resources/tests/test_files.py @@ -37,15 +37,7 @@ class FilesTests: def test_joinpath_with_multiple_args(self): files = resources.files(self.data) binfile = files.joinpath('subdirectory', 'binary.file') - self.assertTrue(binfile.is_file()) - - def test_old_parameter(self): - """ - Files used to take a 'package' parameter. Make sure anyone - passing by name is still supported. - """ - with suppress_known_deprecation(): - resources.files(package=self.data) + assert binfile.is_file() class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase): diff --git a/libs/importlib_resources/tests/test_functional.py b/libs/importlib_resources/tests/test_functional.py index 9eb2d8153..91612095a 100644 --- a/libs/importlib_resources/tests/test_functional.py +++ b/libs/importlib_resources/tests/test_functional.py @@ -38,35 +38,40 @@ class FunctionalAPIBase: with self.subTest(path_parts=path_parts): yield path_parts - def assertEndsWith(self, string, suffix): - """Assert that `string` ends with `suffix`. + @staticmethod + def remove_utf16_bom(string): + """Remove an architecture-specific UTF-16 BOM prefix when present. - Used to ignore an architecture-specific UTF-16 byte-order mark.""" - self.assertEqual(string[-len(suffix) :], suffix) + Some platforms surface UTF-16 BOM bytes as escaped text when the + fixture is intentionally decoded as UTF-8 with ``errors='backslashreplace'``. + Strip that prefix so assertions validate content consistently.""" + for bom in ('\\xff\\xfe', '\\xfe\\xff', '\ufeff'): + if string.startswith(bom): + return string[len(bom) :] + return string def test_read_text(self): - self.assertEqual( - resources.read_text(self.anchor01, 'utf-8.file'), - 'Hello, UTF-8 world!\n', + assert ( + resources.read_text(self.anchor01, 'utf-8.file') == 'Hello, UTF-8 world!\n' ) - self.assertEqual( + assert ( resources.read_text( self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt', encoding='utf-8', - ), - 'a resource', + ) + == 'a resource' ) for path_parts in self._gen_resourcetxt_path_parts(): - self.assertEqual( + assert ( resources.read_text( self.anchor02, *path_parts, encoding='utf-8', - ), - 'a resource', + ) + == 'a resource' ) # Use generic OSError, since e.g. attempting to read a directory can # fail with PermissionError rather than IsADirectoryError @@ -76,46 +81,42 @@ class FunctionalAPIBase: resources.read_text(self.anchor01, 'no-such-file') with self.assertRaises(UnicodeDecodeError): resources.read_text(self.anchor01, 'utf-16.file') - self.assertEqual( + assert ( resources.read_text( self.anchor01, 'binary.file', encoding='latin1', - ), - '\x00\x01\x02\x03', + ) + == '\x00\x01\x02\x03' ) - self.assertEndsWith( # ignore the BOM + assert self.remove_utf16_bom( resources.read_text( self.anchor01, 'utf-16.file', errors='backslashreplace', ), - 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( - errors='backslashreplace', - ), + ) == 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( + errors='backslashreplace', ) def test_read_binary(self): - self.assertEqual( - resources.read_binary(self.anchor01, 'utf-8.file'), - b'Hello, UTF-8 world!\n', + assert ( + resources.read_binary(self.anchor01, 'utf-8.file') + == b'Hello, UTF-8 world!\n' ) for path_parts in self._gen_resourcetxt_path_parts(): - self.assertEqual( - resources.read_binary(self.anchor02, *path_parts), - b'a resource', - ) + assert resources.read_binary(self.anchor02, *path_parts) == b'a resource' def test_open_text(self): with resources.open_text(self.anchor01, 'utf-8.file') as f: - self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') + assert f.read() == 'Hello, UTF-8 world!\n' for path_parts in self._gen_resourcetxt_path_parts(): with resources.open_text( self.anchor02, *path_parts, encoding='utf-8', ) as f: - self.assertEqual(f.read(), 'a resource') + assert f.read() == 'a resource' # Use generic OSError, since e.g. attempting to read a directory can # fail with PermissionError rather than IsADirectoryError with self.assertRaises(OSError): @@ -130,53 +131,49 @@ class FunctionalAPIBase: 'binary.file', encoding='latin1', ) as f: - self.assertEqual(f.read(), '\x00\x01\x02\x03') + assert f.read() == '\x00\x01\x02\x03' with resources.open_text( self.anchor01, 'utf-16.file', errors='backslashreplace', ) as f: - self.assertEndsWith( # ignore the BOM - f.read(), - 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( - errors='backslashreplace', - ), + assert self.remove_utf16_bom(f.read()) == 'Hello, UTF-16 world!\n'.encode( + 'utf-16-le' + ).decode( + errors='backslashreplace', ) def test_open_binary(self): with resources.open_binary(self.anchor01, 'utf-8.file') as f: - self.assertEqual(f.read(), b'Hello, UTF-8 world!\n') + assert f.read() == b'Hello, UTF-8 world!\n' for path_parts in self._gen_resourcetxt_path_parts(): with resources.open_binary( self.anchor02, *path_parts, ) as f: - self.assertEqual(f.read(), b'a resource') + assert f.read() == b'a resource' def test_path(self): with resources.path(self.anchor01, 'utf-8.file') as path: with open(str(path), encoding='utf-8') as f: - self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') + assert f.read() == 'Hello, UTF-8 world!\n' with resources.path(self.anchor01) as path: with open(os.path.join(path, 'utf-8.file'), encoding='utf-8') as f: - self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') + assert f.read() == 'Hello, UTF-8 world!\n' def test_is_resource(self): is_resource = resources.is_resource - self.assertTrue(is_resource(self.anchor01, 'utf-8.file')) - self.assertFalse(is_resource(self.anchor01, 'no_such_file')) - self.assertFalse(is_resource(self.anchor01)) - self.assertFalse(is_resource(self.anchor01, 'subdirectory')) + assert is_resource(self.anchor01, 'utf-8.file') + assert not is_resource(self.anchor01, 'no_such_file') + assert not is_resource(self.anchor01) + assert not is_resource(self.anchor01, 'subdirectory') for path_parts in self._gen_resourcetxt_path_parts(): - self.assertTrue(is_resource(self.anchor02, *path_parts)) + assert is_resource(self.anchor02, *path_parts) def test_contents(self): with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01) - self.assertGreaterEqual( - set(c), - {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, - ) + assert set(c) >= {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'} with ( self.assertRaises(OSError), warnings_helper.check_warnings(( @@ -197,10 +194,7 @@ class FunctionalAPIBase: list(resources.contents(self.anchor01, *path_parts)) with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') - self.assertGreaterEqual( - set(c), - {'binary.file'}, - ) + assert set(c) >= {'binary.file'} @warnings_helper.ignore_warnings(category=DeprecationWarning) def test_common_errors(self): diff --git a/libs/importlib_resources/tests/test_open.py b/libs/importlib_resources/tests/test_open.py index 8a4b68e32..720a833cc 100644 --- a/libs/importlib_resources/tests/test_open.py +++ b/libs/importlib_resources/tests/test_open.py @@ -24,19 +24,19 @@ class OpenTests: target = resources.files(self.data) / 'binary.file' with target.open('rb') as fp: result = fp.read() - self.assertEqual(result, bytes(range(4))) + assert result == bytes(range(4)) def test_open_text_default_encoding(self): target = resources.files(self.data) / 'utf-8.file' with target.open(encoding='utf-8') as fp: result = fp.read() - self.assertEqual(result, 'Hello, UTF-8 world!\n') + assert result == 'Hello, UTF-8 world!\n' def test_open_text_given_encoding(self): target = resources.files(self.data) / 'utf-16.file' with target.open(encoding='utf-16', errors='strict') as fp: result = fp.read() - self.assertEqual(result, 'Hello, UTF-16 world!\n') + assert result == 'Hello, UTF-16 world!\n' def test_open_text_with_errors(self): """ @@ -47,11 +47,10 @@ class OpenTests: self.assertRaises(UnicodeError, fp.read) with target.open(encoding='utf-8', errors='ignore') as fp: result = fp.read() - self.assertEqual( - result, + assert result == ( 'H\x00e\x00l\x00l\x00o\x00,\x00 ' '\x00U\x00T\x00F\x00-\x001\x006\x00 ' - '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00' ) def test_open_binary_FileNotFoundError(self): diff --git a/libs/importlib_resources/tests/test_path.py b/libs/importlib_resources/tests/test_path.py index 0be673d2b..eb33d1f84 100644 --- a/libs/importlib_resources/tests/test_path.py +++ b/libs/importlib_resources/tests/test_path.py @@ -20,9 +20,9 @@ class PathTests: """ target = resources.files(self.data) / 'utf-8.file' with resources.as_file(target) as path: - self.assertIsInstance(path, pathlib.Path) - self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) - self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8')) + assert isinstance(path, pathlib.Path) + assert path.name.endswith("utf-8.file"), repr(path) + assert 'Hello, UTF-8 world!\n' == path.read_text(encoding='utf-8') class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase): diff --git a/libs/importlib_resources/tests/test_read.py b/libs/importlib_resources/tests/test_read.py index 216c8feb1..9865953d9 100644 --- a/libs/importlib_resources/tests/test_read.py +++ b/libs/importlib_resources/tests/test_read.py @@ -19,23 +19,25 @@ class CommonTextTests(util.CommonTests, unittest.TestCase): class ReadTests: def test_read_bytes(self): result = resources.files(self.data).joinpath('binary.file').read_bytes() - self.assertEqual(result, bytes(range(4))) + assert result == bytes(range(4)) def test_read_text_default_encoding(self): result = ( - resources.files(self.data) + resources + .files(self.data) .joinpath('utf-8.file') .read_text(encoding='utf-8') ) - self.assertEqual(result, 'Hello, UTF-8 world!\n') + assert result == 'Hello, UTF-8 world!\n' def test_read_text_given_encoding(self): result = ( - resources.files(self.data) + resources + .files(self.data) .joinpath('utf-16.file') .read_text(encoding='utf-16') ) - self.assertEqual(result, 'Hello, UTF-16 world!\n') + assert result == 'Hello, UTF-16 world!\n' def test_read_text_with_errors(self): """ @@ -44,11 +46,10 @@ class ReadTests: target = resources.files(self.data) / 'utf-16.file' self.assertRaises(UnicodeError, target.read_text, encoding='utf-8') result = target.read_text(encoding='utf-8', errors='ignore') - self.assertEqual( - result, + assert result == ( 'H\x00e\x00l\x00l\x00o\x00,\x00 ' '\x00U\x00T\x00F\x00-\x001\x006\x00 ' - '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00' ) @@ -60,13 +61,13 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('data01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() - self.assertEqual(result, bytes(range(4, 8))) + assert result == bytes(range(4, 8)) def test_read_submodule_resource_by_name(self): result = ( resources.files('data01.subdirectory').joinpath('binary.file').read_bytes() ) - self.assertEqual(result, bytes(range(4, 8))) + assert result == bytes(range(4, 8)) class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase): @@ -79,15 +80,16 @@ class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('namespacedata01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() - self.assertEqual(result, bytes(range(12, 16))) + assert result == bytes(range(12, 16)) def test_read_submodule_resource_by_name(self): result = ( - resources.files('namespacedata01.subdirectory') + resources + .files('namespacedata01.subdirectory') .joinpath('binary.file') .read_bytes() ) - self.assertEqual(result, bytes(range(12, 16))) + assert result == bytes(range(12, 16)) if __name__ == '__main__': diff --git a/libs/importlib_resources/tests/test_reader.py b/libs/importlib_resources/tests/test_reader.py index f8cfd8dea..3fdaac0c5 100644 --- a/libs/importlib_resources/tests/test_reader.py +++ b/libs/importlib_resources/tests/test_reader.py @@ -31,9 +31,7 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase): contents.remove('__pycache__') except (KeyError, ValueError): pass - self.assertEqual( - contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'} - ) + assert contents == {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'} def test_iterdir_duplicate(self): contents = { @@ -44,16 +42,19 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase): contents.remove(remove) except (KeyError, ValueError): pass - self.assertEqual( - contents, - {'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'}, - ) + assert contents == { + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + } def test_is_dir(self): - self.assertEqual(MultiplexedPath(self.folder).is_dir(), True) + assert MultiplexedPath(self.folder).is_dir() def test_is_file(self): - self.assertEqual(MultiplexedPath(self.folder).is_file(), False) + assert not MultiplexedPath(self.folder).is_file() def test_open_file(self): path = MultiplexedPath(self.folder) @@ -67,19 +68,17 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase): def test_join_path(self): prefix = str(self.folder.parent) path = MultiplexedPath(self.folder, self.data01) - self.assertEqual( - str(path.joinpath('binary.file'))[len(prefix) + 1 :], - os.path.join('namespacedata01', 'binary.file'), + assert str(path.joinpath('binary.file'))[len(prefix) + 1 :] == os.path.join( + 'namespacedata01', 'binary.file' ) sub = path.joinpath('subdirectory') assert isinstance(sub, MultiplexedPath) assert 'namespacedata01' in str(sub) assert 'data01' in str(sub) - self.assertEqual( - str(path.joinpath('imaginary'))[len(prefix) + 1 :], - os.path.join('namespacedata01', 'imaginary'), + assert str(path.joinpath('imaginary'))[len(prefix) + 1 :] == os.path.join( + 'namespacedata01', 'imaginary' ) - self.assertEqual(path.joinpath(), path) + assert path.joinpath() == path def test_join_path_compound(self): path = MultiplexedPath(self.folder) @@ -88,23 +87,16 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase): def test_join_path_common_subdir(self): prefix = str(self.data02.parent) path = MultiplexedPath(self.data01, self.data02) - self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath) - self.assertEqual( - str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :], - os.path.join('data02', 'subdirectory', 'subsubdir'), + assert isinstance(path.joinpath('subdirectory'), MultiplexedPath) + assert str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :] == ( + os.path.join('data02', 'subdirectory', 'subsubdir') ) def test_repr(self): - self.assertEqual( - repr(MultiplexedPath(self.folder)), - f"MultiplexedPath('{self.folder}')", - ) + assert repr(MultiplexedPath(self.folder)) == f"MultiplexedPath('{self.folder}')" def test_name(self): - self.assertEqual( - MultiplexedPath(self.folder).name, - os.path.basename(self.folder), - ) + assert MultiplexedPath(self.folder).name == os.path.basename(self.folder) class NamespaceReaderTest(util.DiskSetup, unittest.TestCase): @@ -119,18 +111,14 @@ class NamespaceReaderTest(util.DiskSetup, unittest.TestCase): reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) root = self.data.__path__[0] - self.assertEqual( - reader.resource_path('binary.file'), os.path.join(root, 'binary.file') - ) - self.assertEqual( - reader.resource_path('imaginary'), os.path.join(root, 'imaginary') - ) + assert reader.resource_path('binary.file') == os.path.join(root, 'binary.file') + assert reader.resource_path('imaginary') == os.path.join(root, 'imaginary') def test_files(self): reader = NamespaceReader(self.data.__spec__.submodule_search_locations) root = self.data.__path__[0] - self.assertIsInstance(reader.files(), MultiplexedPath) - self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')") + assert isinstance(reader.files(), MultiplexedPath) + assert repr(reader.files()) == f"MultiplexedPath('{root}')" if __name__ == '__main__': diff --git a/libs/importlib_resources/tests/test_resource.py b/libs/importlib_resources/tests/test_resource.py index c80afdc72..63a8bfbe1 100644 --- a/libs/importlib_resources/tests/test_resource.py +++ b/libs/importlib_resources/tests/test_resource.py @@ -1,3 +1,4 @@ +import types import unittest from importlib import import_module @@ -11,16 +12,16 @@ class ResourceTests: def test_is_file_exists(self): target = resources.files(self.data) / 'binary.file' - self.assertTrue(target.is_file()) + assert target.is_file() def test_is_file_missing(self): target = resources.files(self.data) / 'not-a-file' - self.assertFalse(target.is_file()) + assert not target.is_file() def test_is_dir(self): target = resources.files(self.data) / 'subdirectory' - self.assertFalse(target.is_file()) - self.assertTrue(target.is_dir()) + assert not target.is_file() + assert target.is_dir() class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase): @@ -40,7 +41,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase): package = util.create_package( file=self.data, path=self.data.__file__, contents=['A', 'B', 'C'] ) - self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) + assert names(resources.files(package)) == {'A', 'B', 'C'} def test_is_file(self): package = util.create_package( @@ -48,7 +49,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase): path=self.data.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'], ) - self.assertTrue(resources.files(package).joinpath('B').is_file()) + assert resources.files(package).joinpath('B').is_file() def test_is_dir(self): package = util.create_package( @@ -56,7 +57,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase): path=self.data.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'], ) - self.assertTrue(resources.files(package).joinpath('D').is_dir()) + assert resources.files(package).joinpath('D').is_dir() def test_resource_missing(self): package = util.create_package( @@ -64,7 +65,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase): path=self.data.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'], ) - self.assertFalse(resources.files(package).joinpath('Z').is_file()) + assert not resources.files(package).joinpath('Z').is_file() class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase): @@ -84,30 +85,26 @@ class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase): module.__file__ = '/path/which/shall/not/be/named' module.__spec__.loader = module.__loader__ module.__spec__.origin = module.__file__ - self.assertFalse(resources.files(module).joinpath('A').is_file()) + assert not resources.files(module).joinpath('A').is_file() class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase): def test_is_submodule_resource(self): submodule = import_module('data01.subdirectory') - self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) + assert resources.files(submodule).joinpath('binary.file').is_file() def test_read_submodule_resource_by_name(self): - self.assertTrue( - resources.files('data01.subdirectory').joinpath('binary.file').is_file() - ) + assert resources.files('data01.subdirectory').joinpath('binary.file').is_file() def test_submodule_contents(self): submodule = import_module('data01.subdirectory') - self.assertEqual( - names(resources.files(submodule)), {'__init__.py', 'binary.file'} - ) + assert names(resources.files(submodule)) == {'__init__.py', 'binary.file'} def test_submodule_contents_by_name(self): - self.assertEqual( - names(resources.files('data01.subdirectory')), - {'__init__.py', 'binary.file'}, - ) + assert names(resources.files('data01.subdirectory')) == { + '__init__.py', + 'binary.file', + } def test_as_file_directory(self): with resources.as_file(resources.files('data01')) as data: @@ -126,14 +123,8 @@ class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase): Test thata zip with two unrelated subpackages return distinct resources. Ref python/importlib_resources#44. """ - self.assertEqual( - names(resources.files('data02.one')), - {'__init__.py', 'resource1.txt'}, - ) - self.assertEqual( - names(resources.files('data02.two')), - {'__init__.py', 'resource2.txt'}, - ) + assert names(resources.files('data02.one')) == {'__init__.py', 'resource1.txt'} + assert names(resources.files('data02.two')) == {'__init__.py', 'resource2.txt'} class DeletingZipsTest(util.ZipSetup, unittest.TestCase): @@ -170,16 +161,15 @@ class DeletingZipsTest(util.ZipSetup, unittest.TestCase): class ResourceFromNamespaceTests: def test_is_submodule_resource(self): - self.assertTrue( - resources.files(import_module('namespacedata01')) + assert ( + resources + .files(import_module('namespacedata01')) .joinpath('binary.file') .is_file() ) def test_read_submodule_resource_by_name(self): - self.assertTrue( - resources.files('namespacedata01').joinpath('binary.file').is_file() - ) + assert resources.files('namespacedata01').joinpath('binary.file').is_file() def test_submodule_contents(self): contents = names(resources.files(import_module('namespacedata01'))) @@ -187,9 +177,7 @@ class ResourceFromNamespaceTests: contents.remove('__pycache__') except KeyError: pass - self.assertEqual( - contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} - ) + assert contents == {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} def test_submodule_contents_by_name(self): contents = names(resources.files('namespacedata01')) @@ -197,9 +185,7 @@ class ResourceFromNamespaceTests: contents.remove('__pycache__') except KeyError: pass - self.assertEqual( - contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} - ) + assert contents == {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} def test_submodule_sub_contents(self): contents = names(resources.files(import_module('namespacedata01.subdirectory'))) @@ -207,7 +193,7 @@ class ResourceFromNamespaceTests: contents.remove('__pycache__') except KeyError: pass - self.assertEqual(contents, {'binary.file'}) + assert contents == {'binary.file'} def test_submodule_sub_contents_by_name(self): contents = names(resources.files('namespacedata01.subdirectory')) @@ -215,7 +201,7 @@ class ResourceFromNamespaceTests: contents.remove('__pycache__') except KeyError: pass - self.assertEqual(contents, {'binary.file'}) + assert contents == {'binary.file'} class ResourceFromNamespaceDiskTests( @@ -234,5 +220,24 @@ class ResourceFromNamespaceZipTests( MODULE = 'namespacedata01' +class MainModuleTests(unittest.TestCase): + def test_main_module_with_none_spec(self): + """ + __main__ module with no spec should raise TypeError (for clarity). + + See python/cpython#138531 for details. + """ + # construct a __main__ module with no __spec__. + mainmodule = types.ModuleType("__main__") + + assert mainmodule.__spec__ is None + + with self.assertRaises( + TypeError, + msg="Cannot access resources for '__main__' as it does not appear to correspond to an importable module (its __spec__ is None).", + ): + resources.files(mainmodule) + + if __name__ == '__main__': unittest.main() diff --git a/libs/importlib_resources/tests/util.py b/libs/importlib_resources/tests/util.py index 0340c150f..67088a660 100644 --- a/libs/importlib_resources/tests/util.py +++ b/libs/importlib_resources/tests/util.py @@ -121,7 +121,7 @@ class CommonTestsBase(metaclass=abc.ABCMeta): bytes_data = io.BytesIO(b'Hello, world!') package = create_package(file=bytes_data, path=FileNotFoundError()) self.execute(package, 'utf-8.file') - self.assertEqual(package.__loader__._path, 'utf-8.file') + assert package.__loader__._path == 'utf-8.file' def test_extant_path(self): # Attempting to open or read or request the path when the @@ -132,7 +132,7 @@ class CommonTestsBase(metaclass=abc.ABCMeta): path = __file__ package = create_package(file=bytes_data, path=path) self.execute(package, 'utf-8.file') - self.assertEqual(package.__loader__._path, 'utf-8.file') + assert package.__loader__._path == 'utf-8.file' def test_useless_loader(self): package = create_package(file=FileNotFoundError(), path=FileNotFoundError()) diff --git a/libs/mako-1.3.10.dist-info/INSTALLER b/libs/mako-1.3.12.dist-info/INSTALLER similarity index 100% rename from libs/mako-1.3.10.dist-info/INSTALLER rename to libs/mako-1.3.12.dist-info/INSTALLER diff --git a/libs/mako-1.3.10.dist-info/METADATA b/libs/mako-1.3.12.dist-info/METADATA similarity index 99% rename from libs/mako-1.3.10.dist-info/METADATA rename to libs/mako-1.3.12.dist-info/METADATA index 067e8fa64..4041c65d6 100644 --- a/libs/mako-1.3.10.dist-info/METADATA +++ b/libs/mako-1.3.12.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: Mako -Version: 1.3.10 +Version: 1.3.12 Summary: A super-fast templating language that borrows the best ideas from the existing templating languages. Home-page: https://www.makotemplates.org/ Author: Mike Bayer diff --git a/libs/mako-1.3.10.dist-info/RECORD b/libs/mako-1.3.12.dist-info/RECORD similarity index 75% rename from libs/mako-1.3.10.dist-info/RECORD rename to libs/mako-1.3.12.dist-info/RECORD index 419b44e1a..79b413221 100644 --- a/libs/mako-1.3.10.dist-info/RECORD +++ b/libs/mako-1.3.12.dist-info/RECORD @@ -1,13 +1,13 @@ ../../bin/mako-render,sha256=Fzn7OVefsZWTvGZ5vS8Vwrp5MP4vyqxWITpWxPDqdgc,189 -mako-1.3.10.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -mako-1.3.10.dist-info/METADATA,sha256=VAbwPOfALVKbDJYv5l848ZceyGwynnE9s_TXP6_dr5o,2919 -mako-1.3.10.dist-info/RECORD,, -mako-1.3.10.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -mako-1.3.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -mako-1.3.10.dist-info/entry_points.txt,sha256=LsKkUsOsJQYbJ2M72hZCm968wi5K8Ywb5uFxCuN8Obk,512 -mako-1.3.10.dist-info/licenses/LICENSE,sha256=aNcGTlPj_6jp9CCp5gS9LiBZ2cMwSS-m69TrPUgRFok,1098 -mako-1.3.10.dist-info/top_level.txt,sha256=LItdH8cDPetpUu8rUyBG3DObS6h9Gcpr9j_WLj2S-R0,5 -mako/__init__.py,sha256=n-XoSW8ChrO6NkJ2xUHElzvSnU6XUu5ruMZe86QldTI,243 +mako-1.3.12.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +mako-1.3.12.dist-info/METADATA,sha256=qZWUMGDgIFY6RtfTBlru7hRNGJdBw1HSCrtc6H00l1k,2919 +mako-1.3.12.dist-info/RECORD,, +mako-1.3.12.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +mako-1.3.12.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +mako-1.3.12.dist-info/entry_points.txt,sha256=LsKkUsOsJQYbJ2M72hZCm968wi5K8Ywb5uFxCuN8Obk,512 +mako-1.3.12.dist-info/licenses/LICENSE,sha256=aNcGTlPj_6jp9CCp5gS9LiBZ2cMwSS-m69TrPUgRFok,1098 +mako-1.3.12.dist-info/top_level.txt,sha256=LItdH8cDPetpUu8rUyBG3DObS6h9Gcpr9j_WLj2S-R0,5 +mako/__init__.py,sha256=mAsKAzmwuTcXxyLr3x1Sy5dr3r2NWg4ciMo1GFT_5bg,243 mako/_ast_util.py,sha256=hCbfnnizWEa3xRCA-uVyShC2HohSpmVvexz5as_lHc8,20247 mako/ast.py,sha256=xYrdSiJFbf1CxJ9tU9pcPEWK0BYfwF2aDNDNLQG9PqQ,6642 mako/cache.py,sha256=kA6FKGl5NeTBnSTcnhoPkSaeJ0JeYpF6GM8qzEZGSts,7680 @@ -26,12 +26,12 @@ mako/ext/pygmentplugin.py,sha256=TnpJDyQeWTTGHZraMDpw8FB3PNzbmXhaZcjyIBudyi0,475 mako/ext/turbogears.py,sha256=egv8hradAnnSJwxtmW8uXAsqPUzX8cZZCXclmO_8hMA,2141 mako/filters.py,sha256=IBXyGOby4eFE3UGvNLhJlzbe1FfwJ2dcEr1-gKO_Ljc,4658 mako/lexer.py,sha256=GMq8yf0dEn04-xw2EVDEzaOLXMSVVQz9HyUbfwKnZKg,16321 -mako/lookup.py,sha256=4ALORJiL0wIdDvK1okW8rbjq2jL5F_TNASekDFQSULY,12428 +mako/lookup.py,sha256=LAVlo8a-SB2iqng-H-DyQHCWhbNPuAMO1KLwKr7OUPY,12447 mako/parsetree.py,sha256=-wmyX_mklAoKhc-7Psx0U15EKpNSd8oGRXFmvk2JQWo,19021 mako/pygen.py,sha256=689_jR0GG_8Am62Dmlm5h59VY6eyZAU3GroodqEDnZ0,10416 -mako/pyparser.py,sha256=8ZqUbCDIlgEup4si2zP7-xJns14JvVzX_QMO9sCCGJ0,7558 +mako/pyparser.py,sha256=YOsFk3eeV9yPusOe6JpVa33-ePBQ6LpdFhvDc4IaqPk,7557 mako/runtime.py,sha256=ZsUEN22nX3d3dECQujF69mBKDQS6yVv2nvz_0eTvFGg,27804 -mako/template.py,sha256=Vn5nLoBY-YzSQJKvRPFGb4fiPyZryA5Q-8mWutv0b9A,23563 +mako/template.py,sha256=5Q471dE83P3hwuVCINjOKkvmjMWbJvg8NdjayWfQXfY,23527 mako/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 mako/testing/_config.py,sha256=k-qpnsnbXUoN-ykMN5BRpg84i1x0p6UsAddKQnrIytU,3566 mako/testing/assertions.py,sha256=pfbGl84QlW7QWGg3_lo3wP8XnBAVo9AjzNp2ajmn7FA,5161 diff --git a/libs/mako-1.3.10.dist-info/REQUESTED b/libs/mako-1.3.12.dist-info/REQUESTED similarity index 100% rename from libs/mako-1.3.10.dist-info/REQUESTED rename to libs/mako-1.3.12.dist-info/REQUESTED diff --git a/libs/importlib_resources-6.5.2.dist-info/WHEEL b/libs/mako-1.3.12.dist-info/WHEEL similarity index 100% rename from libs/importlib_resources-6.5.2.dist-info/WHEEL rename to libs/mako-1.3.12.dist-info/WHEEL diff --git a/libs/mako-1.3.10.dist-info/entry_points.txt b/libs/mako-1.3.12.dist-info/entry_points.txt similarity index 100% rename from libs/mako-1.3.10.dist-info/entry_points.txt rename to libs/mako-1.3.12.dist-info/entry_points.txt diff --git a/libs/mako-1.3.10.dist-info/licenses/LICENSE b/libs/mako-1.3.12.dist-info/licenses/LICENSE similarity index 100% rename from libs/mako-1.3.10.dist-info/licenses/LICENSE rename to libs/mako-1.3.12.dist-info/licenses/LICENSE diff --git a/libs/mako-1.3.10.dist-info/top_level.txt b/libs/mako-1.3.12.dist-info/top_level.txt similarity index 100% rename from libs/mako-1.3.10.dist-info/top_level.txt rename to libs/mako-1.3.12.dist-info/top_level.txt diff --git a/libs/mako/__init__.py b/libs/mako/__init__.py index 71f8f6036..ce9c2347b 100644 --- a/libs/mako/__init__.py +++ b/libs/mako/__init__.py @@ -5,4 +5,4 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php -__version__ = "1.3.10" +__version__ = "1.3.12" diff --git a/libs/mako/lookup.py b/libs/mako/lookup.py index 1a431c805..fae03af04 100644 --- a/libs/mako/lookup.py +++ b/libs/mako/lookup.py @@ -241,7 +241,7 @@ class TemplateLookup(TemplateCollection): else: return self._collection[uri] except KeyError as e: - u = re.sub(r"^\/+", "", uri) + u = re.sub(r"^\/+", "", uri.replace("\\", "/")) for dir_ in self.directories: # make sure the path seperators are posix - os.altsep is empty # on POSIX and cannot be used. diff --git a/libs/mako/pyparser.py b/libs/mako/pyparser.py index 1da9ddb55..072d96e75 100644 --- a/libs/mako/pyparser.py +++ b/libs/mako/pyparser.py @@ -10,9 +10,8 @@ Parsing to AST is done via _ast on Python > 2.5, otherwise the compiler module is used. """ -import operator - import _ast +import operator from mako import _ast_util from mako import compat diff --git a/libs/mako/template.py b/libs/mako/template.py index 82c7cba8b..d85a7f5ea 100644 --- a/libs/mako/template.py +++ b/libs/mako/template.py @@ -259,9 +259,7 @@ class Template: self.module_id = "memory:" + hex(id(self)) self.uri = self.module_id - u_norm = self.uri - if u_norm.startswith("/"): - u_norm = u_norm[1:] + u_norm = self.uri.replace("\\", "/").lstrip("/") u_norm = os.path.normpath(u_norm) if u_norm.startswith(".."): raise exceptions.TemplateLookupException( diff --git a/libs/msgpack-1.1.2.dist-info/RECORD b/libs/msgpack-1.1.2.dist-info/RECORD deleted file mode 100644 index d7f2ca222..000000000 --- a/libs/msgpack-1.1.2.dist-info/RECORD +++ /dev/null @@ -1,12 +0,0 @@ -msgpack-1.1.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -msgpack-1.1.2.dist-info/METADATA,sha256=hWxCylxh7YtECg6LELP_zJVXOXpQDuIjwQ2f-ZRb-As,8091 -msgpack-1.1.2.dist-info/RECORD,, -msgpack-1.1.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -msgpack-1.1.2.dist-info/WHEEL,sha256=DkV-ntIm-3VWD87iCIyvXSz28ISStjMGlDRnHg-QlnQ,110 -msgpack-1.1.2.dist-info/licenses/COPYING,sha256=SS3tuoXaWHL3jmCRvNH-pHTWYNNay03ulkuKqz8AdCc,614 -msgpack-1.1.2.dist-info/top_level.txt,sha256=2tykSY1pXdiA2xYTDR6jPw0qI5ZGxRihyhf4S5hZyXk,8 -msgpack/__init__.py,sha256=RA8gcqK17YpkxBnNwXJVa1oa2LygWDgfF1nA1NPw3mo,1109 -msgpack/_cmsgpack.cpython-310-darwin.so,sha256=imGj-cEEW5KnWaHgqeP7Ba6P2WPfmio5MTVdEs4ryvA,211008 -msgpack/exceptions.py,sha256=dCTWei8dpkrMsQDcjQk74ATl9HsIBH0ybt8zOPNqMYc,1081 -msgpack/ext.py,sha256=kteJv03n9tYzd5oo3xYopVTo4vRaAxonBQQJhXohZZo,5726 -msgpack/fallback.py,sha256=0g1Pzp0vtmBEmJ5w9F3s_-JMVURP8RS4G1cc5TRaAsI,32390 diff --git a/libs/msgpack-1.1.2.dist-info/INSTALLER b/libs/msgpack-1.2.1.dist-info/INSTALLER similarity index 100% rename from libs/msgpack-1.1.2.dist-info/INSTALLER rename to libs/msgpack-1.2.1.dist-info/INSTALLER diff --git a/libs/msgpack-1.1.2.dist-info/METADATA b/libs/msgpack-1.2.1.dist-info/METADATA similarity index 95% rename from libs/msgpack-1.1.2.dist-info/METADATA rename to libs/msgpack-1.2.1.dist-info/METADATA index d23f73ba4..10314aa1d 100644 --- a/libs/msgpack-1.1.2.dist-info/METADATA +++ b/libs/msgpack-1.2.1.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: msgpack -Version: 1.1.2 +Version: 1.2.1 Summary: MessagePack serializer Author-email: Inada Naoki License-Expression: Apache-2.0 @@ -8,7 +8,7 @@ Project-URL: Homepage, https://msgpack.org/ Project-URL: Documentation, https://msgpack-python.readthedocs.io/ Project-URL: Repository, https://github.com/msgpack/msgpack-python/ Project-URL: Tracker, https://github.com/msgpack/msgpack-python/issues -Project-URL: Changelog, https://github.com/msgpack/msgpack-python/blob/main/ChangeLog.rst +Project-URL: Changelog, https://github.com/msgpack/msgpack-python/blob/main/CHANGELOG.md Keywords: msgpack,messagepack,serializer,serialization,binary Classifier: Development Status :: 5 - Production/Stable Classifier: Operating System :: OS Independent @@ -16,7 +16,7 @@ Classifier: Topic :: File Formats Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy -Requires-Python: >=3.9 +Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: COPYING Dynamic: license-file @@ -95,6 +95,10 @@ for unpacked in unpacker: print(unpacked) ``` +> [!IMPORTANT] +> If `Unpacker.unpack()` stops with an exception other than `OutOfData`, that `Unpacker` cannot be reused. +> Create a new `Unpacker` when reading another stream. + ### Packing/unpacking of custom data types @@ -142,15 +146,15 @@ It is also possible to pack/unpack custom data types using the **ext** type. >>> import array >>> def default(obj): ... if isinstance(obj, array.array) and obj.typecode == 'd': -... return msgpack.ExtType(42, obj.tostring()) +... return msgpack.ExtType(42, obj.tobytes()) ... raise TypeError("Unknown type: %r" % (obj,)) ... >>> def ext_hook(code, data): ... if code == 42: ... a = array.array('d') -... a.fromstring(data) +... a.frombytes(data) ... return a -... return ExtType(code, data) +... return msgpack.ExtType(code, data) ... >>> data = array.array('d', [1.2, 3.4]) >>> packed = msgpack.packb(data, default=default) @@ -243,7 +247,7 @@ When upgrading from msgpack-0.4 or earlier, do `pip uninstall msgpack-python` be * The extension module no longer supports Python 2. The pure Python implementation (`msgpack.fallback`) is used for Python 2. - + * msgpack 1.0.6 drops official support of Python 2.7, as pip and GitHub Action "setup-python" no longer supports Python 2.7. diff --git a/libs/msgpack-1.2.1.dist-info/RECORD b/libs/msgpack-1.2.1.dist-info/RECORD new file mode 100644 index 000000000..3849fc45d --- /dev/null +++ b/libs/msgpack-1.2.1.dist-info/RECORD @@ -0,0 +1,12 @@ +msgpack-1.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +msgpack-1.2.1.dist-info/METADATA,sha256=Fv93GsM-y87XpvTM41W-U43bJ-DGhv4JurRd8HFiHq8,8273 +msgpack-1.2.1.dist-info/RECORD,, +msgpack-1.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +msgpack-1.2.1.dist-info/WHEEL,sha256=DkV-ntIm-3VWD87iCIyvXSz28ISStjMGlDRnHg-QlnQ,110 +msgpack-1.2.1.dist-info/licenses/COPYING,sha256=SS3tuoXaWHL3jmCRvNH-pHTWYNNay03ulkuKqz8AdCc,614 +msgpack-1.2.1.dist-info/top_level.txt,sha256=2tykSY1pXdiA2xYTDR6jPw0qI5ZGxRihyhf4S5hZyXk,8 +msgpack/__init__.py,sha256=VHnCSJiHgikrXuSJKJ94I6-tx0_GfPMsa1-BOv5ctuU,1109 +msgpack/_cmsgpack.cpython-310-darwin.so,sha256=4wfyKX-wAHJU57jxHLTHlxiJiQFP6VtwwQTFOMP63pg,204672 +msgpack/exceptions.py,sha256=dCTWei8dpkrMsQDcjQk74ATl9HsIBH0ybt8zOPNqMYc,1081 +msgpack/ext.py,sha256=0j-OWB-bZIwWP-0X2H9uUfkIr3rn6HVdz1Yw0M1M33I,5731 +msgpack/fallback.py,sha256=jO5tLolPFtz69Y1A8xdWO8jALSwCHLVNoklYYusbBN0,32820 diff --git a/libs/msgpack-1.1.2.dist-info/REQUESTED b/libs/msgpack-1.2.1.dist-info/REQUESTED similarity index 100% rename from libs/msgpack-1.1.2.dist-info/REQUESTED rename to libs/msgpack-1.2.1.dist-info/REQUESTED diff --git a/libs/msgpack-1.1.2.dist-info/WHEEL b/libs/msgpack-1.2.1.dist-info/WHEEL similarity index 100% rename from libs/msgpack-1.1.2.dist-info/WHEEL rename to libs/msgpack-1.2.1.dist-info/WHEEL diff --git a/libs/msgpack-1.1.2.dist-info/licenses/COPYING b/libs/msgpack-1.2.1.dist-info/licenses/COPYING similarity index 100% rename from libs/msgpack-1.1.2.dist-info/licenses/COPYING rename to libs/msgpack-1.2.1.dist-info/licenses/COPYING diff --git a/libs/msgpack-1.1.2.dist-info/top_level.txt b/libs/msgpack-1.2.1.dist-info/top_level.txt similarity index 100% rename from libs/msgpack-1.1.2.dist-info/top_level.txt rename to libs/msgpack-1.2.1.dist-info/top_level.txt diff --git a/libs/msgpack/__init__.py b/libs/msgpack/__init__.py index f3266b701..eeb85afad 100644 --- a/libs/msgpack/__init__.py +++ b/libs/msgpack/__init__.py @@ -4,8 +4,8 @@ import os from .exceptions import * # noqa: F403 from .ext import ExtType, Timestamp -version = (1, 1, 2) -__version__ = "1.1.2" +version = (1, 2, 1) +__version__ = "1.2.1" if os.environ.get("MSGPACK_PUREPYTHON"): diff --git a/libs/msgpack/ext.py b/libs/msgpack/ext.py index 9694819a7..92ea4530e 100644 --- a/libs/msgpack/ext.py +++ b/libs/msgpack/ext.py @@ -167,4 +167,4 @@ class Timestamp: :rtype: Timestamp """ - return Timestamp(seconds=int(dt.timestamp()), nanoseconds=dt.microsecond * 1000) + return Timestamp(seconds=int(dt.timestamp() // 1), nanoseconds=dt.microsecond * 1000) diff --git a/libs/msgpack/fallback.py b/libs/msgpack/fallback.py index b02e47cfb..860d94ac0 100644 --- a/libs/msgpack/fallback.py +++ b/libs/msgpack/fallback.py @@ -52,7 +52,7 @@ TYPE_RAW = 3 TYPE_BIN = 4 TYPE_EXT = 5 -DEFAULT_RECURSE_LIMIT = 511 +DEFAULT_RECURSE_LIMIT = 1024 def _check_type_strict(obj, t, type=type, tuple=tuple): @@ -328,7 +328,7 @@ class Unpacker: self._buf_checkpoint = 0 # Use extend here: INPLACE_ADD += doesn't reliably typecast memoryview in jython - self._buffer.extend(view) + self._buffer.extend(view if view.contiguous else view.tobytes()) view.release() def _consume(self): @@ -518,9 +518,15 @@ class Unpacker: self._unpack(EX_SKIP) return if self._object_pairs_hook is not None: - ret = self._object_pairs_hook( - (self._unpack(EX_CONSTRUCT), self._unpack(EX_CONSTRUCT)) for _ in range(n) - ) + + def _gen(): + for _ in range(n): + key = self._unpack(EX_CONSTRUCT) + if self._strict_map_key and type(key) not in (str, bytes): + raise ValueError("%s is not allowed for map key" % str(type(key))) + yield key, self._unpack(EX_CONSTRUCT) + + ret = self._object_pairs_hook(_gen()) else: ret = {} for _ in range(n): @@ -861,6 +867,10 @@ class Packer: self._buffer.write(b"\xc9" + struct.pack(">I", L)) self._buffer.write(struct.pack("B", typecode)) self._buffer.write(data) + if self._autoreset: + ret = self._buffer.getvalue() + self._buffer = BytesIO() + return ret def _pack_array_header(self, n): if n <= 0x0F: diff --git a/libs/platformdirs-4.9.4.dist-info/INSTALLER b/libs/platformdirs-4.10.0.dist-info/INSTALLER similarity index 100% rename from libs/platformdirs-4.9.4.dist-info/INSTALLER rename to libs/platformdirs-4.10.0.dist-info/INSTALLER diff --git a/libs/platformdirs-4.9.4.dist-info/METADATA b/libs/platformdirs-4.10.0.dist-info/METADATA similarity index 83% rename from libs/platformdirs-4.9.4.dist-info/METADATA rename to libs/platformdirs-4.10.0.dist-info/METADATA index c2b3e9e8f..d77dee306 100644 --- a/libs/platformdirs-4.9.4.dist-info/METADATA +++ b/libs/platformdirs-4.10.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: platformdirs -Version: 4.9.4 +Version: 4.10.0 Summary: A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`. Project-URL: Changelog, https://platformdirs.readthedocs.io/en/latest/changelog.html Project-URL: Documentation, https://platformdirs.readthedocs.io @@ -75,14 +75,26 @@ user_config_path("MyApp", "MyCompany") # returns pathlib.Path ## Directory types +**Application directories** — scoped to your app name and version: + - **Data**: Persistent application data (`user_data_dir`, `site_data_dir`) - **Config**: Configuration files and settings (`user_config_dir`, `site_config_dir`) +- **Preference**: User preferences, distinct from config on macOS (`user_preference_dir`) - **Cache**: Cached data that can be regenerated (`user_cache_dir`, `site_cache_dir`) - **State**: Non-essential runtime state like window positions (`user_state_dir`, `site_state_dir`) - **Logs**: Log files (`user_log_dir`, `site_log_dir`) - **Runtime**: Runtime files like sockets and PIDs (`user_runtime_dir`, `site_runtime_dir`) -Each type has both `user_*` (per-user, writable) and `site_*` (system-wide, read-only for users) variants. +App dirs have both `user_*` (per-user, writable) and `site_*` (system-wide, read-only) variants where applicable. + +**User media directories** — standard user-facing folders, not scoped to app name: + +- **Documents** (`user_documents_dir`), **Downloads** (`user_downloads_dir`) +- **Pictures** (`user_pictures_dir`), **Videos** (`user_videos_dir`), **Music** (`user_music_dir`) +- **Desktop** (`user_desktop_dir`), **Projects** (`user_projects_dir`) +- **Public share** (`user_publicshare_dir`), **Templates** (`user_templates_dir`) +- **Fonts** (`user_fonts_dir`) — user-writable font installation directory +- **Executable** (`user_bin_dir`, `site_bin_dir`), **Applications** (`user_applications_dir`, `site_applications_dir`) ## Documentation diff --git a/libs/platformdirs-4.10.0.dist-info/RECORD b/libs/platformdirs-4.10.0.dist-info/RECORD new file mode 100644 index 000000000..7cc4aa020 --- /dev/null +++ b/libs/platformdirs-4.10.0.dist-info/RECORD @@ -0,0 +1,16 @@ +platformdirs-4.10.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +platformdirs-4.10.0.dist-info/METADATA,sha256=xO4Y9bbSs4USowwokgCxRj8pioJhyPpj8lhOqX6Tz3g,5463 +platformdirs-4.10.0.dist-info/RECORD,, +platformdirs-4.10.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +platformdirs-4.10.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +platformdirs-4.10.0.dist-info/licenses/LICENSE,sha256=KeD9YukphQ6G6yjD_czwzv30-pSHkBHP-z0NS-1tTbY,1089 +platformdirs/__init__.py,sha256=j-8oWLg3d5yoqzoiISxjwOuoHijzs2ehIoyjQgWMteI,32329 +platformdirs/__main__.py,sha256=H36IZqfc9v0ePzWsua50wmq4ShRCONCYv66rzpIVptI,1761 +platformdirs/_xdg.py,sha256=5w6S07vVabEROhaUB_X7i0hmLNbdYtC9aEvPbsEIS8M,7894 +platformdirs/android.py,sha256=BBj3wfw-dfuVxsCsKsvFByKVspRDgfv8gwIzhpE1JRQ,11566 +platformdirs/api.py,sha256=EaIsFgXGu8cDTb41WM3hse_SrIoOUht8_TCo5XdOOSM,14887 +platformdirs/macos.py,sha256=PWudeDZAY9UuwLP_5rN3exSRFtYdY542hDhjah4dSWA,9538 +platformdirs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +platformdirs/unix.py,sha256=X7Cip1xOHFOnSVveqKWzU1P7jn4zuDGpRgmVxJnk4vk,13317 +platformdirs/version.py,sha256=dJvdm7L_7wobEcxrNoJtH7CtmYcG_VR5yNmtBshJ-xs,522 +platformdirs/windows.py,sha256=YOdSGNhdpxm_ydZq7uG74i9mX2_WWbcMSA4fobXwL0M,15798 diff --git a/libs/platformdirs-4.9.4.dist-info/REQUESTED b/libs/platformdirs-4.10.0.dist-info/REQUESTED similarity index 100% rename from libs/platformdirs-4.9.4.dist-info/REQUESTED rename to libs/platformdirs-4.10.0.dist-info/REQUESTED diff --git a/libs/platformdirs-4.9.4.dist-info/WHEEL b/libs/platformdirs-4.10.0.dist-info/WHEEL similarity index 67% rename from libs/platformdirs-4.9.4.dist-info/WHEEL rename to libs/platformdirs-4.10.0.dist-info/WHEEL index b1b94fd58..bd8422674 100644 --- a/libs/platformdirs-4.9.4.dist-info/WHEEL +++ b/libs/platformdirs-4.10.0.dist-info/WHEEL @@ -1,4 +1,4 @@ Wheel-Version: 1.0 -Generator: hatchling 1.29.0 +Generator: hatchling 1.30.1 Root-Is-Purelib: true Tag: py3-none-any diff --git a/libs/platformdirs-4.9.4.dist-info/licenses/LICENSE b/libs/platformdirs-4.10.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/platformdirs-4.9.4.dist-info/licenses/LICENSE rename to libs/platformdirs-4.10.0.dist-info/licenses/LICENSE diff --git a/libs/platformdirs-4.9.4.dist-info/RECORD b/libs/platformdirs-4.9.4.dist-info/RECORD deleted file mode 100644 index 5fad1e7ce..000000000 --- a/libs/platformdirs-4.9.4.dist-info/RECORD +++ /dev/null @@ -1,16 +0,0 @@ -platformdirs-4.9.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -platformdirs-4.9.4.dist-info/METADATA,sha256=Jl0ycpV78hiBFdFunnqwuP71goJfqOUuNL5odYuR3Xo,4683 -platformdirs-4.9.4.dist-info/RECORD,, -platformdirs-4.9.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -platformdirs-4.9.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 -platformdirs-4.9.4.dist-info/licenses/LICENSE,sha256=KeD9YukphQ6G6yjD_czwzv30-pSHkBHP-z0NS-1tTbY,1089 -platformdirs/__init__.py,sha256=hL_ZyhsQ5EfYf4W_Tr4O6F0lAOKLvQi0TZ4e_WGCIc4,30718 -platformdirs/__main__.py,sha256=MvM6fKJpBAeiTZckNJGSihXFKkS46tS9_P5gySZg3RI,1633 -platformdirs/_xdg.py,sha256=zzf6nX5N329OzNe2PhFEFWAUhN7ijmghapDb81ll2Us,6457 -platformdirs/android.py,sha256=nbEJY4dyNjF-OM5RalA0WJIQHCUy8OKioGDS9G_-ZGg,10591 -platformdirs/api.py,sha256=iuNVDTrHntrQjBknqA-BSCsxWqix8aJugdYo4SbuBAo,12734 -platformdirs/macos.py,sha256=YP2C4B1krgWVSA7yOq1T6NrUqduZqaFXIUxVtB4FuEg,7869 -platformdirs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -platformdirs/unix.py,sha256=mcg5c1grrP1w566JAxDLSWtfq7L9qvS7foAr4b-IIrw,12189 -platformdirs/version.py,sha256=wnb6jtnQL3AvpQz2Kc9V7_5rRAT9pBykIPguVZgvo48,704 -platformdirs/windows.py,sha256=eQFkbOcRcI5YplmPx59FOd7yXJlhq27PpL7HVaC7se4,14564 diff --git a/libs/platformdirs/__init__.py b/libs/platformdirs/__init__.py index 4ab450ea9..e9d3cb672 100644 --- a/libs/platformdirs/__init__.py +++ b/libs/platformdirs/__init__.py @@ -344,6 +344,31 @@ def user_desktop_dir() -> str: return PlatformDirs().user_desktop_dir +def user_projects_dir() -> str: + """:returns: projects directory tied to the user""" + return PlatformDirs().user_projects_dir + + +def user_publicshare_dir() -> str: + """:returns: public share directory tied to the user""" + return PlatformDirs().user_publicshare_dir + + +def user_templates_dir() -> str: + """:returns: templates directory tied to the user""" + return PlatformDirs().user_templates_dir + + +def user_fonts_dir() -> str: + """:returns: fonts directory tied to the user""" + return PlatformDirs().user_fonts_dir + + +def user_preference_dir() -> str: + """:returns: preference directory tied to the user""" + return PlatformDirs().user_preference_dir + + def user_bin_dir() -> str: """:returns: bin directory tied to the user""" return PlatformDirs().user_bin_dir @@ -720,6 +745,31 @@ def user_desktop_path() -> Path: return PlatformDirs().user_desktop_path +def user_projects_path() -> Path: + """:returns: projects path tied to the user""" + return PlatformDirs().user_projects_path + + +def user_publicshare_path() -> Path: + """:returns: public share path tied to the user""" + return PlatformDirs().user_publicshare_path + + +def user_templates_path() -> Path: + """:returns: templates path tied to the user""" + return PlatformDirs().user_templates_path + + +def user_fonts_path() -> Path: + """:returns: fonts path tied to the user""" + return PlatformDirs().user_fonts_path + + +def user_preference_path() -> Path: + """:returns: preference path tied to the user""" + return PlatformDirs().user_preference_path + + def user_bin_path() -> Path: """:returns: bin path tied to the user""" return PlatformDirs().user_bin_path @@ -842,16 +892,26 @@ __all__ = [ "user_documents_path", "user_downloads_dir", "user_downloads_path", + "user_fonts_dir", + "user_fonts_path", "user_log_dir", "user_log_path", "user_music_dir", "user_music_path", "user_pictures_dir", "user_pictures_path", + "user_preference_dir", + "user_preference_path", + "user_projects_dir", + "user_projects_path", + "user_publicshare_dir", + "user_publicshare_path", "user_runtime_dir", "user_runtime_path", "user_state_dir", "user_state_path", + "user_templates_dir", + "user_templates_path", "user_videos_dir", "user_videos_path", ] diff --git a/libs/platformdirs/__main__.py b/libs/platformdirs/__main__.py index 2490ffbbe..7a52b7d40 100644 --- a/libs/platformdirs/__main__.py +++ b/libs/platformdirs/__main__.py @@ -15,6 +15,11 @@ PROPS = ( "user_pictures_dir", "user_videos_dir", "user_music_dir", + "user_projects_dir", + "user_publicshare_dir", + "user_templates_dir", + "user_fonts_dir", + "user_preference_dir", "user_bin_dir", "site_bin_dir", "user_applications_dir", diff --git a/libs/platformdirs/_xdg.py b/libs/platformdirs/_xdg.py index c4aadd6d0..b2af284ae 100644 --- a/libs/platformdirs/_xdg.py +++ b/libs/platformdirs/_xdg.py @@ -21,7 +21,7 @@ class XDGMixin(PlatformDirsABC): def _site_data_dirs(self) -> list[str]: if xdg_dirs := os.environ.get("XDG_DATA_DIRS", "").strip(): return [self._append_app_name_and_version(p) for p in xdg_dirs.split(os.pathsep) if p.strip()] - return super()._site_data_dirs # type: ignore[misc] + return super()._site_data_dirs @property def site_data_dir(self) -> str: @@ -40,7 +40,7 @@ class XDGMixin(PlatformDirsABC): def _site_config_dirs(self) -> list[str]: if xdg_dirs := os.environ.get("XDG_CONFIG_DIRS", "").strip(): return [self._append_app_name_and_version(p) for p in xdg_dirs.split(os.pathsep) if p.strip()] - return super()._site_config_dirs # type: ignore[misc] + return super()._site_config_dirs @property def site_config_dir(self) -> str: @@ -118,6 +118,34 @@ class XDGMixin(PlatformDirsABC): return os.path.expanduser(path) # noqa: PTH111 return super().user_desktop_dir + @property + def user_projects_dir(self) -> str: + """:returns: projects directory tied to the user, from ``$XDG_PROJECTS_DIR`` if set, else platform default""" + if path := os.environ.get("XDG_PROJECTS_DIR", "").strip(): + return os.path.expanduser(path) # noqa: PTH111 # API returns str, not Path + return super().user_projects_dir + + @property + def user_publicshare_dir(self) -> str: + """:returns: public share directory tied to the user, from ``$XDG_PUBLICSHARE_DIR`` if set, else platform default""" + if path := os.environ.get("XDG_PUBLICSHARE_DIR", "").strip(): + return os.path.expanduser(path) # noqa: PTH111 # API returns str, not Path + return super().user_publicshare_dir + + @property + def user_templates_dir(self) -> str: + """:returns: templates directory tied to the user, from ``$XDG_TEMPLATES_DIR`` if set, else platform default""" + if path := os.environ.get("XDG_TEMPLATES_DIR", "").strip(): + return os.path.expanduser(path) # noqa: PTH111 # API returns str, not Path + return super().user_templates_dir + + @property + def user_fonts_dir(self) -> str: + """:returns: fonts directory tied to the user, from ``$XDG_DATA_HOME/fonts`` if set, else platform default""" + if path := os.environ.get("XDG_DATA_HOME", "").strip(): + return f"{os.path.expanduser(path)}/fonts" # noqa: PTH111 # API returns str, not Path + return super().user_fonts_dir + @property def user_applications_dir(self) -> str: """:returns: applications directory tied to the user, from ``$XDG_DATA_HOME`` if set, else platform default""" @@ -129,7 +157,7 @@ class XDGMixin(PlatformDirsABC): def _site_applications_dirs(self) -> list[str]: if xdg_dirs := os.environ.get("XDG_DATA_DIRS", "").strip(): return [os.path.join(p, "applications") for p in xdg_dirs.split(os.pathsep) if p.strip()] # noqa: PTH118 - return super()._site_applications_dirs # type: ignore[misc] + return super()._site_applications_dirs @property def site_applications_dir(self) -> str: diff --git a/libs/platformdirs/android.py b/libs/platformdirs/android.py index 8c798b95e..885e8c6e1 100644 --- a/libs/platformdirs/android.py +++ b/libs/platformdirs/android.py @@ -107,6 +107,31 @@ class Android(PlatformDirsABC): # noqa: PLR0904 """:returns: desktop directory tied to the user e.g. ``/storage/emulated/0/Desktop``""" return "/storage/emulated/0/Desktop" + @property + def user_projects_dir(self) -> str: + """:returns: projects directory tied to the user e.g. ``/storage/emulated/0/Projects``""" + return "/storage/emulated/0/Projects" + + @property + def user_publicshare_dir(self) -> str: + """:returns: public share directory tied to the user e.g. ``/storage/emulated/0/Public``""" + return "/storage/emulated/0/Public" + + @property + def user_templates_dir(self) -> str: + """:returns: templates directory tied to the user e.g. ``/storage/emulated/0/Templates``""" + return "/storage/emulated/0/Templates" + + @property + def user_fonts_dir(self) -> str: + """:returns: fonts directory tied to the user e.g. ``/storage/emulated/0/fonts``""" + return "/storage/emulated/0/fonts" + + @property + def user_preference_dir(self) -> str: + """:returns: preference directory tied to the user, same as ``user_config_dir``""" + return self.user_config_dir + @property def user_bin_dir(self) -> str: """:returns: bin directory tied to the user, e.g. ``/data/user///files/bin``""" diff --git a/libs/platformdirs/api.py b/libs/platformdirs/api.py index 1ee29adb5..91ef90285 100644 --- a/libs/platformdirs/api.py +++ b/libs/platformdirs/api.py @@ -50,6 +50,13 @@ class PlatformDirsABC(ABC): # noqa: PLR0904 Typically, it is the owning company name. Defaults to `appname`. You may pass ``False`` to disable it. + .. note:: + + On Windows, the directory structure is ``//``. When ``appauthor`` is ``None`` (the + default), it falls back to ``appname``, resulting in ``//`` (e.g. + ``AppData/Local/myapp/myapp``). Pass ``appauthor=False`` to omit the author directory entirely and get + ``/``. + """ self.version = version """An optional version path element to append to the path. @@ -123,6 +130,10 @@ class PlatformDirsABC(ABC): # noqa: PLR0904 def site_data_dir(self) -> str: """:returns: data directory shared by users""" + @property + def _site_data_dirs(self) -> list[str]: + raise NotImplementedError + @property @abstractmethod def user_config_dir(self) -> str: @@ -133,6 +144,10 @@ class PlatformDirsABC(ABC): # noqa: PLR0904 def site_config_dir(self) -> str: """:returns: config directory shared by users""" + @property + def _site_config_dirs(self) -> list[str]: + raise NotImplementedError + @property @abstractmethod def user_cache_dir(self) -> str: @@ -193,6 +208,31 @@ class PlatformDirsABC(ABC): # noqa: PLR0904 def user_desktop_dir(self) -> str: """:returns: desktop directory tied to the user""" + @property + @abstractmethod + def user_projects_dir(self) -> str: + """:returns: projects directory tied to the user""" + + @property + @abstractmethod + def user_publicshare_dir(self) -> str: + """:returns: public share directory tied to the user""" + + @property + @abstractmethod + def user_templates_dir(self) -> str: + """:returns: templates directory tied to the user""" + + @property + @abstractmethod + def user_fonts_dir(self) -> str: + """:returns: fonts directory tied to the user""" + + @property + @abstractmethod + def user_preference_dir(self) -> str: + """:returns: preference directory tied to the user""" + @property @abstractmethod def user_bin_dir(self) -> str: @@ -213,6 +253,10 @@ class PlatformDirsABC(ABC): # noqa: PLR0904 def site_applications_dir(self) -> str: """:returns: applications directory shared by users""" + @property + def _site_applications_dirs(self) -> list[str]: + raise NotImplementedError + @property @abstractmethod def user_runtime_dir(self) -> str: @@ -303,6 +347,31 @@ class PlatformDirsABC(ABC): # noqa: PLR0904 """:returns: desktop path tied to the user""" return Path(self.user_desktop_dir) + @property + def user_projects_path(self) -> Path: + """:returns: projects path tied to the user""" + return Path(self.user_projects_dir) + + @property + def user_publicshare_path(self) -> Path: + """:returns: public share path tied to the user""" + return Path(self.user_publicshare_dir) + + @property + def user_templates_path(self) -> Path: + """:returns: templates path tied to the user""" + return Path(self.user_templates_dir) + + @property + def user_fonts_path(self) -> Path: + """:returns: fonts path tied to the user""" + return Path(self.user_fonts_dir) + + @property + def user_preference_path(self) -> Path: + """:returns: preference path tied to the user""" + return Path(self.user_preference_dir) + @property def user_bin_path(self) -> Path: """:returns: bin path tied to the user""" diff --git a/libs/platformdirs/macos.py b/libs/platformdirs/macos.py index 249324672..9ebc2a0c5 100644 --- a/libs/platformdirs/macos.py +++ b/libs/platformdirs/macos.py @@ -25,32 +25,43 @@ class _MacOSDefaults(PlatformDirsABC): # noqa: PLR0904 """ - @property - def user_data_dir(self) -> str: - """:returns: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``""" + def _base_user_app_support_dir(self) -> str: return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support")) # noqa: PTH111 - @property - def _site_data_dirs(self) -> list[str]: + def _base_site_dirs(self) -> list[str]: is_homebrew = "/opt/python" in sys.prefix homebrew_prefix = sys.prefix.split("/opt/python")[0] if is_homebrew else "" path_list = [self._append_app_name_and_version(f"{homebrew_prefix}/share")] if is_homebrew else [] path_list.append(self._append_app_name_and_version("/Library/Application Support")) return path_list + @property + def user_data_dir(self) -> str: + """:returns: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``""" + return self._base_user_app_support_dir() + + @property + def _site_data_dirs(self) -> list[str]: + return self._base_site_dirs() + @property def site_data_path(self) -> Path: """:returns: data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``""" return self._first_item_as_path_if_multipath(self.site_data_dir) + @property + def site_config_path(self) -> Path: + """:returns: config path shared by users. Only return the first item, even if ``multipath`` is set to ``True``""" + return self._first_item_as_path_if_multipath(self.site_config_dir) + @property def user_config_dir(self) -> str: """:returns: config directory tied to the user, same as `user_data_dir`""" - return self.user_data_dir + return self._base_user_app_support_dir() @property def _site_config_dirs(self) -> list[str]: - return self._site_data_dirs + return self._base_site_dirs() @property def user_cache_dir(self) -> str: @@ -76,12 +87,12 @@ class _MacOSDefaults(PlatformDirsABC): # noqa: PLR0904 @property def user_state_dir(self) -> str: """:returns: state directory tied to the user, same as `user_data_dir`""" - return self.user_data_dir + return self._base_user_app_support_dir() @property def site_state_dir(self) -> str: """:returns: state directory shared by users, same as `site_data_dir`""" - return self.site_data_dir + return self._base_site_dirs()[0] @property def user_log_dir(self) -> str: @@ -123,6 +134,31 @@ class _MacOSDefaults(PlatformDirsABC): # noqa: PLR0904 """:returns: desktop directory tied to the user, e.g. ``~/Desktop``""" return os.path.expanduser("~/Desktop") # noqa: PTH111 + @property + def user_projects_dir(self) -> str: + """:returns: projects directory tied to the user, e.g. ``~/Projects``""" + return os.path.expanduser("~/Projects") # noqa: PTH111 + + @property + def user_publicshare_dir(self) -> str: + """:returns: public share directory tied to the user, e.g. ``~/Public``""" + return os.path.expanduser("~/Public") # noqa: PTH111 # API returns str, not Path + + @property + def user_templates_dir(self) -> str: + """:returns: templates directory tied to the user, e.g. ``~/Templates``""" + return os.path.expanduser("~/Templates") # noqa: PTH111 # API returns str, not Path + + @property + def user_fonts_dir(self) -> str: + """:returns: fonts directory tied to the user, e.g. ``~/Library/Fonts``""" + return os.path.expanduser("~/Library/Fonts") # noqa: PTH111 # API returns str, not Path + + @property + def user_preference_dir(self) -> str: + """:returns: preference directory tied to the user, e.g. ``~/Library/Preferences/AppName``""" + return self._append_app_name_and_version(os.path.expanduser("~/Library/Preferences")) # noqa: PTH111 # API returns str, not Path + @property def user_bin_dir(self) -> str: """:returns: bin directory tied to the user, e.g. ``~/.local/bin``""" diff --git a/libs/platformdirs/unix.py b/libs/platformdirs/unix.py index 0638700c1..500d3a4be 100644 --- a/libs/platformdirs/unix.py +++ b/libs/platformdirs/unix.py @@ -123,6 +123,31 @@ class _UnixDefaults(PlatformDirsABC): # noqa: PLR0904 """:returns: desktop directory tied to the user, e.g. ``~/Desktop``""" return _get_user_media_dir("XDG_DESKTOP_DIR", "~/Desktop") + @property + def user_projects_dir(self) -> str: + """:returns: projects directory tied to the user, e.g. ``~/Projects``""" + return _get_user_media_dir("XDG_PROJECTS_DIR", "~/Projects") + + @property + def user_publicshare_dir(self) -> str: + """:returns: public share directory tied to the user, e.g. ``~/Public``""" + return _get_user_media_dir("XDG_PUBLICSHARE_DIR", "~/Public") + + @property + def user_templates_dir(self) -> str: + """:returns: templates directory tied to the user, e.g. ``~/Templates``""" + return _get_user_media_dir("XDG_TEMPLATES_DIR", "~/Templates") + + @property + def user_fonts_dir(self) -> str: + """:returns: fonts directory tied to the user, e.g. ``~/.local/share/fonts``""" + return f"{os.path.expanduser('~/.local/share')}/fonts" # noqa: PTH111 # API returns str, not Path + + @property + def user_preference_dir(self) -> str: + """:returns: preference directory tied to the user, same as ``user_config_dir``""" + return self.user_config_dir + @property def user_bin_dir(self) -> str: """:returns: bin directory tied to the user, e.g. ``~/.local/bin``""" @@ -199,12 +224,14 @@ class _UnixDefaults(PlatformDirsABC): # noqa: PLR0904 def iter_config_dirs(self) -> Iterator[str]: """:yield: all user and site configuration directories.""" - yield self.user_config_dir + if not self._use_site: + yield self.user_config_dir yield from self._site_config_dirs def iter_data_dirs(self) -> Iterator[str]: """:yield: all user and site data directories.""" - yield self.user_data_dir + if not self._use_site: + yield self.user_data_dir yield from self._site_data_dirs diff --git a/libs/platformdirs/version.py b/libs/platformdirs/version.py index 190c600dc..a281775fd 100644 --- a/libs/platformdirs/version.py +++ b/libs/platformdirs/version.py @@ -1,5 +1,6 @@ -# file generated by setuptools-scm +# file generated by vcs-versioning # don't change, don't track in version control +from __future__ import annotations __all__ = [ "__version__", @@ -10,25 +11,14 @@ __all__ = [ "commit_id", ] -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] - COMMIT_ID = Union[str, None] -else: - VERSION_TUPLE = object - COMMIT_ID = object - version: str __version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE -commit_id: COMMIT_ID -__commit_id__: COMMIT_ID +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None -__version__ = version = '4.9.4' -__version_tuple__ = version_tuple = (4, 9, 4) +__version__ = version = '4.10.0' +__version_tuple__ = version_tuple = (4, 10, 0) __commit_id__ = commit_id = None diff --git a/libs/platformdirs/windows.py b/libs/platformdirs/windows.py index 47403dea5..476ffe736 100644 --- a/libs/platformdirs/windows.py +++ b/libs/platformdirs/windows.py @@ -4,6 +4,7 @@ from __future__ import annotations import os import sys +from pathlib import Path from typing import TYPE_CHECKING, Final from .api import PlatformDirsABC @@ -133,6 +134,31 @@ class Windows(PlatformDirsABC): # noqa: PLR0904 r""":returns: desktop directory tied to the user, e.g. ``%USERPROFILE%\Desktop``""" return os.path.normpath(get_win_folder("CSIDL_DESKTOPDIRECTORY")) + @property + def user_projects_dir(self) -> str: + r""":returns: projects directory tied to the user, e.g. ``%USERPROFILE%\Projects``""" + return os.path.normpath(os.path.expanduser("~/Projects")) # noqa: PTH111 + + @property + def user_publicshare_dir(self) -> str: + r""":returns: public share directory e.g. ``C:\Users\Public``""" + return os.path.normpath(os.environ.get("PUBLIC", str(Path("~").expanduser().parent / "Public"))) + + @property + def user_templates_dir(self) -> str: + r""":returns: templates directory tied to the user e.g. ``%APPDATA%\Microsoft\Windows\Templates``""" + return os.path.normpath(str(Path(get_win_folder("CSIDL_APPDATA")) / "Microsoft" / "Windows" / "Templates")) + + @property + def user_fonts_dir(self) -> str: + r""":returns: fonts directory tied to the user e.g. ``%LOCALAPPDATA%\Microsoft\Windows\Fonts``""" + return os.path.normpath(str(Path(get_win_folder("CSIDL_LOCAL_APPDATA")) / "Microsoft" / "Windows" / "Fonts")) + + @property + def user_preference_dir(self) -> str: + r""":returns: preference directory tied to the user, same as ``user_config_dir``""" + return self.user_config_dir + @property def user_bin_dir(self) -> str: r""":returns: bin directory tied to the user, e.g. ``%LOCALAPPDATA%\Programs``""" diff --git a/libs/pygments-2.19.2.dist-info/RECORD b/libs/pygments-2.19.2.dist-info/RECORD deleted file mode 100644 index a7b490ad4..000000000 --- a/libs/pygments-2.19.2.dist-info/RECORD +++ /dev/null @@ -1,347 +0,0 @@ -../../bin/pygmentize,sha256=fEHn7Q2L56HE6EpmQFzaq1L3_hYLGbRUcUwIerLenIY,191 -pygments-2.19.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -pygments-2.19.2.dist-info/METADATA,sha256=euEA1n1nAGxkeYA92DX89HqbWfrHlEQeqOZqp_WYTYI,2512 -pygments-2.19.2.dist-info/RECORD,, -pygments-2.19.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -pygments-2.19.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 -pygments-2.19.2.dist-info/entry_points.txt,sha256=uUXw-XhMKBEX4pWcCtpuTTnPhL3h7OEE2jWi51VQsa8,53 -pygments-2.19.2.dist-info/licenses/AUTHORS,sha256=BmDjGKbyFYAq3Icxq4XQxl_yfPzKP10oWX8wZHYZW9k,10824 -pygments-2.19.2.dist-info/licenses/LICENSE,sha256=qdZvHVJt8C4p3Oc0NtNOVuhjL0bCdbvf_HBWnogvnxc,1331 -pygments/__init__.py,sha256=_3UT86TGpHuW8FekdZ8uLidEZH1NhmcLiOy2KKNPCt4,2959 -pygments/__main__.py,sha256=p8AJyoyCOMYGvzWHdnq0_A9qaaVqaj02nIu3xhJp1_4,348 -pygments/cmdline.py,sha256=4pL9Kpn2PUEKPobgrsQgg-vCx2NjsrapKzQ6LxQR7Q0,23536 -pygments/console.py,sha256=AagDWqwea2yBWf10KC9ptBgMpMjxKp8yABAmh-NQOVk,1718 -pygments/filter.py,sha256=YLtpTnZiu07nY3oK9nfR6E9Y1FBHhP5PX8gvkJWcfag,1910 -pygments/filters/__init__.py,sha256=B00KqPCQh5E0XhzaDK74Qa1E4fDSTlD6b0Pvr1v-vEQ,40344 -pygments/formatter.py,sha256=H_4J-moKkKfRWUOW9J0u7hhw6n1LiO-2Xu1q2B0sE5w,4366 -pygments/formatters/__init__.py,sha256=7OuvmoYLyoPzoOQV_brHG8GSKYB_wjFSkAQng6x2y9g,5349 -pygments/formatters/_mapping.py,sha256=1Cw37FuQlNacnxRKmtlPX4nyLoX9_ttko5ZwscNUZZ4,4176 -pygments/formatters/bbcode.py,sha256=s0Ka35OKuIchoSgEAGf6rj0rl2a9ym9L31JVNSRbZFQ,3296 -pygments/formatters/groff.py,sha256=pLcIHj4jJS_lRAVFnyJODKDu1Xlyl9_AEIdOtbl3DT0,5082 -pygments/formatters/html.py,sha256=FrHJ69FUliEyPY0zTfab0C1gPf7LXsKgeRlhwkniqIs,35953 -pygments/formatters/img.py,sha256=aRpFo8mBmWTL3sBUjRCWkeS3rc6FZrSFC4EksDrl53g,23301 -pygments/formatters/irc.py,sha256=R0Js0TYWySlI2yE9sW6tN4d4X-x3k9ZmudsijGPnLmU,4945 -pygments/formatters/latex.py,sha256=BRYtbLeW_YD1kwhhnFInhJIKylurnri8CF1lP069KWE,19258 -pygments/formatters/other.py,sha256=8pYW27sU_7XicLUqOEt2yWSO0h1IEUM3TIv34KODLwo,4986 -pygments/formatters/pangomarkup.py,sha256=pcFvEC7K1Me0EjGeOZth4oCnEY85bfqc77XzZASEPpY,2206 -pygments/formatters/rtf.py,sha256=kcKMCxTXu-2-hpgEftlGJRm7Ss-yA_Sy8OsHH_qzykA,11921 -pygments/formatters/svg.py,sha256=R6A2ME6JsMQWFiyn8wcKwFUOD6vsu-HLwiIztLu-77E,7138 -pygments/formatters/terminal.py,sha256=J_F_dFXwR9LHWvatIDnwqRYJyjVmSo1Zx8K_XDh6SyM,4626 -pygments/formatters/terminal256.py,sha256=7GQFLE5cfmeu53CAzANO74-kBk2BFkXfn5phmZjYkhM,11717 -pygments/lexer.py,sha256=ib-F_0GxHkwGpb6vWP0DeLMLc7EYgjo3hWFKN5IgOq0,35109 -pygments/lexers/__init__.py,sha256=6YhzxGKlWk38P6JpIJUQ1rVvV0DEZjEmdYsdMQ58hSk,12067 -pygments/lexers/_ada_builtins.py,sha256=CA_OnShtdc7wWh9oYcRlcrkDAQwYUKl6w7tdSbALQd4,1543 -pygments/lexers/_asy_builtins.py,sha256=cd9M00YH19w5ZL7aqucmC3nwpJGTS04U-01NLy5E2_4,27287 -pygments/lexers/_cl_builtins.py,sha256=kQeUIyZjP4kX0frkICDcKxBYQCLqzIDXa5WV5cevhDo,13994 -pygments/lexers/_cocoa_builtins.py,sha256=Ka1lLJe7JfWtdho4IFIB82X9yBvrbfHCCmEG-peXXhQ,105173 -pygments/lexers/_csound_builtins.py,sha256=qnQYKeI26ZHim316uqy_hDiRiCoHo2RHjD3sYBALyXs,18414 -pygments/lexers/_css_builtins.py,sha256=aD-dhLFXVd1Atn_bZd7gEdQn7Mhe60_VHpvZ340WzDI,12446 -pygments/lexers/_googlesql_builtins.py,sha256=IkrOk-T2v1yzbGzUEEQh5_Cf4uC_cmL_uuhwDpZlTug,16132 -pygments/lexers/_julia_builtins.py,sha256=N2WdSw5zgI2fhDat_i4YeVqurRTC_P8x71ez00SCN6U,11883 -pygments/lexers/_lasso_builtins.py,sha256=8q1gbsrMJeaeUhxIYKhaOxC9j_B-NBpq_XFj2Ze41X0,134510 -pygments/lexers/_lilypond_builtins.py,sha256=XTbGL1z1oKMoqWLEktG33jx5GdGTI9CpeO5NheEi4Y0,108094 -pygments/lexers/_lua_builtins.py,sha256=PhFdZV5-Tzz2j_q4lvG9lr84ELGfL41BhnrSDNNTaG4,8108 -pygments/lexers/_luau_builtins.py,sha256=-IDrU04kUVfjXwSQzMMpXmMYhNsQxZVVZk8cuAA0Lo0,955 -pygments/lexers/_mapping.py,sha256=9fv7xYOUAOr6LzfdFS4MDbPu78o4OQQH-2nsI1bNZf4,70438 -pygments/lexers/_mql_builtins.py,sha256=ybRQjlb7Cul0sDstnzxJl3h0qS6Ieqsr811fqrxyumU,24713 -pygments/lexers/_mysql_builtins.py,sha256=y0kAWZVAs0z2dTFJJV42OZpILgRnd8T3zSlBFv-g_oA,25838 -pygments/lexers/_openedge_builtins.py,sha256=Sz4j9-CPWIaxMa-2fZgY66j7igcu1ob1GR2UtI8zAkg,49398 -pygments/lexers/_php_builtins.py,sha256=Jd4BZpjMDELPi4EVoSxK1-8BFTc63HUwYfm1rLrGj0M,107922 -pygments/lexers/_postgres_builtins.py,sha256=Pqh4z0RBRbnW6rCQtWUdzWCJxNyqpJ7_0HOktxHDxk4,13343 -pygments/lexers/_qlik_builtins.py,sha256=xuJy9c9uZDXv6h8z582P5PrxqkxTZ_nS8gPl9OD9VN8,12595 -pygments/lexers/_scheme_builtins.py,sha256=2hNtJOJmP21lUsikpqMJ2gAmLT3Rwn_KEeqhXwCjgfk,32564 -pygments/lexers/_scilab_builtins.py,sha256=oZYPB1XPdIEz3pII11pFDe6extRRyWGA7pY06X8KZ8w,52411 -pygments/lexers/_sourcemod_builtins.py,sha256=H8AFLsNDdEpymIWOpDwbDJGCP1w-x-1gSlzPDioMF4o,26777 -pygments/lexers/_sql_builtins.py,sha256=oe8F9wWuO2iS6nEsZAdJtCUChBTjgM1Sq_aipu74jXM,6767 -pygments/lexers/_stan_builtins.py,sha256=dwi1hllM_NsaCv-aXJy7lEi57X5Hh5gSD97aCQyT9KM,13445 -pygments/lexers/_stata_builtins.py,sha256=Hqrr6j77zWU3cGGpBPohwexZci43YA4_sVYE4E1sNow,27227 -pygments/lexers/_tsql_builtins.py,sha256=Pi2RhTXcLE3glI9oxNhyVsOMn-fK_1TRxJ-EsYP5LcI,15460 -pygments/lexers/_usd_builtins.py,sha256=c9hbU1cwqBUCFIhNfu_Dob8ywv1rlPhi9w2OTj3kR8s,1658 -pygments/lexers/_vbscript_builtins.py,sha256=MqJ2ABywD21aSRtWYZRG64CCbGstC1kfsiHGJmZzxiw,4225 -pygments/lexers/_vim_builtins.py,sha256=bA4mH8t1mPPQfEiUCKEqRO1O0rL2DUG0Ux1Bt8ZSu0E,57066 -pygments/lexers/actionscript.py,sha256=JBngCe5UhYT_0dLD2j7PnPO0xRRJhmypEuQ-C5in8pY,11727 -pygments/lexers/ada.py,sha256=58k5ra1vGS4iLpW3h1ItY9ftzF3WevaeAAXzAYTiYkQ,5353 -pygments/lexers/agile.py,sha256=DN-7AVIqtG1MshA94rtSGYI_884hVHgzq405wD0_dl8,896 -pygments/lexers/algebra.py,sha256=yGTu9Tt-cQzAISQYIC5MS5a3z4QmL-tGcXnd_pkWGbk,9952 -pygments/lexers/ambient.py,sha256=UnzKpIlfSm3iitHvMd7XTMSY8TjZYYhKOC3AiARS_cE,2605 -pygments/lexers/amdgpu.py,sha256=S8qjn2UMLhBFm3Yn_c06XAGf8cl5x_ZeluelWG_-JAw,1723 -pygments/lexers/ampl.py,sha256=ZBRfDXm760gR1a1gqItnsHuoO3JdUcTBjJ5tFY9UtPA,4176 -pygments/lexers/apdlexer.py,sha256=Zr5-jgjxC8PKzRlEeclakZXPHci7FHBZghQ6wwiuT7A,30800 -pygments/lexers/apl.py,sha256=PTQMp-bxT5P-DbrEvFha10HBTcsDJ5srL3I1s9ljz58,3404 -pygments/lexers/archetype.py,sha256=pQVlP1Fb5OA8nn7QwmFaaaOSvvpoIsQVw43FVCQCve4,11538 -pygments/lexers/arrow.py,sha256=2PKdbWq3xQLF1KoDbWvSxpjwKRrznnDiArTflRGZzBo,3564 -pygments/lexers/arturo.py,sha256=U5MtRNHJtnBn4ZOeWmW6MKlVRG7SX6KhTRamDqzn9tA,11414 -pygments/lexers/asc.py,sha256=-DgZl9jccBDHPlDmjCsrEqx0-Q7ap7XVdNKtxLNWG1w,1693 -pygments/lexers/asm.py,sha256=xm2Y5mcT-sF3oQvair4SWs9EWTyndoaUoSsDy5v6shI,41967 -pygments/lexers/asn1.py,sha256=BlcloIX2bu6Q7BxGcksuhYFHGsXLVKyB4B9mFd4Pj6E,4262 -pygments/lexers/automation.py,sha256=Q61qon8EwpfakMh_2MS2E2zUUT16rG3UNIKPYjITeTs,19831 -pygments/lexers/bare.py,sha256=tWoei86JJX1k-ADhaXd5TgX6ItDTici9yFWpkTPhnfM,3020 -pygments/lexers/basic.py,sha256=qpVe5h8Fa7NJo1EihN-4R_UZpHO6my2Ssgkb-BktkKs,27989 -pygments/lexers/bdd.py,sha256=yysefcOFAEyk9kJ2y4EXmzJTecgLYUHlWixt_3YzPMU,1641 -pygments/lexers/berry.py,sha256=zxGowFb8HMIyN15-m8nmWnW6bPRR4esKtSEVugc9uXM,3209 -pygments/lexers/bibtex.py,sha256=yuNoPxwrJf9DCGUT17hxfDzbq_HtCLkQkRbBtiTVmeQ,4811 -pygments/lexers/blueprint.py,sha256=NzvWHMxCLDWt8hc6gB5jokltxVJgNa7Jwh4c61ng388,6188 -pygments/lexers/boa.py,sha256=dOot1XWNZThPIio2UyAX67K6EpISjSRCFjotD7dcnwE,3921 -pygments/lexers/bqn.py,sha256=nJiwrPKKbRF-qdai5tfqipwBkkko2P3weiZAjHUMimY,3671 -pygments/lexers/business.py,sha256=lRtekOJfsDkb12AGbuz10-G67OJrVJgCBtihTQ8_aoY,28345 -pygments/lexers/c_cpp.py,sha256=D7ZIswaHASlGBgoTlwnSqTQHf8_JyvvSt2L2q1W-F6g,18059 -pygments/lexers/c_like.py,sha256=FTGp17ds6X2rDZOHup2hH6BEn3gKK4nLm9pydNEhm0E,32021 -pygments/lexers/capnproto.py,sha256=XQJAh1WS-0ulqbTn9TdzR6gEgWLcuBqb4sj3jNsrhsY,2174 -pygments/lexers/carbon.py,sha256=av12YuTGZGpOa1Cmxp3lppx3LfSJUWbvOu0ixmUVll0,3211 -pygments/lexers/cddl.py,sha256=MKa70IwABgjBjYu15_Q9v8rsu2sr1a-i2jkiaPTI6sM,5076 -pygments/lexers/chapel.py,sha256=0n_fL3ehLC4pw4YKnmq9jxIXOJcxGPka1Wr1t1zsXPc,5156 -pygments/lexers/clean.py,sha256=dkDPAwF5BTALPeuKFoRKOSD3RfsKcGWbaRo6_G8LHng,6418 -pygments/lexers/codeql.py,sha256=ebvghn2zbrnETV4buVozMDmRCVKSdGiIN8ycLlHpGsE,2576 -pygments/lexers/comal.py,sha256=TC3NzcJ58ew5jw7qwK0kJ-okTA47psZje0yAIS39HR4,3179 -pygments/lexers/compiled.py,sha256=Slfo1sjWqcPawUwf0dIIZLBCL5pkOIoAX2S8Lxs02Mc,1426 -pygments/lexers/configs.py,sha256=wW8pY0Sa5a10pnAeTLGf48HhixQTVageIyHEf1aYMCc,50913 -pygments/lexers/console.py,sha256=-jAG120dupvV3kG3zC70brLJvSLwTFqMubBQuj_GVnU,4180 -pygments/lexers/cplint.py,sha256=DkbyE5EKydLgf6BRr1FhQrK-IeQPL7Zmjk0DVdlRFnQ,1389 -pygments/lexers/crystal.py,sha256=xU-RnpIkpjrquoxtOuOcP8fcesSJl4xhU7kO9m42LZY,15754 -pygments/lexers/csound.py,sha256=ioSw4Q04wdwjUAbnTZ1qLhUq1vxdWFxhh3QtEl5RAJc,16998 -pygments/lexers/css.py,sha256=JN1RBYsee-jrpHWrSmhN3TKc4TkOBn-_BEGpgTCzcqE,25376 -pygments/lexers/d.py,sha256=piOy0EJeiAwPHugiM3gVv0z7HNh3u2gZQoCUSASRbY4,9920 -pygments/lexers/dalvik.py,sha256=deFg2JPBktJ9mEGb9EgxNkmd6vaMjJFQVzUHo8NKIa8,4606 -pygments/lexers/data.py,sha256=o0x0SmB5ms_CPUPljEEEenOON4IQWn86DkwFjkJYCOg,27026 -pygments/lexers/dax.py,sha256=ASi73qmr7OA7cVZXF2GTYGt01Ly1vY8CgD_Pnpm8k-4,8098 -pygments/lexers/devicetree.py,sha256=RecSQCidt8DRE1QFCPUbwwR0hiRlNtsFihdGldeUn3k,4019 -pygments/lexers/diff.py,sha256=F6vxZ64wm5Nag_97de1H_3F700ZwCVnYjKvtT5jilww,5382 -pygments/lexers/dns.py,sha256=Hh5hJ7MXfrq36KgfyIRwK3X8o1LdR98IKERcV4eZ7HY,3891 -pygments/lexers/dotnet.py,sha256=NDE0kOmpe96GLO-zwNLazmj77E9ORGmKpa4ZMCXDXxQ,39441 -pygments/lexers/dsls.py,sha256=GnHKhGL5GxsRFnqC7-65NTPZLOZdmnllNrGP86x_fQE,36746 -pygments/lexers/dylan.py,sha256=7zZ1EbHWXeVHqTD36AqykKqo3fhuIh4sM-whcxUaH_Y,10409 -pygments/lexers/ecl.py,sha256=vhmpa2LBrHxsPkYcf3kPZ1ItVaLRDTebi186wY0xGZA,6371 -pygments/lexers/eiffel.py,sha256=5ydYIEFcgcMoEj4BlK31hZ0aJb8OX0RdAvuCNdlxwqw,2690 -pygments/lexers/elm.py,sha256=uRCddU8jK5vVkH6Y66y8KOsDJprIfrOgeYq3hv1PxAM,3152 -pygments/lexers/elpi.py,sha256=O9j_WKBPyvNFjCRuPciVpW4etVSnILm_T79BhCPZYmo,6877 -pygments/lexers/email.py,sha256=ZZL6yvwCRl1CEQyysuOu0lbabp5tjMutS7f3efFKGR4,4804 -pygments/lexers/erlang.py,sha256=bU11eVHvooLwmVknzN6Xkb2DMk7HbenqdNlYSzhThDM,19147 -pygments/lexers/esoteric.py,sha256=Jfp8UUKyKYsqLaqXRZT3GSM9dzkF65zduwfnH1GoGhU,10500 -pygments/lexers/ezhil.py,sha256=22r-xjvvBVpExTqCI-HycAwunDb1p5gY4tIfDmM0vDw,3272 -pygments/lexers/factor.py,sha256=urZ4En4uKFCLXdEkXLWg9EYUFGHQTTDCwNXtyq-ngok,19530 -pygments/lexers/fantom.py,sha256=JJ13-NwykD-iIESnuzCefCYeQDO95cHMJA8TasF4gHA,10231 -pygments/lexers/felix.py,sha256=F-v0si4zPtRelqzDQWXI1-tarCE-BvawziODxRU7378,9655 -pygments/lexers/fift.py,sha256=rOCwp3v5ocK5YOWvt7Td3Md--97_8e-7Sonx52uS8mA,1644 -pygments/lexers/floscript.py,sha256=aHh82k52jMuDuzl9LatrcSANJiXTCyjGU3SO53bwbb0,2667 -pygments/lexers/forth.py,sha256=ZMtsHdNbnS_0IdSYlfAlfTSPEr0MEsRo-YZriQNueTQ,7193 -pygments/lexers/fortran.py,sha256=1PE5dTxf4Df6LUeXFcmNtyeXWsC8tSiK5dYwPHIJeeQ,10382 -pygments/lexers/foxpro.py,sha256=CBkW62Fuibz3yfyelZCaEO8GGdFJWsuRhqwtsSeBwLM,26295 -pygments/lexers/freefem.py,sha256=LFBQk-m1-nNCgrl-VDH3QwnVWurvb7W29i06LoT207A,26913 -pygments/lexers/func.py,sha256=OR2rkM7gf9fKvad5WcFQln-_U_pb-RUCM9eQatToF4A,3700 -pygments/lexers/functional.py,sha256=fYT2AGZ642cRkIAId0rnXFBsx1c8LLEDRN_VuCEkUyM,693 -pygments/lexers/futhark.py,sha256=Vf1i4t-tR3zqaktVjhTzFNg_ts_9CcyA4ZDfDizbCmk,3743 -pygments/lexers/gcodelexer.py,sha256=4Xs9ax4-JZGupW_qSnHon39wQGpb-tNA3xorMKg841E,874 -pygments/lexers/gdscript.py,sha256=Ws7JKxy0M0IyZ_1iMfRvJPrizEwmeCNLDoeMIFaM-CU,7566 -pygments/lexers/gleam.py,sha256=XIlTcq6cB743pCqbNYo8PocSkjZyDPR6hHgdaJNJ1Vc,2392 -pygments/lexers/go.py,sha256=4LezefgyuqZWHzLZHieUkKTi-ssY6aHJxx7Z-LFaLK0,3783 -pygments/lexers/grammar_notation.py,sha256=LvzhRQHgwZzq9oceukZS_hwnKK58ee7Z5d0cwXOR734,8043 -pygments/lexers/graph.py,sha256=WFqoPA1c_hHYrV0i_F7-eUw3Co4_HmZY3GJ-TyDr670,4108 -pygments/lexers/graphics.py,sha256=tmF9NNALnvPnax8ywYC3pLOla45YXtp9UA0H-5EiTQY,39145 -pygments/lexers/graphql.py,sha256=O_zcrGrBaDaKTlUoJGRruxqk7CJi-NR92Y0Cs-KkCvw,5601 -pygments/lexers/graphviz.py,sha256=mzdXOMpwz9_V-be1eTAMyhkKCBl6UxCIXuq6C2yrtsw,1934 -pygments/lexers/gsql.py,sha256=VPZk9sb26-DumRkWfEaSTeoc0lx5xt5n-6eDDLezMtc,3990 -pygments/lexers/hare.py,sha256=PGCOuILktJsmtTpCZZKkMFtObfJuBpei8HM8HHuq1Tw,2649 -pygments/lexers/haskell.py,sha256=MYr74-PAC8kGJRX-dZmvZsHTc7a2u6yFS2B19LfDD7g,33262 -pygments/lexers/haxe.py,sha256=WHCy_nrXHnfLITfbdp3Ji3lqQU4HAsTUpXsLCp2_4sk,30974 -pygments/lexers/hdl.py,sha256=MOWxhmAuE4Ei0CKDqqaON7T8tl43geancrNYM136Z0U,22738 -pygments/lexers/hexdump.py,sha256=1lj9oJ-KiZXSVYvTMfGmEAQzNEW08WlMcC2I5aYvHK4,3653 -pygments/lexers/html.py,sha256=MxYTI4EeT7QxoGleCAyQq-8n_Sgly6tD95H5zanCNmk,21977 -pygments/lexers/idl.py,sha256=rcihUAGhfuGEaSW6pgFq6NzplT_pv0DagUoefg4zAmk,15449 -pygments/lexers/igor.py,sha256=wVefbUjb3ftaW3LCKGtX1JgLgiY4EmRor5gVOn8vQA8,31633 -pygments/lexers/inferno.py,sha256=ChE_5y5SLH_75Uv7D2dKWQMk2dlN6z1gY1IDjlJZ8rU,3135 -pygments/lexers/installers.py,sha256=ZHliit4Pxz1tYKOIjKkDXI5djTkpzYUMVIPR1xvUrL8,14435 -pygments/lexers/int_fiction.py,sha256=0ZzIa1sZDUQsltd1oHuS-BoNiOF8zKQfcVuDyK1Ttv8,56544 -pygments/lexers/iolang.py,sha256=L6dNDCLH0kxkIUi00fI4Z14QnRu79UcNDrgv02c5Zw8,1905 -pygments/lexers/j.py,sha256=DqNdwQGFLiZW3mCNLRg81gpmsy4Hgcai_9NP3LbWhNU,4853 -pygments/lexers/javascript.py,sha256=TGKQLSrCprCKfhLLGAq_0EOdvqvJKX9pOdKo7tCRurQ,63243 -pygments/lexers/jmespath.py,sha256=R5yA5LJ2nTIaDwnFIpSNGAThd0sAYFccwawA9xBptlg,2082 -pygments/lexers/jslt.py,sha256=OeYQf8O2_9FCaf9W6Q3a7rPdAFLthePCtVSgCrOTcl8,3700 -pygments/lexers/json5.py,sha256=8JZbc8EiTEZdKaIdQg3hXEh0mHWSzPlwd473a0nUuT0,2502 -pygments/lexers/jsonnet.py,sha256=bx2G6J4tJqGrJV1PyZrIWzWHXcoefCX-4lIxxtbn2gw,5636 -pygments/lexers/jsx.py,sha256=wGsoGSB40qAJrVfXwRPtan7OcK0O87RVsHHk0m6gogk,2693 -pygments/lexers/julia.py,sha256=0ZDJ9X83V5GqJzA6T6p0TTN8WHy2JAjvu-FSBXvfXdc,11710 -pygments/lexers/jvm.py,sha256=Yt1iQ3QodXRY-x_HUOGedhyuBBHn5jYH-I8NzOzHTlE,72667 -pygments/lexers/kuin.py,sha256=3dKKJVJlskgrvMKv2tY9NOsFfDjyo-3MLcJ1lFKdXSg,11405 -pygments/lexers/kusto.py,sha256=kaxkoPpEBDsBTCvCOkZZx7oGfv0jk_UNIRIRbfVAsBE,3477 -pygments/lexers/ldap.py,sha256=77vF4t_19x9V522cxRCM5d3HW8Ne3giYsFsMPVYYBw4,6551 -pygments/lexers/lean.py,sha256=7HWRgxFsxS1N9XKqw0vfKwaxl27s5YiVYtZeRUoTHFo,8570 -pygments/lexers/lilypond.py,sha256=yd2Tuv67um6EyCIr-VwBnlPhTHxMaQsBJ4nGgO5fjIk,9752 -pygments/lexers/lisp.py,sha256=EHUy1g4pzEsYPE-zGj2rAXm3YATE1j9dCQOr5-JPSkU,157668 -pygments/lexers/macaulay2.py,sha256=zkV-vxjQYa0Jj9TGfFP1iMgpTZ4ApQuAAIdJVGWb2is,33366 -pygments/lexers/make.py,sha256=YMI5DBCrxWca-pz9cVXcyfuHLcikPx9R_3pW_98Myqo,7831 -pygments/lexers/maple.py,sha256=Rs0dEmOMD3C1YQPd0mntN-vzReq4XfHegH6xV4lvJWo,7960 -pygments/lexers/markup.py,sha256=zWtxsyIx_1OxQzS6wLe8bEqglePv4RqvJjbia8AvV5c,65088 -pygments/lexers/math.py,sha256=P3ZK1ePd8ZnLdlmHezo2irCA8T2-nlHBoSaBoT5mEVI,695 -pygments/lexers/matlab.py,sha256=F9KO4qowIhfP8oVhCRRzE_1sqg4zmQbsB2NZH193PiM,133027 -pygments/lexers/maxima.py,sha256=a0h9Ggs9JEovTrzbJT-BLVbOqI29yPnaMZlkU5f_FeY,2715 -pygments/lexers/meson.py,sha256=BMrsDo6BH2lzTFw7JDwQ9SDNMTrRkXCNRDVf4aFHdsI,4336 -pygments/lexers/mime.py,sha256=yGrf3h37LK4b6ERBpFiL_qzn3JgOfGR5KLagnbWFl6c,7582 -pygments/lexers/minecraft.py,sha256=Nu88snDDPzM0D-742fFdUriczL-EE911pAd4_I4-pAw,13696 -pygments/lexers/mips.py,sha256=STKiZT67b3QERXXn7XKVxlPBu7vwbPC5EyCpuf3Jfbw,4656 -pygments/lexers/ml.py,sha256=t8sCv4BjvuBq6AihKKUwStEONIgdXCC2RMtO0RopNbM,35390 -pygments/lexers/modeling.py,sha256=M7B58bGB-Zwd1EmPxKqtRvg7TgNCyem3MVUHv0_H2SQ,13683 -pygments/lexers/modula2.py,sha256=NtpXBRoUCeHfflgB39LknSkCwhBHBKv2Er_pinjVsNE,53072 -pygments/lexers/mojo.py,sha256=8JRVoftN1E-W2woG0K-4n8PQXTUM9iY6Sl5sWb2uGNg,24233 -pygments/lexers/monte.py,sha256=baWU6zlXloenw9MO1MtEVGE9i3CfiXAYhqU621MIjRk,6289 -pygments/lexers/mosel.py,sha256=gjRdedhA1jTjoYoM1Gpaoog_I9o7TRbYMHk97N1TXwg,9297 -pygments/lexers/ncl.py,sha256=zJ6ahlitit4S0pBXc7Wu96PB7xOn59MwfR2HdY5_C60,63999 -pygments/lexers/nimrod.py,sha256=Q1NSqEkLC5wWt7xJyKC-vzWw_Iw2SfDNP_pyMFBuIfA,6413 -pygments/lexers/nit.py,sha256=p_hVD8GzMRl3CABVKHtYgnXFUQk0i5F2FbWFA6WXm6s,2725 -pygments/lexers/nix.py,sha256=NOrv20gdq-2A7eZ6c2gElPHv1Xx2pvv20-qOymL9GMg,4421 -pygments/lexers/numbair.py,sha256=fxkp2CXeXWKBMewfi1H4JSYkmm4kU58wZ2Sh9BDYAWQ,1758 -pygments/lexers/oberon.py,sha256=jw403qUUs7zpTHAs5CbLjb8qiuwtxLk0spDIYqGZwAw,4210 -pygments/lexers/objective.py,sha256=Fo1WB3JMj8sNeYnvB84H4_qwhOt4WNJtJWjVEOwrJGk,23297 -pygments/lexers/ooc.py,sha256=kD1XaJZaihDF_s-Vyu1Bx68S_9zFt2rhox7NF8LpOZM,3002 -pygments/lexers/openscad.py,sha256=h9I1k8kiuQmhX5vZm6VDSr2fa5Finy0sN8ZDIE-jx1c,3700 -pygments/lexers/other.py,sha256=WLVyqPsvm9oSXIbZwbfyJloS6HGgoFW5nVTaU1uQpTw,1763 -pygments/lexers/parasail.py,sha256=DWMGhtyQgGTXbIgQl_mID6CKqi-Dhbvs_dTkmvrZXfE,2719 -pygments/lexers/parsers.py,sha256=feNgxroPoWRf0NEsON2mtmKDUfslIQppukw6ndEsQ3M,26596 -pygments/lexers/pascal.py,sha256=N2tRAjlXnTxggAzzk2tOOAVzeC2MBzrXy97_HQl5n44,30989 -pygments/lexers/pawn.py,sha256=LWUYQYsebMMt2d5oxX1HYWvBqbakR1h7Av_z8Vw94Wg,8253 -pygments/lexers/pddl.py,sha256=Mk4_BzlROJCd0xR4KKRRSrbj0F7LLQcBRjmsmtWmrCg,2989 -pygments/lexers/perl.py,sha256=9BXn3tyHMA49NvzbM9E2czSCHjeU7bvaPLUcoZrhz-4,39192 -pygments/lexers/phix.py,sha256=hZqychqo5sFMBDESzDPXg1DYHQe_9sn294UfbjihaFk,23249 -pygments/lexers/php.py,sha256=l4hzQrlm0525i5dSw9Vmjcai3TzbPT6DkjzxPg9l6Zc,13061 -pygments/lexers/pointless.py,sha256=WSDjqQyGrNIGmTCdaMxl4zk7OZTlJAMzeUZ02kfgcTI,1974 -pygments/lexers/pony.py,sha256=EXrMkacqMZblI7v4AvBRQe-3Py8__bx5FOgjCLdfXxQ,3279 -pygments/lexers/praat.py,sha256=4UFK-nbC6WkZBhJgcQqEGqq9CocJkW7AmT_OJQbjWzk,12676 -pygments/lexers/procfile.py,sha256=05W2fyofLTP-FbEdSXD1eles-PPqVNfF6RWXjQdW2us,1155 -pygments/lexers/prolog.py,sha256=9Kc5YNUFqkfWu2sYoyzC3RX65abf1bm7oHr86z1s4kQ,12866 -pygments/lexers/promql.py,sha256=n-0vo-o8-ZasqP3Va4ujs562UfZSLfZF-RzT71yL0Tk,4738 -pygments/lexers/prql.py,sha256=PFReuvhbv4K5aeu6lvDfw4m-3hULkB3r43bKAy948os,8747 -pygments/lexers/ptx.py,sha256=KSHAvbiNVUntKilQ6EPYoLFocmJpRsBy_7fW6_Nrs1Y,4501 -pygments/lexers/python.py,sha256=WZe7fBAHKZ_BxPg8qIU26UGhk8qwUYyENJ3IyPW64mc,53805 -pygments/lexers/q.py,sha256=WQFUh3JrpK2j-VGW_Ytn3uJ5frUNmQIFnLtMVGRA9DI,6936 -pygments/lexers/qlik.py,sha256=2wqwdfIjrAz6RNBsP4MyeLX8Z7QpIGzxtf1CvaOlr_g,3693 -pygments/lexers/qvt.py,sha256=XMBnsWRrvCDf989OuDeb-KpszAkeETiACyaghZeL1ns,6103 -pygments/lexers/r.py,sha256=B6WgrD9SY1UTCV1fQBSlZbezPfpYsARn3FQIHcFYOiM,6474 -pygments/lexers/rdf.py,sha256=qUzxLna9v071bHhZAjdsBi8dKaJNk_h9g1ZRUAYCfoo,16056 -pygments/lexers/rebol.py,sha256=4u3N4kzui55HapopXDu3Kt0jczxDZ4buzwR7Mt4tQiM,18259 -pygments/lexers/rego.py,sha256=Rx5Gphbktr9ojg5DbqlyxHeQqqtF7g8W-oF0rmloDNY,1748 -pygments/lexers/resource.py,sha256=ioEzgWksB5HCjoz85XNkQPSd7n5kL0SZiuPkJP1hunQ,2927 -pygments/lexers/ride.py,sha256=kCWdxuR3PclVi4wiA0uUx4CYEFwuTqoMsKjhSW4X3yg,5035 -pygments/lexers/rita.py,sha256=Mj1QNxx1sWAZYC02kw8piVckaiw9B0MqQtiIiDFH0pA,1127 -pygments/lexers/rnc.py,sha256=g7ZD334PMGUqy_Ij64laSN1vJerwHqVkegfMCa3E-y8,1972 -pygments/lexers/roboconf.py,sha256=HbYuK5CqmQdd63SRY2nle01r7-p7mil0SnoauYDmEOY,2074 -pygments/lexers/robotframework.py,sha256=c4U1B9Q9ITBCTohqJTZOvkfyeVbenN4xhzSWIoZh5eU,18448 -pygments/lexers/ruby.py,sha256=uG617E5abBZcECRCqkhIfc-IbZcRb5cGuUZq_xpax90,22753 -pygments/lexers/rust.py,sha256=ZY-9vtsreBP0NfDd0WCouLSp_9MChAL8U8Abe-m9PB8,8260 -pygments/lexers/sas.py,sha256=C1Uz2s9DU6_s2kL-cB_PAGPtpyK5THlmhNmCumC1l48,9456 -pygments/lexers/savi.py,sha256=jrmruK0GnXktgBTWXW3oN3TXtofn3HBbkMlHnR84cko,4878 -pygments/lexers/scdoc.py,sha256=DXRmFDmYuc7h3gPAAVhfcL1OEbNBK5RdPpJqQzF3ZTk,2524 -pygments/lexers/scripting.py,sha256=eaYlkDK-_cAwTcCBHP6QXBCz8n6OzbhzdkRe0uV0xWY,81814 -pygments/lexers/sgf.py,sha256=w6C513ENaO2YCnqrduK7k03NaMDf-pgygvfzq2NaSRk,1985 -pygments/lexers/shell.py,sha256=dCS1zwkf5KwTog4__MnMC7h3Xmwv4_d3fnEV29tSwXI,36381 -pygments/lexers/sieve.py,sha256=eob-L84yf2jmhdNyYZUlbUJozdcd6GXcHW68lmAe8WE,2514 -pygments/lexers/slash.py,sha256=I-cRepmaxhL1SgYvD1hHX3gNBFI8NPszdU7hn1o5JlA,8484 -pygments/lexers/smalltalk.py,sha256=ue2PmqDK2sw0j75WdseiiENJBdZ1OwysH2Op1QN1r24,7204 -pygments/lexers/smithy.py,sha256=VREWoeuz7ANap_Uiopn7rs0Tnsfc-xBisDJKRGQY_y8,2659 -pygments/lexers/smv.py,sha256=He_VBSMbWONMWZmkrB5RYR0cfHVnMyKIXz68IFYl-a8,2805 -pygments/lexers/snobol.py,sha256=qDzb41xQQWMNmjB2MtZs23pFoFgZ2gbRZhK_Ir03r7I,2778 -pygments/lexers/solidity.py,sha256=Tixfnwku4Yezj6nNm8xVaw7EdV1qgAgdwahdTFP0St8,3163 -pygments/lexers/soong.py,sha256=Vm18vV4g6T8UPgjjY2yTRlSXGDpZowmuqQUBFfm4A9A,2339 -pygments/lexers/sophia.py,sha256=2YtYIT8iwAoW0B7TZuuoG_ZILhJV-2A7oBGat-98naE,3376 -pygments/lexers/special.py,sha256=8JuR2Vex8X-RWnC36S0HXTHWp2qmZclc90-TrLUWyaY,3585 -pygments/lexers/spice.py,sha256=m4nK0q4Sq_OFQez7kGWfki0No4ZV24YrONfHVj1Piqs,2790 -pygments/lexers/sql.py,sha256=WSG6vOsR87EEEwSQefP_Z7TauUG_BjqMHUFmPaSOVj4,41476 -pygments/lexers/srcinfo.py,sha256=B8vDs-sJogG3mWa5Hp_7JfHHUMyYRwGvKv6cKbFQXLM,1746 -pygments/lexers/stata.py,sha256=Zr9BC52D5O_3BbdW0N-tzoUmy0NTguL2sC-saXRVM-c,6415 -pygments/lexers/supercollider.py,sha256=_H5wDrn0DiGnlhB_cz6Rt_lo2TvqjSm0o6NPTd9R4Ko,3697 -pygments/lexers/tablegen.py,sha256=1JjedXYY18BNiY9JtNGLOtGfiwduNDZpQLBGTeQ6jAw,3987 -pygments/lexers/tact.py,sha256=X_lsxjFUMaC1TmYysXJq9tmAGifRnil83Bt1zA86Xdo,10809 -pygments/lexers/tal.py,sha256=xS9PlaWQOPj8MVr56fUNq31vUQKRWoLTlyWj9ZHm8AM,2904 -pygments/lexers/tcl.py,sha256=lK97ju4nikkt-oGOzIeyFEM98yq4dZSI8uEmYsq0R6c,5512 -pygments/lexers/teal.py,sha256=t3dqy_Arwv8_yExbX_xiFxv1TqJLPv4vh1MVKjKwS4Y,3522 -pygments/lexers/templates.py,sha256=BVdjYeoacIUuFyHTG39j4PxeNCe5E1oUURjH1rITrI4,75731 -pygments/lexers/teraterm.py,sha256=ciwztagW5Drg2gr17Qykrh6GwMsKy7e4xdQshX95GyQ,9718 -pygments/lexers/testing.py,sha256=YZgDgUEaLEYKSKEqpDsUi3Bn-Db_D42IlyiSsr1oX8U,10810 -pygments/lexers/text.py,sha256=nOCQPssIlKdVWU3PKxZiBPkf_KFM2V48IOssSyqhFY8,1068 -pygments/lexers/textedit.py,sha256=ttT4Ph-hIdgFLG6maRy_GskkziTFK0Wcg28yU0s6lek,7760 -pygments/lexers/textfmts.py,sha256=mi9KLEq4mrzDJbEc8G3VM-mSki_Tylkzodu47yH6z84,15524 -pygments/lexers/theorem.py,sha256=51ppBAEdhJmwU_lC916zMyjEoKLXqf89VAE_Lr0PNCc,17855 -pygments/lexers/thingsdb.py,sha256=x_fHNkLA-hIJyeIs6rg_X8n5OLYvFqaSu1FhI3apI5Y,6017 -pygments/lexers/tlb.py,sha256=ue2gqm45BI512lM13O8skAky9zAb7pLMrxZ8pbt5zRU,1450 -pygments/lexers/tls.py,sha256=_uQUVuMRDOhN-XUyGR5DIlVCk1CUZ1fIOSN4_WQYPKk,1540 -pygments/lexers/tnt.py,sha256=pK4LgoKON7u1xF66JYFncAPSbD8DZaeI_WTZ9HqEFlY,10456 -pygments/lexers/trafficscript.py,sha256=X3B8kgxS54ecuok9ic6Hkp-UMn5DvOmCK0p70Tz27Cw,1506 -pygments/lexers/typoscript.py,sha256=mBuePiVZUoAORPKsHwrx6fBWiy3fAIqG-2O67QmMiFI,8332 -pygments/lexers/typst.py,sha256=zIJBEhUXtWp5OiyAmvFA5m8d1EQG-ocwrJ677dvTUAk,7167 -pygments/lexers/ul4.py,sha256=rCaw0J9j3cdql9lX_HTilg65k9-9S118zOA6TAYfxaM,10499 -pygments/lexers/unicon.py,sha256=RAqoCnAAJBYOAGdR8ng0g6FtB39bGemLRlIqv5mcg9E,18625 -pygments/lexers/urbi.py,sha256=ajNP70NJg32jNnFDZsLvr_-4TToSGqRGkFyAPIJLfCU,6082 -pygments/lexers/usd.py,sha256=2eEGouolodYS402P_gtBrn4lLzpg1z8uHwPCKqjUb_k,3304 -pygments/lexers/varnish.py,sha256=dSh0Ku9SrjmlB29Fi_mWdWavN7M0cMKeepR4a34sOyI,7473 -pygments/lexers/verification.py,sha256=Qu433Q_h3EK3uS4bJoLRFZK0kIVwzX5AFKsa4Z-qnxA,3934 -pygments/lexers/verifpal.py,sha256=buyOOzCo_dGnoC40h0tthylHVVpgDt8qXu4olLvYy_4,2661 -pygments/lexers/vip.py,sha256=2lEV4cLV9p4E37wctBL7zkZ4ZU4p3HVsiLJFzB1bie0,5711 -pygments/lexers/vyper.py,sha256=Zq6sQIUBk6mBdpgOVgu3A6swGoBne0kDlRyjZznm2BY,5615 -pygments/lexers/web.py,sha256=4W9a7vcskrGJnxt4KmoE3SZydWB1qLq7lP2XS85J_m8,913 -pygments/lexers/webassembly.py,sha256=zgcMouzLawcbeFr6w_SOvGoUR68ZtqnnsbOcWEVleLk,5698 -pygments/lexers/webidl.py,sha256=ODtVmw4gVzI8HQWxuEckP6KMwm8WP2G2lSZEjagDXts,10516 -pygments/lexers/webmisc.py,sha256=-_-INDVdk47e2jlj-9bFcuLtntqVorBqIjlnwPfZFdI,40564 -pygments/lexers/wgsl.py,sha256=9igd9dzixGIgNewruv9mPnFms-c9BahkZcCCrZygv84,11880 -pygments/lexers/whiley.py,sha256=lMr750lA4MZsB4xqzVsIRtVMJIC3_dArhFYTHvOPwvA,4017 -pygments/lexers/wowtoc.py,sha256=8xxvf0xGeYtf4PE7KtkHZ_ly9xY_XXHrpCitdKE42Ro,4076 -pygments/lexers/wren.py,sha256=goGXnAMKKa13LLL40ybT3aMGPrk3gCRwZQFYAkKB_w0,3229 -pygments/lexers/x10.py,sha256=Q-AmgdF2E-N7mtOPpZ07CsxrTVnikyqC4uRRv6H75sk,1943 -pygments/lexers/xorg.py,sha256=9ttrBd3_Y2nXANsqtMposSgblYmMYqWXQ-Iz5RH9RsU,925 -pygments/lexers/yang.py,sha256=13CWbSaNr9giOHz4o0SXSklh0bfWt0ah14jJGpTvcn0,4499 -pygments/lexers/yara.py,sha256=jUSv78KTDfguCoAoAZKbYzQERkkyxBBWv5dInVrkDxo,2427 -pygments/lexers/zig.py,sha256=f-80MVOSp1KnczAMokQLVM-_wAEOD16EcGFnaCNlsN0,3976 -pygments/modeline.py,sha256=K5eSkR8GS1r5OkXXTHOcV0aM_6xpk9eWNEIAW-OOJ2g,1005 -pygments/plugin.py,sha256=tPx0rJCTIZ9ioRgLNYG4pifCbAwTRUZddvLw-NfAk2w,1891 -pygments/regexopt.py,sha256=wXaP9Gjp_hKAdnICqoDkRxAOQJSc4v3X6mcxx3z-TNs,3072 -pygments/scanner.py,sha256=nNcETRR1tRuiTaHmHSTTECVYFPcLf6mDZu1e4u91A9E,3092 -pygments/sphinxext.py,sha256=VEe_oHNgLoEGMHc2ROfbee2mF2PPREFyE6_m_JN5FvQ,7898 -pygments/style.py,sha256=Cpw9dCAyW3_JAwFRXOJXmtKb5ZwO2_5KSmlq6q4fZw4,6408 -pygments/styles/__init__.py,sha256=f9KCQXN4uKbe8aI8-L3qTC-_XPfT563FwTg6VTGVfwI,2006 -pygments/styles/_mapping.py,sha256=6lovFUE29tz6EsV3XYY4hgozJ7q1JL7cfO3UOlgnS8w,3312 -pygments/styles/abap.py,sha256=64Uwr8uPdEdcT-tE-Y2VveTXfH3SkqH9qdMgY49YHQI,749 -pygments/styles/algol.py,sha256=fCuk8ITTehvbJSufiaKlgnFsKbl-xFxxR82xhltc-cQ,2262 -pygments/styles/algol_nu.py,sha256=Gv9WfHJvYegGcUk1zcufQgsdXPNjCUNk8sAHyrSGGh4,2283 -pygments/styles/arduino.py,sha256=NoUB8xk7M1HGPoLfuySOLU0sVwoTuLcZqllXl2EO_iE,4557 -pygments/styles/autumn.py,sha256=fLLfjHXjxCl6crBAxEsBLH372ALMkFacA2bG6KFbJi4,2195 -pygments/styles/borland.py,sha256=_0ySKp4KGCSgtYjPe8uzD6gQhlmAIR4T43i-FoRYNOM,1611 -pygments/styles/bw.py,sha256=vhk8Xoj64fLPdA9IQU6mUVsYMel255jR-FDU7BjIHtI,1406 -pygments/styles/coffee.py,sha256=NqLt-fc7LONma1BGggbceVRY9uDE70WBuZXqK4zwaco,2308 -pygments/styles/colorful.py,sha256=mYcSbehtH7itH_QV9NqJp4Wna1X4lrwl2wkVXS2u-5A,2832 -pygments/styles/default.py,sha256=RTgG2zKWWUxPTDCFxhTnyZI_WZBIVgu5XsUpNvFisCA,2588 -pygments/styles/dracula.py,sha256=vRJmixBoSKV9o8NVQhXGViQqchhIYugfikLmvX0DoBw,2182 -pygments/styles/emacs.py,sha256=TiOG9oc83qToMCRMnJrXtWYqnzAqYycRz_50OoCKtxc,2535 -pygments/styles/friendly.py,sha256=oAi-l9anQTs9STDmUzXGDlOegatEOH4hpD0j6o6dZGM,2604 -pygments/styles/friendly_grayscale.py,sha256=a7Cqkzt6-uTiXvj6GoYBXzRvX5_zviCjjRB04Kf_-Q0,2828 -pygments/styles/fruity.py,sha256=GfSUTG0stlJr5Ow_saCaxbI2IB4-34Dp2TuRTpfUJBs,1324 -pygments/styles/gh_dark.py,sha256=ruNX3d4rf22rx-8HnwvGbNbXRQpXCNcHU1HNq6N4uNg,3590 -pygments/styles/gruvbox.py,sha256=KrFoHEoVnZW6XM9udyXncPomeGyZgIDsNWOH3kCrxFQ,3387 -pygments/styles/igor.py,sha256=fYYPhM0dRCvcDTMVrMVO5oFKnYm-8YVlsuVBoczFLtY,737 -pygments/styles/inkpot.py,sha256=jggSeX9NV15eOL2oJaVmZ6vmV7LWRzXJQRUqcWEqGRs,2404 -pygments/styles/lightbulb.py,sha256=Y8u1qdvlHfBqI2jJex55SkvVatVo_FjEUzE6h-X7m-0,3172 -pygments/styles/lilypond.py,sha256=Y6fp_sEL-zESmxAaMxzjtrKk90cuDC_DalNdC8wj0nw,2066 -pygments/styles/lovelace.py,sha256=cA9uhmbnzY04MccsiYSgMY7fvb4WMRbegWBUrGvXh1M,3178 -pygments/styles/manni.py,sha256=g9FyO7plTwfMm2cU4iiKgdlkMlvQLG6l2Lwkgz5ITS4,2443 -pygments/styles/material.py,sha256=LDmgomAbgtJDZhbv446_zIwgYh50UAqEEtgYNUns1rQ,4201 -pygments/styles/monokai.py,sha256=lrxTJpkBarV9gTLkBQryZ6oNSjekAVheJueKJP5iEYA,5184 -pygments/styles/murphy.py,sha256=-AKZiLkpiWej-otjHMsYCE-I-_IzCOLJY-_GBdKRZRw,2805 -pygments/styles/native.py,sha256=l6tezGSQTB8p_SyOXJ0PWI7KzCeEdtsPmVc4Yn4_CwU,2043 -pygments/styles/nord.py,sha256=GDt3WAaqaWsiCeqpIBPxd8TEUX708fGfwaA7S0w0oy0,5391 -pygments/styles/onedark.py,sha256=k80cZEppCEF-HLoxy_FEA0QmQDZze68nHVMNGyUVa28,1719 -pygments/styles/paraiso_dark.py,sha256=Jkrg4nUKIVNF8U4fPNV_Smq_g9NFbb9eiUrjYpVgQZg,5662 -pygments/styles/paraiso_light.py,sha256=MxN964ZEpze3wF0ss-igaa2I7E684MHe-Zq0rWPH3wo,5668 -pygments/styles/pastie.py,sha256=ZvAs9UpBNYFC-5PFrCRGYnm3FoPKb-eKR-ozbWZP-4g,2525 -pygments/styles/perldoc.py,sha256=HSxB93e4UpQkZspReQ34FeJbZ-59ksGvdaH-hToehi8,2230 -pygments/styles/rainbow_dash.py,sha256=4ugL18Or7aNtaLfPfCLFRiFy0Gu2RA4a9G2LQUE9SrM,2390 -pygments/styles/rrt.py,sha256=fgzfpC0PC_SCcLOMCNEIQTjPUMOncRe7SR10GfSRbXY,1006 -pygments/styles/sas.py,sha256=yzoXmbfQ2ND1WWq93b4vVGYkQSZHPqb4ymes9YYRT3w,1440 -pygments/styles/solarized.py,sha256=qupILFZn02WspnAF5SPYb-W8guo9xnUtjb1HeLw3XgE,4247 -pygments/styles/staroffice.py,sha256=CLbBeMoxay21Xyu3Af2p4xUXyG1_6ydCbvs5RJKYe5w,831 -pygments/styles/stata_dark.py,sha256=vX8SwHV__sG92F4CKribG08MJfSVq98dgs7gEA_n9yc,1257 -pygments/styles/stata_light.py,sha256=uV3GE-ylvffQ0yN3py1YAVqBB5wflIKZbceyK1Lqvrc,1289 -pygments/styles/tango.py,sha256=O2wcM4hHuU1Yt071M9CK7JPtiiSCqyxtT9tbiQICV28,7137 -pygments/styles/trac.py,sha256=9kMv1ZZyMKACWlx2fQVjRP0I2pgcRYCNrd7iGGZg9qk,1981 -pygments/styles/vim.py,sha256=J7_TqvrGkTX_XuTHW0In5wqPLAUPRWyr1122XueZWmM,2019 -pygments/styles/vs.py,sha256=s7YnzbIPuFU3LIke27mc4lAQSn2R3vbbHc1baMGSU_U,1130 -pygments/styles/xcode.py,sha256=PbQdzgGaA4a9LAU1i58alY9kM4IFlQX5jHQwOYmf_Rk,1504 -pygments/styles/zenburn.py,sha256=suZEKzBTCYdhf2cxNwcY7UATJK1tq5eYhGdBcXdf6MU,2203 -pygments/token.py,sha256=WbdWGhYm_Vosb0DDxW9lHNPgITXfWTsQmHt6cy9RbcM,6226 -pygments/unistring.py,sha256=al-_rBemRuGvinsrM6atNsHTmJ6DUbw24q2O2Ru1cBc,63208 -pygments/util.py,sha256=oRtSpiAo5jM9ulntkvVbgXUdiAW57jnuYGB7t9fYuhc,10031 diff --git a/libs/pygments-2.19.2.dist-info/INSTALLER b/libs/pygments-2.20.0.dist-info/INSTALLER similarity index 100% rename from libs/pygments-2.19.2.dist-info/INSTALLER rename to libs/pygments-2.20.0.dist-info/INSTALLER diff --git a/libs/pygments-2.19.2.dist-info/METADATA b/libs/pygments-2.20.0.dist-info/METADATA similarity index 91% rename from libs/pygments-2.19.2.dist-info/METADATA rename to libs/pygments-2.20.0.dist-info/METADATA index 2eff6a0c3..3dea62032 100644 --- a/libs/pygments-2.19.2.dist-info/METADATA +++ b/libs/pygments-2.20.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: Pygments -Version: 2.19.2 +Version: 2.20.0 Summary: Pygments is a syntax highlighting package written in Python. Project-URL: Homepage, https://pygments.org Project-URL: Documentation, https://pygments.org/docs @@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/pygments/pygments/blob/master/CHANGES Author-email: Georg Brandl Maintainer: Matthäus G. Chajdas Maintainer-email: Georg Brandl , Jean Abou Samra -License: BSD-2-Clause +License-Expression: BSD-2-Clause License-File: AUTHORS License-File: LICENSE Keywords: syntax highlighting @@ -18,21 +18,20 @@ Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Text Processing :: Filters Classifier: Topic :: Utilities -Requires-Python: >=3.8 +Requires-Python: >=3.9 Provides-Extra: plugins Provides-Extra: windows-terminal Requires-Dist: colorama>=0.4.6; extra == 'windows-terminal' @@ -54,5 +53,5 @@ are: formats that PIL supports and ANSI sequences * it is usable as a command-line tool and as a library -Copyright 2006-2025 by the Pygments team, see ``AUTHORS``. +Copyright 2006-present by the Pygments team, see ``AUTHORS``. Licensed under the BSD, see ``LICENSE`` for details. diff --git a/libs/pygments-2.20.0.dist-info/RECORD b/libs/pygments-2.20.0.dist-info/RECORD new file mode 100644 index 000000000..dd9090d43 --- /dev/null +++ b/libs/pygments-2.20.0.dist-info/RECORD @@ -0,0 +1,348 @@ +../../bin/pygmentize,sha256=fEHn7Q2L56HE6EpmQFzaq1L3_hYLGbRUcUwIerLenIY,191 +pygments-2.20.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pygments-2.20.0.dist-info/METADATA,sha256=4FKPUbMEJ_rpRyNmK6Yi-NjbKk2NPxNlaY1npSRQqEU,2476 +pygments-2.20.0.dist-info/RECORD,, +pygments-2.20.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pygments-2.20.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +pygments-2.20.0.dist-info/entry_points.txt,sha256=uUXw-XhMKBEX4pWcCtpuTTnPhL3h7OEE2jWi51VQsa8,53 +pygments-2.20.0.dist-info/licenses/AUTHORS,sha256=DbYDpfRJn2kMRCVHf_ZkwWbaMl06zDtajU3j2wckQ9A,10873 +pygments-2.20.0.dist-info/licenses/LICENSE,sha256=qdZvHVJt8C4p3Oc0NtNOVuhjL0bCdbvf_HBWnogvnxc,1331 +pygments/__init__.py,sha256=ZzpnXpvnv0c7r_OIS8h7UeaESqYNKMy__pV7KCiWXxw,2962 +pygments/__main__.py,sha256=QZWj0T6TTRsqr-w-0YvVILo1DyAvzzydGCRONwdEzqo,351 +pygments/cmdline.py,sha256=_dOnrta_2GIe8Jg-1Y0pb5vWi8L6QTiJzyoTuHrYrbM,23542 +pygments/console.py,sha256=C189JAwhC1Qh0AKgshzb1wVDzFqBnv1VZ45kTEDIO30,1721 +pygments/filter.py,sha256=1dnbkq2AdC3AkHt3DaXwnOkTBLChl1kR6naSLwnr_tc,1913 +pygments/filters/__init__.py,sha256=03ZYdIYmxnWCh7gNXD7lEQ869Df4kzHnOGM44iJxan8,40349 +pygments/formatter.py,sha256=PTBnTW0EHke2vzlAKuvHJkj3-_CMj8duIx-3yVUie-o,4369 +pygments/formatters/__init__.py,sha256=qPG4q5cuaZRGBglmEJVLP4SDv43QI3tAUskj30M-mOY,5352 +pygments/formatters/_mapping.py,sha256=1Cw37FuQlNacnxRKmtlPX4nyLoX9_ttko5ZwscNUZZ4,4176 +pygments/formatters/bbcode.py,sha256=lvG1REZJv0pM6VMe-QwLSuo0Yl1Ra_X51wh93fW8A2k,3299 +pygments/formatters/groff.py,sha256=anF3fNbDYwwOlppOUoKTZg4FzzPbZrE4jcJCMd49_1g,5085 +pygments/formatters/html.py,sha256=qe4P6qIV462HkZovS8s5xKKyYXPQQvUjz6Wj7HX1CxY,36053 +pygments/formatters/img.py,sha256=41uSY0pKg9VmKJYrHuavCVgg0z-mg81p28QEMktwf1o,23304 +pygments/formatters/irc.py,sha256=hdOqAvF02bI8CY8_tutnUptBZpWEBLzVAqS4_YA0tew,4907 +pygments/formatters/latex.py,sha256=cQmE1Nj4E9q5N0XQJBqpc6dLtu5UNOtYq3plPyCX1Hk,19261 +pygments/formatters/other.py,sha256=Hq6qY4POBZ_llAWRr1gzSO9UoeYPONlLMBK60RkW4bg,4989 +pygments/formatters/pangomarkup.py,sha256=L4jU6oO18UqEMqXeN5FkPtkisXrXuXq9wcecgy_6dzo,2209 +pygments/formatters/rtf.py,sha256=YQYW8NTrB4XfOjLbQzutyD4j35TimFNZ0T39dET9DOo,11924 +pygments/formatters/svg.py,sha256=oksXT-ZnTHDsn1WtWgBs9V7qmKFswAKx7jV37tb2tYY,7141 +pygments/formatters/terminal.py,sha256=q7jLLanle33eCZkDr4CoHAN-dHBFf1DBhi4FcBQ8_1E,4629 +pygments/formatters/terminal256.py,sha256=PpA_oATHCih3UTjrbfKqIRUcvsyp_TeBaGw3kpIxeBA,11717 +pygments/lexer.py,sha256=gNMYzmdSkTNyWfqiLJ37oUd1KrN_dMXtsPyaX2-n9EA,35154 +pygments/lexers/__init__.py,sha256=G4dtqE5QMEAqoaaD1rwSZZeuqKhMyS4utlMz1szkrTg,12070 +pygments/lexers/_ada_builtins.py,sha256=dZb-lodsSM6L5emLlgLg-rClm2HumvnHK72CetnSRdA,1546 +pygments/lexers/_asy_builtins.py,sha256=zg54fGhgzWXQUk6-qZ6OW5GTL9d4OrnB8SJQkjrD0xs,27290 +pygments/lexers/_cl_builtins.py,sha256=oBF00ZkJyD14LkYuR603EDFIMyitkze12aT7UzDYLYs,13997 +pygments/lexers/_cocoa_builtins.py,sha256=ab6sq-iy5LapE1cNYyl8PJXLg6EINXx-lMRbwERdKYs,105176 +pygments/lexers/_csound_builtins.py,sha256=wuWiQjmEMhaVvsV8b_efsebgU0YKIoMC0ECpOavTCIM,18417 +pygments/lexers/_css_builtins.py,sha256=qhmC4tRGG53zvzMQE1qn794LvbVODRtVacxWCP3kIkA,12449 +pygments/lexers/_googlesql_builtins.py,sha256=mGfOGuKKjZoHHAAZq6Hpc68x0qKycx-SX_wGDScSOYI,16135 +pygments/lexers/_julia_builtins.py,sha256=u6v0yAzZjqUENjiIQ5qMfcso1r_6ERqGbj3aHwFnyqA,11886 +pygments/lexers/_lasso_builtins.py,sha256=YO_c_f05ZoxspHxCR3oWQCpZhbAJUzBJBhVuuwTjr5c,134513 +pygments/lexers/_lilypond_builtins.py,sha256=-_4i4gpgDgcFHvaZMJL2s1rI9dSNY2ZC6vO6ul7Hml0,115114 +pygments/lexers/_lua_builtins.py,sha256=MsDV9sEbJngKoXSZu0xXawGvd6XjFf125nuXBNoffdU,8111 +pygments/lexers/_luau_builtins.py,sha256=YLUj2bcZ0Cb0SBkIlNXWHFGDUFkpddOJx-ukrZx1cMc,958 +pygments/lexers/_mapping.py,sha256=YbQJB1eqeGk7ol5NgMMPLrD3VGdTwgQCl3TvLwtxwxA,70758 +pygments/lexers/_mql_builtins.py,sha256=CkcvMyHYh4U5rGgV6AgShxRnTOv0tmqEbOiVdpaFLqs,24716 +pygments/lexers/_mysql_builtins.py,sha256=SqiVYVVirtnN4lSgtfeRPxtVpcumjUvvUq9n8JObuQ4,26876 +pygments/lexers/_openedge_builtins.py,sha256=AzF1o6eAkMc6vnvmeVqNnUZUVijzbNYkjzrUdtCQe3M,49401 +pygments/lexers/_php_builtins.py,sha256=9g-WLT3qbSGolQJB_4PS6FW_r7HVXBWvI5OWxKtbwok,108054 +pygments/lexers/_postgres_builtins.py,sha256=QGg2mBTThgv4LdXjFm5wrKaKnCU9DubmJi6lXB7y2Mg,13346 +pygments/lexers/_qlik_builtins.py,sha256=3ZkCSDjtxSxXDI9erigWTLSUct_ap_W0LqfZjFeKQYI,12598 +pygments/lexers/_scheme_builtins.py,sha256=O3BRtt5suTESbZ_bSReQ6D0r0G_Sq_i4wCcUkdUrdS0,32567 +pygments/lexers/_scilab_builtins.py,sha256=E33R0mknNc-5eZ9Ugu7u7Av_vUxAaQoMTtdx3MceyxU,52414 +pygments/lexers/_sourcemod_builtins.py,sha256=8qspLxCLsfN9J7N0SWZRNs0A-qJOIGRBAJaNMeWaOBQ,26780 +pygments/lexers/_sql_builtins.py,sha256=fLphonB6wv6Oit8zanJiExgRoLvZoFW4l9lNH-PTYvw,6770 +pygments/lexers/_stan_builtins.py,sha256=sIoONg4TjLkGN7Ab0tZB1HM7tQ071_Z2c41n9lXZl04,16623 +pygments/lexers/_stata_builtins.py,sha256=ymh8GxEca6eSCgHnqsLqsi9OrfRqYnt5O8yBeinJ7DE,27230 +pygments/lexers/_tsql_builtins.py,sha256=Mw6UnMByju0U-OBgT8af3A4utfrS00Tn5lDnaxBtj9c,15463 +pygments/lexers/_usd_builtins.py,sha256=SGp_ePf2VuktrFV59q6df4mTyxZV7AZYs0TJFSWoBAI,1661 +pygments/lexers/_vbscript_builtins.py,sha256=TFtyc11yvpD_OlNrrFbcCOJoUYkWMq3EeHHCpw_zJNc,4228 +pygments/lexers/_vim_builtins.py,sha256=bB6kdLFM33uoY-sAtsRuUZOoccaa4gDAsdFg4WAEsoU,57069 +pygments/lexers/actionscript.py,sha256=kbHhoDl7JjVzREMv1UuxYBRJaPrxHDkmEAC5COmOIVw,11737 +pygments/lexers/ada.py,sha256=m2O7dYzJVc3iuSgaC-Ti-Gh3CG-JmpZit_DPZpu1IIM,5356 +pygments/lexers/agile.py,sha256=Du-vjnGZEPHuE40XxTMO-XLt56pYcIaKIAxIwcYHFH0,899 +pygments/lexers/algebra.py,sha256=2iQSGxfVuBXBHAJdzbbPvtpvROYTfxfMivDagk9yfh8,10032 +pygments/lexers/ambient.py,sha256=8Nk8WX4qrId9zj_zgjJBuFT40yQXG2OXxFhhF3R00g8,2608 +pygments/lexers/amdgpu.py,sha256=KMDLdb-1aK9RJaNsSkPyiU3DQE_HdjBF0paw_yiJMRA,1726 +pygments/lexers/ampl.py,sha256=IKuMVE6aXsa4S6qpuc_DvEsJmJpyKDNga09BPFLvYxA,4179 +pygments/lexers/apdlexer.py,sha256=puSpLBWevciSc8G0wg2Y_yQIoGgqPes0UBkpFImqvO4,30803 +pygments/lexers/apl.py,sha256=-jhPYsBVT4ckL7CZmukIZNi6tfaJJbBk2vutld372rs,3407 +pygments/lexers/archetype.py,sha256=buhH2WHtxBowy_KD5SPm5vgHpmLoKMdV9tgMs_bE4xU,11577 +pygments/lexers/arrow.py,sha256=e6mb3Ix4jL7TMGNUpk8iRbJy_VF7VY2J0VUknlo1EvA,3567 +pygments/lexers/arturo.py,sha256=GkklOaiEJTOz8E0zdxd_hd1zXwv7lYgp8IJl7uh13hg,11417 +pygments/lexers/asc.py,sha256=CvFSi7FYDW6eu-5oiKGc0MN3aF1XVZDoFI5-3mHpBDk,1696 +pygments/lexers/asm.py,sha256=jHJ3CDtvHgu4hX4kVvvJyciFLWM6FAYFDPG_zjBTyrc,42219 +pygments/lexers/asn1.py,sha256=MLakOeBFkdhKKr42eHFzonpGITEJxR_God8vqjWPva4,4267 +pygments/lexers/automation.py,sha256=gdSYslkW9sblDcvMeK1ay15rb9vRuK56pPOJzdxlIvg,19834 +pygments/lexers/bare.py,sha256=AdqL9Z20nd0bhNGWielLhAHej6V8_8m1OWUkCOrSBx8,3023 +pygments/lexers/basic.py,sha256=NqNxIeHhjBUtenwCdJ88WWEE3N7a16DA9g1_D1mdmF0,27992 +pygments/lexers/bdd.py,sha256=KeuOzXBLJRxxM2gV7aMuYsdV_aJFHyBuF8ptBpWxD5M,1644 +pygments/lexers/berry.py,sha256=vIfT4sBtm6ao64WdNkJTVJmS00BiFdEWvb3obBXBeX8,3212 +pygments/lexers/bibtex.py,sha256=sgfFqXyNyCxROHIe8C0gre69rzbADALnpDtTaqHfSas,4814 +pygments/lexers/blueprint.py,sha256=dW-L5bFIiWxRPDashee2EOYmXIRHdwuCZ1y3iN-a6_0,6191 +pygments/lexers/boa.py,sha256=VqYF1Yg_XdDA5tK_Eiuo1rr7ediBsKUTpB6xza_B5d4,3924 +pygments/lexers/bqn.py,sha256=se4W2XPsNiT36z0Am9X5F4yNLrQvMI5X8miY6R9SEM8,3674 +pygments/lexers/business.py,sha256=MXYomjQiYaTmnUgjQ7LUxjcU1i2iVDjZdXPTeqOlDjM,28348 +pygments/lexers/c_cpp.py,sha256=2ViQoLY22Y6xdzXDmwz30rMcij7EemNq4vpTEsfl7sg,18321 +pygments/lexers/c_like.py,sha256=rafLNQqcEbf-97VpDtSIQZSSmdiMD4oBYeEs7DZyByg,32024 +pygments/lexers/capnproto.py,sha256=kc3rT95GkeQmrW1LeP5HMVb7Nk8sqXkW6OJkkWIHYbA,2177 +pygments/lexers/carbon.py,sha256=_M-0YbofjZMrJIaiXKoT6wpkd3_6fFq-OdSZ7V961I4,3214 +pygments/lexers/cddl.py,sha256=7Wri2q2yKexWo0CD7s2c7AoCAMbrwyEE6f_gz5B6x1Y,5079 +pygments/lexers/chapel.py,sha256=SvZXNJijW0greFToDHZQ0yeo4_3niyTx31Iz_yLctck,5159 +pygments/lexers/clean.py,sha256=_oV3Tbmrpl3S1phPMS6gyMWyGZGj6Wy0q8adpf5vunY,6421 +pygments/lexers/codeql.py,sha256=hAX51uWeG5Zk9JhXSnOxPMaznFOpaX4ZnZHZ2krrPGk,2579 +pygments/lexers/comal.py,sha256=0fcTyV5h36zPlxoQPWWGet2yaYi5ESXiFpx6EbsqP00,3182 +pygments/lexers/compiled.py,sha256=PRimy7eb2weP7X6bEukz1JaSLuQAETO34K7maHLGbxA,1429 +pygments/lexers/configs.py,sha256=YDBIWV_R2SiRb0Cbxol-S8RRieDfAg_YcThc1BmZ90I,50925 +pygments/lexers/console.py,sha256=MrTIkXDYQ71z0yDepwbpO9WwXPQRPHAFAOvt_ngscxU,4183 +pygments/lexers/cplint.py,sha256=wPs_Zh0OJ2_UUeF9_mu_r-X_65DJSWMCTKOmOyjWLz8,1392 +pygments/lexers/crystal.py,sha256=k8Xy8TpfGKTPvsP1WikxILScILD8iXZ5qceLZopsLYE,15757 +pygments/lexers/csound.py,sha256=7KHoJv8wtuIe9dques4mFFkQLULcGQ23dgZ_KBd0f_k,17001 +pygments/lexers/css.py,sha256=OVjsd0Kn9pNH6iltS4ikjy9nnilPlCVx-VAZ26n3UJ8,26439 +pygments/lexers/d.py,sha256=ee5oImP1BcHYZ93UwXoHIzw6BztgR3WCCv3ksu2J-o0,9923 +pygments/lexers/dalvik.py,sha256=0f5mB5y8g0l1ID2liJ_5qM2NjyR_SBs7ckuIDeNbbj8,4609 +pygments/lexers/data.py,sha256=MhLXoZXYMZFmmKlqL8RV5zp-eUwVb0yFzsyzXYrpKiw,27049 +pygments/lexers/dax.py,sha256=N-5fxukBfFtFCT2LJoyg1VJsfldWHxXKLY3ZPLnPrig,8101 +pygments/lexers/devicetree.py,sha256=Tig-tTSrpmGzbGxN2ka4MMTiC344roav9A4hoc9o_kw,4826 +pygments/lexers/diff.py,sha256=-627lmuYpJ2uUVUM_orVGgXJGXIBGSVHbmlqflCEeaM,5385 +pygments/lexers/dns.py,sha256=ElzSN3IEoRs1g6iWMnzVIV8H0x9WSEF1wBjm2a0ByxE,3894 +pygments/lexers/dotnet.py,sha256=GmoaOxSoITkYdAUaVJ0Z1RQa6MvcRxh1gQ3XxgEs4X8,39444 +pygments/lexers/dsls.py,sha256=a6Jdv3w1aF3vpQJrnF32MNHAl6pEZkkeGB1cYv-x2Hw,36753 +pygments/lexers/dylan.py,sha256=fi8mSyni4dnGP5x1crE6BKNqLWJs4dDWdmkur-TbbNc,10412 +pygments/lexers/ecl.py,sha256=XJ66PU9EjkXJwA4f1OfC0RdfP49v8N62RZXnFVVAYNo,6374 +pygments/lexers/eiffel.py,sha256=m2eVPDG9J-VXieCeOy0S7zHPjiFHQB9F6bOIy-K3MDQ,2693 +pygments/lexers/elm.py,sha256=Xj3HlbUTEkEUFwhem4NCujx55tQ2TS61_Kcf6BCPHAI,3155 +pygments/lexers/elpi.py,sha256=RZqAVzdxfA_BqBmtMHj8ATw5f9e24SKL-QeO14QuKk4,7904 +pygments/lexers/email.py,sha256=NPlGc2L6F3KWWDop3ZOJGpUM6LDnOEqKZj1YJG3vP2M,4807 +pygments/lexers/erlang.py,sha256=2aUlVBVFtQdR2QISsDeKTOPjGrV7r1ckyXL17KJot9c,19150 +pygments/lexers/esoteric.py,sha256=tmJ9Pa1wYtYgR6yDqgp3zDYuXPFzXw7SLTHOH_eE3p0,10503 +pygments/lexers/ezhil.py,sha256=tbGYtGnqZAB8ffnyPdeMMJHtKYJ7OAciUFRrR_8QpXM,3275 +pygments/lexers/factor.py,sha256=HBUqNcwd4VplYw30ScuC8YgdipFAYiqjRgWsyT0s4xQ,19533 +pygments/lexers/fantom.py,sha256=w9IU1x61Codd7Oxv5DpvVlkVPn_bfvVLiP8Ae3660zM,10234 +pygments/lexers/felix.py,sha256=DvPOs_Em6fFtUBQCaZWWHWgnvAFeBrDe97Cy9XyQ8ps,9658 +pygments/lexers/fift.py,sha256=Q9OLJivp4ngqFso7APCvX85h-ghdbp9ZqpP8GKHws1M,1647 +pygments/lexers/floscript.py,sha256=TcyuCiv9bAb89x1_7_Zq6nUhsdsHAF2DPsKZ6m1W1NU,2670 +pygments/lexers/forth.py,sha256=4e0OCfJGcElkQrHInmQMucvsI9dljuPx4FYg38dyjno,7196 +pygments/lexers/fortran.py,sha256=ouvOHdMbj56j40Nrj21WCRf4rNKpuwphxmtlUVsnzhc,10385 +pygments/lexers/foxpro.py,sha256=nV8yyES39QoFmTNlM8xl3MteOsvmt6yV1l6x6xnXqIU,26298 +pygments/lexers/freefem.py,sha256=ZmZLIYLXtCK2kWqGzBmrkp7ychOy7vgfaorafUkUl1I,26916 +pygments/lexers/func.py,sha256=ZjOerj3njJcaGnHWc_npWjxb4D5O-udxBRK7RLHEibU,3703 +pygments/lexers/functional.py,sha256=4m9JlAnfpXxSkQQIrNOob2VTwin6hPLAqWuhv9aVkAg,697 +pygments/lexers/futhark.py,sha256=mCU2yvloJF5LN7uRE_8rCVqA8HAINny04HXVxjSiM4g,3746 +pygments/lexers/gcodelexer.py,sha256=t8JMWaaDjWZwdGiRGwrpsYtfjKTmibwlRyOLCiJZ8qw,877 +pygments/lexers/gdscript.py,sha256=VNOwd7KcDREvoBAfw66GjxxQygPsHH2akdzRbPNKqoQ,7569 +pygments/lexers/gleam.py,sha256=Jx6LnUpWV9pmjdSYLUMt16c3UamZ-v5X6CcuEhWxf-8,2395 +pygments/lexers/go.py,sha256=ynHVStw2XUJFlUiTS4UqY65fLkvcJBMNzoYtSNSl_BU,3786 +pygments/lexers/grammar_notation.py,sha256=ZGYcwhSvOolw1J7cA9Pc7hVpcIselaCgxwsvAf5yRgc,8046 +pygments/lexers/graph.py,sha256=X_IsdjxXZn5p9Dq4cLXyqwo4C8emcYA8NP6VZ9ZTBaU,4111 +pygments/lexers/graphics.py,sha256=IeruxvEQR1Wsu-YyLpAjkUnisLDzUrk2cd0xXrDCbjw,39148 +pygments/lexers/graphql.py,sha256=RWE_kfVUDEAnv_-SYyOi9IJUks61NvOakmUfWOlorzo,5604 +pygments/lexers/graphviz.py,sha256=DRNWwetEWasJDtiLkdqkKsQHDryJ5W80wUD2PE6oKCs,1937 +pygments/lexers/gsql.py,sha256=pzSj5Pd4vtNLES-pIdxaG0vOVTqUWE9UuOUw0RrY-oc,3993 +pygments/lexers/hare.py,sha256=eCrRPwcswXL-ZHTWNlhE1A05G9rxzS20_IqSddv2M9Y,2652 +pygments/lexers/haskell.py,sha256=sr5gq3U7xt0D2Bi41xdTsBaponX6otP1qPyRMCuxM7E,33323 +pygments/lexers/haxe.py,sha256=_MzQJTWx18kD9bP_sniBshKbtOZDRm4OZnifaOgw_aY,31169 +pygments/lexers/hdl.py,sha256=2jYX3fRWZ9mAZ_QER1WH58SvJaOdYQt0OmOxo3X2JK0,22741 +pygments/lexers/hexdump.py,sha256=VvYp_NTaE-6NUuSG4FMezwcjxbPnAz0fOWMzr3Y2Gm4,3656 +pygments/lexers/html.py,sha256=O6qkpyOylH2n1Zi1qzlmBsIEW31RfvzBn7jKTaAR2LQ,21999 +pygments/lexers/idl.py,sha256=W2QRH1j9LMaNvC5jb6MkP4dLYo_OQU6lQlCAlQveFH4,15452 +pygments/lexers/igor.py,sha256=Sv5EGBJeKy0UOyl6-GF3lBeeoWArVcMDJtQtl2RXRYo,31636 +pygments/lexers/inferno.py,sha256=cjVMyOg6jhJ6nYX333Z6KaCCeS0pNVHv4WTxyFGY8Tg,3138 +pygments/lexers/installers.py,sha256=ioK2JUPSJk9vcsoe3NASvMt-paXd1_Nn3e8WmaqJ7CE,14494 +pygments/lexers/int_fiction.py,sha256=BUjvXilUiuMF-euXEQDzQZs45jn_aLs17eTKcfEptrI,56547 +pygments/lexers/iolang.py,sha256=kMUBUqVvdHlB79ez0pFZPRf0NgiEC2SdtYUwdjnbE90,1908 +pygments/lexers/j.py,sha256=56qUbs1C7wKdZ6-WpNnxHntNqsxWroqiO7keI2yoR1o,4856 +pygments/lexers/javascript.py,sha256=muyCVZQAsXbhT7IHZkoqAkI5myhRaHze6JMAR4zILNs,63246 +pygments/lexers/jmespath.py,sha256=726PDpRr1g2Ky_x5pZORhbXofXxkfr5HXQ8eHMhX5xg,2085 +pygments/lexers/jslt.py,sha256=UOVw1J3uK-hVRWomoFK2jMS_yeuo5iqga-LhXUJI660,3703 +pygments/lexers/json5.py,sha256=GyeZ58AxKkJJiiH7JuPmakDZXropa6bDkFRguJHifqU,2505 +pygments/lexers/jsonnet.py,sha256=_T7Y16sW7nFuAoGFuyWUKCrUyJAC4FTWZVoeSt0mTXk,5639 +pygments/lexers/jsx.py,sha256=O0evHioyudtWLhVmAXRrIfkEd5KRiokgHu8l9_Kn8sM,2696 +pygments/lexers/julia.py,sha256=F0twMg0iLBNDdedsoJG4Rn5LoMciVu9cufyUVdxKQPY,11713 +pygments/lexers/jvm.py,sha256=cf5V7NOLIvB2s7RM9wghESYRCoCDyrGjRuPmY8bfGAw,72936 +pygments/lexers/kuin.py,sha256=n2lMina8SskpfA2KbPYQFwVJOSHgNYu10U5pz5YiajE,11408 +pygments/lexers/kusto.py,sha256=pyUfWLf9Q0Qwqu74DyPAbRh20lkPDLhxnExD1FVl8Z4,3480 +pygments/lexers/ldap.py,sha256=_ya4_InnSIRj-RYjGagWFZJDy1NdgQUocQzVWjiRp68,6554 +pygments/lexers/lean.py,sha256=qP_iwUQ7MJZ1C5s3KYBW8Wu6-uuVj4PAG3a2Ghjp99U,8588 +pygments/lexers/lilypond.py,sha256=o264ovGZFrcx8ZwpKRX-gjUECK1w5T8T0n6LoTjP92U,9755 +pygments/lexers/lisp.py,sha256=a5MGcmtrWTaeEj_5Uv8YMODr3dVV6s1v1_SMBh_Ch9Q,157903 +pygments/lexers/macaulay2.py,sha256=hLNDs1TdubudvqqovYN6a4Ne9JjnU4FvNE6EE2hPn8Q,34139 +pygments/lexers/make.py,sha256=wAV0KRRXTAFMliLfAKXDihQIE2VAVUvWjFZ-yuM7a08,7834 +pygments/lexers/maple.py,sha256=OBjODNLgqanyw522CE7n9thZctWi47Uua2hPBfj7KF4,7963 +pygments/lexers/markup.py,sha256=4TMRfujXvOE9xiWwTiT_fq1lau4dKSoXYac1hWMLYRs,65264 +pygments/lexers/math.py,sha256=Nspl6IZtCyh9egiYUahboWSmy4cJCVZUBxyVnCuF1ag,698 +pygments/lexers/matlab.py,sha256=QYVBdA-IRNcuWggBJNC9lpq3jgX_1w9GQwpv907VfZM,133030 +pygments/lexers/maxima.py,sha256=ha-f-JzGkjggAr5kxmVq7_uJTCcpHstQWnsLHYIhNwU,2718 +pygments/lexers/meson.py,sha256=ZpNVp7lSHwJHEj7pwXlGcy9Lftdc6MMgJiOoY2TnfM8,4345 +pygments/lexers/mime.py,sha256=l5BsFkad3agJ6KDg_IHI6nU0Ao0Y9EcwjYoYP8vQD-g,7585 +pygments/lexers/minecraft.py,sha256=bBbMbqvsVgTfO7CBxf03oGP_qOAgrw06PSzH1QF7txs,13701 +pygments/lexers/mips.py,sha256=QpgBoMPzsk1t90cIF0W_EV-QZ8LwgTapS-fSISxZcFc,4659 +pygments/lexers/ml.py,sha256=shDPgWARTzTEhGDeblV7ZW2j6yZEeBiYqx2z24SDP7A,35393 +pygments/lexers/modeling.py,sha256=2_ucVFy7Z4yoKx90E9-ASI9t4t1LIoeQ3yHarhghz-c,13764 +pygments/lexers/modula2.py,sha256=dXW0KNFu3ETgABsPMsIhdJI0DKF-KWtT831FKsz2VXw,53075 +pygments/lexers/mojo.py,sha256=imOlg8mQCoKwi_rsuMiifZrzsAsSqT2KckCs_MV5uVE,24236 +pygments/lexers/monte.py,sha256=JTx-jSrFKlWRwA1qzhPd647ETGSElwhP4YMlVrfXyX8,6292 +pygments/lexers/mosel.py,sha256=HEYOmZiANhe9VdkTpBt7mhPi_tPGBUkVsnzTh2HVdVI,9300 +pygments/lexers/ncl.py,sha256=RZXkxOK-iHspLX_sFVT0wc5H6v-pa4kKghP79fJGQPQ,64002 +pygments/lexers/nimrod.py,sha256=L8Ww5CUaP-ojywoQZXnjC08m5Be64fBynSf5aWGIaPw,6416 +pygments/lexers/nit.py,sha256=xwu2P49Hu8YxEL7mgpV5WWFw6OYF1iJg9DVnc7H1SXk,2728 +pygments/lexers/nix.py,sha256=M2E-k--F7pneuMAOae9ag4srJsKhWJ1f1fRueO6PtKM,4424 +pygments/lexers/numbair.py,sha256=KZOw96Tj7Gly-f9F8NA-tdGd53SIt5UgbpokZsfnzWM,1761 +pygments/lexers/oberon.py,sha256=uH1FkPeXCfdd0IQ_--S8SHUNaf2dnjiQZjtIT-jxu4A,4216 +pygments/lexers/objective.py,sha256=KME-J0UL2HJYAk2fHNhf7ApQe4XF8GH6qGOvYA1wPS4,23300 +pygments/lexers/ooc.py,sha256=HEjWHdQDVk7tRb_TuEb1_C5qi-peJwwyYBVAhf49MS0,3005 +pygments/lexers/openscad.py,sha256=te2iL8VkfXul_PYXlz8UK3_QtMjdmNgt0YHCYEvdEtM,3703 +pygments/lexers/other.py,sha256=OAlXzsrVDUx4Ma25fyG98U5LaBEHyt-LJZ2IHvMJJWY,1766 +pygments/lexers/parasail.py,sha256=oPcs7fRYNkV0isPSRmw-2cPfMgzTtNPi6bJOm7vq-9o,2722 +pygments/lexers/parsers.py,sha256=g4tVvf36yhT_aH_b737o0o1joojcf-XckD2HdNoNbsQ,26598 +pygments/lexers/pascal.py,sha256=4dewXkwc12f_iiMfdNDqbxb_oqAEX2dCzb5VIZ62V54,30992 +pygments/lexers/pawn.py,sha256=adsa-7sPuPk7mO7lB08auabEpeZdNewVqTBg6FS8mCk,8256 +pygments/lexers/pddl.py,sha256=a8A2keCF9qNQvVZQirgBV5fH4u3cjrta8-eZJ8tsxmY,2992 +pygments/lexers/perl.py,sha256=uYMj6amZPawLf-KjICg9LLU5-ADKzqEYKqi7VXnvm0k,39195 +pygments/lexers/phix.py,sha256=nEWWt5-OoIDapw4IJ_cNL19Th8Ztt6OIQgeuogVed10,23252 +pygments/lexers/php.py,sha256=U6wmxPM-pS-SY1N3qdv0ebUDBxLH6-EFIFcatUvg74w,13171 +pygments/lexers/pointless.py,sha256=gNcuhhOY9cEpIPyGFjMh2DItNB1UD8CmjceXkDRModE,1977 +pygments/lexers/pony.py,sha256=HKqf5AngUOdXN1N1VvSg0jTj3NUAzGBOPdXwSFamvmM,3282 +pygments/lexers/praat.py,sha256=sfVAd7zfRsI-TcCouuYMWlcU8EBxAwJ4wkh0ZuNsZ34,12679 +pygments/lexers/procfile.py,sha256=fhRTtscyMMPcPbvzjxVtXix1mYlpO-4JuWUL7Db4ENI,1158 +pygments/lexers/prolog.py,sha256=4YvPZbAFLWNZMbZhvtIulWib0PQLA_TB93gjkNywFRA,12869 +pygments/lexers/promql.py,sha256=04fS_R6HBWhpKNe3WPp_QXgIdFvJr5p6KxStZYw7yHU,4741 +pygments/lexers/prql.py,sha256=XJhd8dpEWPBIKmQXx_09-z1NyotLXIskpuzX36HykHo,8750 +pygments/lexers/ptx.py,sha256=dEaNSReAjAymIwVM_MnbdY4hd3muK79q200hYwN72cw,4504 +pygments/lexers/python.py,sha256=79A_yJqjVHp_ZeS1rY8Pcc4cwZ_7-zI2WWkeUmZgUrA,54202 +pygments/lexers/q.py,sha256=2CbJYgRu8uz8wOSp5FMr086KCC7IjSRmIODVY0uKrCA,6939 +pygments/lexers/qlik.py,sha256=9b6Q-6jXeeraIRcWtsKsYWCOlBhfmjNzIN49pUvMR-I,3696 +pygments/lexers/qvt.py,sha256=rpT5oD4awEKMs3uBCbNlzY1zCz7AVeqM-BikLTQHo8k,6106 +pygments/lexers/r.py,sha256=hzgUUH9gCsqqwTG9eCt1JVMd9RW0dy6IY_jTvRntF-U,6477 +pygments/lexers/rdf.py,sha256=FM154fB1lxfOpSpeW6bWOVld5kJBgEuYyT9udt-EcK0,16063 +pygments/lexers/rebol.py,sha256=CQ3pMaAz64UMqiQVrYoREnOgjU5QPeF_H3aaHNdVRC8,18262 +pygments/lexers/rego.py,sha256=Yi0G4secTWOL7CUhLLCzJ9gA6SJzDJpPJNij3hXixC0,1751 +pygments/lexers/rell.py,sha256=0gZStI953aFjFMyMVX_UKfwZmzV3uj3Tnl_5F20BPx0,2487 +pygments/lexers/resource.py,sha256=RDEY7iSv2hgQ3V-I1DeOKVOumNKgFqbV2Me_9Y21o10,2930 +pygments/lexers/ride.py,sha256=1zg7kGYPKRIekyuZY3f6OVqRlXyrX-sR-tHEX1M9nz4,5038 +pygments/lexers/rita.py,sha256=fCNElPik6dDIZzGd96kKgdlY4Qqp_zVI87waTBHMBfg,1130 +pygments/lexers/rnc.py,sha256=PNfnnTlnZjNvvG33BNMCqPzdA9LZEYvDsGoaDd_2mn8,1975 +pygments/lexers/roboconf.py,sha256=l6BeJIS-ZAUb3zc5GEuVIb0edrRpPFmKdffEBx74Zxg,2077 +pygments/lexers/robotframework.py,sha256=8s1U7GldhzRPGR7d8_rMSG0ZJz9ZOm-LGqBTGILwt2U,18451 +pygments/lexers/ruby.py,sha256=BxndG-3gLg7wEGvICiNDAMygQR9Qs0xrmq7DwfoLvMs,22756 +pygments/lexers/rust.py,sha256=7qD3KGVir-tUWf_bPBt4EX8l9aIWe8TblMCFI75oorc,8263 +pygments/lexers/sas.py,sha256=9hmGYhmKo7ri9oCmZdsoEFMz6z3AwKD4LcTUIn52lYU,9459 +pygments/lexers/savi.py,sha256=HCllgBe3rzP_7-2b1hSUEbE2dYz08Iysxc6Dcxj3f2c,4881 +pygments/lexers/scdoc.py,sha256=9n64S-bO1dI4x-2Kzh2jrDM6gEmB5YI7ch6ndPB4-cw,2527 +pygments/lexers/scripting.py,sha256=c-i_cMFhYEHQZZbLgJn0jyiD52vWeZVxUK6wFV2Fib4,82959 +pygments/lexers/sgf.py,sha256=ya_sG4TOvDWwzEM6cIi40XSbhkTuHCqvo-uHnNYfi5Q,1988 +pygments/lexers/shell.py,sha256=si6MAn6S7pHfyX65NkwQdjxtLtEu1M4l7K2lUHvyzWw,36384 +pygments/lexers/sieve.py,sha256=CXR9S1nGeVTln2u5R600hxgkAmJFVVTTTBh_k3aw-pk,2517 +pygments/lexers/slash.py,sha256=molh5sNG8UtheHffQGjjHLxVtyg2oCp4B8cvgDyf004,8487 +pygments/lexers/smalltalk.py,sha256=q52NHegl3pjn1jMkJWA9J0nOA_A5VzUULZAPgb-6uNU,7207 +pygments/lexers/smithy.py,sha256=hqEImo4B-i0hddNo1r0k5eHLOlArtRW4W5PsLBdllk4,2662 +pygments/lexers/smv.py,sha256=D51In9Qr2nWFicYAOX_bJpeFgmJ3BUkEXnvXPmA04D4,2808 +pygments/lexers/snobol.py,sha256=BX_1VPUZi-ckKCYlF2sttQprF2bLUHOi8blhInFHv9s,2781 +pygments/lexers/solidity.py,sha256=pz0DZ0xiHwufhbQ9hdCPrHMjA7NMsa6hxZ1nO-A4Y3E,3166 +pygments/lexers/soong.py,sha256=TVqBJzJxLwCEDmb5Aix2_AVuZILYyPWcAzFbzINNQAM,2342 +pygments/lexers/sophia.py,sha256=MAkWLYxhHJNcc3UUzWD3VDLzVRbf6IAd2uz4q7DY3wI,3379 +pygments/lexers/special.py,sha256=8gpTiLICFNwIcahm282mi8Nqi_6gwYMsAfjJIM2Bq6k,3588 +pygments/lexers/spice.py,sha256=UcrjK2KJDDKSEgEOeLXTayqRwirWdsywrfH9MqwPPIY,2801 +pygments/lexers/sql.py,sha256=zz_TFZtf5R29cvkBFG1PDJ-0KKrY7FjA-RDky-gfNlk,41656 +pygments/lexers/srcinfo.py,sha256=MHj02VB7WP3AcoOz1WVYAqNM_3oFLjEaAda075kXVBU,1749 +pygments/lexers/stata.py,sha256=KojmkxWHlEk-HvmxI313a52nzH_JHNqfv6r3jJoFDy8,6418 +pygments/lexers/supercollider.py,sha256=H-qwP6sUaotemsXxuugpui6KnU3jtIdDfS3bGt3Jvh4,3700 +pygments/lexers/tablegen.py,sha256=ryuzw-ArLdvlY55YJBn6ebOzDo3L8UnbR5xcZB8xMso,4012 +pygments/lexers/tact.py,sha256=YbOWYNp302ZBPM5affzM4-CudJ4xRZyJb-_vSDCozCo,10812 +pygments/lexers/tal.py,sha256=xZYmhv8mBr-A3oeoFkcSP7nDbX1r21kKOnhsLTtL3bE,2907 +pygments/lexers/tcl.py,sha256=MXMAo2wCZeq5oR7VzsNbamawun_vMOk2gOEDnGk6yOc,5515 +pygments/lexers/teal.py,sha256=pgpPi9xWPumSr-heSpGBGDcU5s3qN0aFUj2maHLE3MM,3525 +pygments/lexers/templates.py,sha256=ndJMdue33qQ_thsortAra1fm_-szMN40S5bZXBLY54w,75734 +pygments/lexers/teraterm.py,sha256=YCdvILRq-FdOJ_HfiIkYVToH3c6I2QRbQoH17Q49Hx8,10045 +pygments/lexers/testing.py,sha256=GF5SpanGwjgKKoYari32SdtdOWWXXje8xVX5-ZzTjLg,10813 +pygments/lexers/text.py,sha256=yq6mOLz3PizKNMm4_Y8UHn9vEeBfqEY5W-3M7dk1jYs,1071 +pygments/lexers/textedit.py,sha256=V-Ijh0eULTWtx3S_6vT7JcyjQO-CNirpAbEpkczLGCE,7763 +pygments/lexers/textfmts.py,sha256=WSiJDzNKCcOsBoTU4NCYU33ti5ZHGi0Px29eGjT6pB0,15527 +pygments/lexers/theorem.py,sha256=c-eg6tIYWUbxSYgrOtLZfFxro4g91JcAIwrIJGLPooU,17903 +pygments/lexers/thingsdb.py,sha256=FNKjArS1vadHEN3ZXaZdDLqvcG59GbF6Ak5M8RMsTLU,6257 +pygments/lexers/tlb.py,sha256=vWsFq_MrIrzgG2q-qdzpcoNiv9KkNow4x9ebs9_HAwA,1453 +pygments/lexers/tls.py,sha256=GZ1lvZ8PUk1-LThq1KuiqSEnMqJVqiHo_A-16aKuJOc,1543 +pygments/lexers/tnt.py,sha256=MNYTfkix0x5L1pDV8V8zGLS7GU8z7bt-wpiBap2W09U,10459 +pygments/lexers/trafficscript.py,sha256=1Tawom6bKJM-u1kYey21xFaHep3D9kl62rV3RaHSvcI,1509 +pygments/lexers/typoscript.py,sha256=dWAuOYYk0X4xE_fxKgcXz-sMYlG2wxQR4m_d7PW2PYs,8335 +pygments/lexers/typst.py,sha256=ZtV37NCQCoSqEKrAhffuZtUp1kDahKQx8PxMLrR2G6k,7170 +pygments/lexers/ul4.py,sha256=joM-US0-2BEWRHHK6eyZ-Y--306YgbMrevMY0hg8QTY,10502 +pygments/lexers/unicon.py,sha256=9D-GilKOqIEaowGHmdvHRjhqyigRk320ARAr-G_snew,18628 +pygments/lexers/urbi.py,sha256=j87k6fe60kZdbJ6AJA4g9E0no1EP0bxJY06UVP0YgnU,6085 +pygments/lexers/usd.py,sha256=tP9kHPZUJcE_flPvKEsF7wTck4FuQW75S8ELh4IYJEI,3307 +pygments/lexers/varnish.py,sha256=INCtYbol1yV_NXMh51CMDZDacEHgiB3SmR3H5uW-Fkc,7476 +pygments/lexers/verification.py,sha256=QISJmmz7cmYaXZphW5so4PfdKJo0rdZWuj_de64mafk,3937 +pygments/lexers/verifpal.py,sha256=0iZTQWawF9f7lFZnFNPTwYRiu6L_25PtEI83tuCAmKY,2664 +pygments/lexers/vip.py,sha256=pM5xFoeu-2GPWqAVzZJe6xPYr6WJPlozVI5084SHQ80,5714 +pygments/lexers/vyper.py,sha256=rw2yD9c2L7yPtXqKPd0RdxJSZK-aJbW5jEa0gHfKEO0,5618 +pygments/lexers/web.py,sha256=YMrxoHlKlQwfFJlae9_T9F1Vp03YA7IWsmgqLzh6pgI,916 +pygments/lexers/webassembly.py,sha256=Y48VdBp8b4SI10PEt8biOm-gteYgmNc_bRR-KvulfnQ,5701 +pygments/lexers/webidl.py,sha256=MQMaZFskluB29lMGpiGP2f6fzoja4vV8FtoCGFQH6fM,10519 +pygments/lexers/webmisc.py,sha256=GnSTSHfsAUGy5DYF_qwFmMsyabnzTstaEG96Fub53fk,40567 +pygments/lexers/wgsl.py,sha256=vgDtY_q42TGRvcsyS7viSadU8D3eHvatfoCNNnEP0zE,11883 +pygments/lexers/whiley.py,sha256=MC2V7o1s7LIoLy0Fk-i8OymBoN0dpK2BPjIrdQwsnVk,4020 +pygments/lexers/wowtoc.py,sha256=XnLSSX7p_RkwZIzlmYXO873d4byX9-A3dfvzNL5Eocw,4079 +pygments/lexers/wren.py,sha256=2lhzwpS27xW6nERY_NeODgNN9X6mB7vl3odXSPngqY8,3232 +pygments/lexers/x10.py,sha256=77pAyohtv9PFwPvJPGgq-Vw8tEXo9Sny1MnTchFpTTI,1946 +pygments/lexers/xorg.py,sha256=LUKU91t1Opc3W_F64UGzx0nB-HqKlN3Cn_6tFzt5IYw,928 +pygments/lexers/yang.py,sha256=yIvwteHWBWL2-8zZscnCPJOnq3rMOXh5lxJujywA_Zo,4502 +pygments/lexers/yara.py,sha256=_ISzhko7v9AhJ-1cWINZSyG3my2vJOyFSHxSjm4VdTY,2430 +pygments/lexers/zig.py,sha256=q0tuplpRFAUSS29hg_XiRH22Ctve2EFAT5xo7TNAlrk,3975 +pygments/modeline.py,sha256=me8g5rySidvPtMBOrDy2O1sMqZd02lBHMY5KDZefUws,1008 +pygments/plugin.py,sha256=P6zIw-vkSQ0k1WKHrzbBTjpwlCZPbR-6Qe_dO9Bjdu0,1928 +pygments/regexopt.py,sha256=d2hTvazlow5zzZIOCVnfeEG2CY0GrY_igH1kCSSf7ow,3308 +pygments/scanner.py,sha256=DtoLi1pOKpNu-6jiakJpSQLUi_ep29SQQhv7oAwYWME,3095 +pygments/sphinxext.py,sha256=qmiWv5b7qq6bNUT2Y2wZejV2ImQmvtzGJFg6LcGENl0,7901 +pygments/style.py,sha256=Hrie373bgWU81ZNwVjZM-GNBoxBxVe1W1Tr3MzJLdkY,6411 +pygments/styles/__init__.py,sha256=2vgGKnbyt0nf_CSDEtMhHxppPSAPr2jafzl5FiXFuGs,2009 +pygments/styles/_mapping.py,sha256=6lovFUE29tz6EsV3XYY4hgozJ7q1JL7cfO3UOlgnS8w,3312 +pygments/styles/abap.py,sha256=T7Ad121Kjz8ZbuphyhulULwXyvXubSS6Czc0MDOFiJ8,752 +pygments/styles/algol.py,sha256=6v6ZXLxPJsm_1qPqOu5ihAuWhVk644NdPXPcZqiOTTI,2265 +pygments/styles/algol_nu.py,sha256=581Lf5db303g7zDb7z-VlvGlFNpbXMIlAnrRBxtjGts,2286 +pygments/styles/arduino.py,sha256=oFF5gjfbQNBigKxFhC7giUIHWl0ieCshhsm9UxCIzqg,4560 +pygments/styles/autumn.py,sha256=P9IX5utjDvdJgevM0kNGwE-wvVysRh1lgMkjO1dhDDE,2198 +pygments/styles/borland.py,sha256=V8qTD8SrS0wLohp2udIirlijbHfPoIlWZkYpqxSdkSw,1614 +pygments/styles/bw.py,sha256=e4Fo6Kyax2aRhqKsjsqvi2CW6lkU9QnjKoPrESZAj-I,1409 +pygments/styles/coffee.py,sha256=0jxdctCEKbR1AqzA59fFQK4sVBdErrRkGCUn6fPF4sg,2311 +pygments/styles/colorful.py,sha256=opkfOcjFTtDn9Fty6ogM7okFMB6bA0iR0VtTdBPE9NE,2835 +pygments/styles/default.py,sha256=WFLDucKJS_ac5Jqutmglbz4ITc4jTTQEOnqLIMpSttA,2591 +pygments/styles/dracula.py,sha256=H-EM1WM3Ixd2OBY9bzoObKl4IzdmhirfDVFbJ9Wsi0Q,2185 +pygments/styles/emacs.py,sha256=iEYWPgQrDQk4RdDf8-g7j4mtoFRzo2xcKvZE1ys9mJU,2538 +pygments/styles/friendly.py,sha256=S43XMczW53tKftmarEL4-nTroPVdN6k8fbNrINzR_QU,2607 +pygments/styles/friendly_grayscale.py,sha256=el2E804DEkKZ6ApXt3N5FTmHAkIhqkJj-omhE1E8mdE,2831 +pygments/styles/fruity.py,sha256=mrNDixp39QIoitRBo-yMOr8tnYLAwGd0EIMw2A6Gd8g,1327 +pygments/styles/gh_dark.py,sha256=yb4EOBAYsQ9g5IGEOqyZ5LKI-MUbwz32l9WB5EcMLaA,3593 +pygments/styles/gruvbox.py,sha256=_QZtRq9y1s3NYADHqek-DXELQqueOnVDXvWJ_1aTGgo,3390 +pygments/styles/igor.py,sha256=QO_M5-Z1xIxVnusEU4LZr4VT--nTERVt-cpfbnOv4yk,740 +pygments/styles/inkpot.py,sha256=bJvNWikqnAWLjx2AH4t_tW4jR0TsrUyVFXFVPAfOgkM,2407 +pygments/styles/lightbulb.py,sha256=CnDv1mF1X5k1msVvRS_diR4tkp06E_jg0tK9cdHEFCQ,3175 +pygments/styles/lilypond.py,sha256=gSCiazPPWfR_9-BV5BYHe6-2gSor4vOHnz3nyFQI2QA,2069 +pygments/styles/lovelace.py,sha256=vZ-S9tS-QUOwfA0RHYBhSTRE3h1ZU-bZnuMW7O6ksyw,3181 +pygments/styles/manni.py,sha256=dpsJC1Zees0e9IcWirqWuSzQxwD0jIbjhFquM23dmWM,2446 +pygments/styles/material.py,sha256=eyyAaJgp1Fnjl4-_D39_l1wH5CW4W0VWScQ3Q0jtAgU,4204 +pygments/styles/monokai.py,sha256=3CwJJm_YVGibzFi8nhK_b5d4cNzgV8z-L6RzC1UVA7Q,5187 +pygments/styles/murphy.py,sha256=HsPd80nObb3Ov3-7yJQbnJr3z9mCyz30Pjn_nkWkMYo,2808 +pygments/styles/native.py,sha256=oazEJUbuaHqqOZR5vx-jIhTPIw7dC6QZnKxW-xLr7fs,2046 +pygments/styles/nord.py,sha256=t7fZj04LUgsLaHpio66FqOGMKb7a2VC3teUlmInPfRg,5394 +pygments/styles/onedark.py,sha256=UXrMzVvA9OVP7OhCQ5gL0CaZrk5jo_I84HJm7LEN5GQ,2126 +pygments/styles/paraiso_dark.py,sha256=rx5_j4gnZRy2j-3hAhFmWpnjnd-Vfz3Z7fcwEXRUGm0,5665 +pygments/styles/paraiso_light.py,sha256=iERjbYRemqgsisv_9TjanBig4bJ_-80xRz5B5puhMc0,5671 +pygments/styles/pastie.py,sha256=mnKebFiJ2do9A_wTPq4tKtgw-TQqpb0lP3kNbemQ2to,2528 +pygments/styles/perldoc.py,sha256=-e1T4QBEQNE_l17RYW-WBowNprg2uL_hFjhSDcaMiDM,2233 +pygments/styles/rainbow_dash.py,sha256=YP2HPVXJLmOy8Yz8p8u5GbkAyuKxLlNmu6ITZBMa1Mc,2393 +pygments/styles/rrt.py,sha256=vs4pnWwmMIQR0eGczp06AYazsYiR0KFcRiRUzSf3rXc,1295 +pygments/styles/sas.py,sha256=ER-JjZgU2wEJWk5K06iHAFPjVYD1iMkvjjE0rlNOt2Y,1443 +pygments/styles/solarized.py,sha256=VdtLYhQ7xDj_iTHP_Ekr6rAdMcE3fRSrglngVSal63M,4250 +pygments/styles/staroffice.py,sha256=peExkIBqPkRhqXFpW99n49t6pMwehI_pDYVUmQwkgdA,834 +pygments/styles/stata_dark.py,sha256=N6imWykqHoT5drOn-sVRUzt1AZMKG3HHU-Q7LKz2d50,1260 +pygments/styles/stata_light.py,sha256=YW7ih4teXoEuUHImOK2rGSCVxfnj43tlcKPXUsrDkRg,1292 +pygments/styles/tango.py,sha256=Af0WVTxpH_cUdHr_ua69JDijTLI5aG1RmBTF7o2KTgk,7140 +pygments/styles/trac.py,sha256=-h5iU-LjSmt-dQ7y5jXwJ5LQ9ABR5QD5RlPRjhnzmv8,1984 +pygments/styles/vim.py,sha256=dWqCVC2YT45dLSpMTwLBPkKYgRsT1-cFf7ZHoc2PQpA,2022 +pygments/styles/vs.py,sha256=7HJehhtiHE5nibjC3r3X-aYHBm5BccL33LgO_4anEiU,1133 +pygments/styles/xcode.py,sha256=HmB6aPkxvmo4za5DijhLb5AXSyv6UbWnwOUoRUNBaZA,1507 +pygments/styles/zenburn.py,sha256=Ax7iBbMzvVQNTpT3YdMwCEkL-nIXnpr60aVrR4Lpa6o,2206 +pygments/token.py,sha256=DVil5T2ltHkTgQTUYy1dMtgyigjavCs1ypE4Qf2CrGo,6229 +pygments/unistring.py,sha256=Z4w4HfOVUhueCURRkfhAqQ2b-69UzkhY4xuHevYEy5g,63211 +pygments/util.py,sha256=zk935tJSpwSA9zxNmV1TuhcEP0MfboXiRZSGQnLPEqk,10046 diff --git a/libs/pygments-2.19.2.dist-info/REQUESTED b/libs/pygments-2.20.0.dist-info/REQUESTED similarity index 100% rename from libs/pygments-2.19.2.dist-info/REQUESTED rename to libs/pygments-2.20.0.dist-info/REQUESTED diff --git a/libs/pygments-2.20.0.dist-info/WHEEL b/libs/pygments-2.20.0.dist-info/WHEEL new file mode 100644 index 000000000..bd8422674 --- /dev/null +++ b/libs/pygments-2.20.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/pygments-2.19.2.dist-info/entry_points.txt b/libs/pygments-2.20.0.dist-info/entry_points.txt similarity index 100% rename from libs/pygments-2.19.2.dist-info/entry_points.txt rename to libs/pygments-2.20.0.dist-info/entry_points.txt diff --git a/libs/pygments-2.19.2.dist-info/licenses/AUTHORS b/libs/pygments-2.20.0.dist-info/licenses/AUTHORS similarity index 99% rename from libs/pygments-2.19.2.dist-info/licenses/AUTHORS rename to libs/pygments-2.20.0.dist-info/licenses/AUTHORS index 811c66ae1..909089df6 100644 --- a/libs/pygments-2.19.2.dist-info/licenses/AUTHORS +++ b/libs/pygments-2.20.0.dist-info/licenses/AUTHORS @@ -119,6 +119,7 @@ Other contributors, listed alphabetically, are: * Hiroaki Itoh -- Shell console rewrite, Lexers for PowerShell session, MSDOS session, BC, WDiff * Brian R. Jackson -- Tea lexer +* Alex Jeffery, ChromaWay AB -- Rell lexer * Christian Jann -- ShellSession lexer * Jonas Camillus Jeppesen -- Line numbers and line highlighting for RTF-formatter @@ -257,7 +258,7 @@ Other contributors, listed alphabetically, are: * Matthias Vallentin -- Bro lexer * Benoît Vinot -- AMPL lexer * Linh Vu Hong -- RSL lexer -* Taavi Väänänen -- Debian control lexer +* Taavi Väänänen -- Debian control, PHP lexers * Immanuel Washington -- Smithy lexer * Nathan Weizenbaum -- Haml and Sass lexers * Nathan Whetsell -- Csound lexers diff --git a/libs/pygments-2.19.2.dist-info/licenses/LICENSE b/libs/pygments-2.20.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/pygments-2.19.2.dist-info/licenses/LICENSE rename to libs/pygments-2.20.0.dist-info/licenses/LICENSE diff --git a/libs/pygments/__init__.py b/libs/pygments/__init__.py index 2a391c3e4..3a5ba538b 100644 --- a/libs/pygments/__init__.py +++ b/libs/pygments/__init__.py @@ -21,12 +21,12 @@ .. _Pygments master branch: https://github.com/pygments/pygments/archive/master.zip#egg=Pygments-dev - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ from io import StringIO, BytesIO -__version__ = '2.19.2' +__version__ = '2.20.0' __docformat__ = 'restructuredtext' __all__ = ['lex', 'format', 'highlight'] diff --git a/libs/pygments/__main__.py b/libs/pygments/__main__.py index 4890a6c76..818bfc3ae 100644 --- a/libs/pygments/__main__.py +++ b/libs/pygments/__main__.py @@ -4,7 +4,7 @@ Main entry point for ``python -m pygments``. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/cmdline.py b/libs/pygments/cmdline.py index 2878fd55a..bd4a1fb43 100644 --- a/libs/pygments/cmdline.py +++ b/libs/pygments/cmdline.py @@ -4,7 +4,7 @@ Command line interface. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -185,7 +185,7 @@ def main_inner(parser, argns): return 0 if argns.V: - print(f'Pygments version {__version__}, (c) 2006-2024 by Georg Brandl, Matthäus ' + print(f'Pygments version {__version__}, (c) 2006-present by Georg Brandl, Matthäus ' 'Chajdas and contributors.') return 0 diff --git a/libs/pygments/console.py b/libs/pygments/console.py index ee1ac27a2..d9009168c 100644 --- a/libs/pygments/console.py +++ b/libs/pygments/console.py @@ -4,7 +4,7 @@ Format colored console output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/filter.py b/libs/pygments/filter.py index 5efff438d..6c926c26c 100644 --- a/libs/pygments/filter.py +++ b/libs/pygments/filter.py @@ -4,7 +4,7 @@ Module that implements the default filter. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/filters/__init__.py b/libs/pygments/filters/__init__.py index 2fed761a0..c7ba6adb5 100644 --- a/libs/pygments/filters/__init__.py +++ b/libs/pygments/filters/__init__.py @@ -5,7 +5,7 @@ Module containing filter lookup functions and default filters. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -95,8 +95,10 @@ class CodeTagFilter(Filter): class SymbolFilter(Filter): - """Convert mathematical symbols such as \\ in Isabelle - or \\longrightarrow in LaTeX into Unicode characters. + """Convert mathematical symbols into Unicode characters. + + Examples are ``\\`` in Isabelle or + ``\\longrightarrow`` in LaTeX. This is mostly useful for HTML or console output when you want to approximate the source rendering you'd see in an IDE. @@ -685,8 +687,9 @@ class SymbolFilter(Filter): class KeywordCaseFilter(Filter): - """Convert keywords to lowercase or uppercase or capitalize them, which - means first letter uppercase, rest lowercase. + """Convert keywords to lowercase or uppercase or capitalize them. + + This means first letter uppercase, rest lowercase. This can be useful e.g. if you highlight Pascal code and want to adapt the code to your styleguide. @@ -866,7 +869,7 @@ class VisibleWhitespaceFilter(Filter): class GobbleFilter(Filter): - """Gobbles source code lines (eats initial characters). + """Gobble source code lines (eats initial characters). This filter drops the first ``n`` characters off every line of code. This may be useful when the source code fed to the lexer is indented by a fixed @@ -905,8 +908,7 @@ class GobbleFilter(Filter): class TokenMergeFilter(Filter): - """Merges consecutive tokens with the same token type in the output - stream of a lexer. + """Merge consecutive tokens with the same token type in the output stream. .. versionadded:: 1.2 """ diff --git a/libs/pygments/formatter.py b/libs/pygments/formatter.py index a20d3039d..4fa603d55 100644 --- a/libs/pygments/formatter.py +++ b/libs/pygments/formatter.py @@ -4,7 +4,7 @@ Base formatter class. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/__init__.py b/libs/pygments/formatters/__init__.py index b24931cd1..829496dc9 100644 --- a/libs/pygments/formatters/__init__.py +++ b/libs/pygments/formatters/__init__.py @@ -4,7 +4,7 @@ Pygments formatters. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/bbcode.py b/libs/pygments/formatters/bbcode.py index 339edf9dc..90d8d806f 100644 --- a/libs/pygments/formatters/bbcode.py +++ b/libs/pygments/formatters/bbcode.py @@ -4,7 +4,7 @@ BBcode formatter. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/groff.py b/libs/pygments/formatters/groff.py index 028fec4ea..f5d3146b4 100644 --- a/libs/pygments/formatters/groff.py +++ b/libs/pygments/formatters/groff.py @@ -4,7 +4,7 @@ Formatter for groff output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/html.py b/libs/pygments/formatters/html.py index 4ef183681..17e674145 100644 --- a/libs/pygments/formatters/html.py +++ b/libs/pygments/formatters/html.py @@ -4,7 +4,7 @@ Formatter for HTML output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -18,6 +18,8 @@ from pygments.formatter import Formatter from pygments.token import Token, Text, STANDARD_TYPES from pygments.util import get_bool_opt, get_int_opt, get_list_opt +import html + try: import ctags except ImportError: @@ -70,7 +72,7 @@ def _get_ttype_class(ttype): CSSFILE_TEMPLATE = '''\ /* generated by Pygments -Copyright 2006-2025 by the Pygments team. +Copyright 2006-present by the Pygments team. Licensed under the BSD license, see LICENSE for details. */ %(styledefs)s @@ -81,7 +83,7 @@ DOC_HEADER = '''\ "http://www.w3.org/TR/html4/strict.dtd"> @@ -422,14 +424,14 @@ class HtmlFormatter(Formatter): self.nowrap = get_bool_opt(options, 'nowrap', False) self.noclasses = get_bool_opt(options, 'noclasses', False) self.classprefix = options.get('classprefix', '') - self.cssclass = self._decodeifneeded(options.get('cssclass', 'highlight')) - self.cssstyles = self._decodeifneeded(options.get('cssstyles', '')) + self.cssclass = html.escape(self._decodeifneeded(options.get('cssclass', 'highlight'))) + self.cssstyles = html.escape(self._decodeifneeded(options.get('cssstyles', ''))) self.prestyles = self._decodeifneeded(options.get('prestyles', '')) self.cssfile = self._decodeifneeded(options.get('cssfile', '')) self.noclobber_cssfile = get_bool_opt(options, 'noclobber_cssfile', False) self.tagsfile = self._decodeifneeded(options.get('tagsfile', '')) self.tagurlformat = self._decodeifneeded(options.get('tagurlformat', '')) - self.filename = self._decodeifneeded(options.get('filename', '')) + self.filename = html.escape(self._decodeifneeded(options.get('filename', ''))) self.wrapcode = get_bool_opt(options, 'wrapcode', False) self.span_element_openers = {} self.debug_token_types = get_bool_opt(options, 'debug_token_types', False) @@ -452,9 +454,9 @@ class HtmlFormatter(Formatter): self.linenostep = abs(get_int_opt(options, 'linenostep', 1)) self.linenospecial = abs(get_int_opt(options, 'linenospecial', 0)) self.nobackground = get_bool_opt(options, 'nobackground', False) - self.lineseparator = options.get('lineseparator', '\n') - self.lineanchors = options.get('lineanchors', '') - self.linespans = options.get('linespans', '') + self.lineseparator = html.escape(options.get('lineseparator', '\n')) + self.lineanchors = html.escape(options.get('lineanchors', '')) + self.linespans = html.escape(options.get('linespans', '')) self.anchorlinenos = get_bool_opt(options, 'anchorlinenos', False) self.hl_lines = set() for lineno in get_list_opt(options, 'hl_lines', []): diff --git a/libs/pygments/formatters/img.py b/libs/pygments/formatters/img.py index de4e9ffab..43d841b58 100644 --- a/libs/pygments/formatters/img.py +++ b/libs/pygments/formatters/img.py @@ -4,7 +4,7 @@ Formatter for Pixmap output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os diff --git a/libs/pygments/formatters/irc.py b/libs/pygments/formatters/irc.py index d6ca0699c..ed8dbb22b 100644 --- a/libs/pygments/formatters/irc.py +++ b/libs/pygments/formatters/irc.py @@ -4,7 +4,7 @@ Formatter for IRC output - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -91,7 +91,6 @@ def ircformat(color, text): add += '\x03' + str(IRC_COLOR_MAP[color]).zfill(2) sub = '\x03' + sub return add + text + sub - return '<'+add+'>'+text+'' class IRCFormatter(Formatter): diff --git a/libs/pygments/formatters/latex.py b/libs/pygments/formatters/latex.py index f14234a64..2f8aa6ee2 100644 --- a/libs/pygments/formatters/latex.py +++ b/libs/pygments/formatters/latex.py @@ -4,7 +4,7 @@ Formatter for LaTeX fancyvrb output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/other.py b/libs/pygments/formatters/other.py index 6b0eecacb..ed6fb07bc 100644 --- a/libs/pygments/formatters/other.py +++ b/libs/pygments/formatters/other.py @@ -4,7 +4,7 @@ Other formatters: NullFormatter, RawTokenFormatter. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/pangomarkup.py b/libs/pygments/formatters/pangomarkup.py index 812520944..c5046c6c8 100644 --- a/libs/pygments/formatters/pangomarkup.py +++ b/libs/pygments/formatters/pangomarkup.py @@ -4,7 +4,7 @@ Formatter for Pango markup output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/rtf.py b/libs/pygments/formatters/rtf.py index fe0968db7..bab6066a6 100644 --- a/libs/pygments/formatters/rtf.py +++ b/libs/pygments/formatters/rtf.py @@ -4,7 +4,7 @@ A formatter that generates RTF files. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/svg.py b/libs/pygments/formatters/svg.py index 7a8ec9ad2..04cae80b0 100644 --- a/libs/pygments/formatters/svg.py +++ b/libs/pygments/formatters/svg.py @@ -4,7 +4,7 @@ Formatter for SVG output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/terminal.py b/libs/pygments/formatters/terminal.py index 511c20087..7b501c274 100644 --- a/libs/pygments/formatters/terminal.py +++ b/libs/pygments/formatters/terminal.py @@ -4,7 +4,7 @@ Formatter for terminal output with ANSI sequences. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/formatters/terminal256.py b/libs/pygments/formatters/terminal256.py index 2504b4ae6..cebef764b 100644 --- a/libs/pygments/formatters/terminal256.py +++ b/libs/pygments/formatters/terminal256.py @@ -10,7 +10,7 @@ Formatter version 1. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -169,19 +169,19 @@ class Terminal256Formatter(Formatter): self.xterm_colors.append((0x00, 0xff, 0xff)) # 14 self.xterm_colors.append((0xff, 0xff, 0xff)) # 15 - # colors 16..232: the 6x6x6 color cube + # colors 16..231: the 6x6x6 color cube valuerange = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) - for i in range(217): + for i in range(216): r = valuerange[(i // 36) % 6] g = valuerange[(i // 6) % 6] b = valuerange[i % 6] self.xterm_colors.append((r, g, b)) - # colors 233..253: grayscale + # colors 232..255: grayscale - for i in range(1, 22): + for i in range(24): v = 8 + i * 10 self.xterm_colors.append((v, v, v)) @@ -189,7 +189,7 @@ class Terminal256Formatter(Formatter): distance = 257*257*3 # "infinity" (>distance from #000000 to #ffffff) match = 0 - for i in range(0, 254): + for i in range(0, 256): values = self.xterm_colors[i] rd = r - values[0] diff --git a/libs/pygments/lexer.py b/libs/pygments/lexer.py index 453d675a3..45e6585c9 100644 --- a/libs/pygments/lexer.py +++ b/libs/pygments/lexer.py @@ -4,7 +4,7 @@ Base lexer classes. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -444,8 +444,9 @@ def using(_other, **kwargs): # function has to create a new lexer instance if kwargs: # XXX: cache that somehow - kwargs.update(lexer.options) - lx = lexer.__class__(**kwargs) + d = dict(lexer.options) + d.update(kwargs) + lx = lexer.__class__(**d) else: lx = lexer s = match.start() @@ -456,8 +457,9 @@ def using(_other, **kwargs): else: def callback(lexer, match, ctx=None): # XXX: cache that somehow - kwargs.update(lexer.options) - lx = _other(**kwargs) + d = dict(lexer.options) + d.update(kwargs) + lx = _other(**d) s = match.start() for i, t, v in lx.get_tokens_unprocessed(match.group(), **gt_kwargs): diff --git a/libs/pygments/lexers/__init__.py b/libs/pygments/lexers/__init__.py index da5100095..2a10d7c78 100644 --- a/libs/pygments/lexers/__init__.py +++ b/libs/pygments/lexers/__init__.py @@ -4,7 +4,7 @@ Pygments lexers. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_ada_builtins.py b/libs/pygments/lexers/_ada_builtins.py index a2491fb38..bf76c4216 100644 --- a/libs/pygments/lexers/_ada_builtins.py +++ b/libs/pygments/lexers/_ada_builtins.py @@ -4,7 +4,7 @@ Ada builtins. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_asy_builtins.py b/libs/pygments/lexers/_asy_builtins.py index 976e1bc62..9f9b3a6cd 100644 --- a/libs/pygments/lexers/_asy_builtins.py +++ b/libs/pygments/lexers/_asy_builtins.py @@ -9,7 +9,7 @@ TODO: perl/python script in Asymptote SVN similar to asy-list.pl but only for function and variable names. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_cl_builtins.py b/libs/pygments/lexers/_cl_builtins.py index 6e4516d68..1a8ac20b6 100644 --- a/libs/pygments/lexers/_cl_builtins.py +++ b/libs/pygments/lexers/_cl_builtins.py @@ -4,7 +4,7 @@ ANSI Common Lisp builtins. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_cocoa_builtins.py b/libs/pygments/lexers/_cocoa_builtins.py index ef0a9682f..1398d7d3b 100644 --- a/libs/pygments/lexers/_cocoa_builtins.py +++ b/libs/pygments/lexers/_cocoa_builtins.py @@ -7,7 +7,7 @@ File may be also used as standalone generator for above. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_csound_builtins.py b/libs/pygments/lexers/_csound_builtins.py index 3431819ed..9f0ea6ae3 100644 --- a/libs/pygments/lexers/_csound_builtins.py +++ b/libs/pygments/lexers/_csound_builtins.py @@ -2,7 +2,7 @@ pygments.lexers._csound_builtins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_css_builtins.py b/libs/pygments/lexers/_css_builtins.py index 72953195a..4e8149347 100644 --- a/libs/pygments/lexers/_css_builtins.py +++ b/libs/pygments/lexers/_css_builtins.py @@ -4,7 +4,7 @@ This file is autogenerated by scripts/get_css_properties.py - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_googlesql_builtins.py b/libs/pygments/lexers/_googlesql_builtins.py index 13ef3a2b0..250c1a39f 100644 --- a/libs/pygments/lexers/_googlesql_builtins.py +++ b/libs/pygments/lexers/_googlesql_builtins.py @@ -4,7 +4,7 @@ Autogenerated data files for the GoogleSQL lexer. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_julia_builtins.py b/libs/pygments/lexers/_julia_builtins.py index 2849afe58..cbb83d78c 100644 --- a/libs/pygments/lexers/_julia_builtins.py +++ b/libs/pygments/lexers/_julia_builtins.py @@ -4,7 +4,7 @@ Julia builtins. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_lasso_builtins.py b/libs/pygments/lexers/_lasso_builtins.py index a559847e3..eb3b7edd0 100644 --- a/libs/pygments/lexers/_lasso_builtins.py +++ b/libs/pygments/lexers/_lasso_builtins.py @@ -4,7 +4,7 @@ Built-in Lasso types, traits, methods, and members. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_lilypond_builtins.py b/libs/pygments/lexers/_lilypond_builtins.py index b00dbbfd4..2d5074ff4 100644 --- a/libs/pygments/lexers/_lilypond_builtins.py +++ b/libs/pygments/lexers/_lilypond_builtins.py @@ -4,12 +4,12 @@ LilyPond builtins. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ -# Contents generated by the script lilypond-builtins-generator.ly -# found in the external/ directory of the source tree. +# Contents generated by the script `lilypond-builtins-generator.ly` +# found in the `external/` directory of the source tree. keywords = [ "accepts", @@ -84,7 +84,13 @@ clefs = [ "mensural-c4", "mensural-c5", "mensural-f", + "mensural-f2", + "mensural-f3", + "mensural-f4", + "mensural-f5", "mensural-g", + "mensural-g1", + "mensural-g2", "mezzosoprano", "moderntab", "neomensural-c1", @@ -160,7 +166,6 @@ chord_modifiers = [ ] pitch_language_names = [ - "arabic", "catalan", "català", "deutsch", @@ -186,12 +191,8 @@ pitches = [ "a-natural", "a-sharp", "a-sharpsharp", - "ab", "acousticbassdrum", "acousticsnare", - "ad", - "adb", - "add", "aeh", "aes", "aeseh", @@ -201,8 +202,6 @@ pitches = [ "aessess", "af", "aff", - "afhb", - "afhd", "agh", "agl", "ah", @@ -220,13 +219,9 @@ pitches = [ "asas", "aseh", "ases", - "ashb", - "ashd", "ass", "asseh", "assess", - "atqb", - "atqd", "atqf", "atqs", "ax", @@ -240,8 +235,6 @@ pitches = [ "bb", "bd", "bda", - "bdb", - "bdd", "beh", "bes", "beseh", @@ -249,8 +242,6 @@ pitches = [ "bess", "bf", "bff", - "bfhb", - "bfhd", "bih", "bis", "bisih", @@ -264,11 +255,7 @@ pitches = [ "bqf", "bqs", "bs", - "bshb", - "bshd", "bss", - "btqb", - "btqd", "btqf", "btqs", "bx", @@ -281,9 +268,6 @@ pitches = [ "cab", "cabasa", "cb", - "cd", - "cdb", - "cdd", "ceh", "ces", "ceseh", @@ -293,8 +277,6 @@ pitches = [ "cessess", "cf", "cff", - "cfhb", - "cfhd", "cgh", "cghm", "cgho", @@ -319,11 +301,7 @@ pitches = [ "crashcymbala", "crashcymbalb", "cs", - "cshb", - "cshd", "css", - "ctqb", - "ctqd", "ctqf", "ctqs", "cuim", @@ -343,10 +321,6 @@ pitches = [ "d-natural", "d-sharp", "d-sharpsharp", - "db", - "dd", - "ddb", - "ddd", "deh", "des", "deseh", @@ -356,8 +330,6 @@ pitches = [ "dessess", "df", "dff", - "dfhb", - "dfhd", "dih", "dis", "disih", @@ -400,11 +372,7 @@ pitches = [ "dqf", "dqs", "ds", - "dshb", - "dshd", "dss", - "dtqb", - "dtqd", "dtqf", "dtqs", "dx", @@ -414,10 +382,6 @@ pitches = [ "e-natural", "e-sharp", "e-sharpsharp", - "eb", - "ed", - "edb", - "edd", "eeh", "ees", "eeseh", @@ -427,8 +391,6 @@ pitches = [ "eessess", "ef", "eff", - "efhb", - "efhd", "eh", "eih", "eis", @@ -443,13 +405,9 @@ pitches = [ "es", "eseh", "eses", - "eshb", - "eshd", "ess", "esseh", "essess", - "etqb", - "etqd", "etqf", "etqs", "ex", @@ -491,10 +449,6 @@ pitches = [ "fatqd", "fatqs", "fax", - "fb", - "fd", - "fdb", - "fdd", "feh", "fes", "feseh", @@ -504,8 +458,6 @@ pitches = [ "fessess", "ff", "fff", - "ffhb", - "ffhd", "fih", "fis", "fisih", @@ -516,11 +468,7 @@ pitches = [ "fqf", "fqs", "fs", - "fshb", - "fshd", "fss", - "ftqb", - "ftqd", "ftqf", "ftqs", "fx", @@ -530,10 +478,6 @@ pitches = [ "g-natural", "g-sharp", "g-sharpsharp", - "gb", - "gd", - "gdb", - "gdd", "geh", "ges", "geseh", @@ -543,8 +487,6 @@ pitches = [ "gessess", "gf", "gff", - "gfhb", - "gfhd", "gih", "gis", "gisih", @@ -555,11 +497,7 @@ pitches = [ "gqf", "gqs", "gs", - "gshb", - "gshd", "gss", - "gtqb", - "gtqd", "gtqf", "gtqs", "gui", @@ -581,6 +519,7 @@ pitches = [ "hhho", "hho", "hhp", + "hhs", "hiagogo", "hibongo", "hiconga", @@ -809,6 +748,7 @@ pitches = [ "soltqs", "solx", "splashcymbal", + "splashhihat", "ss", "ssh", "ssl", @@ -851,12 +791,16 @@ music_functions = [ "alternative", "ambitusAfter", "appendToTag", + "appendToTagMarkup", "applyContext", "applyMusic", "applyOutput", "appoggiatura", "assertBeamQuant", "assertBeamSlope", + "atLeft", + "atRight", + "augmentum", "autoChange", "balloonGrobText", "balloonText", @@ -876,6 +820,7 @@ music_functions = [ "codaMark", "compoundMeter", "compressMMRests", + "contextPropertyCheck", "crossStaff", "cueClef", "cueClefUnset", @@ -904,6 +849,7 @@ music_functions = [ "inStaffSegno", "incipit", "inherit-acceptability", + "initialContextFrom", "instrumentSwitch", "inversion", "invertChords", @@ -915,6 +861,7 @@ music_functions = [ "language", "languageRestore", "languageSaveAndChange", + "ligature", "magnifyMusic", "magnifyStaff", "makeClusters", @@ -923,6 +870,7 @@ music_functions = [ "markupMap", "modalInversion", "modalTranspose", + "musicLength", "musicMap", "noPageBreak", "noPageTurn", @@ -950,6 +898,7 @@ music_functions = [ "pointAndClickOff", "pointAndClickOn", "pointAndClickTypes", + "popContextProperty", "preBend", "preBendHold", "propertyOverride", @@ -957,7 +906,9 @@ music_functions = [ "propertySet", "propertyTweak", "propertyUnset", + "pushContextProperty", "pushToTag", + "pushToTagMarkup", "quoteDuring", "raiseNote", "reduceChords", @@ -965,6 +916,7 @@ music_functions = [ "removeWithTag", "repeat", "resetRelativeOctave", + "responsum", "retrograde", "revert", "revertTimeSignatureSettings", @@ -1006,11 +958,13 @@ music_functions = [ "unfoldRepeats", "unfolded", "unset", + "versus", "voices", "void", "volta", "vshape", "withMusicProperty", + "withRelativeDir", "xNote", ] @@ -1060,8 +1014,11 @@ articulations = [ "]", "^", "accent", + "accentus", "arpeggio", + "bachschleifer", "breakDynamicSpan", + "circulus", "coda", "dashBang", "dashDash", @@ -1085,6 +1042,7 @@ articulations = [ "haydnturn", "henzelongfermata", "henzeshortfermata", + "ictus", "laissezVibrer", "lheel", "lineprall", @@ -1105,6 +1063,7 @@ articulations = [ "rheel", "rtoe", "segno", + "semicirculus", "shortfermata", "signumcongruentiae", "slashturn", @@ -1141,6 +1100,8 @@ articulations = [ ] music_commands = [ + "IIJ", + "IJ", "[", "]", "aikenHeads", @@ -1155,6 +1116,8 @@ music_commands = [ "arpeggioNormal", "arpeggioParenthesis", "arpeggioParenthesisDashed", + "ascendens", + "auctum", "autoBeamOff", "autoBeamOn", "autoBreaksOff", @@ -1173,6 +1136,7 @@ music_commands = [ "break", "cadenzaOff", "cadenzaOn", + "cavum", "compressEmptyMeasures", "crescHairpin", "crescTextCresc", @@ -1180,14 +1144,19 @@ music_commands = [ "deadNotesOn", "defaultNoteHeads", "defaultTimeSignature", + "deminutum", "deprecatedcresc", "deprecateddim", "deprecatedendcresc", "deprecatedenddim", + "descendens", "dimHairpin", "dimTextDecr", "dimTextDecresc", "dimTextDim", + "divisioMaior", + "divisioMaxima", + "divisioMinima", "dotsDown", "dotsNeutral", "dotsUp", @@ -1198,7 +1167,11 @@ music_commands = [ "easyHeadsOn", "endSkipNCs", "expandEmptyMeasures", + "finalis", "fine", + "flagStyleDefault", + "flagStyleStacked", + "flexa", "frenchChords", "funkHeads", "funkHeadsMinor", @@ -1209,12 +1182,16 @@ music_commands = [ "hideStaffSwitch", "huge", "ignatzekExceptionMusic", + "iij", + "ij", "improvisationOff", "improvisationOn", + "inclinatum", "italianChords", "kievanOff", "kievanOn", "large", + "linea", "markLengthOff", "markLengthOn", "medianChordGridStyle", @@ -1229,6 +1206,7 @@ music_commands = [ "normalsize", "numericTimeSignature", "oneVoice", + "oriscus", "palmMuteOff", "partCombineApart", "partCombineAutomatic", @@ -1236,6 +1214,7 @@ music_commands = [ "partCombineSoloI", "partCombineSoloII", "partCombineUnisono", + "pes", "phrasingSlurDashed", "phrasingSlurDotted", "phrasingSlurDown", @@ -1246,11 +1225,14 @@ music_commands = [ "phrasingSlurUp", "predefinedFretboardsOff", "predefinedFretboardsOn", + "quilisma", "romanStringNumbers", "sacredHarpHeads", "sacredHarpHeadsMinor", "section", "semiGermanChords", + "senzaMisuraTimeSignatureOff", + "senzaMisuraTimeSignatureX", "setDefaultDurationToQuarter", "shiftOff", "shiftOn", @@ -1276,6 +1258,7 @@ music_commands = [ "startGraceMusic", "startMeasureCount", "startMeasureSpanner", + "startOptionalMaterial", "startSlashedGraceMusic", "startStaff", "stemDown", @@ -1286,9 +1269,11 @@ music_commands = [ "stopGraceMusic", "stopMeasureCount", "stopMeasureSpanner", + "stopOptionalMaterial", "stopSlashedGraceMusic", "stopStaff", "stopStaffHighlight", + "stropha", "tabFullNotation", "teeny", "textLengthOff", @@ -1309,6 +1294,8 @@ music_commands = [ "tupletNeutral", "tupletUp", "unHideNotes", + "virga", + "virgula", "voiceFour", "voiceFourStyle", "voiceNeutralStyle", @@ -1328,11 +1315,16 @@ music_commands = [ markup_commands = [ "abs-fontsize", + "abs-hspace", + "abs-vspace", "accidental", "align-on-other", + "annotate-moving", + "append-to-tag", "arrow-head", "auto-footnote", "backslashed-digit", + "bar-line", "beam", "bold", "box", @@ -1389,12 +1381,14 @@ markup_commands = [ "hspace", "huge", "if", + "image", "italic", "justified-lines", "justify", "justify-field", "justify-line", "justify-string", + "keep-with-tag", "large", "larger", "left-align", @@ -1409,13 +1403,13 @@ markup_commands = [ "markletter", "markup", "markuplist", - "medium", "multi-measure-rest-by-number", "musicglyph", "natural", "normal-size-sub", "normal-size-super", "normal-text", + "normal-weight", "normalsize", "note", "note-by-number", @@ -1439,8 +1433,11 @@ markup_commands = [ "polygon", "postscript", "property-recursive", + "push-to-tag", "put-adjacent", + "qr-code", "raise", + "remove-with-tag", "replace", "rest", "rest-by-number", @@ -1448,7 +1445,6 @@ markup_commands = [ "right-align", "right-brace", "right-column", - "roman", "rotate", "rounded-box", "sans", @@ -1458,6 +1454,7 @@ markup_commands = [ "segno", "semiflat", "semisharp", + "serif", "sesquiflat", "sesquisharp", "sharp", @@ -1477,8 +1474,15 @@ markup_commands = [ "super", "table", "table-of-contents", + "tag", + "tag-list", "teeny", - "text", + "text-accidental", + "text-doubleflat", + "text-doublesharp", + "text-flat", + "text-natural", + "text-sharp", "tie", "tied-lyric", "tiny", @@ -1494,6 +1498,7 @@ markup_commands = [ "varcoda", "vcenter", "verbatim-file", + "volta-number", "vspace", "whiteout", "with-color", @@ -1614,6 +1619,7 @@ grobs = [ "NoteHead", "NoteName", "NoteSpacing", + "OptionalMaterialBracket", "OttavaBracket", "PaperColumn", "Parentheses", @@ -1700,6 +1706,7 @@ contexts = [ "GregorianTranscriptionStaff", "GregorianTranscriptionVoice", "InternalGregorianStaff", + "InternalMensuralStaff", "KievanStaff", "KievanVoice", "Lyrics", @@ -1722,6 +1729,7 @@ contexts = [ "TabVoice", "Timing", "VaticanaLyrics", + "VaticanaScore", "VaticanaStaff", "VaticanaVoice", "Voice", @@ -1792,6 +1800,7 @@ translators = [ "Grid_point_engraver", "Grob_pq_engraver", "Horizontal_bracket_engraver", + "Horizontal_script_engraver", "Hyphen_engraver", "Instrument_name_engraver", "Instrument_switch_engraver", @@ -1826,6 +1835,7 @@ translators = [ "Note_name_engraver", "Note_performer", "Note_spacing_engraver", + "Optional_material_bracket_engraver", "Ottava_spanner_engraver", "Output_property_engraver", "Page_turn_engraver", @@ -1896,6 +1906,7 @@ scheme_functions = [ "*parser*", "Alteration_glyph_engraver", "Beat_performer", + "Bend_engraver", "Bend_spanner_engraver", "Breathing_sign_engraver", "Centered_bar_number_align_engraver", @@ -1907,21 +1918,27 @@ scheme_functions = [ "Finger_glide_engraver", "G_", "Grid_chord_name_engraver", + "Horizontal_script_engraver", "Lyric_repeat_count_engraver", "Measure_counter_engraver", + "Measure_grouping_engraver", "Measure_spanner_engraver", "Merge_mmrest_numbers_engraver", "Merge_rests_engraver", + "Optional_material_bracket_engraver", "Show_control_points_engraver", "Signum_repetitionis_engraver", "Skip_typesetting_engraver", "Span_stem_engraver", "Spanner_tracking_engraver", "Staff_highlight_engraver", + "Tab_tie_follow_engraver", "Text_mark_engraver", "Trill_spanner_engraver", "_i", "abs-fontsize-markup", + "abs-hspace-markup", + "abs-vspace-markup", "accidental->markup", "accidental->markup-italian", "accidental-interface::calc-alteration", @@ -1929,12 +1946,9 @@ scheme_functions = [ "accidental-invalid?", "accidental-markup", "add-bar-glyph-print-procedure", - "add-font", "add-grace-property", "add-music", - "add-music-fonts", "add-new-clef", - "add-pango-fonts", "add-point", "add-quotable", "add-score", @@ -1946,6 +1960,7 @@ scheme_functions = [ "align-on-other-markup", "aligned-text-stencil-function", "alist->hash-table", + "alist-keys", "alistbyte-list", + "bit-list->int", "bold-markup", "book-first-page", + "boolean-or-fraction?", "boolean-or-number?", + "boolean-or-symbol-or-symbol-list?", "boolean-or-symbol?", "bounding-note-heads-pitches", "box-grob-stencil", @@ -2053,20 +2079,23 @@ scheme_functions = [ "break-alignment-list", "breathe::midi-length", "buildflag", + "byte-list->bit-list", "cached-file-contents", "caesura-script-interface::before-line-breaking", "caesura-to-bar-line-or-divisio", "caesura-to-divisio", "calc-harmonic-pitch", "calc-line-thickness", + "calc-measure-length", + "calc-pattern-element", "calc-repeat-slash-count", - "calculate-complex-compound-time", - "calculate-compound-base-beat", - "calculate-compound-base-beat-full", + "calculate-compound-beat-base", + "calculate-compound-beat-base-as-moment", "calculate-compound-beat-grouping", "calculate-compound-measure-length", - "calculate-time-fraction", + "calculate-compound-measure-length-as-moment", "call-after-session", + "call-with-output-bytevector", "caps-markup", "car-or-identity", "car<", @@ -2077,6 +2106,7 @@ scheme_functions = [ "centered-spanner-interface::calc-x-offset", "centered-stencil", "chain-assoc-get", + "change-el->music", "change-pitches", "char-markup", "cheap-list?", @@ -2123,12 +2153,14 @@ scheme_functions = [ "clarinet-rh-one-key-stencil", "clarinet-rh-three-key-stencil", "clarinet-rh-two-key-stencil", + "clef-modifier::print", "clef-transposition-markup", "clef::print-modern-tab-if-set", "clip-systems-to-region-stencils", "clipped-systems-stencils", "close-enough?", "close-port-rename", + "closest-staff-line", "coda-markup", "collect-book-music-for-book", "collect-bookpart-for-book", @@ -2140,6 +2172,7 @@ scheme_functions = [ "column-lines-markup-list", "column-markup", "combine-markup", + "combine-tag-markup-additions", "comparable-note-events", "comparator-from-key", "compile-all-markup-args", @@ -2180,6 +2213,7 @@ scheme_functions = [ "cue-substitute", "current-or-previous-voice-states", "customTabClef-markup", + "custos::print", "cyclic-base-value", "debugf", "def-grace-function", @@ -2187,6 +2221,8 @@ scheme_functions = [ "default-flag", "default-paren-color", "define-bar-line", + "define-color", + "define-deprecated-property", "define-event-class", "define-event-function", "define-fonts", @@ -2206,6 +2242,10 @@ scheme_functions = [ "define-void-function", "degree-first-true", "degrees->radians", + "deprecated-getter-object-property", + "deprecated-setter-object-property", + "deprecated-translation-getter-description", + "deprecated-translation-setter-description", "descend-to-context", "determine-frets", "determine-split-list", @@ -2214,6 +2254,7 @@ scheme_functions = [ "dimension-arrows", "dir-basename", "dir-column-markup", + "direction-scaled", "display-lily-music", "display-music", "display-scheme-music", @@ -2238,7 +2279,6 @@ scheme_functions = [ "dump-zombies", "duration", "duration-dot-factor", - "duration-length", "duration-line::calc", "duration-line::calc-thickness", "duration-line::print", @@ -2253,8 +2293,8 @@ scheme_functions = [ "ellipse-markup", "ellipse-radius", "ellipse-stencil", - "empty-music", "end-broken-spanner?", + "ensure-list", "entry-greater-than-x?", "eps-file->stencil", "epsfile-markup", @@ -2280,7 +2320,9 @@ scheme_functions = [ "extract-typed-music", "eyeglasses-markup", "fermata-markup", + "figured-bass-continuation::print", "figured-bass-markup", + "fill-integer-interval", "fill-line-markup", "fill-with-pattern-markup", "filled-box-markup", @@ -2288,6 +2330,7 @@ scheme_functions = [ "find-pitch-entry", "find-value-to-offset", "finger-glide::print", + "finger-key-glide", "finger-markup", "fingering::calc-text", "first-assoc", @@ -2300,6 +2343,7 @@ scheme_functions = [ "flared-hairpin", "flat-flag", "flat-markup", + "flat-zip-longest", "flatten-alist", "flatten-list", "flip-stencil", @@ -2316,11 +2360,9 @@ scheme_functions = [ "flute-rh-ees-key-stencil", "flute-rh-gz-key-stencil", "fold-some-music", - "font-children", - "font-default", + "fold-values", "font-name-split", "font-name-style", - "font-qualifier", "fontCaps-markup", "fontsize-markup", "footnote-markup", @@ -2329,7 +2371,6 @@ scheme_functions = [ "format", "format-bass-figure", "format-coda-mark", - "format-compound-time", "format-dal-segno-text", "format-dal-segno-text-brief", "format-mark-alphabet", @@ -2349,10 +2390,6 @@ scheme_functions = [ "format-segno-mark", "format-segno-mark-considering-bar-lines", "format-sign-with-number", - "format-time-element", - "format-time-fraction", - "format-time-list", - "format-time-numerator", "format-varcoda-mark", "fraction->moment", "fraction-markup", @@ -2372,7 +2409,6 @@ scheme_functions = [ "fromproperty-markup", "function-chain", "g", - "g-lookup-font", "general-align-markup", "general-column", "generate-bassoon-family-entry", @@ -2388,7 +2424,7 @@ scheme_functions = [ "get-chord-shape", "get-current-filename", "get-current-suffix", - "get-fill-space", + "get-fill-spaces", "get-key", "get-named-spreadsheet-column", "get-next-unique-voice-name", @@ -2420,6 +2456,7 @@ scheme_functions = [ "grob::all-objects", "grob::calc-property-by-copy", "grob::compose-function", + "grob::directional-value", "grob::display-objects", "grob::has-interface", "grob::inherit-parent-property", @@ -2427,6 +2464,7 @@ scheme_functions = [ "grob::name", "grob::objects-from-interface", "grob::offset-function", + "grob::relay-directional-property", "grob::relay-other-property", "grob::rhythmic-location", "grob::show-skylines-if-debug-skylines-set", @@ -2435,8 +2473,10 @@ scheme_functions = [ "group-automate-rule", "group-draw-rule", "group-extra-offset-rule", + "group-into-ranges", "gs-cmd-args", "gs-safe-run", + "gulp-file-with-encoding", "hairpin::calc-grow-direction", "halign-markup", "harp-pedal-check", @@ -2451,15 +2491,21 @@ scheme_functions = [ "header-to-file", "headers-property-alist-chain", "hook-stencil", + "horizontal-bracket::calc-break-overshoot", + "horizontal-script::calc-staff-position", + "horizontal-script::extra-spacing-height", "horizontal-slash-interval", "hspace-markup", "huge-markup", "if-markup", "ignatzek-chord-names", + "image-markup", "index-cell", + "index-map", "index-or-markup?", "index?", "insert-markups", + "int->bit-list", "internal-set-paper-size", "interpret-markup", "interpret-markup-list", @@ -2488,6 +2534,7 @@ scheme_functions = [ "justify-line-markup", "justify-markup", "justify-string-markup", + "keep-with-tag-markup", "key-crawler", "key-entry-alteration", "key-entry-bar-number", @@ -2504,12 +2551,15 @@ scheme_functions = [ "keyword->make-markup", "large-markup", "larger-markup", + "last-or-identity", "layout-blot-diameter", "layout-extract-page-properties", "layout-line-thickness", "layout-set-absolute-staff-size", "layout-set-absolute-staff-size-in-module", "layout-set-staff-size", + "ledger-lines::positions-from-ledgered-grob", + "left-align-at-split-notes", "left-align-markup", "left-brace-markup", "left-column-markup", @@ -2526,10 +2576,12 @@ scheme_functions = [ "list-element-index", "list-insert-separator", "list-join", + "list-map", + "list-pad-left", + "list-pad-right", "listener->once-listener", "little-elliptical-key-stencil", "long-midline-stencil", - "lookup-font", "lookup-markup", "lookup-markup-command", "lookup-markup-command-aux", @@ -2557,6 +2609,7 @@ scheme_functions = [ "ly:all-stencil-expressions", "ly:alternative-sequence-iterator::constructor", "ly:angle", + "ly:append-to-option", "ly:apply-context-iterator::constructor", "ly:arpeggio::brew-chord-bracket", "ly:arpeggio::brew-chord-slur", @@ -2583,15 +2636,17 @@ scheme_functions = [ "ly:balloon-interface::pure-height", "ly:balloon-interface::remove-irrelevant-spanner", "ly:balloon-interface::width", - "ly:bar-check-iterator::constructor", "ly:bar-line::calc-anchor", "ly:bar-line::calc-bar-extent", + "ly:bar-line::calc-short-bar-extent", "ly:bar-line::print", + "ly:base64-encode", "ly:basic-progress", "ly:beam::calc-beam-segments", "ly:beam::calc-beaming", "ly:beam::calc-cross-staff", "ly:beam::calc-direction", + "ly:beam::calc-knee", "ly:beam::calc-normal-stems", "ly:beam::calc-stem-shorten", "ly:beam::calc-x-positions", @@ -2600,6 +2655,7 @@ scheme_functions = [ "ly:beam::quanting", "ly:beam::rest-collision-callback", "ly:beam::set-stem-lengths", + "ly:beam::tremolo-springs-and-rods", "ly:bezier-extent", "ly:bezier-extract", "ly:book-add-bookpart!", @@ -2648,6 +2704,7 @@ scheme_functions = [ "ly:command-line-code", "ly:command-line-options", "ly:connect-dispatchers", + "ly:context-children", "ly:context-current-moment", "ly:context-def-lookup", "ly:context-def-modify", @@ -2664,8 +2721,13 @@ scheme_functions = [ "ly:context-output-def", "ly:context-parent", "ly:context-property", + "ly:context-property-pop", + "ly:context-property-push", + "ly:context-property-stack-pop", + "ly:context-property-stack-push", "ly:context-property-where-defined", "ly:context-pushpop-property", + "ly:context-schedule-moment", "ly:context-set-property!", "ly:context-specced-music-iterator::constructor", "ly:context-unset-property", @@ -2680,11 +2742,12 @@ scheme_functions = [ "ly:dispatcher?", "ly:dot-column::calc-positioning-done", "ly:dots::print", + "ly:duration->moment", + "ly:duration->number", "ly:duration->string", "ly:duration-compress", "ly:duration-dot-count", "ly:duration-factor", - "ly:duration-length", "ly:duration-log", "ly:duration-scale", "ly:duration::less?", @@ -2702,6 +2765,7 @@ scheme_functions = [ "ly:event-chord-iterator::constructor", "ly:event-deep-copy", "ly:event-iterator::constructor", + "ly:event-length", "ly:event-property", "ly:event-set-property!", "ly:event-warning", @@ -2710,7 +2774,6 @@ scheme_functions = [ "ly:expect-warning", "ly:extract-subfont-from-collection", "ly:figured-bass-continuation::center-on-figures", - "ly:figured-bass-continuation::print", "ly:find-file", "ly:fine-iterator::constructor", "ly:fingering-column::calc-positioning-done", @@ -2727,13 +2790,11 @@ scheme_functions = [ "ly:font-design-size", "ly:font-file-name", "ly:font-get-glyph", - "ly:font-glyph-name-to-charcode", "ly:font-glyph-name-to-index", "ly:font-index-to-charcode", "ly:font-magnification", "ly:font-metric?", "ly:font-name", - "ly:font-sub-fonts", "ly:format", "ly:format-output", "ly:generic-bound-extent", @@ -2772,6 +2833,7 @@ scheme_functions = [ "ly:grob-property-data", "ly:grob-pure-height", "ly:grob-pure-property", + "ly:grob-pure-relative-coordinate", "ly:grob-relative-coordinate", "ly:grob-robust-relative-extent", "ly:grob-script-priority-less", @@ -2823,6 +2885,7 @@ scheme_functions = [ "ly:horizontal-line-spanner::calc-right-bound-info", "ly:in-event-class?", "ly:inch", + "ly:initial-context-music-iterator::constructor", "ly:input-both-locations", "ly:input-file-line-char-column", "ly:input-location?", @@ -2874,10 +2937,11 @@ scheme_functions = [ "ly:make-output-def", "ly:make-page-label-marker", "ly:make-page-permission-marker", - "ly:make-pango-description-string", "ly:make-paper-outputter", "ly:make-pitch", "ly:make-prob", + "ly:make-rand-seed", + "ly:make-regex", "ly:make-rotation", "ly:make-scale", "ly:make-scaling", @@ -2886,11 +2950,11 @@ scheme_functions = [ "ly:make-spring", "ly:make-stencil", "ly:make-stream-event", + "ly:make-tmpfile-name", "ly:make-transform", "ly:make-translation", "ly:make-unpure-pure-container", "ly:measure-grouping::print", - "ly:measure-spanner::calc-connect-to-neighbors", "ly:measure-spanner::print", "ly:melody-spanner::calc-neutral-stem-direction", "ly:mensural-ligature::brew-ligature-primitive", @@ -2961,6 +3025,8 @@ scheme_functions = [ "ly:note-head::print", "ly:note-head::stem-attachment", "ly:note-head::stem-x-shift", + "ly:note-scale?", + "ly:number->duration", "ly:number->string", "ly:number-pair->string", "ly:one-line-auto-height-breaking", @@ -2982,6 +3048,7 @@ scheme_functions = [ "ly:output-def-set-variable!", "ly:output-def?", "ly:output-description", + "ly:output-file-name-for-input-file-name", "ly:output-find-context-def", "ly:outputter-close", "ly:outputter-dump-stencil", @@ -3006,12 +3073,12 @@ scheme_functions = [ "ly:paper-get-number", "ly:paper-outputscale", "ly:paper-score-paper-systems", - "ly:paper-system-minimum-distance", "ly:paper-system?", "ly:parse-file", "ly:parse-init", "ly:parse-string-expression", "ly:parsed-undead-list!", + "ly:parser-append-to-include-path", "ly:parser-clear-error", "ly:parser-clone", "ly:parser-define!", @@ -3023,8 +3090,6 @@ scheme_functions = [ "ly:parser-parse-string", "ly:parser-set-note-names", "ly:part-combine-iterator::constructor", - "ly:partial-iterator::constructor", - "ly:partial-iterator::finalization", "ly:percent-repeat-interface::beat-slash", "ly:percent-repeat-interface::double-percent", "ly:percent-repeat-interface::percent", @@ -3043,9 +3108,10 @@ scheme_functions = [ "ly:pitch-steps", "ly:pitch-tones", "ly:pitch-transpose", - "ly:pitch::less?", "ly:pitcheps-dump", + "ly:png-dimensions", "ly:pointer-group-interface::add-grob", "ly:pop-property-iterator::constructor", "ly:position-on-line?", @@ -3066,7 +3132,17 @@ scheme_functions = [ "ly:pure-from-neighbor-interface::calc-pure-relevant-grobs", "ly:push-property-iterator::constructor", "ly:quote-iterator::constructor", - "ly:randomize-rand-seed", + "ly:regex-exec", + "ly:regex-exec->list", + "ly:regex-match-positions", + "ly:regex-match-prefix", + "ly:regex-match-substring", + "ly:regex-match-suffix", + "ly:regex-match?", + "ly:regex-quote", + "ly:regex-replace", + "ly:regex-split", + "ly:regex?", "ly:register-stencil-expression", "ly:register-translator", "ly:relative-group-extent", @@ -3075,6 +3151,7 @@ scheme_functions = [ "ly:relative-octave-music::relative-callback", "ly:rename-file", "ly:reset-all-fonts", + "ly:reset-options", "ly:rest-collision::calc-positioning-done", "ly:rest-collision::force-shift-callback-rest", "ly:rest::calc-cross-staff", @@ -3113,7 +3190,6 @@ scheme_functions = [ "ly:semi-tie::calc-control-points", "ly:separation-item::calc-skylines", "ly:sequential-iterator::constructor", - "ly:set-color-names", "ly:set-default-scale", "ly:set-grob-creation-callback", "ly:set-grob-modification-callback", @@ -3121,6 +3197,7 @@ scheme_functions = [ "ly:set-option", "ly:set-origin!", "ly:set-property-cache-callback", + "ly:set-rand-seed", "ly:side-position-interface::calc-cross-staff", "ly:side-position-interface::move-to-extremal-staff", "ly:side-position-interface::pure-y-aligned-side", @@ -3164,8 +3241,10 @@ scheme_functions = [ "ly:span-bar::width", "ly:spanner-bound", "ly:spanner-broken-into", + "ly:spanner-broken-neighbor", "ly:spanner-set-bound!", "ly:spanner::bounds-width", + "ly:spanner::calc-connect-to-neighbors", "ly:spanner::calc-normalized-endpoints", "ly:spanner::kill-zero-spanned-time", "ly:spanner::set-spacing-rods", @@ -3178,6 +3257,7 @@ scheme_functions = [ "ly:staff-symbol-referencer::callback", "ly:staff-symbol-staff-radius", "ly:staff-symbol-staff-space", + "ly:staff-symbol::calc-line-positions", "ly:staff-symbol::height", "ly:staff-symbol::print", "ly:stderr-redirect", @@ -3239,8 +3319,8 @@ scheme_functions = [ "ly:system::get-vertical-alignment", "ly:system::height", "ly:system::vertical-skyline-elements", + "ly:text-interface::grob-interpret-markup", "ly:text-interface::interpret-markup", - "ly:text-interface::interpret-string", "ly:text-interface::print", "ly:tie-column::before-line-breaking", "ly:tie-column::calc-positioning-done", @@ -3248,6 +3328,11 @@ scheme_functions = [ "ly:tie::calc-direction", "ly:tie::print", "ly:time-signature::print", + "ly:time-signature::print-x", + "ly:time-tracer-include-and-remove-file", + "ly:time-tracer-restart", + "ly:time-tracer-set-file", + "ly:time-tracer-stop", "ly:transform->list", "ly:transform?", "ly:translate-cpp-warning-scheme", @@ -3259,19 +3344,18 @@ scheme_functions = [ "ly:transpose-key-alist", "ly:ttf->pfa", "ly:ttf-ps-name", - "ly:tuplet-bracket::calc-connect-to-neighbors", + "ly:tuplet-bracket::calc-beam", "ly:tuplet-bracket::calc-cross-staff", "ly:tuplet-bracket::calc-direction", "ly:tuplet-bracket::calc-positions", + "ly:tuplet-bracket::calc-potential-beam", "ly:tuplet-bracket::calc-x-positions", "ly:tuplet-bracket::print", + "ly:tuplet-description?", "ly:tuplet-iterator::constructor", - "ly:tuplet-number::calc-cross-staff", "ly:tuplet-number::calc-x-offset", "ly:tuplet-number::calc-y-offset", "ly:tuplet-number::print", - "ly:type1->pfa", - "ly:unit", "ly:unpure-call", "ly:unpure-pure-container-pure-part", "ly:unpure-pure-container-unpure-part", @@ -3298,16 +3382,21 @@ scheme_functions = [ "magstep", "maj7-modifier", "make-abs-fontsize-markup", + "make-abs-hspace-markup", + "make-abs-vspace-markup", "make-accidental-dodecaphonic-rule", "make-accidental-markup", "make-accidental-rule", "make-align-on-other-markup", + "make-annotate-moving-markup", + "make-append-to-tag-markup", "make-apply-context", "make-arrow-head-markup", "make-articulation", "make-auto-footnote-markup", "make-autochange-music", "make-backslashed-digit-markup", + "make-bar-line-markup", "make-beam-markup", "make-bezier-sandwich-stencil", "make-bold-markup", @@ -3340,7 +3429,6 @@ scheme_functions = [ "make-cue-clef-unset", "make-customTabClef-markup", "make-dashed-bar-line", - "make-default-fonts-tree", "make-dir-column-markup", "make-dotted-bar-line", "make-doubleflat-markup", @@ -3370,8 +3458,6 @@ scheme_functions = [ "make-finger-markup", "make-first-visible-markup", "make-flat-markup", - "make-font-tree-leaf", - "make-font-tree-node", "make-fontCaps-markup", "make-fontsize-markup", "make-footnote-markup", @@ -3396,12 +3482,15 @@ scheme_functions = [ "make-hspace-markup", "make-huge-markup", "make-if-markup", + "make-image-markup", + "make-initial-context-music", "make-italic-markup", "make-justified-lines-markup-list", "make-justify-field-markup", "make-justify-line-markup", "make-justify-markup", "make-justify-string-markup", + "make-keep-with-tag-markup", "make-key-alist", "make-key-symbols", "make-kievan-bar-line", @@ -3422,7 +3511,6 @@ scheme_functions = [ "make-markalphabet-markup", "make-markletter-markup", "make-markup", - "make-medium-markup", "make-modal-inverter", "make-modal-transposer", "make-multi-measure-rest", @@ -3437,6 +3525,7 @@ scheme_functions = [ "make-normal-size-sub-markup", "make-normal-size-super-markup", "make-normal-text-markup", + "make-normal-weight-markup", "make-normalsize-markup", "make-note-by-number-markup", "make-note-markup", @@ -3454,14 +3543,15 @@ scheme_functions = [ "make-pad-markup-markup", "make-pad-to-box-markup", "make-pad-x-markup", + "make-padding", "make-page-link-markup", "make-page-ref-markup", - "make-pango-font-tree", "make-parenthesis-stencil", "make-parenthesize-markup", "make-part-combine-context-changes", "make-part-combine-marks", "make-partial-ellipse-stencil", + "make-partial-set", "make-path-markup", "make-path-stencil", "make-pattern-markup", @@ -3472,10 +3562,13 @@ scheme_functions = [ "make-property-recursive-markup", "make-property-set", "make-property-unset", + "make-push-to-tag-markup", "make-put-adjacent-markup", + "make-qr-code-markup", "make-raise-markup", "make-relative", "make-relative::to-relative-callback", + "make-remove-with-tag-markup", "make-repeat", "make-replace-markup", "make-rest-by-number-markup", @@ -3486,7 +3579,6 @@ scheme_functions = [ "make-right-brace-markup", "make-right-column-markup", "make-right-hand-key-addresses", - "make-roman-markup", "make-rotate-markup", "make-rounded-box-markup", "make-sans-markup", @@ -3500,6 +3592,7 @@ scheme_functions = [ "make-semisharp-markup", "make-semitone->pitch", "make-sequential-music", + "make-serif-markup", "make-sesquiflat-markup", "make-sesquisharp-markup", "make-session-variable", @@ -3531,8 +3624,15 @@ scheme_functions = [ "make-symbol-alist", "make-tab-heads-transparent", "make-table-markup-list", + "make-tag-list-markup-list", + "make-tag-markup", "make-teeny-markup", - "make-text-markup", + "make-text-accidental-markup", + "make-text-doubleflat-markup", + "make-text-doublesharp-markup", + "make-text-flat-markup", + "make-text-natural-markup", + "make-text-sharp-markup", "make-thick-bar-line", "make-tick-bar-line", "make-tie-markup", @@ -3565,6 +3665,7 @@ scheme_functions = [ "make-voice-props-revert", "make-voice-props-set", "make-voice-states", + "make-volta-number-markup", "make-volta-set", "make-vspace-markup", "make-whiteout-markup", @@ -3586,8 +3687,6 @@ scheme_functions = [ "make-wordwrap-markup", "make-wordwrap-string-internal-markup-list", "make-wordwrap-string-markup", - "map-alist-keys", - "map-alist-vals", "map-markup-commands-markup-list", "map-selected-alist-keys", "map-some-music", @@ -3606,6 +3705,7 @@ scheme_functions = [ "markup-expression->make-markup", "markup-function-as-string-method", "markup-function-category", + "markup-function-internal?", "markup-function-properties", "markup-function?", "markup-join", @@ -3619,14 +3719,15 @@ scheme_functions = [ "markup-typecheck?", "markup?", "match-predicate", + "matrix-rotate-counterclockwise", "measure-counter::text", - "medium-markup", "mensural-flag", "merge-details", "metronome-markup", "middle-broken-spanner?", "midi-program", "midline-stencil", + "minmax/cmp", "minor-modifier", "mkdir-if-not-exist", "mm-rest-child-list", @@ -3655,6 +3756,10 @@ scheme_functions = [ "music-selective-map", "music-separator?", "music-type-predicate", + "musical-length->number", + "musical-length-as-moment?", + "musical-length-as-number?", + "musical-length?", "musicglyph-markup", "n-true-entries", "narrow-glyph?", @@ -3663,11 +3768,13 @@ scheme_functions = [ "negate-extent", "neo-modern-accidental-rule", "no-flag", + "non-negative-number?", "normal-flag", "normal-size-sub-markup", "normal-size-super-markup", "normal-text-markup", - "normalize-fraction", + "normal-weight-markup", + "normalize-color", "normalsize-markup", "not-first-broken-spanner?", "not-last-broken-spanner?", @@ -3686,12 +3793,13 @@ scheme_functions = [ "note-to-cluster", "notes-to-clusters", "null-markup", - "number->octal-string", "number-column-stencil", "number-format", "number-list?", "number-markup", + "number-or-false?", "number-or-grob?", + "number-or-number-pair?", "number-or-pair?", "number-or-string?", "number-pair-list?", @@ -3733,6 +3841,7 @@ scheme_functions = [ "old-straight-flag", "on-the-fly-markup", "only-if-beamed", + "optional-material-bracket::positions", "ordered-cons", "other-axis", "output-module?", @@ -3759,16 +3868,18 @@ scheme_functions = [ "pango-pf-font-name", "pango-pf-fontindex", "paper-variable", - "parentheses-interface::calc-angled-bracket-stencils", + "parentheses-interface::calc-Y-extent", + "parentheses-interface::calc-Y-offset", "parentheses-interface::calc-parenthesis-stencils", + "parentheses-interface::calc-pure-Y-offset", + "parentheses-interface::maybe-pure-Y-offset", "parentheses-interface::print", - "parentheses-interface::y-extent", - "parenthesize-elements", "parenthesize-markup", "parenthesize-stencil", "parse-and-check-version", "parse-lily-version", "parse-terse-string", + "passage-delimiter::break-visibility", "path-markup", "pattern-markup", "percussion?", @@ -3781,6 +3892,14 @@ scheme_functions = [ "polar->rectangular", "polygon-markup", "position-true-endpoint", + "positive-exact-integer?", + "positive-exact-rational?", + "positive-fraction?", + "positive-integer-or-false?", + "positive-musical-length-as-moment?", + "positive-musical-length-as-number?", + "positive-musical-length?", + "positive-number?", "postprocess-output", "postscript->pdf", "postscript->png", @@ -3800,13 +3919,18 @@ scheme_functions = [ "print-keys-verbose", "process-fill-value", "property-recursive-markup", + "props-with-tag-additions", + "props-with-tags-to-keep", + "props-with-tags-to-remove", "pure-chain-offset-callback", "pure-from-neighbor-interface::account-for-span-bar", "pure-from-neighbor-interface::extra-spacing-height", "pure-from-neighbor-interface::extra-spacing-height-at-beginning-of-line", "pure-from-neighbor-interface::extra-spacing-height-including-staff", "pure-from-neighbor-interface::pure-height", + "push-to-tag-markup", "put-adjacent-markup", + "qr-code-markup", "quarterdiff->string", "quote-substitute", "raise-markup", @@ -3819,17 +3943,15 @@ scheme_functions = [ "recent-enough?", "recompute-music-length", "recording-group-emulate", - "regexp-split", "relevant-book-systems", "relevant-dump-systems", "remove-grace-property", "remove-step", "remove-whitespace", - "repeat-tie::handle-tab-note-head", + "remove-with-tag-markup", "replace-markup", "replace-step", "replicate-modify", - "reset-stencil-colors", "rest-by-number-markup", "rest-markup", "retrieve-glyph-flag", @@ -3865,10 +3987,11 @@ scheme_functions = [ "right-column-markup", "ring-column-circle-stencil", "robust-bar-number-function", - "roman-markup", "rotate-markup", "rounded-box-markup", "rounded-box-stencil", + "sane-simple-time-signature?", + "sane-time-signature?", "sans-markup", "sans-serif-stencil", "saxophone-lh-T-key-stencil", @@ -3907,8 +4030,11 @@ scheme_functions = [ "scorify-music", "script-interface::calc-x-offset", "script-or-side-position-cross-staff", + "script::ledger-lines", "search-executable", "seconds->moment", + "segment->music", + "segment-el->music", "segno-markup", "select-head-glyph", "select-option", @@ -3918,6 +4044,7 @@ scheme_functions = [ "semiflat-markup", "semisharp-markup", "sequential-music-to-chord-exceptions", + "serif-markup", "sesquiflat-markup", "sesquisharp-markup", "session-replay", @@ -3928,7 +4055,6 @@ scheme_functions = [ "set-bar-number-visibility", "set-counter-text!", "set-default-paper-size", - "set-global-fonts", "set-global-staff-size", "set-mus-properties!", "set-output-property", @@ -3945,6 +4071,7 @@ scheme_functions = [ "sign", "silence-events", "simple-markup", + "simple-split->state", "simple-stencil-alist", "skip->rest", "skip-as-needed", @@ -3964,25 +4091,28 @@ scheme_functions = [ "split-at-predicate", "split-index", "split-list", + "split-list-by-group-lengths", "split-list-by-separator", + "splits-to-states-using", "stack-lines", "stack-stencil-line", "stack-stencils", "stack-stencils-padding-list", "stack-thirds", - "staff-ellipsis::calc-y-extent", "staff-ellipsis::print", + "staff-ellipsis::pure-height", "staff-highlight::height", "staff-highlight::print", "staff-highlight::width", "staff-magnification-is-changing?", "staff-symbol-line-count", - "staff-symbol-line-positions", "staff-symbol-line-span", - "staff-symbol-y-extent-from-line-positions", "staff-symbol::calc-widened-extent", "standard-e-stencil", "standard-path-stencil", + "state+moment", + "states-to-change-list", + "states-to-segments", "stderr", "stem-connectable?", "stem-is-root?", @@ -3997,6 +4127,7 @@ scheme_functions = [ "stencil-fretboard-extent", "stencil-fretboard-offset", "stencil-markup", + "stencil-pad-around", "stencil-true-extent", "stencil-whiteout", "stencil-whiteout-box", @@ -4012,7 +4143,6 @@ scheme_functions = [ "string-or-music?", "string-or-pair?", "string-or-symbol?", - "string-regexp-substitute", "string-startswith", "string-thickness", "strip-string-annotation", @@ -4036,10 +4166,12 @@ scheme_functions = [ "symlink-or-copy-if-not-exist", "symmetric-interval", "synced?", + "syntax-function-procedure-arguments", "system-start-text::calc-x-offset", "system-start-text::calc-y-offset", "system-start-text::print", "tab-note-head::calc-glyph-name", + "tab-note-head::handle-ties", "tab-note-head::print", "tab-note-head::print-custom-fret-label", "tab-note-head::whiteout-if-style-set", @@ -4048,18 +4180,29 @@ scheme_functions = [ "tabvoice::draw-double-stem-for-half-notes", "tabvoice::make-double-stem-width-for-half-notes", "tag-group-get", + "tag-list-markup-list", + "tag-markup", "tags-keep-predicate", "tags-remove-predicate", + "tags-visible?", "teaching-accidental-rule", "teeny-markup", + "text-accidental-markup", + "text-doubleflat-markup", + "text-doublesharp-markup", "text-fill-translate", + "text-flat-markup", "text-mark-interface::calc-break-visibility", "text-mark-interface::calc-self-alignment-X", - "text-markup", + "text-natural-markup", + "text-sharp-markup", "tie-markup", - "tie::handle-tab-note-head", "tied-lyric-markup", + "time-signature->fraction", + "time-signature->list", + "time-signature?", "tiny-markup", + "to-staff-space", "translate-draw-instructions", "translate-key-instruction", "translate-markup", @@ -4067,9 +4210,12 @@ scheme_functions = [ "translator-property-description", "transparent-markup", "transposer-factory", + "traverse-state-machine", "triangle-markup", "trill-pitch-group::pure-height", "true-entry?", + "tsig-abbr-expand", + "tsig-fraction-abbr-expand", "tuning", "tuplet-number::append-note-wrapper", "tuplet-number::calc-denominator-text", @@ -4095,6 +4241,7 @@ scheme_functions = [ "unity-if-multimeasure", "universal-color", "unless-markup", + "unspecified-music?", "update-possb-list", "upper-key-stencil", "upright-markup", @@ -4110,8 +4257,10 @@ scheme_functions = [ "voicify-list", "voicify-music", "void?", + "volta-bracket-interface::calc-text", "volta-bracket-interface::pure-height", "volta-bracket::calc-hook-visibility", + "volta-number-markup", "volta-spec-music", "vspace-markup", "whiteout-markup", @@ -4158,15 +4307,17 @@ context_properties = [ "autoBeamCheck", "autoBeaming", "autoCautionaries", - "barCheckLastFail", + "autoExtenders", "barCheckSynchronize", "barExtraVelocity", "barNumberFormatter", "barNumberVisibility", - "baseMoment", "beamExceptions", "beamHalfMeasure", + "beamMaximumSubdivision", "beamMelismaBusy", + "beamMinimumSubdivision", + "beatBase", "beatExtraVelocity", "beatStructure", "breathMarkDefinitions", @@ -4212,6 +4363,7 @@ context_properties = [ "currentMusicalColumn", "currentPerformanceMarkEvent", "currentRehearsalMarkEvent", + "currentTupletDescription", "dalSegnoTextFormatter", "decrescendoSpanner", "decrescendoText", @@ -4306,8 +4458,6 @@ context_properties = [ "midiReverbLevel", "midiSkipOffset", "minimumFret", - "minimumPageTurnLength", - "minimumRepeatLengthForPageTurn", "minorChordModifier", "noChordSymbol", "noteNameFunction", @@ -4318,6 +4468,8 @@ context_properties = [ "ottavation", "ottavationMarkups", "output", + "pageTurnMinimumRepeatLength", + "pageTurnMinimumRestLength", "partCombineForced", "partCombineTextsOnNote", "partialBusy", @@ -4329,10 +4481,13 @@ context_properties = [ "pedalUnaCordaStyle", "predefinedDiagramTable", "printAccidentalNames", + "printInitialRepeatBar", "printKeyCancellation", "printNotesLanguage", "printOctaveNames", "printPartCombineTexts", + "printTrivialVoltaRepeats", + "propertyStacks", "proportionalNotationDuration", "quotedCueEventTypes", "quotedEventTypes", @@ -4340,6 +4495,7 @@ context_properties = [ "rehearsalMarkFormatter", "repeatCommands", "repeatCountVisibility", + "respectIncompleteBeams", "restCompletionBusy", "restNumberThreshold", "restrainOpenStrings", @@ -4383,13 +4539,14 @@ context_properties = [ "suspendRestMerging", "systemStartDelimiter", "systemStartDelimiterHierarchy", + "tabFullNotation", "tabStaffLineLayoutFunction", "tablatureFormat", "tempoHideNote", "tempoWholesPerMinute", "tieMelismaBusy", "tieWaitForNote", - "timeSignatureFraction", + "timeSignature", "timeSignatureSettings", "timing", "tonic", @@ -4400,16 +4557,18 @@ context_properties = [ "underlyingRepeatBarType", "useBassFigureExtenders", "vocalName", - "voltaSpannerDuration", + "voltaBracketMusicalLength", "whichBar", ] grob_properties = [ "X-align-on-main-noteheads", + "X-alignment-extent", "X-attachment", "X-common", "X-extent", "X-offset", + "X-padding", "X-positions", "Y-attachment", "Y-common", @@ -4417,6 +4576,7 @@ grob_properties = [ "Y-offset", "accidental-grob", "accidental-grobs", + "accidental-padding", "add-cauda", "add-join", "add-stem", @@ -4428,6 +4588,7 @@ grob_properties = [ "all-elements", "allow-loose-spacing", "allow-span-bar", + "allow-span-bar-above", "alteration", "alteration-alist", "alteration-glyph-name-alist", @@ -4439,6 +4600,7 @@ grob_properties = [ "arrow-width", "ascendens", "auctum", + "auto-generated", "auto-knee-gap", "automatically-numbered", "average-spacing-wishes", @@ -4449,7 +4611,8 @@ grob_properties = [ "axis-group-parent-X", "axis-group-parent-Y", "bar-extent", - "bars", + "bars-left", + "bars-right", "base-shortest-duration", "baseline-skip", "beam", @@ -4482,6 +4645,9 @@ grob_properties = [ "break-alignment", "break-overshoot", "break-visibility", + "break-visibility-passage-default", + "break-visibility-passage-end", + "break-visibility-passage-start", "breakable", "broken-bound-padding", "c0-position", @@ -4516,12 +4682,12 @@ grob_properties = [ "default-staff-staff-spacing", "delta-position", "deminutum", + "denominator-style", "descendens", "details", "digit-names", "direction", "direction-source", - "display-cautionary", "dot", "dot-count", "dot-negative-kern", @@ -4561,6 +4727,9 @@ grob_properties = [ "font-series", "font-shape", "font-size", + "font-stretch", + "font-variant", + "fonts", "footnote", "footnote-music", "footnote-stencil", @@ -4594,6 +4763,7 @@ grob_properties = [ "has-span-bar", "head-direction", "head-width", + "head-x-offset", "heads", "height", "height-limit", @@ -4608,8 +4778,8 @@ grob_properties = [ "implicit", "important-column-ranks", "in-note-direction", - "in-note-padding", "in-note-stencil", + "in-note-system-padding", "inclinatum", "index", "inspect-quants", @@ -4629,6 +4799,7 @@ grob_properties = [ "ledger-positions", "ledger-positions-function", "left-bound-info", + "left-down-stem", "left-items", "left-neighbor", "left-number-text", @@ -4647,6 +4818,7 @@ grob_properties = [ "main-extent", "make-dead-when", "max-beam-connect", + "max-slope-factor", "max-symbol-separation", "maximum-gap", "maybe-loose", @@ -4660,6 +4832,7 @@ grob_properties = [ "merge-differently-headed", "meta", "minimum-X-extent", + "minimum-X-space", "minimum-Y-extent", "minimum-distance", "minimum-distances", @@ -4668,7 +4841,11 @@ grob_properties = [ "minimum-length-fraction", "minimum-space", "minimum-translations-alist", + "musical-length", "neighbors", + "nested-fraction-mixed", + "nested-fraction-orientation", + "nested-fraction-relative-font-size", "neutral-direction", "neutral-position", "next", @@ -4685,9 +4862,13 @@ grob_properties = [ "note-collision", "note-collision-threshold", "note-columns", + "note-dots-direction", + "note-flag-style", "note-head", + "note-head-style", "note-heads", "note-names", + "note-staff-position", "number-range-separator", "number-type", "numbering-assertion-function", @@ -4710,10 +4891,12 @@ grob_properties = [ "parenthesis-friends", "parenthesis-id", "parenthesized", + "passage-direction", "pedal-text", "pes-or-flexa", "positioning-done", "positions", + "potential-beam", "prefer-dotted-right", "prefix-set", "primitive", @@ -4727,11 +4910,13 @@ grob_properties = [ "quantize-position", "quantized-positions", "quilisma", + "range-collapse-threshold", "rank-on-page", "ratio", "remove-empty", "remove-first", "remove-layer", + "remove-short-autoextender", "replacement-alist", "rest", "rest-collision", @@ -4739,10 +4924,13 @@ grob_properties = [ "rests", "rhythmic-location", "right-bound-info", + "right-down-stem", "right-items", + "right-justified", "right-neighbor", "right-number-text", "right-padding", + "right-up-stem", "rotation", "round-up-exceptions", "round-up-to-longer-rest", @@ -4755,8 +4943,10 @@ grob_properties = [ "segno-kern", "self-alignment-X", "self-alignment-Y", + "senza-misura-stencil", "shape", "sharp-positions", + "short-bar-extent", "shorten", "shorten-pair", "shortest-duration-space", @@ -4784,6 +4974,7 @@ grob_properties = [ "spacing-increment", "spacing-pair", "spacing-wishes", + "span-all-note-heads", "span-start", "spanner-broken", "spanner-id", @@ -4830,11 +5021,9 @@ grob_properties = [ "tremolo-flag", "tuplet-number", "tuplet-slur", - "tuplet-start", "tuplets", "uniform-stretching", "usable-duration-logs", - "use-skylines", "used", "vertical-alignment", "vertical-skyline-elements", @@ -4842,69 +5031,121 @@ grob_properties = [ "virga", "visible-over-note-heads", "voiced-position", + "volta-number-offset", + "volta-numbers", "when", "whiteout", + "whiteout-color", "whiteout-style", "widened-extent", "width", "woodwind-diagram-details", "word-space", - "x-offset", "zigzag-length", "zigzag-width", ] paper_variables = [ + "annotate-spacing", "auto-first-page-number", "basic-distance", "binding-offset", + "blank-after-score-page-penalty", "blank-last-page-penalty", "blank-page-penalty", + "blot-diameter", + "book-title", + "book-title-properties", "bookTitleMarkup", + "bookpart-level-page-numbering", "bottom-margin", + "bp", "check-consistency", + "clip-regions", + "cm", + "debug-beam-scoring", + "debug-slur-scoring", + "debug-tie-scoring", + "dimension-variables", "evenFooterMarkup", "evenHeaderMarkup", "first-page-number", + "footnote-footer-padding", + "footnote-number-raise", + "footnote-numbering-function", + "footnote-padding", "footnote-separator-markup", "horizontal-shift", + "in", + "in-note-padding", + "in-note-system-padding", + "incipit-width", "indent", "inner-margin", + "is-last-bookpart", + "label-alist-table", + "label-page-table", + "landscape", "last-bottom-spacing", "left-margin", + "line-thickness", "line-width", + "make-footer", + "make-header", "markup-markup-spacing", "markup-system-spacing", "max-systems-per-page", "min-systems-per-page", "minimum-distance", + "mm", + "number-footnote-table", "oddFooterMarkup", "oddHeaderMarkup", + "orphan-penalty", "outer-margin", + "output-filename", + "output-scale", + "output-suffix", "padding", "page-breaking", "page-breaking-system-system-spacing", "page-count", "page-number-type", + "page-post-process", "page-spacing-weight", "paper-height", "paper-width", + "papersizename", "print-all-headers", "print-first-page-number", + "print-page-number", + "property-defaults", + "pt", "ragged-bottom", "ragged-last", "ragged-last-bottom", "ragged-right", + "reset-footnotes-on-new-page", "right-margin", "score-markup-spacing", "score-system-spacing", + "score-title", + "score-title-properties", "scoreTitleMarkup", "short-indent", + "staff-height", + "staff-space", "stretchability", "system-count", "system-separator-markup", "system-system-spacing", "systems-per-page", + "tagline", + "text-font-size", + "tocFormatMarkup", + "tocIndentMarkup", + "tocItemMarkup", + "tocTitleMarkup", "top-margin", "top-markup-spacing", "top-system-spacing", @@ -4921,6 +5162,17 @@ header_variables = [ "lsrtags", "meter", "opus", + "pdfarranger", + "pdfauthor", + "pdfcomposer", + "pdfcopyright", + "pdfcreationDate", + "pdfkeywords", + "pdfmodDate", + "pdfpoet", + "pdfsubject", + "pdfsubtitle", + "pdftitle", "piece", "poet", "subsubtitle", diff --git a/libs/pygments/lexers/_lua_builtins.py b/libs/pygments/lexers/_lua_builtins.py index e0977a0a4..9ce4a6d96 100644 --- a/libs/pygments/lexers/_lua_builtins.py +++ b/libs/pygments/lexers/_lua_builtins.py @@ -10,7 +10,7 @@ Run with `python -I` to regenerate. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_luau_builtins.py b/libs/pygments/lexers/_luau_builtins.py index 9e06b07b9..d63971ee9 100644 --- a/libs/pygments/lexers/_luau_builtins.py +++ b/libs/pygments/lexers/_luau_builtins.py @@ -4,7 +4,7 @@ Includes the builtins for Luau and Roblox. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_mapping.py b/libs/pygments/lexers/_mapping.py index cfc534d40..a7a5e6cbc 100644 --- a/libs/pygments/lexers/_mapping.py +++ b/libs/pygments/lexers/_mapping.py @@ -101,9 +101,8 @@ LEXERS = { 'Comal80Lexer': ('pygments.lexers.comal', 'COMAL-80', ('comal', 'comal80'), ('*.cml', '*.comal'), ()), 'CommonLispLexer': ('pygments.lexers.lisp', 'Common Lisp', ('common-lisp', 'cl', 'lisp'), ('*.cl', '*.lisp'), ('text/x-common-lisp',)), 'ComponentPascalLexer': ('pygments.lexers.oberon', 'Component Pascal', ('componentpascal', 'cp'), ('*.cp', '*.cps'), ('text/x-component-pascal',)), - 'CoqLexer': ('pygments.lexers.theorem', 'Coq', ('coq',), ('*.v',), ('text/x-coq',)), 'CplintLexer': ('pygments.lexers.cplint', 'cplint', ('cplint',), ('*.ecl', '*.prolog', '*.pro', '*.pl', '*.P', '*.lpad', '*.cpl'), ('text/x-cplint',)), - 'CppLexer': ('pygments.lexers.c_cpp', 'C++', ('cpp', 'c++'), ('*.cpp', '*.hpp', '*.c++', '*.h++', '*.cc', '*.hh', '*.cxx', '*.hxx', '*.C', '*.H', '*.cp', '*.CPP', '*.tpp'), ('text/x-c++hdr', 'text/x-c++src')), + 'CppLexer': ('pygments.lexers.c_cpp', 'C++', ('cpp', 'c++'), ('*.cpp', '*.hpp', '*.c++', '*.h++', '*.cc', '*.hh', '*.cxx', '*.hxx', '*.C', '*.H', '*.cp', '*.CPP', '*.tpp', '*.cppm', '*.ixx', '*.mxx'), ('text/x-c++hdr', 'text/x-c++src')), 'CppObjdumpLexer': ('pygments.lexers.asm', 'cpp-objdump', ('cpp-objdump', 'c++-objdumb', 'cxx-objdump'), ('*.cpp-objdump', '*.c++-objdump', '*.cxx-objdump'), ('text/x-cpp-objdump',)), 'CrmshLexer': ('pygments.lexers.dsls', 'Crmsh', ('crmsh', 'pcmk'), ('*.crmsh', '*.pcmk'), ()), 'CrocLexer': ('pygments.lexers.d', 'Croc', ('croc',), ('*.croc',), ('text/x-crocsrc',)), @@ -168,7 +167,7 @@ LEXERS = { 'FancyLexer': ('pygments.lexers.ruby', 'Fancy', ('fancy', 'fy'), ('*.fy', '*.fancypack'), ('text/x-fancysrc',)), 'FantomLexer': ('pygments.lexers.fantom', 'Fantom', ('fan',), ('*.fan',), ('application/x-fantom',)), 'FelixLexer': ('pygments.lexers.felix', 'Felix', ('felix', 'flx'), ('*.flx', '*.flxh'), ('text/x-felix',)), - 'FennelLexer': ('pygments.lexers.lisp', 'Fennel', ('fennel', 'fnl'), ('*.fnl',), ()), + 'FennelLexer': ('pygments.lexers.lisp', 'Fennel', ('fennel', 'fnl'), ('*.fnl', '*.fnlm'), ('text/x-fennel',)), 'FiftLexer': ('pygments.lexers.fift', 'Fift', ('fift', 'fif'), ('*.fif',), ()), 'FishShellLexer': ('pygments.lexers.shell', 'Fish', ('fish', 'fishshell'), ('*.fish', '*.load'), ('application/x-fish',)), 'FlatlineLexer': ('pygments.lexers.dsls', 'Flatline', ('flatline',), (), ('text/x-flatline',)), @@ -254,7 +253,7 @@ LEXERS = { 'Json5Lexer': ('pygments.lexers.json5', 'JSON5', ('json5',), ('*.json5',), ()), 'JsonBareObjectLexer': ('pygments.lexers.data', 'JSONBareObject', (), (), ()), 'JsonLdLexer': ('pygments.lexers.data', 'JSON-LD', ('jsonld', 'json-ld'), ('*.jsonld',), ('application/ld+json',)), - 'JsonLexer': ('pygments.lexers.data', 'JSON', ('json', 'json-object'), ('*.json', '*.jsonl', '*.ndjson', 'Pipfile.lock'), ('application/json', 'application/json-object', 'application/x-ndjson', 'application/jsonl', 'application/json-seq')), + 'JsonLexer': ('pygments.lexers.data', 'JSON', ('json', 'json-object'), ('*.json', '*.jsonl', '*.ndjson', 'Pipfile.lock', '*.module', '*.xc'), ('application/json', 'application/json-object', 'application/x-ndjson', 'application/jsonl', 'application/json-seq')), 'JsonnetLexer': ('pygments.lexers.jsonnet', 'Jsonnet', ('jsonnet',), ('*.jsonnet', '*.libsonnet'), ()), 'JspLexer': ('pygments.lexers.templates', 'Java Server Page', ('jsp',), ('*.jsp',), ('application/x-jsp',)), 'JsxLexer': ('pygments.lexers.jsx', 'JSX', ('jsx', 'react'), ('*.jsx', '*.react'), ('text/jsx', 'text/typescript-jsx')), @@ -314,11 +313,11 @@ LEXERS = { 'MarkdownLexer': ('pygments.lexers.markup', 'Markdown', ('markdown', 'md'), ('*.md', '*.markdown'), ('text/x-markdown',)), 'MaskLexer': ('pygments.lexers.javascript', 'Mask', ('mask',), ('*.mask',), ('text/x-mask',)), 'MasonLexer': ('pygments.lexers.templates', 'Mason', ('mason',), ('*.m', '*.mhtml', '*.mc', '*.mi', 'autohandler', 'dhandler'), ('application/x-mason',)), - 'MathematicaLexer': ('pygments.lexers.algebra', 'Mathematica', ('mathematica', 'mma', 'nb'), ('*.nb', '*.cdf', '*.nbp', '*.ma'), ('application/mathematica', 'application/vnd.wolfram.mathematica', 'application/vnd.wolfram.mathematica.package', 'application/vnd.wolfram.cdf')), + 'MathematicaLexer': ('pygments.lexers.algebra', 'Mathematica', ('mathematica', 'mma', 'nb', 'wl', 'wolfram'), ('*.nb', '*.cdf', '*.nbp', '*.ma', '*.wl', '*.wls'), ('application/mathematica', 'application/vnd.wolfram.mathematica', 'application/vnd.wolfram.mathematica.package', 'application/vnd.wolfram.cdf', 'application/vnd.wolfram.wl')), 'MatlabLexer': ('pygments.lexers.matlab', 'Matlab', ('matlab',), ('*.m',), ('text/matlab',)), 'MatlabSessionLexer': ('pygments.lexers.matlab', 'Matlab session', ('matlabsession',), (), ()), 'MaximaLexer': ('pygments.lexers.maxima', 'Maxima', ('maxima', 'macsyma'), ('*.mac', '*.max'), ()), - 'MesonLexer': ('pygments.lexers.meson', 'Meson', ('meson', 'meson.build'), ('meson.build', 'meson_options.txt'), ('text/x-meson',)), + 'MesonLexer': ('pygments.lexers.meson', 'Meson', ('meson', 'meson.build'), ('meson.build', 'meson.options', 'meson_options.txt'), ('text/x-meson',)), 'MiniDLexer': ('pygments.lexers.d', 'MiniD', ('minid',), (), ('text/x-minidsrc',)), 'MiniScriptLexer': ('pygments.lexers.scripting', 'MiniScript', ('miniscript', 'ms'), ('*.ms',), ('text/x-minicript', 'application/x-miniscript')), 'ModelicaLexer': ('pygments.lexers.modeling', 'Modelica', ('modelica',), ('*.mo',), ('text/x-modelica',)), @@ -416,7 +415,7 @@ LEXERS = { 'Python2Lexer': ('pygments.lexers.python', 'Python 2.x', ('python2', 'py2'), (), ('text/x-python2', 'application/x-python2')), 'Python2TracebackLexer': ('pygments.lexers.python', 'Python 2.x Traceback', ('py2tb',), ('*.py2tb',), ('text/x-python2-traceback',)), 'PythonConsoleLexer': ('pygments.lexers.python', 'Python console session', ('pycon', 'python-console'), (), ('text/x-python-doctest',)), - 'PythonLexer': ('pygments.lexers.python', 'Python', ('python', 'py', 'sage', 'python3', 'py3', 'bazel', 'starlark', 'pyi'), ('*.py', '*.pyw', '*.pyi', '*.jy', '*.sage', '*.sc', 'SConstruct', 'SConscript', '*.bzl', 'BUCK', 'BUILD', 'BUILD.bazel', 'WORKSPACE', '*.tac'), ('text/x-python', 'application/x-python', 'text/x-python3', 'application/x-python3')), + 'PythonLexer': ('pygments.lexers.python', 'Python', ('python', 'py', 'sage', 'python3', 'py3', 'bazel', 'starlark', 'pyi'), ('*.py', '*.pyw', '*.pyi', '*.jy', '*.sage', '*.sc', 'SConstruct', 'SConscript', '*.bzl', 'BUCK', 'BUILD', 'BUILD.bazel', 'WORKSPACE', '*.tac', '*.pye'), ('text/x-python', 'application/x-python', 'text/x-python3', 'application/x-python3')), 'PythonTracebackLexer': ('pygments.lexers.python', 'Python Traceback', ('pytb', 'py3tb'), ('*.pytb', '*.py3tb'), ('text/x-python-traceback', 'text/x-python3-traceback')), 'PythonUL4Lexer': ('pygments.lexers.ul4', 'Python+UL4', ('py+ul4',), ('*.pyul4',), ()), 'QBasicLexer': ('pygments.lexers.basic', 'QBasic', ('qbasic', 'basic'), ('*.BAS', '*.bas'), ('text/basic',)), @@ -444,6 +443,7 @@ LEXERS = { 'RedcodeLexer': ('pygments.lexers.esoteric', 'Redcode', ('redcode',), ('*.cw',), ()), 'RegeditLexer': ('pygments.lexers.configs', 'reg', ('registry',), ('*.reg',), ('text/x-windows-registry',)), 'RegoLexer': ('pygments.lexers.rego', 'Rego', ('rego',), ('*.rego',), ('text/x-rego',)), + 'RellLexer': ('pygments.lexers.rell', 'Rell', ('rell',), ('*.rell',), ('text/x-rell',)), 'ResourceLexer': ('pygments.lexers.resource', 'ResourceBundle', ('resourcebundle', 'resource'), (), ()), 'RexxLexer': ('pygments.lexers.scripting', 'Rexx', ('rexx', 'arexx'), ('*.rexx', '*.rex', '*.rx', '*.arexx'), ('text/x-rexx',)), 'RhtmlLexer': ('pygments.lexers.templates', 'RHTML', ('rhtml', 'html+erb', 'html+ruby'), ('*.rhtml',), ('text/html+ruby',)), @@ -452,6 +452,7 @@ LEXERS = { 'RoboconfGraphLexer': ('pygments.lexers.roboconf', 'Roboconf Graph', ('roboconf-graph',), ('*.graph',), ()), 'RoboconfInstancesLexer': ('pygments.lexers.roboconf', 'Roboconf Instances', ('roboconf-instances',), ('*.instances',), ()), 'RobotFrameworkLexer': ('pygments.lexers.robotframework', 'RobotFramework', ('robotframework',), ('*.robot', '*.resource'), ('text/x-robotframework',)), + 'RocqLexer': ('pygments.lexers.theorem', 'Rocq Prover', ('coq', 'rocq', 'rocq-prover'), ('*.v',), ('text/x-coq', 'text/x-rocq')), 'RqlLexer': ('pygments.lexers.sql', 'RQL', ('rql',), ('*.rql',), ('text/x-rql',)), 'RslLexer': ('pygments.lexers.dsls', 'RSL', ('rsl',), ('*.rsl',), ('text/rsl',)), 'RstLexer': ('pygments.lexers.markup', 'reStructuredText', ('restructuredtext', 'rst', 'rest'), ('*.rst', '*.rest'), ('text/x-rst', 'text/prs.fallenstein.rst')), @@ -583,7 +584,7 @@ LEXERS = { 'XQueryLexer': ('pygments.lexers.webmisc', 'XQuery', ('xquery', 'xqy', 'xq', 'xql', 'xqm'), ('*.xqy', '*.xquery', '*.xq', '*.xql', '*.xqm'), ('text/xquery', 'application/xquery')), 'XmlDjangoLexer': ('pygments.lexers.templates', 'XML+Django/Jinja', ('xml+django', 'xml+jinja'), ('*.xml.j2', '*.xml.jinja2'), ('application/xml+django', 'application/xml+jinja')), 'XmlErbLexer': ('pygments.lexers.templates', 'XML+Ruby', ('xml+ruby', 'xml+erb'), (), ('application/xml+ruby',)), - 'XmlLexer': ('pygments.lexers.html', 'XML', ('xml',), ('*.xml', '*.xsl', '*.rss', '*.xslt', '*.xsd', '*.wsdl', '*.wsf'), ('text/xml', 'application/xml', 'image/svg+xml', 'application/rss+xml', 'application/atom+xml')), + 'XmlLexer': ('pygments.lexers.html', 'XML', ('xml',), ('*.xml', '*.xsl', '*.rss', '*.xslt', '*.xsd', '*.wsdl', '*.wsf', '*.xbrl', '*.pom'), ('text/xml', 'application/xml', 'image/svg+xml', 'application/rss+xml', 'application/atom+xml')), 'XmlPhpLexer': ('pygments.lexers.templates', 'XML+PHP', ('xml+php',), (), ('application/xml+php',)), 'XmlSmartyLexer': ('pygments.lexers.templates', 'XML+Smarty', ('xml+smarty',), (), ('application/xml+smarty',)), 'XorgLexer': ('pygments.lexers.xorg', 'Xorg', ('xorg.conf',), ('xorg.conf',), ()), diff --git a/libs/pygments/lexers/_mql_builtins.py b/libs/pygments/lexers/_mql_builtins.py index 181c25d55..f1c88a209 100644 --- a/libs/pygments/lexers/_mql_builtins.py +++ b/libs/pygments/lexers/_mql_builtins.py @@ -4,7 +4,7 @@ Builtins for the MqlLexer. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ types = ( diff --git a/libs/pygments/lexers/_mysql_builtins.py b/libs/pygments/lexers/_mysql_builtins.py index d4a9099ed..b1c82f57c 100644 --- a/libs/pygments/lexers/_mysql_builtins.py +++ b/libs/pygments/lexers/_mysql_builtins.py @@ -6,7 +6,7 @@ Run with `python -I` to update. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -32,6 +32,12 @@ MYSQL_CONSTANTS = ( # This list is also used to strip data types out of the list of # MySQL keywords, which is automatically updated later in the file. # +# For future updates, these pages may be helpful references +# when looking for additional data types and aliases: +# +# https://dev.mysql.com/doc/refman/9.4/en/data-types.html +# https://dev.mysql.com/doc/refman/9.4/en/other-vendor-data-types.html +# MYSQL_DATATYPES = ( # Numeric data types 'bigint', @@ -71,6 +77,7 @@ MYSQL_DATATYPES = ( # String data types 'binary', 'blob', + 'byte', 'char', 'enum', 'long', @@ -167,6 +174,7 @@ MYSQL_FUNCTIONS = ( 'dayofyear', 'degrees', 'elt', + 'etag', 'exp', 'export_set', 'extract', @@ -180,6 +188,7 @@ MYSQL_FUNCTIONS = ( 'from_base64', 'from_days', 'from_unixtime', + 'from_vector', 'get_dd_column_privileges', 'get_dd_create_options', 'get_dd_index_private_data', @@ -187,6 +196,7 @@ MYSQL_FUNCTIONS = ( 'get_dd_property_key_value', 'get_dd_schema_options', 'get_dd_tablespace_private_data', + 'get_jdv_property_key_value', 'get_lock', 'greatest', 'group_concat', @@ -238,6 +248,7 @@ MYSQL_FUNCTIONS = ( 'internal_tablespace_type', 'internal_tablespace_version', 'internal_update_time', + 'internal_use_terminology_previous', 'is_free_lock', 'is_ipv4', 'is_ipv4_compat', @@ -254,6 +265,7 @@ MYSQL_FUNCTIONS = ( 'json_contains', 'json_contains_path', 'json_depth', + 'json_duality_object', 'json_extract', 'json_insert', 'json_keys', @@ -455,6 +467,7 @@ MYSQL_FUNCTIONS = ( 'stddev_samp', 'str_to_date', 'strcmp', + 'string_to_vector', 'subdate', 'substr', 'substring', @@ -470,6 +483,7 @@ MYSQL_FUNCTIONS = ( 'to_base64', 'to_days', 'to_seconds', + 'to_vector', 'trim', 'ucase', 'uncompress', @@ -485,6 +499,8 @@ MYSQL_FUNCTIONS = ( 'var_pop', 'var_samp', 'variance', + 'vector_dim', + 'vector_to_string', 'version', 'wait_for_executed_gtid_set', 'wait_until_sql_thread_after_gtids', @@ -541,6 +557,7 @@ MYSQL_OPTIMIZER_HINTS = ( MYSQL_KEYWORDS = ( + 'absent', 'accessible', 'account', 'action', @@ -552,6 +569,7 @@ MYSQL_KEYWORDS = ( 'aggregate', 'algorithm', 'all', + 'allow_missing_files', 'alter', 'always', 'analyze', @@ -566,21 +584,25 @@ MYSQL_KEYWORDS = ( 'at', 'attribute', 'authentication', + 'auto', 'auto_increment', + 'auto_refresh', + 'auto_refresh_source', 'autoextend_size', 'avg', 'avg_row_length', 'backup', 'before', 'begin', + 'bernoulli', 'between', 'binlog', 'block', 'both', 'btree', 'buckets', + 'bulk', 'by', - 'byte', 'cache', 'call', 'cascade', @@ -675,6 +697,7 @@ MYSQL_KEYWORDS = ( 'do', 'drop', 'dual', + 'duality', 'dumpfile', 'duplicate', 'dynamic', @@ -710,6 +733,8 @@ MYSQL_KEYWORDS = ( 'export', 'extended', 'extent_size', + 'external', + 'external_format', 'factor', 'failed_login_attempts', 'false', @@ -719,6 +744,11 @@ MYSQL_KEYWORDS = ( 'fields', 'file', 'file_block_size', + 'file_format', + 'file_name', + 'file_pattern', + 'file_prefix', + 'files', 'filter', 'finish', 'first', @@ -736,6 +766,7 @@ MYSQL_KEYWORDS = ( 'fulltext', 'function', 'general', + 'generate', 'generated', 'geomcollection', 'get', @@ -750,9 +781,11 @@ MYSQL_KEYWORDS = ( 'grouping', 'groups', 'gtid_only', + 'gtids', 'handler', 'hash', 'having', + 'header', 'help', 'high_priority', 'histogram', @@ -783,6 +816,7 @@ MYSQL_KEYWORDS = ( 'insert_method', 'install', 'instance', + 'intersect', 'interval', 'into', 'invisible', @@ -816,6 +850,7 @@ MYSQL_KEYWORDS = ( 'left', 'less', 'level', + 'library', 'like', 'limit', 'linear', @@ -828,10 +863,12 @@ MYSQL_KEYWORDS = ( 'lock', 'locked', 'locks', + 'log', 'logfile', 'logs', 'loop', 'low_priority', + 'manual', 'master', 'master_auto_position', 'master_bind', @@ -934,6 +971,9 @@ MYSQL_KEYWORDS = ( 'owner', 'pack_keys', 'page', + 'parallel', + 'parameters', + 'parse_tree', 'parser', 'partial', 'partition', @@ -965,6 +1005,7 @@ MYSQL_KEYWORDS = ( 'profiles', 'proxy', 'purge', + 'qualify', 'quarter', 'query', 'quick', @@ -984,6 +1025,7 @@ MYSQL_KEYWORDS = ( 'references', 'regexp', 'registration', + 'relational', 'relay', 'relay_log_file', 'relay_log_pos', @@ -1040,6 +1082,7 @@ MYSQL_KEYWORDS = ( 'row_number', 'rows', 'rtree', + 's3', 'savepoint', 'schedule', 'schema', @@ -1139,6 +1182,7 @@ MYSQL_KEYWORDS = ( 'stored', 'straight_join', 'stream', + 'strict_load', 'string', 'subclass_origin', 'subject', @@ -1153,6 +1197,7 @@ MYSQL_KEYWORDS = ( 'table_checksum', 'table_name', 'tables', + 'tablesample', 'tablespace', 'temporary', 'temptable', @@ -1190,6 +1235,8 @@ MYSQL_KEYWORDS = ( 'until', 'update', 'upgrade', + 'uri', + 'url', 'usage', 'use', 'use_frm', @@ -1204,6 +1251,8 @@ MYSQL_KEYWORDS = ( 'values', 'variables', 'vcpu', + 'vector', + 'verify_key_constraints', 'view', 'virtual', 'visible', @@ -1238,7 +1287,7 @@ if __name__ == '__main__': # pragma: no cover from pygments.util import format_lines # MySQL source code - SOURCE_URL = 'https://github.com/mysql/mysql-server/raw/8.0' + SOURCE_URL = 'https://github.com/mysql/mysql-server/raw/trunk' LEX_URL = SOURCE_URL + '/sql/lex.h' ITEM_CREATE_URL = SOURCE_URL + '/sql/item_create.cc' diff --git a/libs/pygments/lexers/_openedge_builtins.py b/libs/pygments/lexers/_openedge_builtins.py index 33a1333e4..7e0555dea 100644 --- a/libs/pygments/lexers/_openedge_builtins.py +++ b/libs/pygments/lexers/_openedge_builtins.py @@ -4,7 +4,7 @@ Builtin list for the OpenEdgeLexer. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_php_builtins.py b/libs/pygments/lexers/_php_builtins.py index 19825fa2d..1f93215d7 100644 --- a/libs/pygments/lexers/_php_builtins.py +++ b/libs/pygments/lexers/_php_builtins.py @@ -7,7 +7,7 @@ Run with `python -I` to regenerate. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -3254,7 +3254,7 @@ if __name__ == '__main__': # pragma: no cover import tarfile from urllib.request import urlretrieve - PHP_MANUAL_URL = 'http://us3.php.net/distributions/manual/php_manual_en.tar.gz' + PHP_MANUAL_URL = 'https://us3.php.net/distributions/manual/php_manual_en.tar.gz' PHP_MANUAL_DIR = './php-chunked-xhtml/' PHP_REFERENCE_GLOB = 'ref.*' PHP_FUNCTION_RE = r'(.*?)' @@ -3298,7 +3298,10 @@ if __name__ == '__main__': # pragma: no cover def get_php_references(): download = urlretrieve(PHP_MANUAL_URL) with tarfile.open(download[0]) as tar: - tar.extractall() + if hasattr(tarfile.TarFile, 'extraction_filter'): + tar.extractall(filter='data') + else: + tar.extractall() yield from glob.glob(f"{PHP_MANUAL_DIR}{PHP_REFERENCE_GLOB}") os.remove(download[0]) @@ -3318,7 +3321,7 @@ if __name__ == '__main__': # pragma: no cover print('>> Downloading Function Index') modules = get_php_functions() total = sum(len(v) for v in modules.values()) - print('%d functions found' % total) + print(f'{total} functions found') regenerate(__file__, modules) shutil.rmtree(PHP_MANUAL_DIR) diff --git a/libs/pygments/lexers/_postgres_builtins.py b/libs/pygments/lexers/_postgres_builtins.py index 80bc728e9..7a0a5848c 100644 --- a/libs/pygments/lexers/_postgres_builtins.py +++ b/libs/pygments/lexers/_postgres_builtins.py @@ -6,7 +6,7 @@ Run with `python -I` to update itself. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_qlik_builtins.py b/libs/pygments/lexers/_qlik_builtins.py index c2fe0cf4b..78d4e9a57 100644 --- a/libs/pygments/lexers/_qlik_builtins.py +++ b/libs/pygments/lexers/_qlik_builtins.py @@ -4,7 +4,7 @@ Qlik builtins. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_scheme_builtins.py b/libs/pygments/lexers/_scheme_builtins.py index c35a533d1..5c468ff3c 100644 --- a/libs/pygments/lexers/_scheme_builtins.py +++ b/libs/pygments/lexers/_scheme_builtins.py @@ -4,7 +4,7 @@ Scheme builtins. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_scilab_builtins.py b/libs/pygments/lexers/_scilab_builtins.py index af49b46a8..f5a14b314 100644 --- a/libs/pygments/lexers/_scilab_builtins.py +++ b/libs/pygments/lexers/_scilab_builtins.py @@ -4,7 +4,7 @@ Builtin list for the ScilabLexer. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_sourcemod_builtins.py b/libs/pygments/lexers/_sourcemod_builtins.py index c98478c6f..8e2954363 100644 --- a/libs/pygments/lexers/_sourcemod_builtins.py +++ b/libs/pygments/lexers/_sourcemod_builtins.py @@ -8,7 +8,7 @@ Run with `python -I` to regenerate. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_sql_builtins.py b/libs/pygments/lexers/_sql_builtins.py index 8b90e53e9..32c720a22 100644 --- a/libs/pygments/lexers/_sql_builtins.py +++ b/libs/pygments/lexers/_sql_builtins.py @@ -4,7 +4,7 @@ Data files for the SQL lexer. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_stan_builtins.py b/libs/pygments/lexers/_stan_builtins.py index 8db10c32a..3a0ccc17e 100644 --- a/libs/pygments/lexers/_stan_builtins.py +++ b/libs/pygments/lexers/_stan_builtins.py @@ -3,9 +3,9 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This file contains the names of functions for Stan used by - ``pygments.lexers.math.StanLexer. This is for Stan language version 2.29.0. + ``pygments.lexers.math.StanLexer. This is for Stan language version 2.36.0. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -20,6 +20,8 @@ KEYWORDS = ( 'reject', 'return', 'while', + 'fatal_error', + 'target' ) TYPES = ( @@ -38,10 +40,21 @@ TYPES = ( 'vector', 'void', 'array', - 'complex' + 'complex', + 'complex_vector', + 'complex_matrix', + 'complex_row_vector', + 'corr_matrix', + 'cholesky_factor_corr', + 'column_stochastic_matrix', + 'row_stochastic_matrix', + 'sum_to_zero_vector', + 'sum_to_zero_matrix' ) FUNCTIONS = ( + 'Phi', + 'Phi_approx', 'abs', 'acos', 'acosh', @@ -83,15 +96,22 @@ FUNCTIONS = ( 'beta_lcdf', 'beta_lpdf', 'beta_lupdf', + 'beta_neg_binomial_cdf', + 'beta_neg_binomial_lccdf', + 'beta_neg_binomial_lcdf', + 'beta_neg_binomial_lpmf', + 'beta_neg_binomial_lupmf', + 'beta_neg_binomial_rng', 'beta_proportion_lccdf', 'beta_proportion_lcdf', 'beta_proportion_rng', 'beta_rng', 'binary_log_loss', 'binomial_cdf', - 'binomial_coefficient_log', 'binomial_lccdf', 'binomial_lcdf', + 'binomial_logit_glm_lpmf', + 'binomial_logit_glm_lupmf', 'binomial_logit_lpmf', 'binomial_logit_lupmf', 'binomial_lpmf', @@ -122,16 +142,32 @@ FUNCTIONS = ( 'chi_square_rng', 'chol2inv', 'cholesky_decompose', + 'cholesky_factor_corr_constrain', + 'cholesky_factor_corr_jacobian', + 'cholesky_factor_corr_unconstrain', + 'cholesky_factor_cov_constrain', + 'cholesky_factor_cov_jacobian', + 'cholesky_factor_cov_unconstrain', 'choose', 'col', 'cols', 'columns_dot_product', 'columns_dot_self', + 'complex_schur_decompose', + 'complex_schur_decompose_t', + 'complex_schur_decompose_u', 'conj', + 'corr_matrix_constrain', + 'corr_matrix_jacobian', + 'corr_matrix_unconstrain', 'cos', 'cosh', 'cov_exp_quad', + 'cov_matrix_constrain', + 'cov_matrix_jacobian', + 'cov_matrix_unconstrain', 'crossprod', + 'csr_extract', 'csr_extract_u', 'csr_extract_v', 'csr_extract_w', @@ -149,6 +185,9 @@ FUNCTIONS = ( 'dims', 'dirichlet_lpdf', 'dirichlet_lupdf', + 'dirichlet_multinomial_lpmf', + 'dirichlet_multinomial_lupmf', + 'dirichlet_multinomial_rng', 'dirichlet_rng', 'discrete_range_cdf', 'discrete_range_lccdf', @@ -166,7 +205,11 @@ FUNCTIONS = ( 'double_exponential_lupdf', 'double_exponential_rng', 'e', + 'eigendecompose', + 'eigendecompose_sym', + 'eigenvalues', 'eigenvalues_sym', + 'eigenvectors', 'eigenvectors_sym', 'erf', 'erfc', @@ -185,9 +228,10 @@ FUNCTIONS = ( 'exponential_lpdf', 'exponential_lupdf', 'exponential_rng', - 'fabs', 'falling_factorial', 'fdim', + 'fft', + 'fft2', 'floor', 'fma', 'fmax', @@ -211,8 +255,13 @@ FUNCTIONS = ( 'gaussian_dlm_obs_lupdf', 'generalized_inverse', 'get_imag', - 'get_lp', 'get_real', + 'gp_dot_prod_cov', + 'gp_exp_quad_cov', + 'gp_exponential_cov', + 'gp_matern23_cov', + 'gp_matern52_cov', + 'gp_periodic_cov', 'gumbel_cdf', 'gumbel_lccdf', 'gumbel_lcdf', @@ -223,8 +272,12 @@ FUNCTIONS = ( 'hmm_hidden_state_prob', 'hmm_latent_rng', 'hmm_marginal', + 'hypergeometric_1F0', + 'hypergeometric_2F1', + 'hypergeometric_3F2', 'hypergeometric_lpmf', 'hypergeometric_lupmf', + 'hypergeometric_pFq', 'hypergeometric_rng', 'hypot', 'identity_matrix', @@ -236,6 +289,7 @@ FUNCTIONS = ( 'integrate_ode_bdf', 'integrate_ode_rk45', 'inv', + 'inv_Phi', 'inv_chi_square_cdf', 'inv_chi_square_lccdf', 'inv_chi_square_lcdf', @@ -244,16 +298,21 @@ FUNCTIONS = ( 'inv_chi_square_rng', 'inv_cloglog', 'inv_erfc', + 'inv_fft', + 'inv_fft2', 'inv_gamma_cdf', 'inv_gamma_lccdf', 'inv_gamma_lcdf', 'inv_gamma_lpdf', 'inv_gamma_lupdf', 'inv_gamma_rng', + 'inv_inc_beta', 'inv_logit', - 'inv_Phi', 'inv_sqrt', 'inv_square', + 'inv_wishart_cholesky_lpdf', + 'inv_wishart_cholesky_lupdf', + 'inv_wishart_cholesky_rng', 'inv_wishart_lpdf', 'inv_wishart_lupdf', 'inv_wishart_rng', @@ -313,6 +372,12 @@ FUNCTIONS = ( 'lognormal_lpdf', 'lognormal_lupdf', 'lognormal_rng', + 'lower_bound_constrain', + 'lower_bound_jacobian', + 'lower_bound_unconstrain', + 'lower_upper_bound_constrain', + 'lower_upper_bound_jacobian', + 'lower_upper_bound_unconstrain', 'machine_precision', 'map_rect', 'matrix_exp', @@ -339,6 +404,10 @@ FUNCTIONS = ( 'multi_normal_prec_lpdf', 'multi_normal_prec_lupdf', 'multi_normal_rng', + 'multi_student_cholesky_t_rng', + 'multi_student_t_cholesky_lpdf', + 'multi_student_t_cholesky_lupdf', + 'multi_student_t_cholesky_rng', 'multi_student_t_lpdf', 'multi_student_t_lupdf', 'multi_student_t_rng', @@ -348,7 +417,6 @@ FUNCTIONS = ( 'multinomial_lpmf', 'multinomial_lupmf', 'multinomial_rng', - 'multiply_log', 'multiply_lower_tri_self_transpose', 'neg_binomial_2_cdf', 'neg_binomial_2_lccdf', @@ -369,6 +437,8 @@ FUNCTIONS = ( 'neg_binomial_rng', 'negative_infinity', 'norm', + 'norm1', + 'norm2', 'normal_cdf', 'normal_id_glm_lpdf', 'normal_id_glm_lupdf', @@ -388,6 +458,9 @@ FUNCTIONS = ( 'ode_ckrk_tol', 'ode_rk45', 'ode_rk45_tol', + 'offset_multiplier_constrain', + 'offset_multiplier_jacobian', + 'offset_multiplier_unconstrain', 'one_hot_array', 'one_hot_int_array', 'one_hot_row_vector', @@ -396,6 +469,8 @@ FUNCTIONS = ( 'ones_int_array', 'ones_row_vector', 'ones_vector', + 'ordered_constrain', + 'ordered_jacobian', 'ordered_logistic_glm_lpmf', 'ordered_logistic_glm_lupmf', 'ordered_logistic_lpmf', @@ -404,6 +479,7 @@ FUNCTIONS = ( 'ordered_probit_lpmf', 'ordered_probit_lupmf', 'ordered_probit_rng', + 'ordered_unconstrain', 'owens_t', 'pareto_cdf', 'pareto_lccdf', @@ -417,8 +493,6 @@ FUNCTIONS = ( 'pareto_type_2_lpdf', 'pareto_type_2_lupdf', 'pareto_type_2_rng', - 'Phi', - 'Phi_approx', 'pi', 'poisson_cdf', 'poisson_lccdf', @@ -433,12 +507,16 @@ FUNCTIONS = ( 'poisson_rng', 'polar', 'positive_infinity', + 'positive_ordered_constrain', + 'positive_ordered_jacobian', + 'positive_ordered_unconstrain', 'pow', - 'print', 'prod', 'proj', + 'qr', 'qr_Q', 'qr_R', + 'qr_thin', 'qr_thin_Q', 'qr_thin_R', 'quad_form', @@ -453,7 +531,6 @@ FUNCTIONS = ( 'rayleigh_lupdf', 'rayleigh_rng', 'reduce_sum', - 'reject', 'rep_array', 'rep_matrix', 'rep_row_vector', @@ -474,6 +551,9 @@ FUNCTIONS = ( 'scaled_inv_chi_square_rng', 'sd', 'segment', + 'simplex_constrain', + 'simplex_jacobian', + 'simplex_unconstrain', 'sin', 'singular_values', 'sinh', @@ -491,6 +571,10 @@ FUNCTIONS = ( 'skew_normal_lupdf', 'skew_normal_rng', 'softmax', + 'solve_newton', + 'solve_newton_tol', + 'solve_powell', + 'solve_powell_tol', 'sort_asc', 'sort_desc', 'sort_indices_asc', @@ -502,10 +586,18 @@ FUNCTIONS = ( 'std_normal_cdf', 'std_normal_lccdf', 'std_normal_lcdf', + 'std_normal_log_qf', 'std_normal_lpdf', 'std_normal_lupdf', + 'std_normal_qf', 'std_normal_rng', 'step', + 'stochastic_column_constrain', + 'stochastic_column_jacobian', + 'stochastic_column_unconstrain', + 'stochastic_row_constrain', + 'stochastic_row_jacobian', + 'stochastic_row_unconstrain', 'student_t_cdf', 'student_t_lccdf', 'student_t_lcdf', @@ -515,6 +607,10 @@ FUNCTIONS = ( 'sub_col', 'sub_row', 'sum', + 'sum_to_zero_constrain', + 'sum_to_zero_jacobian', + 'sum_to_zero_unconstrain', + 'svd', 'svd_U', 'svd_V', 'symmetrize_from_lower_tri', @@ -527,6 +623,7 @@ FUNCTIONS = ( 'to_array_1d', 'to_array_2d', 'to_complex', + 'to_int', 'to_matrix', 'to_row_vector', 'to_vector', @@ -542,6 +639,12 @@ FUNCTIONS = ( 'uniform_lupdf', 'uniform_rng', 'uniform_simplex', + 'unit_vectors_constrain', + 'unit_vectors_jacobian', + 'unit_vectors_unconstrain', + 'upper_bound_constrain', + 'upper_bound_jacobian', + 'upper_bound_unconstrain', 'variance', 'von_mises_cdf', 'von_mises_lccdf', @@ -557,12 +660,16 @@ FUNCTIONS = ( 'weibull_rng', 'wiener_lpdf', 'wiener_lupdf', + 'wishart_cholesky_lpdf', + 'wishart_cholesky_lupdf', + 'wishart_cholesky_rng', 'wishart_lpdf', 'wishart_lupdf', 'wishart_rng', 'zeros_array', 'zeros_int_array', - 'zeros_row_vector' + 'zeros_row_vector', + 'zeros_vector' ) DISTRIBUTIONS = ( @@ -571,14 +678,17 @@ DISTRIBUTIONS = ( 'bernoulli_logit_glm', 'beta', 'beta_binomial', + 'beta_neg_binomial', 'binomial', 'binomial_logit', + 'binomial_logit_glm', 'categorical', 'categorical_logit', 'categorical_logit_glm', 'cauchy', 'chi_square', 'dirichlet', + 'dirichlet_multinomial', 'discrete_range', 'double_exponential', 'exp_mod_normal', @@ -591,6 +701,7 @@ DISTRIBUTIONS = ( 'inv_chi_square', 'inv_gamma', 'inv_wishart', + 'inv_wishart_cholesky', 'lkj_corr', 'lkj_corr_cholesky', 'logistic', @@ -602,6 +713,7 @@ DISTRIBUTIONS = ( 'multi_normal_cholesky', 'multi_normal_prec', 'multi_student_t', + 'multi_student_t_cholesky', 'multinomial', 'multinomial_logit', 'neg_binomial', @@ -629,6 +741,7 @@ DISTRIBUTIONS = ( 'weibull', 'wiener', 'wishart', + 'wishart_cholesky' ) RESERVED = ( diff --git a/libs/pygments/lexers/_stata_builtins.py b/libs/pygments/lexers/_stata_builtins.py index 6a36f0429..b6111a1a7 100644 --- a/libs/pygments/lexers/_stata_builtins.py +++ b/libs/pygments/lexers/_stata_builtins.py @@ -4,7 +4,7 @@ Builtins for Stata - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_tsql_builtins.py b/libs/pygments/lexers/_tsql_builtins.py index 6fa1a1f19..22079e53f 100644 --- a/libs/pygments/lexers/_tsql_builtins.py +++ b/libs/pygments/lexers/_tsql_builtins.py @@ -4,7 +4,7 @@ These are manually translated lists from https://msdn.microsoft.com. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_usd_builtins.py b/libs/pygments/lexers/_usd_builtins.py index b0530a209..8d5a7422e 100644 --- a/libs/pygments/lexers/_usd_builtins.py +++ b/libs/pygments/lexers/_usd_builtins.py @@ -4,7 +4,7 @@ A collection of known USD-related keywords, attributes, and types. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_vbscript_builtins.py b/libs/pygments/lexers/_vbscript_builtins.py index b1ae0ef02..a4a6961fa 100644 --- a/libs/pygments/lexers/_vbscript_builtins.py +++ b/libs/pygments/lexers/_vbscript_builtins.py @@ -5,7 +5,7 @@ These are manually translated lists from http://www.indusoft.com/pdf/VBScript%20Reference.pdf. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/_vim_builtins.py b/libs/pygments/lexers/_vim_builtins.py index abbbf993a..5fbbf81e6 100644 --- a/libs/pygments/lexers/_vim_builtins.py +++ b/libs/pygments/lexers/_vim_builtins.py @@ -4,7 +4,7 @@ This file is autogenerated by scripts/get_vimkw.py - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/actionscript.py b/libs/pygments/lexers/actionscript.py index 3a6ba4291..659cc2be6 100644 --- a/libs/pygments/lexers/actionscript.py +++ b/libs/pygments/lexers/actionscript.py @@ -4,7 +4,7 @@ Lexers for ActionScript and MXML. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -195,8 +195,8 @@ class ActionScript3Lexer(RegexLexer): } def analyse_text(text): - if re.match(r'\w+\s*:\s*\w', text): - return 0.3 + if re.search(r'\bimport\s+flash\.', text): + return 0.2 return 0 diff --git a/libs/pygments/lexers/ada.py b/libs/pygments/lexers/ada.py index cb286ca7c..7f6fec749 100644 --- a/libs/pygments/lexers/ada.py +++ b/libs/pygments/lexers/ada.py @@ -4,7 +4,7 @@ Lexers for Ada family languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/agile.py b/libs/pygments/lexers/agile.py index cb22a13bf..ac88cf679 100644 --- a/libs/pygments/lexers/agile.py +++ b/libs/pygments/lexers/agile.py @@ -4,7 +4,7 @@ Just export lexer classes previously contained in this module. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/algebra.py b/libs/pygments/lexers/algebra.py index a899cfea8..1bb6e9ed8 100644 --- a/libs/pygments/lexers/algebra.py +++ b/libs/pygments/lexers/algebra.py @@ -4,7 +4,7 @@ Lexers for computer algebra systems. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -151,16 +151,17 @@ class MathematicaLexer(RegexLexer): Lexer for Mathematica source code. """ name = 'Mathematica' - url = 'http://www.wolfram.com/mathematica/' - aliases = ['mathematica', 'mma', 'nb'] - filenames = ['*.nb', '*.cdf', '*.nbp', '*.ma'] + url = 'https://www.wolfram.com/language/' + aliases = ['mathematica', 'mma', 'nb', 'wl', 'wolfram'] + filenames = ['*.nb', '*.cdf', '*.nbp', '*.ma', '*.wl', '*.wls'] mimetypes = ['application/mathematica', 'application/vnd.wolfram.mathematica', 'application/vnd.wolfram.mathematica.package', - 'application/vnd.wolfram.cdf'] + 'application/vnd.wolfram.cdf', + 'application/vnd.wolfram.wl'] version_added = '2.0' - # http://reference.wolfram.com/mathematica/guide/Syntax.html + # https://reference.wolfram.com/language/guide/Syntax.html operators = ( ";;", "=", "=.", "!=" "==", ":=", "->", ":>", "/.", "+", "-", "*", "/", "^", "&&", "||", "!", "<>", "|", "/;", "?", "@", "//", "/@", "@@", diff --git a/libs/pygments/lexers/ambient.py b/libs/pygments/lexers/ambient.py index 3862eb0d9..2da64d12d 100644 --- a/libs/pygments/lexers/ambient.py +++ b/libs/pygments/lexers/ambient.py @@ -4,7 +4,7 @@ Lexers for AmbientTalk language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/amdgpu.py b/libs/pygments/lexers/amdgpu.py index c97ad617e..ead9ace89 100644 --- a/libs/pygments/lexers/amdgpu.py +++ b/libs/pygments/lexers/amdgpu.py @@ -4,7 +4,7 @@ Lexers for the AMDGPU ISA assembly. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ampl.py b/libs/pygments/lexers/ampl.py index bc4082b1b..3e051eb18 100644 --- a/libs/pygments/lexers/ampl.py +++ b/libs/pygments/lexers/ampl.py @@ -4,7 +4,7 @@ Lexers for the AMPL language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/apdlexer.py b/libs/pygments/lexers/apdlexer.py index 98beedfbe..eca004e24 100644 --- a/libs/pygments/lexers/apdlexer.py +++ b/libs/pygments/lexers/apdlexer.py @@ -4,7 +4,7 @@ Lexers for ANSYS Parametric Design Language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/apl.py b/libs/pygments/lexers/apl.py index a2ee3f475..147a7e21c 100644 --- a/libs/pygments/lexers/apl.py +++ b/libs/pygments/lexers/apl.py @@ -4,7 +4,7 @@ Lexers for APL. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/archetype.py b/libs/pygments/lexers/archetype.py index 47f34af9e..977b02f75 100644 --- a/libs/pygments/lexers/archetype.py +++ b/libs/pygments/lexers/archetype.py @@ -9,7 +9,7 @@ Contributed by Thomas Beale , . - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -35,8 +35,8 @@ class AtomsLexer(RegexLexer): (r'([ \t]*)(--.*)$', bygroups(Whitespace, Comment)), ], 'archetype_id': [ - (r'([ \t]*)(([a-zA-Z]\w+(\.[a-zA-Z]\w+)*::)?[a-zA-Z]\w+(-[a-zA-Z]\w+){2}' - r'\.\w+[\w-]*\.v\d+(\.\d+){,2}((-[a-z]+)(\.\d+)?)?)', + (r'([ \t]*)(([a-zA-Z]\w{1,100}(\.[a-zA-Z]\w{1,100})*::)?[a-zA-Z]\w{1,100}(-[a-zA-Z]\w{1,100}){2}' + r'\.\w{1,100}[\w-]*\.v\d+(\.\d+){,2}((-[a-z]+)(\.\d+)?)?)', bygroups(Whitespace, Name.Decorator)), ], 'date_constraints': [ @@ -293,7 +293,7 @@ class AdlLexer(AtomsLexer): # numbers and version ids (r'\d+(\.\d+)*', Literal), # Guids - (r'(\d|[a-fA-F])+(-(\d|[a-fA-F])+){3,}', Literal), + (r'[0-9a-fA-F]{1,36}(-[0-9a-fA-F]{1,36}){3,}', Literal), (r'\w+', Name.Class), (r'"', String, 'string'), (r'=', Operator), diff --git a/libs/pygments/lexers/arrow.py b/libs/pygments/lexers/arrow.py index e686cf1b6..3e55725b9 100644 --- a/libs/pygments/lexers/arrow.py +++ b/libs/pygments/lexers/arrow.py @@ -4,7 +4,7 @@ Lexer for Arrow. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/arturo.py b/libs/pygments/lexers/arturo.py index 82fc08983..c082a8981 100644 --- a/libs/pygments/lexers/arturo.py +++ b/libs/pygments/lexers/arturo.py @@ -4,7 +4,7 @@ Lexer for the Arturo language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/asc.py b/libs/pygments/lexers/asc.py index 243c996af..49b82f5aa 100644 --- a/libs/pygments/lexers/asc.py +++ b/libs/pygments/lexers/asc.py @@ -4,7 +4,7 @@ Lexer for various ASCII armored files. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re diff --git a/libs/pygments/lexers/asm.py b/libs/pygments/lexers/asm.py index 74f4a5a8e..2ce9c5777 100644 --- a/libs/pygments/lexers/asm.py +++ b/libs/pygments/lexers/asm.py @@ -4,7 +4,7 @@ Lexers for assembly languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -397,7 +397,14 @@ class LlvmLexer(RegexLexer): ], 'whitespace': [ (r'(\n|\s+)+', Whitespace), - (r';.*?\n', Comment) + (r';.*?\n', Comment), + (r'/\*', Comment, 'c-comment'), + ], + 'c-comment': [ + (r'[^*]+', Comment), + (r'\*/', Comment, '#pop'), + # Consume lone asterisks as non-comment-ending content. + (r'\*', Comment), ], 'keyword': [ # Regular keywords diff --git a/libs/pygments/lexers/asn1.py b/libs/pygments/lexers/asn1.py index 6c7861bdb..711801dc5 100644 --- a/libs/pygments/lexers/asn1.py +++ b/libs/pygments/lexers/asn1.py @@ -4,7 +4,7 @@ Pygments lexers for ASN.1. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -136,7 +136,7 @@ class Asn1Lexer(RegexLexer): (r'--.*$', Comment.Single), (r'/\*', Comment.Multiline, 'comment'), # Numbers: - (r'\d+\.\d*([eE][-+]?\d+)?', Number.Float), + (r'\d+\.\d+([eE][-+]?\d+)?', Number.Float), (r'\d+', Number.Integer), # Identifier: (r"&?[a-z][-a-zA-Z0-9]*[a-zA-Z0-9]\b", Name.Variable), @@ -155,7 +155,7 @@ class Asn1Lexer(RegexLexer): # Type identifier: (r"&?[A-Z][-a-zA-Z0-9]*[a-zA-Z0-9]\b", Name.Type), # Operators: - (r"(::=|\.\.\.|\.\.|\[\[|\]\]|\||\^)", Operator), + (r"(::=|\.\.\.|\.\.|\[\[|\]\]|\||\^|-)", Operator), # Punctuation: (r"(\.|,|\{|\}|\(|\)|\[|\])", Punctuation), # String: diff --git a/libs/pygments/lexers/automation.py b/libs/pygments/lexers/automation.py index 4e9c67811..9f834ac80 100644 --- a/libs/pygments/lexers/automation.py +++ b/libs/pygments/lexers/automation.py @@ -4,7 +4,7 @@ Lexers for automation scripting languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/bare.py b/libs/pygments/lexers/bare.py index d7167dc41..ba512b9d0 100644 --- a/libs/pygments/lexers/bare.py +++ b/libs/pygments/lexers/bare.py @@ -4,7 +4,7 @@ Lexer for the BARE schema. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/basic.py b/libs/pygments/lexers/basic.py index 7c9123de7..a24101c14 100644 --- a/libs/pygments/lexers/basic.py +++ b/libs/pygments/lexers/basic.py @@ -4,7 +4,7 @@ Lexers for BASIC like languages (other than VB.net). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/bdd.py b/libs/pygments/lexers/bdd.py index 6ddb6056d..82a03b64a 100644 --- a/libs/pygments/lexers/bdd.py +++ b/libs/pygments/lexers/bdd.py @@ -4,7 +4,7 @@ Lexer for BDD(Behavior-driven development). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/berry.py b/libs/pygments/lexers/berry.py index 5f772b1ad..04f6cc5ff 100644 --- a/libs/pygments/lexers/berry.py +++ b/libs/pygments/lexers/berry.py @@ -4,7 +4,7 @@ Lexer for Berry. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/bibtex.py b/libs/pygments/lexers/bibtex.py index 32cc40ae7..069219635 100644 --- a/libs/pygments/lexers/bibtex.py +++ b/libs/pygments/lexers/bibtex.py @@ -4,7 +4,7 @@ Lexers for BibTeX bibliography data and styles - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/blueprint.py b/libs/pygments/lexers/blueprint.py index 59f9c42a4..1dcc235e2 100644 --- a/libs/pygments/lexers/blueprint.py +++ b/libs/pygments/lexers/blueprint.py @@ -4,7 +4,7 @@ Lexer for the Blueprint UI markup language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/boa.py b/libs/pygments/lexers/boa.py index 6f4e8cdaf..06977dd19 100644 --- a/libs/pygments/lexers/boa.py +++ b/libs/pygments/lexers/boa.py @@ -4,7 +4,7 @@ Lexers for the Boa language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/bqn.py b/libs/pygments/lexers/bqn.py index 083f69b61..beb292fbf 100644 --- a/libs/pygments/lexers/bqn.py +++ b/libs/pygments/lexers/bqn.py @@ -4,7 +4,7 @@ Lexer for BQN. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/business.py b/libs/pygments/lexers/business.py index 73adb8709..2b4ce6be5 100644 --- a/libs/pygments/lexers/business.py +++ b/libs/pygments/lexers/business.py @@ -4,7 +4,7 @@ Lexers for "business-oriented" languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/c_cpp.py b/libs/pygments/lexers/c_cpp.py index 399f2bc4f..247953a62 100644 --- a/libs/pygments/lexers/c_cpp.py +++ b/libs/pygments/lexers/c_cpp.py @@ -4,7 +4,7 @@ Lexers for C/C++ languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -36,7 +36,7 @@ class CFamilyLexer(RegexLexer): # This includes decimal separators matching. _decpart = r'\d(\'?\d)*' # Integer literal suffix (e.g. 'ull' or 'll'). - _intsuffix = r'(([uU][lL]{0,2})|[lL]{1,2}[uU]?)?' + _intsuffix = r'(([uU]?[zZ])|([zZ][uU])|([uU][lL]{0,2})|([lL]{1,2}[uU]?))?' # Identifier regex with C and C++ Universal Character Name (UCN) support. _ident = r'(?!\d)(?:[\w$]|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8})+' @@ -100,7 +100,7 @@ class CFamilyLexer(RegexLexer): (r'(-)?' + _decpart + _intsuffix, Number.Integer), (r'[~!%^&*+=|?:<>/-]', Operator), (r'[()\[\],.]', Punctuation), - (r'(true|false|NULL)\b', Name.Builtin), + (r'(true|false|NULL|nullptr)\b', Name.Builtin), (_ident, Name) ], 'types': [ @@ -113,11 +113,11 @@ class CFamilyLexer(RegexLexer): 'keywords': [ (r'(struct|union)(\s+)', bygroups(Keyword, Whitespace), 'classname'), (r'case\b', Keyword, 'case-value'), - (words(('asm', 'auto', 'break', 'const', 'continue', 'default', - 'do', 'else', 'enum', 'extern', 'for', 'goto', 'if', - 'register', 'restricted', 'return', 'sizeof', 'struct', - 'static', 'switch', 'typedef', 'volatile', 'while', 'union', - 'thread_local', 'alignas', 'alignof', 'static_assert', '_Pragma'), + (words(('asm', 'auto', 'break', 'const', 'constexpr', 'continue', 'default', + 'do', 'else', 'enum', 'extern', 'for', 'goto', 'if', 'register', + 'restricted', 'return', 'sizeof', 'struct', 'static', 'switch', + 'typedef', 'typeof', 'typeof_unqual', 'volatile', 'while', 'union', + 'thread_local', 'alignas', 'alignof', 'static_assert', '_Pragma', 'fortran'), suffix=r'\b'), Keyword), (words(('inline', '_inline', '__inline', 'naked', 'restrict', 'thread'), suffix=r'\b'), Keyword.Reserved), @@ -128,7 +128,7 @@ class CFamilyLexer(RegexLexer): 'asm', 'based', 'except', 'stdcall', 'cdecl', 'fastcall', 'declspec', 'finally', 'try', 'leave', 'w64', 'unaligned', 'raise', 'noop', - 'identifier', 'forceinline', 'assume'), + 'identifier', 'forceinline', 'assume', 'null'), prefix=r'__', suffix=r'\b'), Keyword.Reserved) ], 'root': [ @@ -301,13 +301,13 @@ class CLexer(CFamilyLexer): tokens = { 'keywords': [ (words(( - '_Alignas', '_Alignof', '_Noreturn', '_Generic', '_Thread_local', - '_Static_assert', '_Imaginary', 'noreturn', 'imaginary', 'complex'), + '_Alignas', '_Alignof', '_Noreturn', '_Countof', '_Generic', '_Thread_local', + '_Static_assert', '_Imaginary', 'countof', 'noreturn', 'imaginary', 'complex'), suffix=r'\b'), Keyword), inherit ], 'types': [ - (words(('_Bool', '_Complex', '_Atomic'), suffix=r'\b'), Keyword.Type), + (words(('_Bool', '_Complex', '_Atomic', '_Decimal32', '_Decimal64', '_Decimal128'), suffix=r'\b'), Keyword.Type), inherit ] } @@ -347,7 +347,8 @@ class CppLexer(CFamilyLexer): aliases = ['cpp', 'c++'] filenames = ['*.cpp', '*.hpp', '*.c++', '*.h++', '*.cc', '*.hh', '*.cxx', '*.hxx', - '*.C', '*.H', '*.cp', '*.CPP', '*.tpp'] + '*.C', '*.H', '*.cp', '*.CPP', '*.tpp', + '*.cppm', '*.ixx', '*.mxx'] mimetypes = ['text/x-c++hdr', 'text/x-c++src'] version_added = '' priority = 0.1 @@ -363,8 +364,8 @@ class CppLexer(CFamilyLexer): 'root': [ inherit, # C++ Microsoft-isms - (words(('virtual_inheritance', 'uuidof', 'super', 'single_inheritance', - 'multiple_inheritance', 'interface', 'event'), + (words(('virtual_inheritance', 'uuidof', 'super', 'extends', 'single_inheritance', + 'multiple_inheritance', 'interface', 'implements', 'event', 'finally', 'null'), prefix=r'__', suffix=r'\b'), Keyword.Reserved), # Offload C++ extensions, http://offload.codeplay.com/ (r'__(offload|blockingoffload|outer)\b', Keyword.Pseudo), @@ -385,11 +386,11 @@ class CppLexer(CFamilyLexer): 'export', 'friend', 'mutable', 'new', 'operator', 'private', 'protected', 'public', 'reinterpret_cast', 'class', '__restrict', 'static_cast', 'template', 'this', 'throw', 'throws', - 'try', 'typeid', 'using', 'virtual', 'constexpr', 'nullptr', 'concept', + 'try', 'typeid', 'using', 'virtual', 'concept', 'decltype', 'noexcept', 'override', 'final', 'constinit', 'consteval', 'co_await', 'co_return', 'co_yield', 'requires', 'import', 'module', 'typename', 'and', 'and_eq', 'bitand', 'bitor', 'compl', 'not', - 'not_eq', 'or', 'or_eq', 'xor', 'xor_eq'), + 'not_eq', 'or', 'or_eq', 'xor', 'xor_eq', 'contract_assert', 'pre', 'post'), suffix=r'\b'), Keyword), (r'namespace\b', Keyword, 'namespace'), (r'(enum)(\s+)', bygroups(Keyword, Whitespace), 'enumname'), diff --git a/libs/pygments/lexers/c_like.py b/libs/pygments/lexers/c_like.py index 1ba269e82..24dea1ad2 100644 --- a/libs/pygments/lexers/c_like.py +++ b/libs/pygments/lexers/c_like.py @@ -4,7 +4,7 @@ Lexers for other C-like languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/capnproto.py b/libs/pygments/lexers/capnproto.py index 333760e15..1e819c7f8 100644 --- a/libs/pygments/lexers/capnproto.py +++ b/libs/pygments/lexers/capnproto.py @@ -4,7 +4,7 @@ Lexers for the Cap'n Proto schema language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/carbon.py b/libs/pygments/lexers/carbon.py index 702b37bc8..d45b78800 100644 --- a/libs/pygments/lexers/carbon.py +++ b/libs/pygments/lexers/carbon.py @@ -4,7 +4,7 @@ Lexers for the Carbon programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re diff --git a/libs/pygments/lexers/cddl.py b/libs/pygments/lexers/cddl.py index fa0c85f10..38346ddb9 100644 --- a/libs/pygments/lexers/cddl.py +++ b/libs/pygments/lexers/cddl.py @@ -8,7 +8,7 @@ More information: https://datatracker.ietf.org/doc/rfc8610/ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/chapel.py b/libs/pygments/lexers/chapel.py index 955a1579a..1075d2f49 100644 --- a/libs/pygments/lexers/chapel.py +++ b/libs/pygments/lexers/chapel.py @@ -4,7 +4,7 @@ Lexer for the Chapel language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/clean.py b/libs/pygments/lexers/clean.py index f96e74373..1500e2c4c 100644 --- a/libs/pygments/lexers/clean.py +++ b/libs/pygments/lexers/clean.py @@ -4,7 +4,7 @@ Lexer for the Clean language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/codeql.py b/libs/pygments/lexers/codeql.py index 3d06bec6c..ab27bf508 100644 --- a/libs/pygments/lexers/codeql.py +++ b/libs/pygments/lexers/codeql.py @@ -7,7 +7,7 @@ The grammar is originating from: https://github.com/github/vscode-codeql/blob/main/syntaxes/README.md - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/comal.py b/libs/pygments/lexers/comal.py index 3c89a59ea..6b0a8d11a 100644 --- a/libs/pygments/lexers/comal.py +++ b/libs/pygments/lexers/comal.py @@ -4,7 +4,7 @@ Lexer for COMAL-80. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/compiled.py b/libs/pygments/lexers/compiled.py index 5e0cc188c..fb0a70364 100644 --- a/libs/pygments/lexers/compiled.py +++ b/libs/pygments/lexers/compiled.py @@ -4,7 +4,7 @@ Just export lexer classes previously contained in this module. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/configs.py b/libs/pygments/lexers/configs.py index 5e7f47b6d..e5435923f 100644 --- a/libs/pygments/lexers/configs.py +++ b/libs/pygments/lexers/configs.py @@ -4,7 +4,7 @@ Lexers for configuration file formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -1127,7 +1127,7 @@ class TOMLLexer(RegexLexer): # Based on the TOML spec: https://toml.io/en/v1.0.0 # The following is adapted from CPython's tomllib: - _time = r"\d\d:\d\d:\d\d(\.\d+)?" + _time = r"\d\d:\d\d(:\d\d(\.\d+)?)?" _datetime = rf"""(?x) \d\d\d\d-\d\d-\d\d # date, e.g., 1988-10-27 ( @@ -1238,9 +1238,9 @@ class TOMLLexer(RegexLexer): default('value'), ], 'inline-table': [ - # Note that unlike inline arrays, inline tables do not - # allow newlines or comments. - (r'[ \t]+', Whitespace), + # Whitespace (since TOML 1.1.0, same as in array) + (r'\s+', Whitespace), + (r'#.*', Comment.Single), # Keys include('key'), @@ -1275,7 +1275,7 @@ class TOMLLexer(RegexLexer): (r"'", String.Single), ], 'escapes': [ - (r'\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}', String.Escape), + (r'\\x[0-9a-fA-F]{2}|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}', String.Escape), (r'\\.', String.Escape), ], } diff --git a/libs/pygments/lexers/console.py b/libs/pygments/lexers/console.py index d66cbaa95..546ba36e9 100644 --- a/libs/pygments/lexers/console.py +++ b/libs/pygments/lexers/console.py @@ -4,7 +4,7 @@ Lexers for misc console output. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/cplint.py b/libs/pygments/lexers/cplint.py index f8424252e..8222afa4f 100644 --- a/libs/pygments/lexers/cplint.py +++ b/libs/pygments/lexers/cplint.py @@ -4,7 +4,7 @@ Lexer for the cplint language - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/crystal.py b/libs/pygments/lexers/crystal.py index f5da4f84c..8fb471e97 100644 --- a/libs/pygments/lexers/crystal.py +++ b/libs/pygments/lexers/crystal.py @@ -4,7 +4,7 @@ Lexer for Crystal. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/csound.py b/libs/pygments/lexers/csound.py index 403b66f96..7be761c5a 100644 --- a/libs/pygments/lexers/csound.py +++ b/libs/pygments/lexers/csound.py @@ -4,7 +4,7 @@ Lexers for Csound languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/css.py b/libs/pygments/lexers/css.py index 97897db65..ed54d93c3 100644 --- a/libs/pygments/lexers/css.py +++ b/libs/pygments/lexers/css.py @@ -4,7 +4,7 @@ Lexers for CSS and related stylesheet formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -590,13 +590,43 @@ class LessCssLexer(CssLexer): version_added = '2.1' tokens = { + # FIXME: It's not currently possible to simply do the following, + # as `include('basics')` doesn't take overrides into account: + # 'basics': [ + # (r'//.*\n', Comment.Single), + # (r'/\*(?:.|\n)*?\*/', Comment.Multiline), + # inherit, + # ], 'root': [ (r'@\w+', Name.Variable), + (r'//.*\n', Comment.Single), + (r'/\*(?:.|\n)*?\*/', Comment.Multiline), + inherit, + ], + 'atcontent': [ + (r'//.*\n', Comment.Single), + (r'/\*(?:.|\n)*?\*/', Comment.Multiline), + inherit, + ], + 'atrule': [ + (r'//.*\n', Comment.Single), + (r'/\*(?:.|\n)*?\*/', Comment.Multiline), inherit, ], 'content': [ (r'\{', Punctuation, '#push'), (r'//.*\n', Comment.Single), + (r'/\*(?:.|\n)*?\*/', Comment.Multiline), + inherit, + ], + 'value-start': [ + (r'//.*\n', Comment.Single), + (r'/\*(?:.|\n)*?\*/', Comment.Multiline), + inherit, + ], + 'function-start': [ + (r'//.*\n', Comment.Single), + (r'/\*(?:.|\n)*?\*/', Comment.Multiline), inherit, ], } diff --git a/libs/pygments/lexers/d.py b/libs/pygments/lexers/d.py index a0b56e9ca..e85c33c94 100644 --- a/libs/pygments/lexers/d.py +++ b/libs/pygments/lexers/d.py @@ -4,7 +4,7 @@ Lexers for D languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/dalvik.py b/libs/pygments/lexers/dalvik.py index 1ac332bfa..2d7056295 100644 --- a/libs/pygments/lexers/dalvik.py +++ b/libs/pygments/lexers/dalvik.py @@ -4,7 +4,7 @@ Pygments lexers for Dalvik VM-related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/data.py b/libs/pygments/lexers/data.py index f43203252..2901d2acb 100644 --- a/libs/pygments/lexers/data.py +++ b/libs/pygments/lexers/data.py @@ -4,7 +4,7 @@ Lexers for data file format. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -447,7 +447,7 @@ class JsonLexer(Lexer): name = 'JSON' url = 'https://www.json.org' aliases = ['json', 'json-object'] - filenames = ['*.json', '*.jsonl', '*.ndjson', 'Pipfile.lock'] + filenames = ['*.json', '*.jsonl', '*.ndjson', 'Pipfile.lock', '*.module', '*.xc'] mimetypes = ['application/json', 'application/json-object', 'application/x-ndjson', 'application/jsonl', 'application/json-seq'] version_added = '1.5' diff --git a/libs/pygments/lexers/dax.py b/libs/pygments/lexers/dax.py index 9c333947e..d9b797051 100644 --- a/libs/pygments/lexers/dax.py +++ b/libs/pygments/lexers/dax.py @@ -4,7 +4,7 @@ Lexer for LilyPond. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/devicetree.py b/libs/pygments/lexers/devicetree.py index a9b0b7aa4..09860b960 100644 --- a/libs/pygments/lexers/devicetree.py +++ b/libs/pygments/lexers/devicetree.py @@ -4,7 +4,7 @@ Lexers for Devicetree language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -63,15 +63,19 @@ class DevicetreeLexer(RegexLexer): '#size-cells', 'reg', 'virtual-reg', 'ranges', 'dma-ranges', 'device_type', 'name'), suffix=r'\b'), Keyword.Reserved), (r'([~!%^&*+=|?:<>/#-])', Operator), - (r'[()\[\]{},.]', Punctuation), - (r'[a-zA-Z_][\w-]*(?=(?:\s*,\s*[a-zA-Z_][\w-]*|(?:' + _ws + r'))*\s*[=;])', + (r'[(){},.\]]', Punctuation), + (r'\[', Punctuation, 'bytestring'), + (r'[a-zA-Z_][\w-]*(?=(?:\s*,' + _ws + r'[a-zA-Z_][\w-]*)*' + _ws + r'[=;])', Name), (r'[a-zA-Z_]\w*', Name.Attribute), ], 'root': [ include('whitespace'), include('macro'), - + # Overlay/fragment: &label { ... } or &{/path/to/node} { ... } + (r'(&)(?:([A-Za-z_]\w*)|(\{)([^}]+)(\}))(' + _ws + r')(\{)', + bygroups(Operator, Name.Function, Punctuation, Name.Namespace, + Punctuation, Comment.Multiline, Punctuation), 'node'), # Nodes (r'([^/*@\s&]+|/)(@?)((?:0x)?[0-9a-fA-F,]*)(' + _ws + r')(\{)', bygroups(Name.Function, Operator, Number.Integer, @@ -87,7 +91,10 @@ class DevicetreeLexer(RegexLexer): 'node': [ include('whitespace'), include('macro'), - + # Overlay/fragment: &label { ... } or &{/path/to/node} { ... } + (r'(&)(?:([A-Za-z_]\w*)|(\{)([^}]+)(\}))(' + _ws + r')(\{)', + bygroups(Operator, Name.Function, Punctuation, Name.Namespace, + Punctuation, Comment.Multiline, Punctuation), '#push'), (r'([^/*@\s&]+|/)(@?)((?:0x)?[0-9a-fA-F,]*)(' + _ws + r')(\{)', bygroups(Name.Function, Operator, Number.Integer, Comment.Multiline, Punctuation), '#push'), @@ -105,4 +112,9 @@ class DevicetreeLexer(RegexLexer): (r'\\\n', String), # line continuation (r'\\', String), # stray backslash ], + 'bytestring': [ + (r'\]', Punctuation, '#pop'), + (r'[0-9a-fA-F]{2}', Number.Hex), + (r'\s+', Whitespace), + ], } diff --git a/libs/pygments/lexers/diff.py b/libs/pygments/lexers/diff.py index ed7007f5b..b298a3c12 100644 --- a/libs/pygments/lexers/diff.py +++ b/libs/pygments/lexers/diff.py @@ -4,7 +4,7 @@ Lexers for diff/patch formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/dns.py b/libs/pygments/lexers/dns.py index 3fb91f571..121cf1f79 100644 --- a/libs/pygments/lexers/dns.py +++ b/libs/pygments/lexers/dns.py @@ -4,7 +4,7 @@ Pygments lexers for DNS - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/dotnet.py b/libs/pygments/lexers/dotnet.py index 2254a8b2e..dbee643a6 100644 --- a/libs/pygments/lexers/dotnet.py +++ b/libs/pygments/lexers/dotnet.py @@ -4,7 +4,7 @@ Lexers for .net languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re diff --git a/libs/pygments/lexers/dsls.py b/libs/pygments/lexers/dsls.py index d30c11249..a928683e7 100644 --- a/libs/pygments/lexers/dsls.py +++ b/libs/pygments/lexers/dsls.py @@ -4,7 +4,7 @@ Lexers for various domain-specific languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -67,7 +67,7 @@ class ProtoBufLexer(RegexLexer): (r'[a-zA-Z_][\w.]*', Name), ], 'package': [ - (r'[a-zA-Z_]\w*', Name.Namespace, '#pop'), + (r'[a-zA-Z_][\w\.]*', Name.Namespace, '#pop'), default('#pop'), ], 'message': [ diff --git a/libs/pygments/lexers/dylan.py b/libs/pygments/lexers/dylan.py index 2109bd52c..c75781580 100644 --- a/libs/pygments/lexers/dylan.py +++ b/libs/pygments/lexers/dylan.py @@ -4,7 +4,7 @@ Lexers for the Dylan language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ecl.py b/libs/pygments/lexers/ecl.py index f1ee3bf17..2e6d34719 100644 --- a/libs/pygments/lexers/ecl.py +++ b/libs/pygments/lexers/ecl.py @@ -4,7 +4,7 @@ Lexers for the ECL language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/eiffel.py b/libs/pygments/lexers/eiffel.py index 691c71873..c8b7eab5f 100644 --- a/libs/pygments/lexers/eiffel.py +++ b/libs/pygments/lexers/eiffel.py @@ -4,7 +4,7 @@ Lexer for the Eiffel language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/elm.py b/libs/pygments/lexers/elm.py index cebbbf002..201ffc340 100644 --- a/libs/pygments/lexers/elm.py +++ b/libs/pygments/lexers/elm.py @@ -4,7 +4,7 @@ Lexer for the Elm programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/elpi.py b/libs/pygments/lexers/elpi.py index 5e4b1e5a0..4e099ba62 100644 --- a/libs/pygments/lexers/elpi.py +++ b/libs/pygments/lexers/elpi.py @@ -4,7 +4,7 @@ Lexer for the `Elpi `_ programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -14,7 +14,7 @@ from pygments.token import Text, Comment, Operator, Keyword, Name, String, \ __all__ = ['ElpiLexer'] -from pygments.lexers.theorem import CoqLexer +from pygments.lexers.theorem import RocqLexer class ElpiLexer(RegexLexer): """ @@ -57,6 +57,9 @@ class ElpiLexer(RegexLexer): (rf"\b(external pred|pred)(\s+)({const_sym_re})", bygroups(Keyword.Declaration, Text.Whitespace, Name.Function), 'elpi-pred-item'), + (rf"\b(func)(\s+)({const_sym_re})", + bygroups(Keyword.Declaration, Text.Whitespace, Name.Function), + 'elpi-func-item'), (rf"\b(external type|type)(\s+)(({const_sym_re}(,\s*)?)+)", bygroups(Keyword.Declaration, Text.Whitespace, Name.Function), 'elpi-type'), @@ -110,6 +113,7 @@ class ElpiLexer(RegexLexer): 'elpi-type': [ (r"(ctype\s+)(\")", bygroups(Keyword.Type, String.Double), 'elpi-string'), (r'->', Keyword.Type), + (r'prop', Keyword.Mode), (constant_re, Keyword.Type), (r"\(|\)", Keyword.Type), (r"\.", Text, '#pop'), @@ -126,19 +130,41 @@ class ElpiLexer(RegexLexer): include('elpi'), ], 'elpi-pred-item': [ - (r"[io]:", Keyword.Mode, 'elpi-ctype'), + (r"[io]:", Keyword.Mode), (r"\.", Text, '#pop'), + (r",", Keyword.Mode), + include('_elpi-inner-pred-fun'), + (r"\)", Keyword.Mode, '#pop'), + (r"\(", Keyword.Type, '_elpi-type-item'), include('_elpi-comment'), + include('_elpi-type-item'), ], - 'elpi-ctype': [ - (r"(ctype\s+)(\")", bygroups(Keyword.Type, String.Double), 'elpi-string'), + 'elpi-func-item': [ + (constant_re, Keyword.Type), + (r"\.", Text, '#pop'), + (r",", Keyword.Mode), + (r'->', Keyword.Mode), + include('_elpi-inner-pred-fun'), + (r"\)", Keyword.Mode, '#pop'), + (r"\(", Keyword.Type, '_elpi-type-item'), + include('_elpi-comment'), + include('_elpi-type-item'), + ], + '_elpi-inner-pred-fun': [ + (r"(\()(\s*)(pred)", bygroups(Keyword.Mode,Text.Whitespace,Keyword.Declaration), 'elpi-pred-item'), + (r"(\()(\s*)(func)", bygroups(Keyword.Mode,Text.Whitespace,Keyword.Declaration), 'elpi-func-item'), + ], + '_elpi-type-item': [ (r'->', Keyword.Type), (constant_re, Keyword.Type), - (r"\(|\)", Keyword.Type), - (r",", Text, '#pop'), - (r"\.", Text, '#pop:2'), + include('_elpi-inner-pred-fun'), + (r"\(", Keyword.Type, '#push'), + (r"\)", Keyword.Type, '#pop'), include('_elpi-comment'), ], + + '' + 'elpi-btick': [ (r'[^` ]+', String.Double), (r'`', String.Double, '#pop'), @@ -156,7 +182,7 @@ class ElpiLexer(RegexLexer): (r"\s+", Text.Whitespace), (r"(lp:)(\{\{)", bygroups(Number, Punctuation), 'elpi-quote-exit'), (rf"(lp:)((?=[A-Z_]){constant_re})", bygroups(Number, Name.Variable)), - (r"((?!lp:|\}\}).)+", using(CoqLexer)), + (r"((?!lp:|\}\}).)+", using(RocqLexer)), ], 'elpi-quote-exit': [ include('elpi'), diff --git a/libs/pygments/lexers/email.py b/libs/pygments/lexers/email.py index ea071c800..194c19e12 100644 --- a/libs/pygments/lexers/email.py +++ b/libs/pygments/lexers/email.py @@ -4,7 +4,7 @@ Lexer for the raw E-mail. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/erlang.py b/libs/pygments/lexers/erlang.py index 120f5049f..5c7cd6163 100644 --- a/libs/pygments/lexers/erlang.py +++ b/libs/pygments/lexers/erlang.py @@ -4,7 +4,7 @@ Lexers for Erlang. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/esoteric.py b/libs/pygments/lexers/esoteric.py index fe3825c35..7b7661297 100644 --- a/libs/pygments/lexers/esoteric.py +++ b/libs/pygments/lexers/esoteric.py @@ -4,7 +4,7 @@ Lexers for esoteric languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ezhil.py b/libs/pygments/lexers/ezhil.py index aeb71ab0c..164424d8a 100644 --- a/libs/pygments/lexers/ezhil.py +++ b/libs/pygments/lexers/ezhil.py @@ -4,7 +4,7 @@ Pygments lexers for Ezhil language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/factor.py b/libs/pygments/lexers/factor.py index 2ec3f9be2..6c1060149 100644 --- a/libs/pygments/lexers/factor.py +++ b/libs/pygments/lexers/factor.py @@ -4,7 +4,7 @@ Lexers for the Factor language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/fantom.py b/libs/pygments/lexers/fantom.py index 7228554ba..556df5145 100644 --- a/libs/pygments/lexers/fantom.py +++ b/libs/pygments/lexers/fantom.py @@ -4,7 +4,7 @@ Lexer for the Fantom language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/felix.py b/libs/pygments/lexers/felix.py index a42ac0805..442616248 100644 --- a/libs/pygments/lexers/felix.py +++ b/libs/pygments/lexers/felix.py @@ -4,7 +4,7 @@ Lexer for the Felix language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/fift.py b/libs/pygments/lexers/fift.py index 1263236c9..c813ee788 100644 --- a/libs/pygments/lexers/fift.py +++ b/libs/pygments/lexers/fift.py @@ -4,7 +4,7 @@ Lexers for fift. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/floscript.py b/libs/pygments/lexers/floscript.py index 9c7eaab5a..776b033ca 100644 --- a/libs/pygments/lexers/floscript.py +++ b/libs/pygments/lexers/floscript.py @@ -4,7 +4,7 @@ Lexer for FloScript - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/forth.py b/libs/pygments/lexers/forth.py index 51f75affb..6d6c764db 100644 --- a/libs/pygments/lexers/forth.py +++ b/libs/pygments/lexers/forth.py @@ -4,7 +4,7 @@ Lexer for the Forth language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/fortran.py b/libs/pygments/lexers/fortran.py index a6230f024..d4bceb8c4 100644 --- a/libs/pygments/lexers/fortran.py +++ b/libs/pygments/lexers/fortran.py @@ -4,7 +4,7 @@ Lexers for Fortran languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/foxpro.py b/libs/pygments/lexers/foxpro.py index 3e7c05697..9ce82c12d 100644 --- a/libs/pygments/lexers/foxpro.py +++ b/libs/pygments/lexers/foxpro.py @@ -4,7 +4,7 @@ Simple lexer for Microsoft Visual FoxPro source code. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/freefem.py b/libs/pygments/lexers/freefem.py index 87b066b1a..895806991 100644 --- a/libs/pygments/lexers/freefem.py +++ b/libs/pygments/lexers/freefem.py @@ -4,7 +4,7 @@ Lexer for FreeFem++ language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/func.py b/libs/pygments/lexers/func.py index d8c912a3e..ef4b27e75 100644 --- a/libs/pygments/lexers/func.py +++ b/libs/pygments/lexers/func.py @@ -4,7 +4,7 @@ Lexers for FunC. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/functional.py b/libs/pygments/lexers/functional.py index 6a051d8fd..261d7d876 100644 --- a/libs/pygments/lexers/functional.py +++ b/libs/pygments/lexers/functional.py @@ -4,7 +4,7 @@ Just export lexer classes previously contained in this module. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,7 +13,7 @@ from pygments.lexers.lisp import SchemeLexer, CommonLispLexer, RacketLexer, \ NewLispLexer, ShenLexer from pygments.lexers.haskell import HaskellLexer, LiterateHaskellLexer, \ KokaLexer -from pygments.lexers.theorem import CoqLexer +from pygments.lexers.theorem import RocqLexer from pygments.lexers.erlang import ErlangLexer, ErlangShellLexer, \ ElixirConsoleLexer, ElixirLexer from pygments.lexers.ml import SMLLexer, OcamlLexer, OpaLexer diff --git a/libs/pygments/lexers/futhark.py b/libs/pygments/lexers/futhark.py index dd2efe83e..2908d3fc7 100644 --- a/libs/pygments/lexers/futhark.py +++ b/libs/pygments/lexers/futhark.py @@ -4,7 +4,7 @@ Lexer for the Futhark language - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/gcodelexer.py b/libs/pygments/lexers/gcodelexer.py index 9e968bd6b..c7d64de00 100644 --- a/libs/pygments/lexers/gcodelexer.py +++ b/libs/pygments/lexers/gcodelexer.py @@ -4,7 +4,7 @@ Lexers for the G Code Language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/gdscript.py b/libs/pygments/lexers/gdscript.py index 6dfe58976..a43c754ca 100644 --- a/libs/pygments/lexers/gdscript.py +++ b/libs/pygments/lexers/gdscript.py @@ -7,7 +7,7 @@ Modified by Daniel J. Ramirez based on the original python.py. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/gleam.py b/libs/pygments/lexers/gleam.py index 19047e13e..0df79553e 100644 --- a/libs/pygments/lexers/gleam.py +++ b/libs/pygments/lexers/gleam.py @@ -4,7 +4,7 @@ Lexer for the Gleam programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/go.py b/libs/pygments/lexers/go.py index 77a0fbce9..f698d4cc6 100644 --- a/libs/pygments/lexers/go.py +++ b/libs/pygments/lexers/go.py @@ -4,7 +4,7 @@ Lexers for the Google Go language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/grammar_notation.py b/libs/pygments/lexers/grammar_notation.py index 45274fd14..4290f2124 100644 --- a/libs/pygments/lexers/grammar_notation.py +++ b/libs/pygments/lexers/grammar_notation.py @@ -4,7 +4,7 @@ Lexers for grammar notations like BNF. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/graph.py b/libs/pygments/lexers/graph.py index 01d75743a..28487174a 100644 --- a/libs/pygments/lexers/graph.py +++ b/libs/pygments/lexers/graph.py @@ -4,7 +4,7 @@ Lexers for graph query languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/graphics.py b/libs/pygments/lexers/graphics.py index 400be4fd8..39b5f5eaf 100644 --- a/libs/pygments/lexers/graphics.py +++ b/libs/pygments/lexers/graphics.py @@ -4,7 +4,7 @@ Lexers for computer graphics and plotting related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/graphql.py b/libs/pygments/lexers/graphql.py index 7de5b6e7c..466bcf395 100644 --- a/libs/pygments/lexers/graphql.py +++ b/libs/pygments/lexers/graphql.py @@ -8,7 +8,7 @@ More information: https://graphql.org/ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/graphviz.py b/libs/pygments/lexers/graphviz.py index 6b4841554..8802e03a1 100644 --- a/libs/pygments/lexers/graphviz.py +++ b/libs/pygments/lexers/graphviz.py @@ -4,7 +4,7 @@ Lexer for the DOT language (graphviz). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/gsql.py b/libs/pygments/lexers/gsql.py index 9ff187889..13ae58c81 100644 --- a/libs/pygments/lexers/gsql.py +++ b/libs/pygments/lexers/gsql.py @@ -4,7 +4,7 @@ Lexers for TigerGraph GSQL graph query language - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/hare.py b/libs/pygments/lexers/hare.py index 56548d5e5..378f099a2 100644 --- a/libs/pygments/lexers/hare.py +++ b/libs/pygments/lexers/hare.py @@ -4,7 +4,7 @@ Lexers for the Hare language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/haskell.py b/libs/pygments/lexers/haskell.py index 0a9f2c15a..c6561d883 100644 --- a/libs/pygments/lexers/haskell.py +++ b/libs/pygments/lexers/haskell.py @@ -4,7 +4,7 @@ Lexers for Haskell and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -53,7 +53,8 @@ class HaskellLexer(RegexLexer): (r'\bmodule\b', Keyword.Reserved, 'module'), (r'\berror\b', Name.Exception), (r'\b({})(?!\')\b'.format('|'.join(reserved)), Keyword.Reserved), - (r"'[^\\]'", String.Char), # this has to come before the TH quote + (r"'[^\\]'", String.Char), # character literal + (r"'\\.'", String.Char), # escape character literal (e.g. '\n') (r'^[_' + uni.Ll + r'][\w\']*', Name.Function), (r"'?[_" + uni.Ll + r"][\w']*", Name), (r"('')?[" + uni.Lu + r"][\w\']*", Keyword.Type), diff --git a/libs/pygments/lexers/haxe.py b/libs/pygments/lexers/haxe.py index 66bd2a350..7af240178 100644 --- a/libs/pygments/lexers/haxe.py +++ b/libs/pygments/lexers/haxe.py @@ -4,7 +4,7 @@ Lexers for Haxe and related stuff. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -890,8 +890,14 @@ class HaxeLexer(ExtendedRegexLexer): } def analyse_text(text): - if re.match(r'\w+\s*:\s*\w', text): - return 0.3 + rv = 0 + if re.search(r'\bimport\s+haxe\.', text): + rv = max(rv, 0.2) + if re.search(r'\bStd\.', text): + rv = max(rv, 0.1) + if re.search(r'\btrace\s*\(', text): + rv = max(rv, 0.05) + return rv class HxmlLexer(RegexLexer): diff --git a/libs/pygments/lexers/hdl.py b/libs/pygments/lexers/hdl.py index d22b66ff6..85f18f319 100644 --- a/libs/pygments/lexers/hdl.py +++ b/libs/pygments/lexers/hdl.py @@ -4,7 +4,7 @@ Lexers for hardware descriptor languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/hexdump.py b/libs/pygments/lexers/hexdump.py index 4da4edb6a..41fb05908 100644 --- a/libs/pygments/lexers/hexdump.py +++ b/libs/pygments/lexers/hexdump.py @@ -4,7 +4,7 @@ Lexers for hexadecimal dumps. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/html.py b/libs/pygments/lexers/html.py index 66ba6609f..4720a3c46 100644 --- a/libs/pygments/lexers/html.py +++ b/libs/pygments/lexers/html.py @@ -4,7 +4,7 @@ Lexers for HTML, XML and related markup. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -202,7 +202,7 @@ class XmlLexer(RegexLexer): name = 'XML' aliases = ['xml'] filenames = ['*.xml', '*.xsl', '*.rss', '*.xslt', '*.xsd', - '*.wsdl', '*.wsf'] + '*.wsdl', '*.wsf', '*.xbrl', '*.pom'] mimetypes = ['text/xml', 'application/xml', 'image/svg+xml', 'application/rss+xml', 'application/atom+xml'] url = 'https://www.w3.org/XML' diff --git a/libs/pygments/lexers/idl.py b/libs/pygments/lexers/idl.py index 21b8d3130..adbe8cd3d 100644 --- a/libs/pygments/lexers/idl.py +++ b/libs/pygments/lexers/idl.py @@ -4,7 +4,7 @@ Lexers for IDL. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/igor.py b/libs/pygments/lexers/igor.py index 136c5b892..efda038ab 100644 --- a/libs/pygments/lexers/igor.py +++ b/libs/pygments/lexers/igor.py @@ -4,7 +4,7 @@ Lexers for Igor Pro. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/inferno.py b/libs/pygments/lexers/inferno.py index b5caf55a0..7807953cd 100644 --- a/libs/pygments/lexers/inferno.py +++ b/libs/pygments/lexers/inferno.py @@ -4,7 +4,7 @@ Lexers for Inferno os and all the related stuff. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/installers.py b/libs/pygments/lexers/installers.py index 3f12a25b2..c45bac90e 100644 --- a/libs/pygments/lexers/installers.py +++ b/libs/pygments/lexers/installers.py @@ -4,7 +4,7 @@ Lexers for installer/packager DSLs and formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -343,7 +343,7 @@ class DebianControlLexer(RegexLexer): (r'\n', Whitespace, '#pop'), (r'[,\s]', Text), (r'[+.a-zA-Z0-9-]+', Name.Function), - (r'\[.*?\]', Name.Entity), + (r'(\[)(!?)(.*?)(\])', bygroups(Punctuation, Operator, Name.Entity, Punctuation)), ], 'package_list_vers': [ (r'\)', Punctuation, '#pop'), diff --git a/libs/pygments/lexers/int_fiction.py b/libs/pygments/lexers/int_fiction.py index b142c5449..75b1ab98b 100644 --- a/libs/pygments/lexers/int_fiction.py +++ b/libs/pygments/lexers/int_fiction.py @@ -4,7 +4,7 @@ Lexers for interactive fiction languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/iolang.py b/libs/pygments/lexers/iolang.py index 0269384fa..f565fe8d6 100644 --- a/libs/pygments/lexers/iolang.py +++ b/libs/pygments/lexers/iolang.py @@ -4,7 +4,7 @@ Lexers for the Io language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/j.py b/libs/pygments/lexers/j.py index 1eb418e1c..3a1144c85 100644 --- a/libs/pygments/lexers/j.py +++ b/libs/pygments/lexers/j.py @@ -4,7 +4,7 @@ Lexer for the J programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/javascript.py b/libs/pygments/lexers/javascript.py index d361b6e71..fd1ac079b 100644 --- a/libs/pygments/lexers/javascript.py +++ b/libs/pygments/lexers/javascript.py @@ -4,7 +4,7 @@ Lexers for JavaScript and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/jmespath.py b/libs/pygments/lexers/jmespath.py index 67374c28c..91b2270d4 100644 --- a/libs/pygments/lexers/jmespath.py +++ b/libs/pygments/lexers/jmespath.py @@ -4,7 +4,7 @@ Lexers for the JMESPath language - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/jslt.py b/libs/pygments/lexers/jslt.py index f2cc5df5f..28dd53f14 100644 --- a/libs/pygments/lexers/jslt.py +++ b/libs/pygments/lexers/jslt.py @@ -4,7 +4,7 @@ Lexers for the JSLT language - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/json5.py b/libs/pygments/lexers/json5.py index 35d769c31..265c58305 100644 --- a/libs/pygments/lexers/json5.py +++ b/libs/pygments/lexers/json5.py @@ -4,7 +4,7 @@ Lexer for Json5 file format. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/jsonnet.py b/libs/pygments/lexers/jsonnet.py index f9aa5953f..91d21c5f8 100644 --- a/libs/pygments/lexers/jsonnet.py +++ b/libs/pygments/lexers/jsonnet.py @@ -4,7 +4,7 @@ Lexer for Jsonnet data templating language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/jsx.py b/libs/pygments/lexers/jsx.py index 6124d8c00..cbfeeeda1 100644 --- a/libs/pygments/lexers/jsx.py +++ b/libs/pygments/lexers/jsx.py @@ -4,7 +4,7 @@ Lexers for JSX (React) and TSX (TypeScript flavor). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/julia.py b/libs/pygments/lexers/julia.py index 77d8bafcd..ba0e0d987 100644 --- a/libs/pygments/lexers/julia.py +++ b/libs/pygments/lexers/julia.py @@ -4,7 +4,7 @@ Lexers for the Julia language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/jvm.py b/libs/pygments/lexers/jvm.py index 3ca257a95..b29bee335 100644 --- a/libs/pygments/lexers/jvm.py +++ b/libs/pygments/lexers/jvm.py @@ -4,7 +4,7 @@ Pygments lexers for JVM languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -55,16 +55,17 @@ class JavaLexer(RegexLexer): r'(\s*)(\()', # signature start bygroups(using(this), Name.Function, Whitespace, Punctuation)), (r'@[^\W\d][\w.]*', Name.Decorator), - (r'(abstract|const|enum|extends|final|implements|native|private|' - r'protected|public|sealed|static|strictfp|super|synchronized|throws|' - r'transient|volatile|yield)\b', Keyword.Declaration), + (r'(abstract|const|enum|exports|extends|final|implements|native|non-sealed|' + r'open|opens|permits|private|protected|provides|public|requires|sealed|static|strictfp|' + r'super|synchronized|throws|to|transient|transitive|uses|volatile|with|yield)\b', Keyword.Declaration), (r'(boolean|byte|char|double|float|int|long|short|void)\b', Keyword.Type), (r'(package)(\s+)', bygroups(Keyword.Namespace, Whitespace), 'import'), (r'(true|false|null)\b', Keyword.Constant), (r'(class|interface)\b', Keyword.Declaration, 'class'), + (r'(module)\b', Keyword.Declaration, 'module'), (r'(var)(\s+)', bygroups(Keyword.Declaration, Whitespace), 'var'), - (r'(import(?:\s+static)?)(\s+)', bygroups(Keyword.Namespace, Whitespace), + (r'(import(?:\s+(?:static|module))?)(\s+)', bygroups(Keyword.Namespace, Whitespace), 'import'), (r'"""\n', String, 'multiline_string'), (r'"', String, 'string'), @@ -95,6 +96,10 @@ class JavaLexer(RegexLexer): (r'\s+', Text), (r'([^\W\d]|\$)[\w$]*', Name.Class, '#pop') ], + 'module': [ + (r'\s+', Text), + (r'([^\W\d]|\$)[\w$]*', Name.Class, '#pop') + ], 'var': [ (r'([^\W\d]|\$)[\w$]*', Name, '#pop') ], diff --git a/libs/pygments/lexers/kuin.py b/libs/pygments/lexers/kuin.py index 686a1c7ec..04a99c175 100644 --- a/libs/pygments/lexers/kuin.py +++ b/libs/pygments/lexers/kuin.py @@ -4,7 +4,7 @@ Lexers for the Kuin language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/kusto.py b/libs/pygments/lexers/kusto.py index ce32d5eee..801234857 100644 --- a/libs/pygments/lexers/kusto.py +++ b/libs/pygments/lexers/kusto.py @@ -4,7 +4,7 @@ Lexers for Kusto Query Language (KQL). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ldap.py b/libs/pygments/lexers/ldap.py index 65473da67..b95fc0c28 100644 --- a/libs/pygments/lexers/ldap.py +++ b/libs/pygments/lexers/ldap.py @@ -4,7 +4,7 @@ Pygments lexers for LDAP. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/lean.py b/libs/pygments/lexers/lean.py index 76af11fe9..41658c5ef 100644 --- a/libs/pygments/lexers/lean.py +++ b/libs/pygments/lexers/lean.py @@ -4,7 +4,7 @@ Lexers for the Lean theorem prover. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -136,7 +136,7 @@ class Lean4Lexer(RegexLexer): """ For the Lean 4 theorem prover. """ - + name = 'Lean4' url = 'https://github.com/leanprover/lean4' aliases = ['lean4'] @@ -186,7 +186,7 @@ class Lean4Lexer(RegexLexer): ) punctuation = ('(', ')', ':', '{', '}', '[', ']', '⦃', '⦄', - ':=', ',') + ':=', ',', ']\'', ']?', ']!') tokens = { 'expression': [ diff --git a/libs/pygments/lexers/lilypond.py b/libs/pygments/lexers/lilypond.py index 03c34ae59..7e7029285 100644 --- a/libs/pygments/lexers/lilypond.py +++ b/libs/pygments/lexers/lilypond.py @@ -4,7 +4,7 @@ Lexer for LilyPond. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/lisp.py b/libs/pygments/lexers/lisp.py index 2c970d939..d74fc460f 100644 --- a/libs/pygments/lexers/lisp.py +++ b/libs/pygments/lexers/lisp.py @@ -4,7 +4,7 @@ Lexers for Lispy languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -441,7 +441,7 @@ class CommonLispLexer(RegexLexer): (r'(#s)(\()', bygroups(Literal.Other, Punctuation), 'body'), # path - (r'#p?"(\\.|[^"])*"', Literal.Other), + (r'#p?"(\\.|[^"\\])*"', Literal.Other), # reference (r'#\d+=', Operator), @@ -2756,19 +2756,22 @@ class FennelLexer(RegexLexer): name = 'Fennel' url = 'https://fennel-lang.org' aliases = ['fennel', 'fnl'] - filenames = ['*.fnl'] + filenames = ['*.fnl', '*.fnlm'] + mimetypes = ['text/x-fennel'] version_added = '2.3' - # this list is current as of Fennel version 0.10.0. + # this list is current as of Fennel version 1.5.3 special_forms = ( - '#', '%', '*', '+', '-', '->', '->>', '-?>', '-?>>', '.', '..', - '/', '//', ':', '<', '<=', '=', '>', '>=', '?.', '^', 'accumulate', - 'and', 'band', 'bnot', 'bor', 'bxor', 'collect', 'comment', 'do', 'doc', - 'doto', 'each', 'eval-compiler', 'for', 'hashfn', 'icollect', 'if', - 'import-macros', 'include', 'length', 'let', 'lshift', 'lua', - 'macrodebug', 'match', 'not', 'not=', 'or', 'partial', 'pick-args', - 'pick-values', 'quote', 'require-macros', 'rshift', 'set', - 'set-forcibly!', 'tset', 'values', 'when', 'while', 'with-open', '~=' + '#', '%', '*', '+', '-', '->', '->>', '-?>', '-?>>', '.', '..', '/', + '//', ':', '<', '<=', '=', '>', '>=', '?.', '^', 'accumulate', 'and', + 'assert-repl', 'band', 'bnot', 'bor', 'bxor', 'case', 'case-try', + 'collect', 'comment', 'do', 'doto', 'each', 'eval-compiler', + 'faccumulate', 'fcollect', 'for', 'hashfn', 'icollect', + 'if', 'import-macros', 'include', 'length', 'let', + 'lshift', 'lua', 'macrodebug', 'match', 'match-try', + 'not', 'not=', 'or', 'partial', 'pick-args', 'pick-values', 'quote', + 'require-macros', 'rshift', 'set', 'set-forcibly!', 'tail!', 'tset', + 'unquote', 'values', 'when', 'while', 'with-open', '~=' ) declarations = ( @@ -2826,6 +2829,9 @@ class FennelLexer(RegexLexer): # the # symbol is shorthand for a lambda function (r'#', Punctuation), + + # backtick is used for quoting macros + (r'`', Punctuation), ] } diff --git a/libs/pygments/lexers/macaulay2.py b/libs/pygments/lexers/macaulay2.py index 77b841d83..bb0786b02 100644 --- a/libs/pygments/lexers/macaulay2.py +++ b/libs/pygments/lexers/macaulay2.py @@ -4,7 +4,7 @@ Lexer for Macaulay2. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -13,13 +13,14 @@ from pygments.token import Comment, Keyword, Name, String, Text __all__ = ['Macaulay2Lexer'] -# Auto-generated for Macaulay2-1.24.11. Do not modify this file manually. +# Auto-generated for Macaulay2-1.25.11. Do not modify this file manually. M2KEYWORDS = ( "SPACE", "TEST", "and", "break", + "breakpoint", "catch", "continue", "do", @@ -37,6 +38,7 @@ M2KEYWORDS = ( "not", "of", "or", + "profile", "return", "shield", "step", @@ -77,7 +79,6 @@ M2DATATYPES = ( "CDATA", "CODE", "COMMENT", - "CacheFunction", "CacheTable", "ChainComplex", "ChainComplexMap", @@ -137,6 +138,7 @@ M2DATATYPES = ( "HypertextContainer", "HypertextParagraph", "HypertextVoid", + "IFRAME", "IMG", "INDENT", "INPUT", @@ -456,6 +458,7 @@ M2FUNCTIONS = ( "diff", "difference", "dim", + "directProduct", "directSum", "disassemble", "discriminant", @@ -589,6 +592,7 @@ M2FUNCTIONS = ( "icMap", "icPIdeal", "ideal", + "idealSheaf", "idealizer", "identity", "image", @@ -633,6 +637,7 @@ M2FUNCTIONS = ( "isDirectSum", "isDirectory", "isEmpty", + "isExact", "isField", "isFinite", "isFinitePrimeField", @@ -646,9 +651,9 @@ M2FUNCTIONS = ( "isIsomorphic", "isIsomorphism", "isLLL", - "isLiftable", "isLinearType", "isListener", + "isLocallyFree", "isMember", "isModule", "isMonomialIdeal", @@ -686,6 +691,7 @@ M2FUNCTIONS = ( "isWellDefined", "isWeylAlgebra", "isc", + "isomorphism", "iterator", "jacobian", "jacobianDual", @@ -838,6 +844,7 @@ M2FUNCTIONS = ( "peek", "permanents", "permutations", + "pfaffian", "pfaffians", "pivots", "plus", @@ -861,7 +868,6 @@ M2FUNCTIONS = ( "printerr", "processID", "product", - "profile", "projectiveHilbertPolynomial", "promote", "protect", @@ -878,6 +884,7 @@ M2FUNCTIONS = ( "random", "randomKRationalPoint", "randomMutableMatrix", + "randomSubset", "rank", "rays", "read", @@ -970,6 +977,7 @@ M2FUNCTIONS = ( "show", "showHtml", "showTex", + "sign", "simpleDocFrob", "sin", "singularLocus", @@ -991,6 +999,7 @@ M2FUNCTIONS = ( "sqrt", "stack", "stacksProject", + "stacktrace", "standardForm", "standardPairs", "stashValue", @@ -1115,6 +1124,7 @@ M2CONSTANTS = ( "Algorithm", "Alignment", "AllCodimensions", + "AllMarkovBases", "allowableThreads", "AnalyzeSheafOnP1", "applicationDirectorySuffix", @@ -1150,6 +1160,7 @@ M2CONSTANTS = ( "Book3264Examples", "BooleanGB", "Boxes", + "Brackets", "Browse", "Bruns", "cache", @@ -1167,6 +1178,7 @@ M2CONSTANTS = ( "CharacteristicClasses", "CheckDocumentation", "Chordal", + "Citation", "cite", "Classic", "clearAll", @@ -1193,6 +1205,7 @@ M2CONSTANTS = ( "ConductorElement", "Configuration", "ConformalBlocks", + "ConnectionMatrices", "Consequences", "Constants", "Contributors", @@ -1202,6 +1215,9 @@ M2CONSTANTS = ( "Core", "CorrespondenceScrolls", "CotangentSchubert", + "cotangentSurjection", + "coverageSummary", + "CpMackeyFunctors", "Cremona", "currentFileDirectory", "currentFileName", @@ -1237,7 +1253,6 @@ M2CONSTANTS = ( "Dispatch", "DivideConquer", "DividedPowers", - "Divisor", "Dmodules", "docExample", "docTemplate", @@ -1253,6 +1268,7 @@ M2CONSTANTS = ( "EllipticCurves", "EllipticIntegrals", "Email", + "embeddedToAbstract", "end", "endl", "Engine", @@ -1263,14 +1279,17 @@ M2CONSTANTS = ( "EquivariantGB", "errorDepth", "EulerConstant", + "eulerSequence", "Example", "ExampleFiles", "ExampleSystems", "Exclude", "exit", "Ext", + "ExteriorExtensions", "ExteriorIdeals", "ExteriorModules", + "ExtLongExactSequence", "false", "FastMinors", "FastNonminimal", @@ -1294,6 +1313,7 @@ M2CONSTANTS = ( "fpLLL", "FrobeniusThresholds", "FunctionFieldDesingularization", + "GameTheory", "GBDegrees", "gbTrace", "GenerateAssertions", @@ -1343,12 +1363,15 @@ M2CONSTANTS = ( "HomePage", "Homogeneous", "Homogeneous2", + "HomologicalAlgebraPackage", "HomotopyLieAlgebra", "HorizontalSpace", "HyperplaneArrangements", "id", + "idealSheafSequence", "IgnoreExampleErrors", "ii", + "IncidenceCorrespondenceCohomology", "incomparable", "Increment", "indeterminate", @@ -1360,6 +1383,7 @@ M2CONSTANTS = ( "Inhomogeneous", "Inputs", "InstallPrefix", + "IntegerProgramming", "IntegralClosure", "interpreterDepth", "Intersection", @@ -1376,7 +1400,7 @@ M2CONSTANTS = ( "Jets", "Join", "JSON", - "Jupyter", + "JSONRPC", "K3Carpets", "K3Surfaces", "Keep", @@ -1393,8 +1417,7 @@ M2CONSTANTS = ( "LengthLimit", "Lex", "LexIdeals", - "Licenses", - "LieTypes", + "LieAlgebraRepresentations", "Limit", "Linear", "LinearAlgebra", @@ -1416,11 +1439,11 @@ M2CONSTANTS = ( "MakeDocumentation", "MakeHTML", "MakeInfo", - "MakeLinks", "MakePDF", "MapleInterface", "Markov", "MatchingFields", + "MatrixFactorizations", "MatrixSchubert", "Matroids", "maxAllowableThreads", @@ -1472,9 +1495,11 @@ M2CONSTANTS = ( "NoetherianOperators", "NoetherNormalization", "NonminimalComplexes", + "NonPrincipalTestIdeals", "NoPrint", "Normaliz", "NormalToricVarieties", + "NotANumber", "notify", "NTL", "null", @@ -1488,6 +1513,7 @@ M2CONSTANTS = ( "NumericSolutions", "numTBBThreads", "OIGroebnerBases", + "OldChainComplexes", "OldPolyhedra", "OldToricVectorBundles", "OnlineLookup", @@ -1501,6 +1527,7 @@ M2CONSTANTS = ( "Options", "Order", "order", + "Oscillators", "OutputDictionary", "Outputs", "PackageCitations", @@ -1515,6 +1542,7 @@ M2CONSTANTS = ( "Parametrization", "Parsing", "path", + "PathSignatures", "PencilsOfQuadrics", "Permanents", "Permutations", @@ -1613,6 +1641,7 @@ M2CONSTANTS = ( "returnCode", "Reverse", "RevLex", + "RHom", "Right", "RInterface", "rootPath", @@ -1694,6 +1723,7 @@ M2CONSTANTS = ( "SyzygyLimit", "SyzygyMatrix", "SyzygyRows", + "Tableaux", "TangentCone", "TateOnProducts", "TensorComplexes", @@ -1712,6 +1742,7 @@ M2CONSTANTS = ( "Tor", "TorAlgebra", "Toric", + "ToricHigherDirectImages", "ToricInvariants", "ToricTopology", "ToricVectorBundles", @@ -1765,12 +1796,14 @@ M2CONSTANTS = ( "VNumber", "WebApp", "Weights", + "WeilDivisors", "WeylAlgebra", "WeylAlgebras", "WeylGroups", "WhitneyStratifications", "Wrap", - "XML" + "XML", + "yonedaSheafExtension" ) class Macaulay2Lexer(RegexLexer): diff --git a/libs/pygments/lexers/make.py b/libs/pygments/lexers/make.py index 3940a0090..21cffbaa0 100644 --- a/libs/pygments/lexers/make.py +++ b/libs/pygments/lexers/make.py @@ -4,7 +4,7 @@ Lexers for Makefiles and similar. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/maple.py b/libs/pygments/lexers/maple.py index c744e7e1a..11b69f934 100644 --- a/libs/pygments/lexers/maple.py +++ b/libs/pygments/lexers/maple.py @@ -4,7 +4,7 @@ Lexers for Maple. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/markup.py b/libs/pygments/lexers/markup.py index acc270b15..41b5d7950 100644 --- a/libs/pygments/lexers/markup.py +++ b/libs/pygments/lexers/markup.py @@ -4,7 +4,7 @@ Lexers for non-HTML markup languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -687,11 +687,8 @@ class OrgLexer(RegexLexer): # Line break operator (r'\\\\$', Operator), - # Deadline, Scheduled, CLOSED - (r'(?i)^( *(?:DEADLINE|SCHEDULED): )(<.+?> *)$', - bygroups(Generic.Error, Literal.Date)), - (r'(?i)^( *CLOSED: )(\[.+?\] *)$', - bygroups(Generic.Deleted, Literal.Date)), + (r'^\s*CLOSED:\s+', Generic.Deleted, 'dateline'), + (r'^\s*(?:DEADLINE:|SCHEDULED:)\s+', Generic.Error, 'dateline'), # Bold (_inline(r'\*', r'\*+'), Generic.Strong), @@ -725,6 +722,14 @@ class OrgLexer(RegexLexer): (r'[^#*+\-0-9:\\/=~_<{\[|\n]+', Text), (r'[#*+\-0-9:\\/=~_<{\[|\n]', Text), ], + 'dateline': [ + (r'\s*CLOSED:\s+', Generic.Deleted), + (r'\s*(?:DEADLINE:|SCHEDULED:)\s+', Generic.Error), + (r'\[.+?\]', Literal.Date), + (r'<[^>]+?>', Literal.Date), + (r'(\s*)$', Text, '#pop'), + (r'.', Text), + ], } class TiddlyWiki5Lexer(RegexLexer): diff --git a/libs/pygments/lexers/math.py b/libs/pygments/lexers/math.py index b225ffcf9..b38c13e12 100644 --- a/libs/pygments/lexers/math.py +++ b/libs/pygments/lexers/math.py @@ -4,7 +4,7 @@ Just export lexers that were contained in this module. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/matlab.py b/libs/pygments/lexers/matlab.py index 8eeffc9d2..b27f4db63 100644 --- a/libs/pygments/lexers/matlab.py +++ b/libs/pygments/lexers/matlab.py @@ -4,7 +4,7 @@ Lexers for Matlab and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/maxima.py b/libs/pygments/lexers/maxima.py index a98308837..f66b8321b 100644 --- a/libs/pygments/lexers/maxima.py +++ b/libs/pygments/lexers/maxima.py @@ -6,7 +6,7 @@ Derived from pygments/lexers/algebra.py. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/meson.py b/libs/pygments/lexers/meson.py index 6f2c6da37..154a2de5a 100644 --- a/libs/pygments/lexers/meson.py +++ b/libs/pygments/lexers/meson.py @@ -4,7 +4,7 @@ Pygments lexer for the Meson build system - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -30,7 +30,7 @@ class MesonLexer(RegexLexer): name = 'Meson' url = 'https://mesonbuild.com/' aliases = ['meson', 'meson.build'] - filenames = ['meson.build', 'meson_options.txt'] + filenames = ['meson.build', 'meson.options', 'meson_options.txt'] mimetypes = ['text/x-meson'] version_added = '2.10' @@ -66,7 +66,7 @@ class MesonLexer(RegexLexer): ], 'expr': [ (r'(in|and|or|not)\b', Operator.Word), - (r'(\*=|/=|%=|\+]=|-=|==|!=|\+|-|=)', Operator), + (r'([\*/%\+-]=?|==|!=|=)', Operator), (r'[\[\]{}:().,?]', Punctuation), (words(('true', 'false'), suffix=r'\b'), Keyword.Constant), include('builtins'), diff --git a/libs/pygments/lexers/mime.py b/libs/pygments/lexers/mime.py index 45f27f428..a2bf7c58e 100644 --- a/libs/pygments/lexers/mime.py +++ b/libs/pygments/lexers/mime.py @@ -4,7 +4,7 @@ Lexer for Multipurpose Internet Mail Extensions (MIME) data. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/minecraft.py b/libs/pygments/lexers/minecraft.py index fe5e9f87c..c1b580035 100644 --- a/libs/pygments/lexers/minecraft.py +++ b/libs/pygments/lexers/minecraft.py @@ -15,7 +15,7 @@ official: https://learn.microsoft.com/en-us/minecraft/creator/reference/content/schemasreference/ community example: https://www.mcbe-dev.net/addons/data-driven/manifest.html - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -99,7 +99,8 @@ class SNBTLexer(RegexLexer): class MCFunctionLexer(RegexLexer): - """Lexer for the mcfunction scripting language used in Minecraft + """Lexer for the mcfunction scripting language used in Minecraft. + Modelled somewhat after the `GitHub mcfunction grammar `_. """ diff --git a/libs/pygments/lexers/mips.py b/libs/pygments/lexers/mips.py index 531f1b876..332bc9f3d 100644 --- a/libs/pygments/lexers/mips.py +++ b/libs/pygments/lexers/mips.py @@ -4,7 +4,7 @@ Lexers for MIPS assembly. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ml.py b/libs/pygments/lexers/ml.py index 48a08da1c..dca1b50d7 100644 --- a/libs/pygments/lexers/ml.py +++ b/libs/pygments/lexers/ml.py @@ -4,7 +4,7 @@ Lexers for ML family languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/modeling.py b/libs/pygments/lexers/modeling.py index d681e7f3b..4c22aeb00 100644 --- a/libs/pygments/lexers/modeling.py +++ b/libs/pygments/lexers/modeling.py @@ -4,7 +4,7 @@ Lexers for modeling languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -314,6 +314,8 @@ class StanLexer(RegexLexer): bygroups(Keyword.Namespace, Text, Punctuation)), # target keyword (r'target\s*\+=', Keyword), + # jacobian += statement + (r'jacobian\s*\+=', Keyword), # Reserved Words (r'({})\b'.format(r'|'.join(_stan_builtins.KEYWORDS)), Keyword), # Truncation diff --git a/libs/pygments/lexers/modula2.py b/libs/pygments/lexers/modula2.py index 713f4722f..c0ebd5c14 100644 --- a/libs/pygments/lexers/modula2.py +++ b/libs/pygments/lexers/modula2.py @@ -4,7 +4,7 @@ Multi-Dialect Lexer for Modula-2. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/mojo.py b/libs/pygments/lexers/mojo.py index 4df18c4f9..84aac4669 100644 --- a/libs/pygments/lexers/mojo.py +++ b/libs/pygments/lexers/mojo.py @@ -4,7 +4,7 @@ Lexers for Mojo and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/monte.py b/libs/pygments/lexers/monte.py index c01d2dc6c..7aeabb232 100644 --- a/libs/pygments/lexers/monte.py +++ b/libs/pygments/lexers/monte.py @@ -4,7 +4,7 @@ Lexer for the Monte programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/mosel.py b/libs/pygments/lexers/mosel.py index 426c9a145..912f47877 100644 --- a/libs/pygments/lexers/mosel.py +++ b/libs/pygments/lexers/mosel.py @@ -5,7 +5,7 @@ Lexers for the mosel language. http://www.fico.com/en/products/fico-xpress-optimization - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ncl.py b/libs/pygments/lexers/ncl.py index d2f476087..a2729efba 100644 --- a/libs/pygments/lexers/ncl.py +++ b/libs/pygments/lexers/ncl.py @@ -4,7 +4,7 @@ Lexers for NCAR Command Language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/nimrod.py b/libs/pygments/lexers/nimrod.py index 365a8dcca..b99e246d7 100644 --- a/libs/pygments/lexers/nimrod.py +++ b/libs/pygments/lexers/nimrod.py @@ -4,7 +4,7 @@ Lexer for the Nim language (formerly known as Nimrod). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/nit.py b/libs/pygments/lexers/nit.py index 2d2122c1f..4ba03a8f2 100644 --- a/libs/pygments/lexers/nit.py +++ b/libs/pygments/lexers/nit.py @@ -4,7 +4,7 @@ Lexer for the Nit language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/nix.py b/libs/pygments/lexers/nix.py index 3fa88c65a..16572afe4 100644 --- a/libs/pygments/lexers/nix.py +++ b/libs/pygments/lexers/nix.py @@ -4,7 +4,7 @@ Lexers for the NixOS Nix language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/numbair.py b/libs/pygments/lexers/numbair.py index 435863e13..28395b41a 100644 --- a/libs/pygments/lexers/numbair.py +++ b/libs/pygments/lexers/numbair.py @@ -4,7 +4,7 @@ Lexer for other Numba Intermediate Representation. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/oberon.py b/libs/pygments/lexers/oberon.py index 61f3c2d28..11cf571df 100644 --- a/libs/pygments/lexers/oberon.py +++ b/libs/pygments/lexers/oberon.py @@ -4,7 +4,7 @@ Lexers for Oberon family languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -114,7 +114,7 @@ class ComponentPascalLexer(RegexLexer): result += 0.01 if 'PROCEDURE' in text: result += 0.01 - if 'END' in text: + if 'MODULE' in text: result += 0.01 return result diff --git a/libs/pygments/lexers/objective.py b/libs/pygments/lexers/objective.py index 899c2c44c..ce281fbd1 100644 --- a/libs/pygments/lexers/objective.py +++ b/libs/pygments/lexers/objective.py @@ -4,7 +4,7 @@ Lexers for Objective-C family languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ooc.py b/libs/pygments/lexers/ooc.py index 8a990801a..f6aea8c83 100644 --- a/libs/pygments/lexers/ooc.py +++ b/libs/pygments/lexers/ooc.py @@ -4,7 +4,7 @@ Lexers for the Ooc language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/openscad.py b/libs/pygments/lexers/openscad.py index b06de227c..1ec8fea05 100644 --- a/libs/pygments/lexers/openscad.py +++ b/libs/pygments/lexers/openscad.py @@ -4,7 +4,7 @@ Lexers for the OpenSCAD languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/other.py b/libs/pygments/lexers/other.py index 2b7dfb4ab..8767d7303 100644 --- a/libs/pygments/lexers/other.py +++ b/libs/pygments/lexers/other.py @@ -4,7 +4,7 @@ Just export lexer classes previously contained in this module. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/parasail.py b/libs/pygments/lexers/parasail.py index 150d6a9c4..9b409609f 100644 --- a/libs/pygments/lexers/parasail.py +++ b/libs/pygments/lexers/parasail.py @@ -4,7 +4,7 @@ Lexer for ParaSail. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/parsers.py b/libs/pygments/lexers/parsers.py index 7a4ed9d1d..baa49b054 100644 --- a/libs/pygments/lexers/parsers.py +++ b/libs/pygments/lexers/parsers.py @@ -4,7 +4,7 @@ Lexers for parser generators. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -33,11 +33,11 @@ __all__ = ['RagelLexer', 'RagelEmbeddedLexer', 'RagelCLexer', 'RagelDLexer', class RagelLexer(RegexLexer): - """A pure `Ragel `_ lexer. Use this - for fragments of Ragel. For ``.rl`` files, use - :class:`RagelEmbeddedLexer` instead (or one of the - language-specific subclasses). + """A pure `Ragel `_ lexer. + Use this for fragments of Ragel. For ``.rl`` files, use + :class:`RagelEmbeddedLexer` instead (or one of the language-specific + subclasses). """ name = 'Ragel' diff --git a/libs/pygments/lexers/pascal.py b/libs/pygments/lexers/pascal.py index 5f40dcc86..ad8a0aec6 100644 --- a/libs/pygments/lexers/pascal.py +++ b/libs/pygments/lexers/pascal.py @@ -4,7 +4,7 @@ Lexers for Pascal family languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/pawn.py b/libs/pygments/lexers/pawn.py index 99d9c963d..9b4234c3a 100644 --- a/libs/pygments/lexers/pawn.py +++ b/libs/pygments/lexers/pawn.py @@ -4,7 +4,7 @@ Lexers for the Pawn languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/pddl.py b/libs/pygments/lexers/pddl.py index 77aa75362..a86ab8b8f 100644 --- a/libs/pygments/lexers/pddl.py +++ b/libs/pygments/lexers/pddl.py @@ -4,7 +4,7 @@ Lexer for the Planning Domain Definition Language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/perl.py b/libs/pygments/lexers/perl.py index 33f91f580..7448d2a1e 100644 --- a/libs/pygments/lexers/perl.py +++ b/libs/pygments/lexers/perl.py @@ -4,7 +4,7 @@ Lexers for Perl, Raku and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/phix.py b/libs/pygments/lexers/phix.py index f0b03775e..c4cb35d5b 100644 --- a/libs/pygments/lexers/phix.py +++ b/libs/pygments/lexers/phix.py @@ -4,7 +4,7 @@ Lexers for Phix. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/php.py b/libs/pygments/lexers/php.py index 82d4aeb3d..ac935c471 100644 --- a/libs/pygments/lexers/php.py +++ b/libs/pygments/lexers/php.py @@ -4,7 +4,7 @@ Lexers for PHP and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -211,6 +211,7 @@ class PhpLexer(RegexLexer): bygroups(Keyword, Text, Operator, Text), 'functionname'), (r'(const)(\s+)(' + _ident_inner + ')', bygroups(Keyword, Text, Name.Constant)), + # source: https://www.php.net/manual/en/reserved.keywords.php (r'(and|E_PARSE|old_function|E_ERROR|or|as|E_WARNING|parent|' r'eval|PHP_OS|break|exit|case|extends|PHP_VERSION|cfunction|' r'FALSE|print|for|require|continue|foreach|require_once|' @@ -220,8 +221,8 @@ class PhpLexer(RegexLexer): r'endif|list|endswitch|new|endwhile|not|' r'array|E_ALL|NULL|final|php_user_filter|interface|' r'implements|public|private|protected|abstract|clone|try|' - r'catch|throw|this|use|namespace|trait|yield|' - r'finally|match)\b', Keyword), + r'catch|throw|this|use|namespace|trait|yield( from)?|' + r'finally|match|readonly)\b', Keyword), (r'(true|false|null)\b', Keyword.Constant), include('magicconstants'), (r'\$\{', Name.Variable, 'variablevariable'), @@ -250,10 +251,10 @@ class PhpLexer(RegexLexer): Name.Function.Magic), ], 'magicconstants': [ - # source: http://php.net/manual/en/language.constants.predefined.php + # source: https://www.php.net/manual/en/language.constants.magic.php (words(( '__LINE__', '__FILE__', '__DIR__', '__FUNCTION__', '__CLASS__', - '__TRAIT__', '__METHOD__', '__NAMESPACE__',), + '__TRAIT__', '__METHOD__', '__NAMESPACE__', '__PROPERTY__',), suffix=r'\b'), Name.Constant), ], diff --git a/libs/pygments/lexers/pointless.py b/libs/pygments/lexers/pointless.py index adedb757d..aab0a5684 100644 --- a/libs/pygments/lexers/pointless.py +++ b/libs/pygments/lexers/pointless.py @@ -4,7 +4,7 @@ Lexers for Pointless. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/pony.py b/libs/pygments/lexers/pony.py index 055423a46..62d16d7d6 100644 --- a/libs/pygments/lexers/pony.py +++ b/libs/pygments/lexers/pony.py @@ -4,7 +4,7 @@ Lexers for Pony and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/praat.py b/libs/pygments/lexers/praat.py index 054f5b61e..c50e9279f 100644 --- a/libs/pygments/lexers/praat.py +++ b/libs/pygments/lexers/praat.py @@ -4,7 +4,7 @@ Lexer for Praat - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/procfile.py b/libs/pygments/lexers/procfile.py index 32619804f..f62c27b2b 100644 --- a/libs/pygments/lexers/procfile.py +++ b/libs/pygments/lexers/procfile.py @@ -4,7 +4,7 @@ Lexer for Procfile file format. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/prolog.py b/libs/pygments/lexers/prolog.py index 7bf951c1c..92abe132c 100644 --- a/libs/pygments/lexers/prolog.py +++ b/libs/pygments/lexers/prolog.py @@ -4,7 +4,7 @@ Lexers for Prolog and Prolog-like languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/promql.py b/libs/pygments/lexers/promql.py index cad3c254a..5cfa73fe1 100644 --- a/libs/pygments/lexers/promql.py +++ b/libs/pygments/lexers/promql.py @@ -4,7 +4,7 @@ Lexer for Prometheus Query Language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/prql.py b/libs/pygments/lexers/prql.py index ee95d2d47..a62cb6bcd 100644 --- a/libs/pygments/lexers/prql.py +++ b/libs/pygments/lexers/prql.py @@ -4,7 +4,7 @@ Lexer for the PRQL query language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ptx.py b/libs/pygments/lexers/ptx.py index 784ca13a6..685c7357e 100644 --- a/libs/pygments/lexers/ptx.py +++ b/libs/pygments/lexers/ptx.py @@ -4,7 +4,7 @@ Lexer for other PTX language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/python.py b/libs/pygments/lexers/python.py index 805f6ff2a..b296c8dbe 100644 --- a/libs/pygments/lexers/python.py +++ b/libs/pygments/lexers/python.py @@ -4,7 +4,7 @@ Lexers for Python and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -55,6 +55,8 @@ class PythonLexer(RegexLexer): 'WORKSPACE', # Twisted Application infrastructure '*.tac', + # Execubot level format + '*.pye', ] mimetypes = ['text/x-python', 'application/x-python', 'text/x-python3', 'application/x-python3'] @@ -118,27 +120,27 @@ class PythonLexer(RegexLexer): include('expr'), ], 'expr': [ - # raw f-strings - ('(?i)(rf|fr)(""")', + # raw f-strings and t-strings + ('(?i)(r[ft]|[ft]r)(""")', bygroups(String.Affix, String.Double), combined('rfstringescape', 'tdqf')), - ("(?i)(rf|fr)(''')", + ("(?i)(r[ft]|[ft]r)(''')", bygroups(String.Affix, String.Single), combined('rfstringescape', 'tsqf')), - ('(?i)(rf|fr)(")', + ('(?i)(r[ft]|[ft]r)(")', bygroups(String.Affix, String.Double), combined('rfstringescape', 'dqf')), - ("(?i)(rf|fr)(')", + ("(?i)(r[ft]|[ft]r)(')", bygroups(String.Affix, String.Single), combined('rfstringescape', 'sqf')), - # non-raw f-strings - ('([fF])(""")', bygroups(String.Affix, String.Double), + # non-raw f-strings and t-strings + ('([fFtT])(""")', bygroups(String.Affix, String.Double), combined('fstringescape', 'tdqf')), - ("([fF])(''')", bygroups(String.Affix, String.Single), + ("([fFtT])(''')", bygroups(String.Affix, String.Single), combined('fstringescape', 'tsqf')), - ('([fF])(")', bygroups(String.Affix, String.Double), + ('([fFtT])(")', bygroups(String.Affix, String.Double), combined('fstringescape', 'dqf')), - ("([fF])(')", bygroups(String.Affix, String.Single), + ("([fFtT])(')", bygroups(String.Affix, String.Single), combined('fstringescape', 'sqf')), # raw bytes and strings ('(?i)(rb|br|r)(""")', @@ -861,7 +863,7 @@ class CythonLexer(RegexLexer): (r'(cp?def)(\s+)', bygroups(Keyword, Whitespace), 'cdef'), # (should actually start a block with only cdefs) (r'(cdef)(:)', bygroups(Keyword, Punctuation)), - (r'(class|struct)(\s+)', bygroups(Keyword, Whitespace), 'classname'), + (r'(class|cppclass|struct)(\s+)', bygroups(Keyword, Whitespace), 'classname'), (r'(from)(\s+)', bygroups(Keyword, Whitespace), 'fromimport'), (r'(c?import)(\s+)', bygroups(Keyword, Whitespace), 'import'), include('builtins'), @@ -879,37 +881,38 @@ class CythonLexer(RegexLexer): ], 'keywords': [ (words(( - 'assert', 'async', 'await', 'break', 'by', 'continue', 'ctypedef', 'del', 'elif', - 'else', 'except', 'except?', 'exec', 'finally', 'for', 'fused', 'gil', - 'global', 'if', 'include', 'lambda', 'nogil', 'pass', 'print', - 'raise', 'return', 'try', 'while', 'yield', 'as', 'with'), suffix=r'\b'), + 'assert', 'async', 'await', 'break', 'by', 'continue', 'ctypedef', 'del', + 'elif', 'else', 'except', 'except?', 'exec', 'finally', 'for', 'fused', 'gil', + 'global', 'if', 'include', 'lambda', 'namespace', 'new', 'noexcept','nogil', + 'pass', 'print', 'raise', 'return', 'try', 'while', 'yield', 'as', 'with'), + suffix=r'\b'), Keyword), + (words(('True', 'False', 'None', 'NULL'), suffix=r'\b'), Keyword.Constant), (r'(DEF|IF|ELIF|ELSE)\b', Comment.Preproc), ], 'builtins': [ (words(( '__import__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bint', - 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', + 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'char', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'delattr', - 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit', + 'dict', 'dir', 'divmod', 'double', 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len', 'list', 'locals', 'long', 'map', 'max', - 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'property', 'Py_ssize_t', - 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed', - 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', - 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'unsigned', - 'vars', 'xrange', 'zip'), prefix=r'(?|+=/?\-@\$]', Punctuation.Marker), + (ident, Name), + (r'(\.)+', Punctuation), + ], + 'function': [ + (r'[ \n\t\r]+', Whitespace), + (ident, Name.Function, '#pop'), + default('#pop'), + ], + } diff --git a/libs/pygments/lexers/resource.py b/libs/pygments/lexers/resource.py index 9593c2124..eb6cb4a2e 100644 --- a/libs/pygments/lexers/resource.py +++ b/libs/pygments/lexers/resource.py @@ -4,7 +4,7 @@ Lexer for resource definition files. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ride.py b/libs/pygments/lexers/ride.py index 4d60c29cb..1ed8f3841 100644 --- a/libs/pygments/lexers/ride.py +++ b/libs/pygments/lexers/ride.py @@ -4,7 +4,7 @@ Lexer for the Ride programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/rita.py b/libs/pygments/lexers/rita.py index 536aafffd..99a574c2a 100644 --- a/libs/pygments/lexers/rita.py +++ b/libs/pygments/lexers/rita.py @@ -4,7 +4,7 @@ Lexers for RITA language - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/rnc.py b/libs/pygments/lexers/rnc.py index b7a06bb91..7ddd33e44 100644 --- a/libs/pygments/lexers/rnc.py +++ b/libs/pygments/lexers/rnc.py @@ -4,7 +4,7 @@ Lexer for Relax-NG Compact syntax - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/roboconf.py b/libs/pygments/lexers/roboconf.py index 31adba9f4..bf0570cc3 100644 --- a/libs/pygments/lexers/roboconf.py +++ b/libs/pygments/lexers/roboconf.py @@ -4,7 +4,7 @@ Lexers for Roboconf DSL. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/robotframework.py b/libs/pygments/lexers/robotframework.py index f92d56750..a481136cc 100644 --- a/libs/pygments/lexers/robotframework.py +++ b/libs/pygments/lexers/robotframework.py @@ -4,7 +4,7 @@ Lexer for Robot Framework. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ruby.py b/libs/pygments/lexers/ruby.py index 72aaeb5fe..6832652a6 100644 --- a/libs/pygments/lexers/ruby.py +++ b/libs/pygments/lexers/ruby.py @@ -4,7 +4,7 @@ Lexers for Ruby and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/rust.py b/libs/pygments/lexers/rust.py index 634104755..6c9e6217a 100644 --- a/libs/pygments/lexers/rust.py +++ b/libs/pygments/lexers/rust.py @@ -4,7 +4,7 @@ Lexers for the Rust language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/sas.py b/libs/pygments/lexers/sas.py index 1b2ad432d..35ee8541a 100644 --- a/libs/pygments/lexers/sas.py +++ b/libs/pygments/lexers/sas.py @@ -4,7 +4,7 @@ Lexer for SAS. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/savi.py b/libs/pygments/lexers/savi.py index 1e443ae30..548a05665 100644 --- a/libs/pygments/lexers/savi.py +++ b/libs/pygments/lexers/savi.py @@ -4,7 +4,7 @@ Lexer for Savi. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/scdoc.py b/libs/pygments/lexers/scdoc.py index 8e850d02e..9b2e01da1 100644 --- a/libs/pygments/lexers/scdoc.py +++ b/libs/pygments/lexers/scdoc.py @@ -4,7 +4,7 @@ Lexer for scdoc, a simple man page generator. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/scripting.py b/libs/pygments/lexers/scripting.py index 6e494c33b..2adb7bbd0 100644 --- a/libs/pygments/lexers/scripting.py +++ b/libs/pygments/lexers/scripting.py @@ -4,7 +4,7 @@ Lexer for scripting and embedded languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -59,6 +59,12 @@ class LuaLexer(RegexLexer): _comment_single = r'(?:--.*$)' _space = r'(?:\s+(?!\s))' _s = rf'(?:{_comment_multiline}|{_comment_single}|{_space})' + # A lookahead-safe version of _s that avoids catastrophic backtracking. + # The _comment_multiline pattern contains [\w\W]*? which, when used + # inside a lookahead with a * quantifier, causes exponential blowup. + # This version skips only whitespace; comments between an identifier + # and a following [.:] or ( are rare enough to sacrifice. + _s_la = r'\s' _name = r'(?:[^\W\d]\w*)' tokens = { @@ -100,8 +106,8 @@ class LuaLexer(RegexLexer): (r'(function)\b', Keyword.Reserved, 'funcname'), (words(all_lua_builtins(), suffix=r"\b"), Name.Builtin), - (fr'[A-Za-z_]\w*(?={_s}*[.:])', Name.Variable, 'varname'), - (fr'[A-Za-z_]\w*(?={_s}*\()', Name.Function), + (fr'[A-Za-z_]\w*(?={_s_la}*[.:])', Name.Variable, 'varname'), + (fr'[A-Za-z_]\w*(?={_s_la}*\()', Name.Function), (r'[A-Za-z_]\w*', Name.Variable), ("'", String.Single, combined('stringescape', 'sqs')), @@ -112,15 +118,15 @@ class LuaLexer(RegexLexer): include('ws'), (r'\.\.', Operator, '#pop'), (r'[.:]', Punctuation), - (rf'{_name}(?={_s}*[.:])', Name.Property), - (rf'{_name}(?={_s}*\()', Name.Function, '#pop'), + (rf'{_name}(?={_s_la}*[.:])', Name.Property), + (rf'{_name}(?={_s_la}*\()', Name.Function, '#pop'), (_name, Name.Property, '#pop'), ], 'funcname': [ include('ws'), (r'[.:]', Punctuation), - (rf'{_name}(?={_s}*[.:])', Name.Class), + (rf'{_name}(?={_s_la}*[.:])', Name.Class), (_name, Name.Function, '#pop'), # inline function (r'\(', Punctuation, '#pop'), @@ -180,7 +186,7 @@ class LuaLexer(RegexLexer): continue yield index, token, value -def _luau_make_expression(should_pop, _s): +def _luau_make_expression(should_pop, _s, _s_la): temp_list = [ (r'0[xX][\da-fA-F_]*', Number.Hex, '#pop'), (r'0[bB][\d_]*', Number.Bin, '#pop'), @@ -195,7 +201,7 @@ def _luau_make_expression(should_pop, _s): (r'(\.)([a-zA-Z_]\w*)(?=%s*[({"\'])', bygroups(Punctuation, Name.Function), '#pop'), (r'(\.)([a-zA-Z_]\w*)', bygroups(Punctuation, Name.Variable), '#pop'), - (rf'[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*(?={_s}*[({{"\'])', Name.Other, '#pop'), + (rf'[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*(?={_s_la}*[({{"\'])', Name.Other, '#pop'), (r'[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*', Name, '#pop'), ] if should_pop: @@ -244,6 +250,9 @@ class LuauLexer(RegexLexer): _comment_multiline = r'(?:--\[(?P=*)\[[\w\W]*?\](?P=level)\])' _comment_single = r'(?:--.*$)' _s = r'(?:{}|{}|{})'.format(_comment_multiline, _comment_single, r'\s+') + # Lookahead-safe version — avoids catastrophic backtracking from + # [\w\W]*? inside _comment_multiline when combined with * quantifier. + _s_la = r'\s' tokens = { 'root': [ @@ -283,7 +292,7 @@ class LuauLexer(RegexLexer): (r'[\])};]+', Punctuation), include('expression_static'), - *_luau_make_expression(False, _s), + *_luau_make_expression(False, _s, _s_la), (r'[\[.,]', Punctuation, 'expression'), ], @@ -305,7 +314,7 @@ class LuauLexer(RegexLexer): (r'function\b', Keyword.Reserved, 'func_name'), include('expression_static'), - *_luau_make_expression(True, _s), + *_luau_make_expression(True, _s, _s_la), default('#pop'), ], @@ -381,7 +390,7 @@ class LuauLexer(RegexLexer): include('ws'), (r'[.:]', Punctuation), - (rf'[a-zA-Z_]\w*(?={_s}*[.:])', Name.Class), + (rf'[a-zA-Z_]\w*(?={_s_la}*[.:])', Name.Class), (r'[a-zA-Z_]\w*', Name.Function), (r'<', Punctuation, 'closing_gt_type'), @@ -546,13 +555,26 @@ class MoonScriptLexer(LuaLexer): 'stringescape': [ (r'''\\([abfnrtv\\"']|\d{1,3})''', String.Escape) ], - 'sqs': [ - ("'", String.Single, '#pop'), - ("[^']+", String) + 'strings': [ + (r'[^#\\\'"]+', String), + # note that strings are multi-line. + # hashmarks, quotes and backslashes must be parsed one at a time + ], + 'interpoling_string': [ + (r'\}', String.Interpol, "#pop"), + include('base') ], 'dqs': [ - ('"', String.Double, '#pop'), - ('[^"]+', String) + (r'"', String.Double, '#pop'), + (r'\\.|\'', String), # double-quoted string don't need ' escapes + (r'#\{', String.Interpol, "interpoling_string"), + (r'#', String), + include('strings') + ], + 'sqs': [ + (r"'", String.Single, '#pop'), + (r'#|\\.|"', String), # single quoted strings don't need " escapses + include('strings') ] } diff --git a/libs/pygments/lexers/sgf.py b/libs/pygments/lexers/sgf.py index f0e56cba5..6d7c30432 100644 --- a/libs/pygments/lexers/sgf.py +++ b/libs/pygments/lexers/sgf.py @@ -4,7 +4,7 @@ Lexer for Smart Game Format (sgf) file format. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/shell.py b/libs/pygments/lexers/shell.py index 744767a1d..c1ce07fcc 100644 --- a/libs/pygments/lexers/shell.py +++ b/libs/pygments/lexers/shell.py @@ -4,7 +4,7 @@ Lexers for various shells. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/sieve.py b/libs/pygments/lexers/sieve.py index fc48980c4..4bda3517e 100644 --- a/libs/pygments/lexers/sieve.py +++ b/libs/pygments/lexers/sieve.py @@ -13,7 +13,7 @@ https://tools.ietf.org/html/rfc5429 https://tools.ietf.org/html/rfc8580 - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/slash.py b/libs/pygments/lexers/slash.py index 1c439d0db..2bcbdf877 100644 --- a/libs/pygments/lexers/slash.py +++ b/libs/pygments/lexers/slash.py @@ -4,7 +4,7 @@ Lexer for the Slash programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/smalltalk.py b/libs/pygments/lexers/smalltalk.py index 674b7b4b3..69e0519b7 100644 --- a/libs/pygments/lexers/smalltalk.py +++ b/libs/pygments/lexers/smalltalk.py @@ -4,7 +4,7 @@ Lexers for Smalltalk and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/smithy.py b/libs/pygments/lexers/smithy.py index bd479aec4..2642bb879 100644 --- a/libs/pygments/lexers/smithy.py +++ b/libs/pygments/lexers/smithy.py @@ -4,7 +4,7 @@ Lexers for the Smithy IDL. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/smv.py b/libs/pygments/lexers/smv.py index bf97b52a5..3d06f99f6 100644 --- a/libs/pygments/lexers/smv.py +++ b/libs/pygments/lexers/smv.py @@ -4,7 +4,7 @@ Lexers for the SMV languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/snobol.py b/libs/pygments/lexers/snobol.py index bab51e9b1..7016c96a5 100644 --- a/libs/pygments/lexers/snobol.py +++ b/libs/pygments/lexers/snobol.py @@ -4,7 +4,7 @@ Lexers for the SNOBOL language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/solidity.py b/libs/pygments/lexers/solidity.py index 3182a148a..3f2d1dd0a 100644 --- a/libs/pygments/lexers/solidity.py +++ b/libs/pygments/lexers/solidity.py @@ -4,7 +4,7 @@ Lexers for Solidity. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/soong.py b/libs/pygments/lexers/soong.py index bbf204dd2..0d7f12ccd 100644 --- a/libs/pygments/lexers/soong.py +++ b/libs/pygments/lexers/soong.py @@ -4,7 +4,7 @@ Lexers for Soong (Android.bp Blueprint) files. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/sophia.py b/libs/pygments/lexers/sophia.py index 37fcec5c3..86435f3fe 100644 --- a/libs/pygments/lexers/sophia.py +++ b/libs/pygments/lexers/sophia.py @@ -6,7 +6,7 @@ Derived from pygments/lexers/reason.py. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/special.py b/libs/pygments/lexers/special.py index 524946fc3..ea345226f 100644 --- a/libs/pygments/lexers/special.py +++ b/libs/pygments/lexers/special.py @@ -4,7 +4,7 @@ Special lexers. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/spice.py b/libs/pygments/lexers/spice.py index 9d2b1a1a8..978f0a71f 100644 --- a/libs/pygments/lexers/spice.py +++ b/libs/pygments/lexers/spice.py @@ -4,7 +4,7 @@ Lexers for the Spice programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -40,7 +40,7 @@ class SpiceLexer(RegexLexer): (r'(f|p|type|struct|interface|enum|alias|operator)\b', Keyword.Declaration), (words(('if', 'else', 'switch', 'case', 'default', 'for', 'foreach', 'do', 'while', 'break', 'continue', 'fallthrough', 'return', 'assert', - 'unsafe', 'ext'), suffix=r'\b'), Keyword), + 'unsafe', 'ext', 'cast'), suffix=r'\b'), Keyword), (words(('const', 'signed', 'unsigned', 'inline', 'public', 'heap', 'compose'), suffix=r'\b'), Keyword.Pseudo), (words(('new', 'yield', 'stash', 'pick', 'sync', 'class'), suffix=r'\b'), diff --git a/libs/pygments/lexers/sql.py b/libs/pygments/lexers/sql.py index d3e6f17f3..1d3458e9a 100644 --- a/libs/pygments/lexers/sql.py +++ b/libs/pygments/lexers/sql.py @@ -36,7 +36,7 @@ The ``tests/examplefiles`` contains a few test files with data to be parsed by these lexers. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -390,7 +390,7 @@ class PostgresExplainLexer(RegexLexer): tokens = { 'root': [ - (r'(:|\(|\)|ms|kB|->|\.\.|\,|\/)', Punctuation), + (r'(:|\(|\)|ms|kB|->|\.\.|\,|\/|=|%)', Punctuation), (r'(\s+)', Whitespace), # This match estimated cost and effectively measured counters with ANALYZE @@ -399,9 +399,11 @@ class PostgresExplainLexer(RegexLexer): (r'(actual)( )(=?)', bygroups(Name.Class, Whitespace, Punctuation), 'instrumentation'), # Misc keywords - (words(('actual', 'Memory Usage', 'Disk Usage', 'Memory', 'Buckets', 'Batches', - 'originally', 'row', 'rows', 'Hits', 'Misses', - 'Evictions', 'Overflows', 'Planned Partitions'), suffix=r'\b'), + (words(('actual', 'Memory Usage', 'Disk Usage', 'Memory', 'Buckets', + 'Batches', 'originally', 'row', 'rows', 'Hits', 'Misses', + 'Evictions', 'Overflows', 'Planned Partitions', 'Estimates', + 'capacity', 'distinct keys', 'lookups', 'hit percent', + 'Index Searches', 'Storage', 'Disk Maximum Storage'), suffix=r'\b'), Comment.Single), (r'(hit|read|dirtied|written|write|time|calls)(=)', bygroups(Comment.Single, Operator)), @@ -421,7 +423,7 @@ class PostgresExplainLexer(RegexLexer): 'TID Cond', 'Run Condition', 'Order By', 'Function Call', 'Table Function Call', 'Inner Unique', 'Params Evaluated', 'Single Copy', 'Sampling', 'One-Time Filter', 'Output', - 'Relations', 'Remote SQL'), suffix=r'\b'), + 'Relations', 'Remote SQL', 'Disabled'), suffix=r'\b'), Comment.Preproc, 'predicate'), # Special keyword to handle ON CONFLICT diff --git a/libs/pygments/lexers/srcinfo.py b/libs/pygments/lexers/srcinfo.py index daf654ea9..f5681ac72 100644 --- a/libs/pygments/lexers/srcinfo.py +++ b/libs/pygments/lexers/srcinfo.py @@ -7,7 +7,7 @@ The description of the format can be found in the wiki: https://wiki.archlinux.org/title/.SRCINFO - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/stata.py b/libs/pygments/lexers/stata.py index dc93cb4e0..3e51bf297 100644 --- a/libs/pygments/lexers/stata.py +++ b/libs/pygments/lexers/stata.py @@ -4,7 +4,7 @@ Lexer for Stata - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/supercollider.py b/libs/pygments/lexers/supercollider.py index 0f19d2af3..7d4b7fcf9 100644 --- a/libs/pygments/lexers/supercollider.py +++ b/libs/pygments/lexers/supercollider.py @@ -4,7 +4,7 @@ Lexer for SuperCollider - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/tablegen.py b/libs/pygments/lexers/tablegen.py index 5bcb3eef6..fc44ee7c0 100644 --- a/libs/pygments/lexers/tablegen.py +++ b/libs/pygments/lexers/tablegen.py @@ -4,7 +4,7 @@ Lexer for LLVM's TableGen DSL. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -118,7 +118,7 @@ class TableGenLexer(RegexLexer): (r'\s+', Whitespace), (r'/\*', Comment.Multiline, 'comment'), - (r'//.*?$', Comment.SingleLine), + (r'//.*?$', Comment.Single), (r'#(define|ifdef|ifndef|else|endif)', Comment.Preproc), # Binary/hex numbers. Note that these take priority over names, @@ -147,7 +147,7 @@ class TableGenLexer(RegexLexer): # String literals (r'"', String, 'dqs'), - (r'\[\{', Text, 'codeblock'), + (r'\[\{', Punctuation, 'codeblock'), # Misc. punctuation (r'[-+\[\]{}()<>\.,;:=?#]+', Punctuation), @@ -167,11 +167,11 @@ class TableGenLexer(RegexLexer): (r'"', String, '#pop'), include('strings'), ], - # No escaping inside a code block - everything is literal + # NOTE: Must escape (pop Cpplexer) codeblock to avoid infinite loop. # Assume that the code inside a code block is C++. This isn't always # true in TableGen, but is the far most common scenario. 'codeblock': [ (r'\}\]', Text, '#pop'), - (r'([^}]+|\}[^]])*', using(CppLexer)), + (r'([^}]+|\}[^]])*', using(CppLexer), "#pop"), ], } diff --git a/libs/pygments/lexers/tact.py b/libs/pygments/lexers/tact.py index 9199a5b14..8018b331a 100644 --- a/libs/pygments/lexers/tact.py +++ b/libs/pygments/lexers/tact.py @@ -4,7 +4,7 @@ Lexers for Tact. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/tal.py b/libs/pygments/lexers/tal.py index 33c1bdc93..57e3643ca 100644 --- a/libs/pygments/lexers/tal.py +++ b/libs/pygments/lexers/tal.py @@ -6,7 +6,7 @@ .. versionadded:: 2.12 - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/tcl.py b/libs/pygments/lexers/tcl.py index f156efa6a..9bba7367d 100644 --- a/libs/pygments/lexers/tcl.py +++ b/libs/pygments/lexers/tcl.py @@ -4,7 +4,7 @@ Lexers for Tcl and related languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/teal.py b/libs/pygments/lexers/teal.py index 54ee268cd..b80ff99ca 100644 --- a/libs/pygments/lexers/teal.py +++ b/libs/pygments/lexers/teal.py @@ -4,7 +4,7 @@ Lexer for TEAL. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/templates.py b/libs/pygments/lexers/templates.py index ff796d3d0..b2b69dd2d 100644 --- a/libs/pygments/lexers/templates.py +++ b/libs/pygments/lexers/templates.py @@ -4,7 +4,7 @@ Lexers for various template engines' markup. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/teraterm.py b/libs/pygments/lexers/teraterm.py index a2c2980d4..1670c9ae0 100644 --- a/libs/pygments/lexers/teraterm.py +++ b/libs/pygments/lexers/teraterm.py @@ -4,7 +4,7 @@ Lexer for Tera Term macro files. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -84,6 +84,7 @@ class TeraTermLexer(RegexLexer): r'crc32file|' r'cygconnect|' r'delpassword|' + r'delpassword2|' r'dirname|' r'dirnamebox|' r'disconnect|' @@ -139,10 +140,12 @@ class TeraTermLexer(RegexLexer): r'getipv6addr|' r'getmodemstatus|' r'getpassword|' + r'getpassword2|' r'getspecialfolder|' r'gettime|' r'gettitle|' r'getttdir|' + r'getttpos|' r'getver|' # 'goto' is handled separately. r'if|' @@ -152,6 +155,7 @@ class TeraTermLexer(RegexLexer): r'int2str|' r'intdim|' r'ispassword|' + r'ispassword2|' r'kmtfinish|' r'kmtget|' r'kmtrecv|' @@ -176,6 +180,7 @@ class TeraTermLexer(RegexLexer): r'quickvanrecv|' r'quickvansend|' r'random|' + r'recvfile' r'recvln|' r'regexoption|' r'restoresetup|' @@ -185,6 +190,7 @@ class TeraTermLexer(RegexLexer): r'scprecv|' r'scpsend|' r'send|' + r'sendbinary|' r'sendbreak|' r'sendbroadcast|' r'sendfile|' @@ -193,6 +199,7 @@ class TeraTermLexer(RegexLexer): r'sendlnbroadcast|' r'sendlnmulticast|' r'sendmulticast|' + r'sendtext|' r'setbaud|' r'setdate|' r'setdebug|' @@ -206,7 +213,10 @@ class TeraTermLexer(RegexLexer): r'setflowctrl|' r'setmulticastname|' r'setpassword|' + r'setpassword2|' r'setrts|' + r'setserialdelaychar' + r'setserialdelayline' r'setspeed|' r'setsync|' r'settime|' diff --git a/libs/pygments/lexers/testing.py b/libs/pygments/lexers/testing.py index e6c59269f..8b7ace8a1 100644 --- a/libs/pygments/lexers/testing.py +++ b/libs/pygments/lexers/testing.py @@ -4,7 +4,7 @@ Lexers for testing languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/text.py b/libs/pygments/lexers/text.py index f5524783e..017e3f73d 100644 --- a/libs/pygments/lexers/text.py +++ b/libs/pygments/lexers/text.py @@ -4,7 +4,7 @@ Lexers for non-source code file types. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/textedit.py b/libs/pygments/lexers/textedit.py index ea6a0a976..e8e94ed5b 100644 --- a/libs/pygments/lexers/textedit.py +++ b/libs/pygments/lexers/textedit.py @@ -4,7 +4,7 @@ Lexers for languages related to text processing. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/textfmts.py b/libs/pygments/lexers/textfmts.py index 6dd4e581b..c97db4100 100644 --- a/libs/pygments/lexers/textfmts.py +++ b/libs/pygments/lexers/textfmts.py @@ -4,7 +4,7 @@ Lexers for various text formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/theorem.py b/libs/pygments/lexers/theorem.py index 9e3f150d4..ef94dcecc 100644 --- a/libs/pygments/lexers/theorem.py +++ b/libs/pygments/lexers/theorem.py @@ -6,7 +6,7 @@ See also :mod:`pygments.lexers.lean` - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -16,19 +16,19 @@ from pygments.token import Text, Comment, Operator, Keyword, Name, String, \ # compatibility import from pygments.lexers.lean import LeanLexer # noqa: F401 -__all__ = ['CoqLexer', 'IsabelleLexer'] +__all__ = ['RocqLexer', 'IsabelleLexer'] -class CoqLexer(RegexLexer): +class RocqLexer(RegexLexer): """ - For the Coq theorem prover. + For the Rocq Prover. """ - name = 'Coq' - url = 'http://coq.inria.fr/' - aliases = ['coq'] + name = 'Rocq Prover' + url = 'https://rocq-prover.org/' + aliases = ['coq', 'rocq', 'rocq-prover'] filenames = ['*.v'] - mimetypes = ['text/x-coq'] + mimetypes = ['text/x-coq', 'text/x-rocq'] version_added = '1.5' flags = 0 # no re.MULTILINE diff --git a/libs/pygments/lexers/thingsdb.py b/libs/pygments/lexers/thingsdb.py index ee859c995..3826bbbe3 100644 --- a/libs/pygments/lexers/thingsdb.py +++ b/libs/pygments/lexers/thingsdb.py @@ -4,7 +4,7 @@ Lexers for the ThingsDB language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -52,7 +52,7 @@ class ThingsDBLexer(RegexLexer): # name constants (r'(FULL|USER|GRANT|CHANGE|JOIN|RUN|QUERY|' r'DEBUG|INFO|WARNING|ERROR|CRITICAL|' - r'NO_IDS|INT_MIN|INT_MAX)\b', Name.Constant), + r'NO_IDS|INT_MIN|INT_MAX|MATH_E|MATH_PI)\b', Name.Constant), # regular expressions (r'(/[^/\\]*(?:\\.[^/\\]*)*/i?)', String.Regex), @@ -75,7 +75,7 @@ class ThingsDBLexer(RegexLexer): r'one|clear|contains|ends_with|name|lower|replace|reverse|' r'starts_with|split|test|trim|trim_left|trim_right|upper|del|ren|' r'to_type|to_thing|get|id|keys|reduce|set|some|value|values|wrap|' - r'unshift|unwrap|search)' + r'unshift|unwrap|search|set_name|bit_count)' r'(\()', bygroups(Name.Function, Name.Function, Punctuation), 'arguments'), (r'(alt_raise|assert|base64_encode|base64_decode|bool|bytes|' @@ -89,7 +89,7 @@ class ThingsDBLexer(RegexLexer): r'json_load|list|log|import|export|root|mod_enum|mod_type|new|' r'new_type|now|raise|rand|range|randint|randstr|refs|rename_enum|' r'set|set_enum|set_type|str|thing|timeval|try|type|type_assert|' - r'type_count|type_info|types_info|nse|wse|backup_info|' + r'type_all|type_count|type_info|types_info|nse|wse|backup_info|' r'backups_info|backups_ok|counters|del_backup|has_backup|' r'new_backup|node_info|nodes_info|reset_counters|restart_module|' r'set_log_level|shutdown|has_module|del_module|module_info|' @@ -105,7 +105,10 @@ class ThingsDBLexer(RegexLexer): r'run|assert_err|auth_err|bad_data_err|cancelled_err|' r'rename_type|forbidden_err|lookup_err|max_quota_err|node_err|' r'num_arguments_err|operation_err|overflow_err|syntax_err|' - r'collection_info|type_err|value_err|zero_div_err)' + r'collection_info|type_err|value_err|zero_div_err|whitelist_add|' + r'whitelist_del|round|abs|ceil|cos|exp|floor|log10|log2|' + r'loge|pow|sin|sqrt|tan|is_module|commit|history|set_history|' + r'del_history|ano)' r'(\()', bygroups(Name.Function, Punctuation), 'arguments'), diff --git a/libs/pygments/lexers/tlb.py b/libs/pygments/lexers/tlb.py index e652eac33..c58d208ea 100644 --- a/libs/pygments/lexers/tlb.py +++ b/libs/pygments/lexers/tlb.py @@ -4,7 +4,7 @@ Lexers for TL-b. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/tls.py b/libs/pygments/lexers/tls.py index a3079a22a..16dfe81b5 100644 --- a/libs/pygments/lexers/tls.py +++ b/libs/pygments/lexers/tls.py @@ -4,7 +4,7 @@ Lexers for the TLS presentation language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re diff --git a/libs/pygments/lexers/tnt.py b/libs/pygments/lexers/tnt.py index 1ae2ed84a..45232df62 100644 --- a/libs/pygments/lexers/tnt.py +++ b/libs/pygments/lexers/tnt.py @@ -4,7 +4,7 @@ Lexer for Typographic Number Theory. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/trafficscript.py b/libs/pygments/lexers/trafficscript.py index 8c801c4cb..cbffad5b2 100644 --- a/libs/pygments/lexers/trafficscript.py +++ b/libs/pygments/lexers/trafficscript.py @@ -4,7 +4,7 @@ Lexer for RiverBed's TrafficScript (RTS) language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/typoscript.py b/libs/pygments/lexers/typoscript.py index d71d5c355..6f51fc68b 100644 --- a/libs/pygments/lexers/typoscript.py +++ b/libs/pygments/lexers/typoscript.py @@ -13,7 +13,7 @@ `TypoScriptHtmlDataLexer` Lexer that highlights markers, constants and registers within html tags. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/typst.py b/libs/pygments/lexers/typst.py index 21f8efd30..8132e3ee7 100644 --- a/libs/pygments/lexers/typst.py +++ b/libs/pygments/lexers/typst.py @@ -4,7 +4,7 @@ Lexers for Typst language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/ul4.py b/libs/pygments/lexers/ul4.py index 552832cae..609b07729 100644 --- a/libs/pygments/lexers/ul4.py +++ b/libs/pygments/lexers/ul4.py @@ -4,7 +4,7 @@ Lexer for the UL4 templating language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/unicon.py b/libs/pygments/lexers/unicon.py index 1077661a3..7b7f32e0c 100644 --- a/libs/pygments/lexers/unicon.py +++ b/libs/pygments/lexers/unicon.py @@ -4,7 +4,7 @@ Lexers for the Icon and Unicon languages, including ucode VM. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/urbi.py b/libs/pygments/lexers/urbi.py index 93cfcedc1..0de9626ad 100644 --- a/libs/pygments/lexers/urbi.py +++ b/libs/pygments/lexers/urbi.py @@ -4,7 +4,7 @@ Lexers for UrbiScript language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/usd.py b/libs/pygments/lexers/usd.py index 468a37369..79f7c40f1 100644 --- a/libs/pygments/lexers/usd.py +++ b/libs/pygments/lexers/usd.py @@ -4,7 +4,7 @@ The module that parses Pixar's Universal Scene Description file format. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/varnish.py b/libs/pygments/lexers/varnish.py index 5c3de7994..e4822edaa 100644 --- a/libs/pygments/lexers/varnish.py +++ b/libs/pygments/lexers/varnish.py @@ -4,7 +4,7 @@ Lexers for Varnish configuration - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/verification.py b/libs/pygments/lexers/verification.py index ac0675797..892a4f36c 100644 --- a/libs/pygments/lexers/verification.py +++ b/libs/pygments/lexers/verification.py @@ -4,7 +4,7 @@ Lexer for Intermediate Verification Languages (IVLs). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/verifpal.py b/libs/pygments/lexers/verifpal.py index e226339dc..1c8ed66b0 100644 --- a/libs/pygments/lexers/verifpal.py +++ b/libs/pygments/lexers/verifpal.py @@ -4,7 +4,7 @@ Lexers for Verifpal languages. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/vip.py b/libs/pygments/lexers/vip.py index 22395e1e9..08d356b7d 100644 --- a/libs/pygments/lexers/vip.py +++ b/libs/pygments/lexers/vip.py @@ -4,7 +4,7 @@ Lexers for Visual Prolog & Grammar files. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/vyper.py b/libs/pygments/lexers/vyper.py index 002ffeddc..1afd7f12a 100644 --- a/libs/pygments/lexers/vyper.py +++ b/libs/pygments/lexers/vyper.py @@ -4,7 +4,7 @@ Lexer for the Vyper Smart Contract language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/web.py b/libs/pygments/lexers/web.py index 3d359662d..23cb47851 100644 --- a/libs/pygments/lexers/web.py +++ b/libs/pygments/lexers/web.py @@ -4,7 +4,7 @@ Just export previously exported lexers. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/webassembly.py b/libs/pygments/lexers/webassembly.py index 78cd4ca31..f9d409f9c 100644 --- a/libs/pygments/lexers/webassembly.py +++ b/libs/pygments/lexers/webassembly.py @@ -8,7 +8,7 @@ and https://webassembly.github.io/spec/core/text/. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/webidl.py b/libs/pygments/lexers/webidl.py index 3bbd12efd..5638f2e1c 100644 --- a/libs/pygments/lexers/webidl.py +++ b/libs/pygments/lexers/webidl.py @@ -4,7 +4,7 @@ Lexers for Web IDL, including some extensions. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/webmisc.py b/libs/pygments/lexers/webmisc.py index 347592d91..0923103f6 100644 --- a/libs/pygments/lexers/webmisc.py +++ b/libs/pygments/lexers/webmisc.py @@ -4,7 +4,7 @@ Lexers for misc. web stuff. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/wgsl.py b/libs/pygments/lexers/wgsl.py index 8fe950d9f..f21836dc0 100644 --- a/libs/pygments/lexers/wgsl.py +++ b/libs/pygments/lexers/wgsl.py @@ -4,7 +4,7 @@ Lexer for the WebGPU Shading Language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/whiley.py b/libs/pygments/lexers/whiley.py index ea98eb2d7..8809d3d79 100644 --- a/libs/pygments/lexers/whiley.py +++ b/libs/pygments/lexers/whiley.py @@ -4,7 +4,7 @@ Lexers for the Whiley language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/wowtoc.py b/libs/pygments/lexers/wowtoc.py index 2b18757d7..edc7d06f5 100644 --- a/libs/pygments/lexers/wowtoc.py +++ b/libs/pygments/lexers/wowtoc.py @@ -6,7 +6,7 @@ TOC files describe game addons. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/wren.py b/libs/pygments/lexers/wren.py index ec2cd54c9..f6989a7d8 100644 --- a/libs/pygments/lexers/wren.py +++ b/libs/pygments/lexers/wren.py @@ -4,7 +4,7 @@ Lexer for Wren. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/x10.py b/libs/pygments/lexers/x10.py index 4b5d097c5..63fc147cb 100644 --- a/libs/pygments/lexers/x10.py +++ b/libs/pygments/lexers/x10.py @@ -4,7 +4,7 @@ Lexers for the X10 programming language. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/xorg.py b/libs/pygments/lexers/xorg.py index 37251e490..e795e4b50 100644 --- a/libs/pygments/lexers/xorg.py +++ b/libs/pygments/lexers/xorg.py @@ -4,7 +4,7 @@ Lexers for Xorg configs. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/yang.py b/libs/pygments/lexers/yang.py index e7f395125..3b9dec91d 100644 --- a/libs/pygments/lexers/yang.py +++ b/libs/pygments/lexers/yang.py @@ -4,7 +4,7 @@ Lexer for the YANG 1.1 modeling language. See :rfc:`7950`. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/yara.py b/libs/pygments/lexers/yara.py index 29694f5a1..4ea427fe7 100644 --- a/libs/pygments/lexers/yara.py +++ b/libs/pygments/lexers/yara.py @@ -4,7 +4,7 @@ Lexers for YARA. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/lexers/zig.py b/libs/pygments/lexers/zig.py index 2b0168acf..6cc4df84e 100644 --- a/libs/pygments/lexers/zig.py +++ b/libs/pygments/lexers/zig.py @@ -4,7 +4,7 @@ Lexers for Zig. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -22,7 +22,7 @@ class ZigLexer(RegexLexer): grammar: https://ziglang.org/documentation/master/#Grammar """ name = 'Zig' - url = 'http://www.ziglang.org' + url = 'http://ziglang.org' aliases = ['zig'] filenames = ['*.zig'] mimetypes = ['text/zig'] diff --git a/libs/pygments/modeline.py b/libs/pygments/modeline.py index c310f0edb..81ec15773 100644 --- a/libs/pygments/modeline.py +++ b/libs/pygments/modeline.py @@ -4,7 +4,7 @@ A simple modeline parser (based on pymodeline). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/plugin.py b/libs/pygments/plugin.py index 498db4238..0d6377f76 100644 --- a/libs/pygments/plugin.py +++ b/libs/pygments/plugin.py @@ -29,9 +29,10 @@ yourfilter = yourfilter:YourFilter - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import functools from importlib.metadata import entry_points LEXER_ENTRY_POINT = 'pygments.lexers' @@ -40,6 +41,7 @@ STYLE_ENTRY_POINT = 'pygments.styles' FILTER_ENTRY_POINT = 'pygments.filters' +@functools.cache def iter_entry_points(group_name): groups = entry_points() if hasattr(groups, 'select'): diff --git a/libs/pygments/regexopt.py b/libs/pygments/regexopt.py index cc8d2c31b..9010eb3fb 100644 --- a/libs/pygments/regexopt.py +++ b/libs/pygments/regexopt.py @@ -5,13 +5,12 @@ An algorithm that generates optimized regexes for matching long lists of literal strings. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from re import escape -from os.path import commonprefix from itertools import groupby from operator import itemgetter @@ -19,6 +18,18 @@ CS_ESCAPE = re.compile(r'[\[\^\\\-\]]') FIRST_ELEMENT = itemgetter(0) +def commonprefix(m): + """Given an iterable of strings, returns the longest common leading substring""" + if not m: + return "" + s1 = min(m) + s2 = max(m) + for i, c in enumerate(s1): + if c != s2[i]: + return s1[:i] + return s1 + + def make_charset(letters): return '[' + CS_ESCAPE.sub(lambda m: '\\' + m.group(), ''.join(letters)) + ']' diff --git a/libs/pygments/scanner.py b/libs/pygments/scanner.py index 3c8c84873..067ebfa9c 100644 --- a/libs/pygments/scanner.py +++ b/libs/pygments/scanner.py @@ -11,7 +11,7 @@ Have a look at the `DelphiLexer` to get an idea of how to use this scanner. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re diff --git a/libs/pygments/sphinxext.py b/libs/pygments/sphinxext.py index c215399ba..5c03e4c64 100644 --- a/libs/pygments/sphinxext.py +++ b/libs/pygments/sphinxext.py @@ -5,7 +5,7 @@ Sphinx extension to generate automatic documentation of lexers, formatters and filters. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/style.py b/libs/pygments/style.py index ac36cad7b..acf25d6d8 100644 --- a/libs/pygments/style.py +++ b/libs/pygments/style.py @@ -4,7 +4,7 @@ Basic style object. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/__init__.py b/libs/pygments/styles/__init__.py index 5d45d8b23..58d1d7775 100644 --- a/libs/pygments/styles/__init__.py +++ b/libs/pygments/styles/__init__.py @@ -4,7 +4,7 @@ Contains built-in styles. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/abap.py b/libs/pygments/styles/abap.py index 584a4e649..235bed759 100644 --- a/libs/pygments/styles/abap.py +++ b/libs/pygments/styles/abap.py @@ -4,7 +4,7 @@ ABAP workbench like style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/algol.py b/libs/pygments/styles/algol.py index 86cafce23..c8bea2ee2 100644 --- a/libs/pygments/styles/algol.py +++ b/libs/pygments/styles/algol.py @@ -25,7 +25,7 @@ [1] `Revised Report on the Algorithmic Language Algol-60 ` - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/algol_nu.py b/libs/pygments/styles/algol_nu.py index d28da8f1b..b8c052657 100644 --- a/libs/pygments/styles/algol_nu.py +++ b/libs/pygments/styles/algol_nu.py @@ -25,7 +25,7 @@ [1] `Revised Report on the Algorithmic Language Algol-60 ` - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/arduino.py b/libs/pygments/styles/arduino.py index dcb325217..651f33984 100644 --- a/libs/pygments/styles/arduino.py +++ b/libs/pygments/styles/arduino.py @@ -4,7 +4,7 @@ Arduino® Syntax highlighting style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/autumn.py b/libs/pygments/styles/autumn.py index 8229ad175..75283e2d9 100644 --- a/libs/pygments/styles/autumn.py +++ b/libs/pygments/styles/autumn.py @@ -4,7 +4,7 @@ A colorful style, inspired by the terminal highlighting style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/borland.py b/libs/pygments/styles/borland.py index bfceaea0f..73e9cd1b0 100644 --- a/libs/pygments/styles/borland.py +++ b/libs/pygments/styles/borland.py @@ -4,7 +4,7 @@ Style similar to the style used in the Borland IDEs. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/bw.py b/libs/pygments/styles/bw.py index 30cd78763..9fa0baa57 100644 --- a/libs/pygments/styles/bw.py +++ b/libs/pygments/styles/bw.py @@ -4,7 +4,7 @@ Simple black/white only style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/coffee.py b/libs/pygments/styles/coffee.py index 66f504d87..98da14e00 100644 --- a/libs/pygments/styles/coffee.py +++ b/libs/pygments/styles/coffee.py @@ -4,7 +4,7 @@ A warm and cozy theme based off gruvbox - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/colorful.py b/libs/pygments/styles/colorful.py index 012d943ac..5511fba1d 100644 --- a/libs/pygments/styles/colorful.py +++ b/libs/pygments/styles/colorful.py @@ -4,7 +4,7 @@ A colorful style, inspired by CodeRay. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/default.py b/libs/pygments/styles/default.py index 30f882e96..521cdf526 100644 --- a/libs/pygments/styles/default.py +++ b/libs/pygments/styles/default.py @@ -4,7 +4,7 @@ The default highlighting style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/dracula.py b/libs/pygments/styles/dracula.py index 805e6630a..552668dda 100644 --- a/libs/pygments/styles/dracula.py +++ b/libs/pygments/styles/dracula.py @@ -7,7 +7,7 @@ Based on the Dracula Theme for pygments by Chris Bracco. See https://github.com/dracula/pygments/tree/fee9ed5613d1086bc01b9d0a5a0e9867a009f571 - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/emacs.py b/libs/pygments/styles/emacs.py index 729486344..8860c7aab 100644 --- a/libs/pygments/styles/emacs.py +++ b/libs/pygments/styles/emacs.py @@ -4,7 +4,7 @@ A highlighting style for Pygments, inspired by Emacs. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/friendly.py b/libs/pygments/styles/friendly.py index e1950f087..aeee356b3 100644 --- a/libs/pygments/styles/friendly.py +++ b/libs/pygments/styles/friendly.py @@ -4,7 +4,7 @@ A modern style based on the VIM pyte theme. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/friendly_grayscale.py b/libs/pygments/styles/friendly_grayscale.py index 23dfaa59c..8e25b5919 100644 --- a/libs/pygments/styles/friendly_grayscale.py +++ b/libs/pygments/styles/friendly_grayscale.py @@ -7,7 +7,7 @@ using the luminosity value calculated by http://www.workwithcolor.com/color-converter-01.htm - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/fruity.py b/libs/pygments/styles/fruity.py index 64c8f96eb..cf3de8867 100644 --- a/libs/pygments/styles/fruity.py +++ b/libs/pygments/styles/fruity.py @@ -4,7 +4,7 @@ pygments version of my "fruity" vim theme. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/gh_dark.py b/libs/pygments/styles/gh_dark.py index 3bfd30003..d0d32f6c3 100644 --- a/libs/pygments/styles/gh_dark.py +++ b/libs/pygments/styles/gh_dark.py @@ -5,7 +5,7 @@ Github's Dark-Colorscheme based theme for Pygments Colors extracted from https://github.com/primer/primitives - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/gruvbox.py b/libs/pygments/styles/gruvbox.py index 235639118..20eaeb551 100644 --- a/libs/pygments/styles/gruvbox.py +++ b/libs/pygments/styles/gruvbox.py @@ -5,7 +5,7 @@ pygments version of the "gruvbox" vim theme. https://github.com/morhetz/gruvbox - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/igor.py b/libs/pygments/styles/igor.py index 467b281d0..3958b0006 100644 --- a/libs/pygments/styles/igor.py +++ b/libs/pygments/styles/igor.py @@ -4,7 +4,7 @@ Igor Pro default style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/inkpot.py b/libs/pygments/styles/inkpot.py index 39902c27c..a5dd52982 100644 --- a/libs/pygments/styles/inkpot.py +++ b/libs/pygments/styles/inkpot.py @@ -4,7 +4,7 @@ A highlighting style for Pygments, inspired by the Inkpot theme for VIM. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/lightbulb.py b/libs/pygments/styles/lightbulb.py index 8bcd21aaf..faba6eed9 100644 --- a/libs/pygments/styles/lightbulb.py +++ b/libs/pygments/styles/lightbulb.py @@ -4,7 +4,7 @@ A minimal dark theme based on the Lightbulb theme for VSCode. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/lilypond.py b/libs/pygments/styles/lilypond.py index b538798f1..113809875 100644 --- a/libs/pygments/styles/lilypond.py +++ b/libs/pygments/styles/lilypond.py @@ -4,7 +4,7 @@ LilyPond-specific style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/lovelace.py b/libs/pygments/styles/lovelace.py index 6e255cd66..3158e83ab 100644 --- a/libs/pygments/styles/lovelace.py +++ b/libs/pygments/styles/lovelace.py @@ -8,7 +8,7 @@ A desaturated, somewhat subdued style created for the Lovelace interactive learning environment. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/manni.py b/libs/pygments/styles/manni.py index df1822a21..e57b7bab7 100644 --- a/libs/pygments/styles/manni.py +++ b/libs/pygments/styles/manni.py @@ -7,7 +7,7 @@ This is a port of the style used in the `php port`_ of pygments by Manni. The style is called 'default' there. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/material.py b/libs/pygments/styles/material.py index 633ef380f..27c0f3040 100644 --- a/libs/pygments/styles/material.py +++ b/libs/pygments/styles/material.py @@ -6,7 +6,7 @@ https://github.com/material-theme/vsc-material-theme - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/monokai.py b/libs/pygments/styles/monokai.py index 4a6ac0cab..40569f924 100644 --- a/libs/pygments/styles/monokai.py +++ b/libs/pygments/styles/monokai.py @@ -6,7 +6,7 @@ http://www.monokai.nl/blog/2006/07/15/textmate-color-theme/ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/murphy.py b/libs/pygments/styles/murphy.py index 1299fd5b6..64af6e4bf 100644 --- a/libs/pygments/styles/murphy.py +++ b/libs/pygments/styles/murphy.py @@ -4,7 +4,7 @@ Murphy's style from CodeRay. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/native.py b/libs/pygments/styles/native.py index c9f818dd5..ae47e3e6b 100644 --- a/libs/pygments/styles/native.py +++ b/libs/pygments/styles/native.py @@ -4,7 +4,7 @@ pygments version of my "native" vim theme. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/nord.py b/libs/pygments/styles/nord.py index 5481e9681..7a3a95941 100644 --- a/libs/pygments/styles/nord.py +++ b/libs/pygments/styles/nord.py @@ -5,7 +5,7 @@ pygments version of the "nord" theme by Arctic Ice Studio https://www.nordtheme.com/ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/onedark.py b/libs/pygments/styles/onedark.py index 75c4b17a4..cc341f63b 100644 --- a/libs/pygments/styles/onedark.py +++ b/libs/pygments/styles/onedark.py @@ -7,13 +7,13 @@ Inspired by one-dark-ui for the code editor Atom (https://atom.io/themes/one-dark-ui). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ from pygments.style import Style from pygments.token import Comment, Keyword, Name, Number, Operator, \ - Punctuation, String, Token + Punctuation, String, Token, Generic __all__ = ['OneDarkStyle'] @@ -59,5 +59,15 @@ class OneDarkStyle(Style): Operator: '#56B6C2', - Comment: '#7F848E' + Comment: '#7F848E', + + Generic.Heading: 'bold #ABB2BF', + Generic.Subheading: 'bold #ABB2BF', + Generic.Deleted: '#E06C75', + Generic.Inserted: '#98C379', + Generic.Error: '#E06C75', + Generic.Emph: 'italic', + Generic.Strong: 'bold', + Generic.Output: '#E06C75', + Generic.Traceback: '#E06C75', } diff --git a/libs/pygments/styles/paraiso_dark.py b/libs/pygments/styles/paraiso_dark.py index 9983e60d0..e460e434f 100644 --- a/libs/pygments/styles/paraiso_dark.py +++ b/libs/pygments/styles/paraiso_dark.py @@ -8,7 +8,7 @@ Created with Base16 Builder by Chris Kempson (https://github.com/chriskempson/base16-builder). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/paraiso_light.py b/libs/pygments/styles/paraiso_light.py index 903ad88d6..6ba861b95 100644 --- a/libs/pygments/styles/paraiso_light.py +++ b/libs/pygments/styles/paraiso_light.py @@ -8,7 +8,7 @@ Created with Base16 Builder by Chris Kempson (https://github.com/chriskempson/base16-builder). - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/pastie.py b/libs/pygments/styles/pastie.py index 819968e1a..5b493fa02 100644 --- a/libs/pygments/styles/pastie.py +++ b/libs/pygments/styles/pastie.py @@ -6,7 +6,7 @@ .. _pastie: http://pastie.caboo.se/ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/perldoc.py b/libs/pygments/styles/perldoc.py index e76697a9a..dc5225c6a 100644 --- a/libs/pygments/styles/perldoc.py +++ b/libs/pygments/styles/perldoc.py @@ -6,7 +6,7 @@ .. _perldoc: http://perldoc.perl.org/ - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/rainbow_dash.py b/libs/pygments/styles/rainbow_dash.py index f98d5ef08..4c8bef693 100644 --- a/libs/pygments/styles/rainbow_dash.py +++ b/libs/pygments/styles/rainbow_dash.py @@ -6,7 +6,7 @@ .. _theme: http://sanssecours.github.io/Rainbow-Dash.tmbundle - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/rrt.py b/libs/pygments/styles/rrt.py index 1e87d2625..737b54d10 100644 --- a/libs/pygments/styles/rrt.py +++ b/libs/pygments/styles/rrt.py @@ -4,13 +4,21 @@ pygments "rrt" theme, based on Zap and Emacs defaults. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ from pygments.style import Style -from pygments.token import Token, Comment, Name, Keyword, String, Number, Operator - +from pygments.token import ( + Comment, + Generic, + Keyword, + Name, + Number, + Operator, + String, + Token, +) __all__ = ['RrtStyle'] @@ -28,11 +36,17 @@ class RrtStyle(Style): styles = { Token: '#dddddd', Comment: '#00ff00', + Generic.Emph: '#ffffff', + Generic.Heading: '#ffff00', + Generic.Subheading: '#ffff00', + Generic.Strong: '#ffffff', + Name.Entity: '#eedd82', Name.Function: '#ffff00', Name.Variable: '#eedd82', + Name.Tag: '#7fffd4', Name.Constant: '#7fffd4', Keyword: '#ff0000', - Operator.Word: '#ff0000', + Operator.Word: '#ff0000', Comment.Preproc: '#e5e5e5', String: '#87ceeb', Keyword.Type: '#ee82ee', diff --git a/libs/pygments/styles/sas.py b/libs/pygments/styles/sas.py index 549a0cc9f..4fd51fd5b 100644 --- a/libs/pygments/styles/sas.py +++ b/libs/pygments/styles/sas.py @@ -6,7 +6,7 @@ meant to be a complete style. It's merely meant to mimic SAS' program editor syntax highlighting. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/solarized.py b/libs/pygments/styles/solarized.py index c05170245..8d3c7e321 100644 --- a/libs/pygments/styles/solarized.py +++ b/libs/pygments/styles/solarized.py @@ -7,7 +7,7 @@ A Pygments style for the Solarized themes (licensed under MIT). See: https://github.com/altercation/solarized - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/staroffice.py b/libs/pygments/styles/staroffice.py index a21aa4157..1b10c79ad 100644 --- a/libs/pygments/styles/staroffice.py +++ b/libs/pygments/styles/staroffice.py @@ -4,7 +4,7 @@ Style similar to StarOffice style, also in OpenOffice and LibreOffice. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/stata_dark.py b/libs/pygments/styles/stata_dark.py index 315c5c6ab..461161725 100644 --- a/libs/pygments/styles/stata_dark.py +++ b/libs/pygments/styles/stata_dark.py @@ -6,7 +6,7 @@ meant to be a complete style, just for Stata's file formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/stata_light.py b/libs/pygments/styles/stata_light.py index 95d72e2f3..aa4eb1b55 100644 --- a/libs/pygments/styles/stata_light.py +++ b/libs/pygments/styles/stata_light.py @@ -5,7 +5,7 @@ Light Style inspired by Stata's do-file editor. Note this is not meant to be a complete style, just for Stata's file formats. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/tango.py b/libs/pygments/styles/tango.py index 986e3ffe6..d5683d4c1 100644 --- a/libs/pygments/styles/tango.py +++ b/libs/pygments/styles/tango.py @@ -32,7 +32,7 @@ have been chosen to have the same style. Similarly, keywords (Keyword.*), and Operator.Word (and, or, in) have been assigned the same style. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/trac.py b/libs/pygments/styles/trac.py index 2dbffa468..c965b110c 100644 --- a/libs/pygments/styles/trac.py +++ b/libs/pygments/styles/trac.py @@ -4,7 +4,7 @@ Port of the default trac highlighter design. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/vim.py b/libs/pygments/styles/vim.py index 6b53d0080..5958b06f7 100644 --- a/libs/pygments/styles/vim.py +++ b/libs/pygments/styles/vim.py @@ -4,7 +4,7 @@ A highlighting style for Pygments, inspired by vim. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/vs.py b/libs/pygments/styles/vs.py index 9ac18c781..272e58454 100644 --- a/libs/pygments/styles/vs.py +++ b/libs/pygments/styles/vs.py @@ -4,7 +4,7 @@ Simple style with MS Visual Studio colors. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/xcode.py b/libs/pygments/styles/xcode.py index 9ef7c8f18..d9483bc1d 100644 --- a/libs/pygments/styles/xcode.py +++ b/libs/pygments/styles/xcode.py @@ -4,7 +4,7 @@ Style similar to the `Xcode` default theme. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/styles/zenburn.py b/libs/pygments/styles/zenburn.py index 4d0e0567f..da0bc1da8 100644 --- a/libs/pygments/styles/zenburn.py +++ b/libs/pygments/styles/zenburn.py @@ -7,7 +7,7 @@ See: https://kippura.org/zenburnpage/ https://github.com/jnurmine/Zenburn - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/token.py b/libs/pygments/token.py index 2f3b97e09..1f756b711 100644 --- a/libs/pygments/token.py +++ b/libs/pygments/token.py @@ -4,7 +4,7 @@ Basic token types and the standard tokens. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/unistring.py b/libs/pygments/unistring.py index e3bd2e727..2a326dff8 100644 --- a/libs/pygments/unistring.py +++ b/libs/pygments/unistring.py @@ -7,7 +7,7 @@ Inspired by chartypes_create.py from the MoinMoin project. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ diff --git a/libs/pygments/util.py b/libs/pygments/util.py index 71c5710ae..548d9d7af 100644 --- a/libs/pygments/util.py +++ b/libs/pygments/util.py @@ -4,7 +4,7 @@ Utility functions. - :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. + :copyright: Copyright 2006-present by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. """ @@ -286,7 +286,7 @@ def guess_decode(text): try: import locale prefencoding = locale.getpreferredencoding() - text = text.decode() + text = text.decode(prefencoding) return text, prefencoding except (UnicodeDecodeError, LookupError): text = text.decode('latin1') diff --git a/libs/pysubs2-1.8.0.dist-info/RECORD b/libs/pysubs2-1.8.0.dist-info/RECORD deleted file mode 100644 index 2f70949f9..000000000 --- a/libs/pysubs2-1.8.0.dist-info/RECORD +++ /dev/null @@ -1,31 +0,0 @@ -../../bin/pysubs2,sha256=XhbCKmkKcAcZBkfEHYiNeFggqX4T9VnjVWkgIhM5CF0,194 -pysubs2-1.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -pysubs2-1.8.0.dist-info/METADATA,sha256=gPc_yaSAsPfEXR7Z4S1IGxrGyZqzvcC44IHmiAkRrqU,3222 -pysubs2-1.8.0.dist-info/RECORD,, -pysubs2-1.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -pysubs2-1.8.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -pysubs2-1.8.0.dist-info/entry_points.txt,sha256=mznmeaia9Ec19KsaIM8D4tt6V0HnOUlPhSg5nXiAfAE,49 -pysubs2-1.8.0.dist-info/licenses/LICENSE.txt,sha256=WYgskvBrUBBDxuKH9JSV1u22g-UBTwHjeuJLlPp6lWU,1063 -pysubs2-1.8.0.dist-info/top_level.txt,sha256=i_V6JX-AWjMX15-Dzyc0RlrInU7Zun9PYfdB9brM95E,8 -pysubs2/__init__.py,sha256=L71MMMS6zcAiGWdddhnc4COZmKLYX__igP0Tr5q-y3Q,783 -pysubs2/__main__.py,sha256=3iAK73rLxjLJqFMYD-jn3PDJvHZ8rIOW_NgsmBkRGhA,134 -pysubs2/cli.py,sha256=lFVZsLChtnnaRov2lNEHlecTJaoNNvTLMQmZe0-4oGE,11916 -pysubs2/common.py,sha256=y83d2egz5UwdmXkLeP67vyJ8f5xXG7WA3OFj3bbjtkU,3587 -pysubs2/exceptions.py,sha256=yOXsGhlD1IvSF74Bd5psD36SKA5vT0e_BlhzgqSlMXs,2574 -pysubs2/formats/__init__.py,sha256=Z8B_0b-gpxlXyQWtwKa_yTQuz82Cyxlua_UHeQHi69o,2758 -pysubs2/formats/base.py,sha256=zzt4L3KIC6VrUNfzwjDHSF5HV7swrdo65dop90vfy70,2959 -pysubs2/formats/jsonformat.py,sha256=whCZeJi7xbXRLqFNWQ83SzkAli2XlqYnVfs8soNOA0g,2283 -pysubs2/formats/microdvd.py,sha256=hfshagBIMMPz3WgqpttYGugtAj_6CheAiTkvTTrn3NE,5970 -pysubs2/formats/mpl2.py,sha256=vpyL5j7gxxkGvWvI8JXwFlHaEyCBHYTn_mtgk4zqNFw,1970 -pysubs2/formats/sami.py,sha256=mT5FfC7J0ZMnLePxF0-ExHZmfEuQrB8OgpryLULodbc,3310 -pysubs2/formats/subrip.py,sha256=tlR6ECFswFQI4vHTLZCwnyblUHbHisFcqHjz8Iyxm_M,7595 -pysubs2/formats/substation.py,sha256=MVa71fP6ODqEUJmhzTlfTn3zKO1MDfh4vR9Nt0i23Xg,17198 -pysubs2/formats/tmp.py,sha256=FKktjpgv07rf6EZ1Og0rkNpcj59Z6kL6dL-V0QLSKQw,4100 -pysubs2/formats/ttml.py,sha256=AG6JuJ9Ry0bjVjvk491v3OelRzG1ryLojls83QUjm-k,9382 -pysubs2/formats/webvtt.py,sha256=Ycur_E5ztjazW2akyUXTsbNUdLs4S-knq93hwompCac,1788 -pysubs2/formats/whisper.py,sha256=fwWrWxRqFeiXSeecLOv02sCcOp7htV28BaGbddMYS_Q,3344 -pysubs2/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 -pysubs2/ssaevent.py,sha256=VR0Pso779Pw5bs-Q-VpIz8Zs-A6p2a2pptQtsFVjRnM,6291 -pysubs2/ssafile.py,sha256=smMZIFk5h8n3uUuzgrcSmzdNsVU7oB3kguKQtUAdQGU,23833 -pysubs2/ssastyle.py,sha256=hlNowFmuiiwOzWTtQxKUakTwcE7Q5KxlQtl64Q6ScMU,4031 -pysubs2/time.py,sha256=Onk1vs9xXKiuwYK0RorZVU29p2FCEARapnfP_yYczTQ,4739 diff --git a/libs/pysubs2-1.8.0.dist-info/top_level.txt b/libs/pysubs2-1.8.0.dist-info/top_level.txt deleted file mode 100644 index 62e5ba64c..000000000 --- a/libs/pysubs2-1.8.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pysubs2 diff --git a/libs/pysubs2-1.8.0.dist-info/INSTALLER b/libs/pysubs2-1.8.1.dist-info/INSTALLER similarity index 100% rename from libs/pysubs2-1.8.0.dist-info/INSTALLER rename to libs/pysubs2-1.8.1.dist-info/INSTALLER diff --git a/libs/pysubs2-1.8.0.dist-info/METADATA b/libs/pysubs2-1.8.1.dist-info/METADATA similarity index 91% rename from libs/pysubs2-1.8.0.dist-info/METADATA rename to libs/pysubs2-1.8.1.dist-info/METADATA index a81fabd04..35b741b3d 100644 --- a/libs/pysubs2-1.8.0.dist-info/METADATA +++ b/libs/pysubs2-1.8.1.dist-info/METADATA @@ -1,14 +1,19 @@ Metadata-Version: 2.4 Name: pysubs2 -Version: 1.8.0 +Version: 1.8.1 Summary: A library for editing subtitle files -Home-page: https://github.com/tkarabela/pysubs2 -Author: Tomas Karabela -Author-email: tkarabela@seznam.cz -License: MIT +Project-URL: Homepage, https://github.com/tkarabela/pysubs2 Project-URL: Documentation, https://pysubs2.readthedocs.io Project-URL: Bug Tracker, https://github.com/tkarabela/pysubs2/issues -Keywords: SubStation,SubRip,MicroDVD,MPL2,WebVTT,ass,srt,sub,vtt,subtitles,captions +Author-email: Tomas Karabela +License-Expression: MIT +License-File: LICENSE.txt +Keywords: MPL2,MicroDVD,SubRip,SubStation,WebVTT,ass,captions,srt,sub,subtitles,vtt +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: End Users/Desktop +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 @@ -16,19 +21,13 @@ Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 -Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Multimedia :: Video Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Text Processing :: Markup -Classifier: Topic :: Multimedia :: Video -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: End Users/Desktop -Classifier: Operating System :: OS Independent -Classifier: License :: OSI Approved :: MIT License Classifier: Typing :: Typed Requires-Python: >=3.9 Description-Content-Type: text/markdown -License-File: LICENSE.txt -Dynamic: license-file pysubs2 ======= diff --git a/libs/pysubs2-1.8.1.dist-info/RECORD b/libs/pysubs2-1.8.1.dist-info/RECORD new file mode 100644 index 000000000..486293b03 --- /dev/null +++ b/libs/pysubs2-1.8.1.dist-info/RECORD @@ -0,0 +1,30 @@ +../../bin/pysubs2,sha256=XhbCKmkKcAcZBkfEHYiNeFggqX4T9VnjVWkgIhM5CF0,194 +pysubs2-1.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pysubs2-1.8.1.dist-info/METADATA,sha256=gTeMNs5H_KUqCjfvMPJRIPQ940wdFZTdwUnnbsKLqXs,3268 +pysubs2-1.8.1.dist-info/RECORD,, +pysubs2-1.8.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pysubs2-1.8.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +pysubs2-1.8.1.dist-info/entry_points.txt,sha256=mznmeaia9Ec19KsaIM8D4tt6V0HnOUlPhSg5nXiAfAE,49 +pysubs2-1.8.1.dist-info/licenses/LICENSE.txt,sha256=RAD0icoVUYd__4p6g69dBCS-z0ce7ddaTeVGXB6_f18,1082 +pysubs2/__init__.py,sha256=LyY1h4hW60aEpjBDxhxzVXmd2EPyOu3bxCEXdQ1xrtY,819 +pysubs2/__main__.py,sha256=AG2jO1aLwBD9Spkoqt-9CDhQguaO8HP5Chj6zJEWdNk,141 +pysubs2/cli.py,sha256=cqeitC7SA8aS8suwYQSYHISRlKM0A_8y3NBGL2LbT4c,12195 +pysubs2/common.py,sha256=JOMBwyr-If1UL_vYIdad03p4Gnmkws3TuliCJGNobEE,3780 +pysubs2/exceptions.py,sha256=MwDHRFxnSmBImOg5JqbeI0LVv3bNdMNP4UhGDF7Aw3Y,2652 +pysubs2/formats/__init__.py,sha256=lyJnYiAVpvR6HKHMtoMM1OREchwo5U6xKM-iYka2kaA,2843 +pysubs2/formats/base.py,sha256=D3PbYbLy8SP7hc4VE3UsTtXrjx4q3NO0c7pyeUW1nDk,3039 +pysubs2/formats/jsonformat.py,sha256=povq2LS98UVdRM3fRv9cIyKW7ZjLoK_OCNDRJ6NPkAE,2347 +pysubs2/formats/microdvd.py,sha256=nJrn5EmfxgxfRxPXhS6qEhieUF7oXKTsK4wQa9lGzYw,6114 +pysubs2/formats/mpl2.py,sha256=5ki5tsrv03MxXrxWx9va5SS9i708QX-aKtmgW2UAj1U,2030 +pysubs2/formats/sami.py,sha256=ck2BJO7rrBwtnJyrDR32NpG8Op6bAyPrLK2Jt0Qsak4,3400 +pysubs2/formats/subrip.py,sha256=ps_alc02_bBWEGfAIQ5nzCGT4VNBLn-Xhkeaqr5mTtE,7761 +pysubs2/formats/substation.py,sha256=CSZyRSPMtKqYiEWIKWixfXE7lYOk6sAhdrNCg7twMJo,17585 +pysubs2/formats/tmp.py,sha256=l-pFPJkxjWKe8oO_EDj-gyRXQjdRpxS8eCtWu5PWlB8,4212 +pysubs2/formats/ttml.py,sha256=v0QuGlj3Bf2vNgVPlSYzyF6r8uZdWHcVcmO89xsl1Hw,10614 +pysubs2/formats/webvtt.py,sha256=qtfCKZvw9h9oOKx3xafczfVr4yWebiL9eo-51R0IWY0,1836 +pysubs2/formats/whisper.py,sha256=GDUN4N1M3Bispl1f7JjyRgaYLjwSk5qG78-e8wpZBg0,3438 +pysubs2/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2 +pysubs2/ssaevent.py,sha256=yorWQu-I11XPTRrgjZakNGFpe6isL0PzVjGLPbkruvY,6669 +pysubs2/ssafile.py,sha256=Rx1K9xwDNV9m9L7b7tVubXCfiEl14LSLbmUywPU_LnA,24758 +pysubs2/ssastyle.py,sha256=YAOQhQteVFq9Gfu4De21XhCwygNOT4oRaSdHardJSGw,4519 +pysubs2/time.py,sha256=ysmFLrnfHOLGnSOn8-Hft8NRCRTIGPWtCNhcxm_Rv1c,4917 diff --git a/libs/pysubs2-1.8.0.dist-info/REQUESTED b/libs/pysubs2-1.8.1.dist-info/REQUESTED similarity index 100% rename from libs/pysubs2-1.8.0.dist-info/REQUESTED rename to libs/pysubs2-1.8.1.dist-info/REQUESTED diff --git a/libs/pysubs2-1.8.1.dist-info/WHEEL b/libs/pysubs2-1.8.1.dist-info/WHEEL new file mode 100644 index 000000000..bd8422674 --- /dev/null +++ b/libs/pysubs2-1.8.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/pysubs2-1.8.0.dist-info/entry_points.txt b/libs/pysubs2-1.8.1.dist-info/entry_points.txt similarity index 100% rename from libs/pysubs2-1.8.0.dist-info/entry_points.txt rename to libs/pysubs2-1.8.1.dist-info/entry_points.txt diff --git a/libs/pysubs2-1.8.0.dist-info/licenses/LICENSE.txt b/libs/pysubs2-1.8.1.dist-info/licenses/LICENSE.txt similarity index 96% rename from libs/pysubs2-1.8.0.dist-info/licenses/LICENSE.txt rename to libs/pysubs2-1.8.1.dist-info/licenses/LICENSE.txt index b29a6c1d5..dc62e8914 100644 --- a/libs/pysubs2-1.8.0.dist-info/licenses/LICENSE.txt +++ b/libs/pysubs2-1.8.1.dist-info/licenses/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2014-2024 Tomas Karabela +Copyright (c) 2014-2026 Tomas Karabela Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/libs/pysubs2/cli.py b/libs/pysubs2/cli.py index 14dc6766d..9083b3d5e 100644 --- a/libs/pysubs2/cli.py +++ b/libs/pysubs2/cli.py @@ -1,12 +1,11 @@ import argparse import codecs -import os import re -import os.path as op +from pathlib import Path from io import TextIOWrapper import sys from textwrap import dedent -from typing import List +from typing import Any, Optional from .formats import get_file_extension, FORMAT_IDENTIFIERS from .time import make_time @@ -37,11 +36,6 @@ def time(s: str) -> int: return make_time(**d) # type: ignore # Argument 1 has incomp. type "**Dict[Any, float]"; expected "Optional[int]" -def change_ext(path: str, ext: str) -> str: - base, _ = op.splitext(path) - return base + ext - - class Pysubs2CLI: def __init__(self) -> None: parser = self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, @@ -60,7 +54,7 @@ class Pysubs2CLI: python -m pysubs2 --shift-back 0.3s --output-dir retimed *.srt python -m pysubs2 --transform-framerate 25 23.976 *.srt""")) - parser.add_argument("files", nargs="*", metavar="FILE", + parser.add_argument("files", nargs="*", metavar="FILE", type=Path, help="Input subtitle files. Can be in SubStation Alpha (*.ass, *.ssa), SubRip (*.srt), " "MicroDVD (*.sub) or other supported format. When no files are specified, " "pysubs2 will work as a pipe, reading from standard input and writing to standard output.") @@ -91,7 +85,7 @@ class Pysubs2CLI: help="This argument specifies framerate for MicroDVD files. By default, framerate " "is detected from the file. Use this when framerate specification is missing " "or to force different framerate.") - parser.add_argument("-o", "--output-dir", metavar="DIR", + parser.add_argument("-o", "--output-dir", metavar="DIR", type=Path, help="Use this to save all files to given directory. By default, every file is saved to its parent directory, " "ie. unless it's being saved in different subtitle format (and thus with different file extension), " "it overwrites the original file.") @@ -123,28 +117,30 @@ class Pysubs2CLI: extra_sub_options.add_argument("--sub-no-write-fps-declaration", action="store_true", help="(output) omit writing FPS as first zero-length subtitle") - def __call__(self, argv: List[str]) -> int: + def __call__(self, argv: list[str]) -> int: try: return self.main(argv) except KeyboardInterrupt: print("\nAborted by user.", file=sys.stderr) return 1 - def main(self, argv: List[str]) -> int: + def main(self, argv: list[str]) -> int: args = self.parser.parse_args(argv) errors = 0 if args.verbose: logging.basicConfig(level=logging.DEBUG) + + output_dir: Optional[Path] = args.output_dir - if args.output_dir and not op.exists(args.output_dir): - os.makedirs(args.output_dir) + if output_dir is not None and not output_dir.exists(): + output_dir.mkdir(parents=True) if args.output_enc is None: args.output_enc = args.input_enc - extra_input_args = {} - extra_output_args = {} + extra_input_args: dict[str, Any] = {} + extra_output_args: dict[str, Any] = {} if args.srt_keep_unknown_html_tags: extra_input_args["keep_unknown_html_tags"] = True if args.srt_keep_html_tags: @@ -156,35 +152,36 @@ class Pysubs2CLI: logging.debug("Extra arguments to SSAFile.from_file(): %r", extra_input_args) logging.debug("Extra arguments to SSAFile.to_file(): %r", extra_output_args) - if args.files: - for path in args.files: - if not op.exists(path): - print("Skipping", path, "(does not exist)") + input_paths: list[Path] = args.files + if input_paths: + for inpath in input_paths: + if not inpath.exists(): + print(f"Skipping {inpath} (does not exist)") errors += 1 - elif not op.isfile(path): - print("Skipping", path, "(not a file)") + elif not inpath.is_file(): + print(f"Skipping {inpath} (not a file)") errors += 1 else: - with open(path, encoding=args.input_enc, errors=args.enc_error_handling) as infile: + with inpath.open("r", encoding=args.input_enc, errors=args.enc_error_handling) as infile: subs = SSAFile.from_file(infile, args.input_format, args.fps, **extra_input_args) self.process(subs, args) if args.output_format is None: - outpath = path + outpath = inpath output_format = subs.format assert output_format is not None, "subs.format must not be None (it was read from file)" else: ext = get_file_extension(args.output_format) - outpath = change_ext(path, ext) + outpath = inpath.with_suffix(ext) output_format = args.output_format assert output_format is not None, "args.output_format must not be None (see if/else)" - if args.output_dir is not None: - _, filename = op.split(outpath) - outpath = op.join(args.output_dir, filename) + if output_dir is not None: + filename = outpath.name + outpath = output_dir / filename - with open(outpath, "w", encoding=args.output_enc, errors=args.enc_error_handling) as outfile: + with outpath.open("w", encoding=args.output_enc, errors=args.enc_error_handling) as outfile: subs.to_file(outfile, output_format, args.fps, apply_styles=not args.clean, **extra_output_args) elif not sys.stdin.isatty(): diff --git a/libs/pysubs2/common.py b/libs/pysubs2/common.py index 9b896eb85..4321ad26d 100644 --- a/libs/pysubs2/common.py +++ b/libs/pysubs2/common.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from typing import Tuple, Union, Optional, Dict, Iterable, Iterator +from os import PathLike +from typing import Union, Optional, Iterable, Iterator, Any from enum import IntEnum import xml.etree.ElementTree as ET from contextlib import contextmanager @@ -56,14 +57,15 @@ class Alignment(IntEnum): return SSA_ALIGNMENT[self.value - 1] -SSA_ALIGNMENT: Tuple[int, ...] = (1, 2, 3, 9, 10, 11, 5, 6, 7) +SSA_ALIGNMENT: tuple[int, ...] = (1, 2, 3, 9, 10, 11, 5, 6, 7) #: Version of the pysubs2 library. -VERSION = "1.8.0" +VERSION = "1.8.1" IntOrFloat = Union[int, float] +PathOrStr = Union[str, PathLike[Any]] def etree_iter_child_nodes(elem: ET.Element) -> Iterator[Union[ET.Element, str]]: @@ -118,7 +120,7 @@ def etree_register_namespace_override() -> Iterator[None]: Workaround for poor namespace handling in ``xml.etree.ElementTree``. """ - namespace_map: Optional[Dict[str, str]] = None + namespace_map: Optional[dict[str, str]] = None namespace_map_original_content = {} try: namespace_map = getattr(ET.register_namespace, "_namespace_map", None) diff --git a/libs/pysubs2/exceptions.py b/libs/pysubs2/exceptions.py index eb08c3280..936e2c5f9 100644 --- a/libs/pysubs2/exceptions.py +++ b/libs/pysubs2/exceptions.py @@ -1,4 +1,4 @@ -from typing import List, Any +from typing import Any __all__ = [ "Pysubs2Error", @@ -71,7 +71,7 @@ class FormatAutodetectionError(Pysubs2Error): content (str): Analyzed subtitle file content formats (list[str]): Format identifiers for detected formats """ - def __init__(self, content: str, formats: List[str]) -> None: + def __init__(self, content: str, formats: list[str]) -> None: self.content = content self.formats = formats if not formats: diff --git a/libs/pysubs2/formats/__init__.py b/libs/pysubs2/formats/__init__.py index 36b87e03f..ad545ef5c 100644 --- a/libs/pysubs2/formats/__init__.py +++ b/libs/pysubs2/formats/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, Type +from typing import Type from .base import FormatBase from .microdvd import MicroDVDFormat @@ -13,8 +13,9 @@ from .webvtt import WebVTTFormat from .whisper import WhisperJAXFormat from ..exceptions import UnknownFormatIdentifierError, UnknownFileExtensionError, FormatAutodetectionError + #: Dict mapping file extensions to format identifiers. -FILE_EXTENSION_TO_FORMAT_IDENTIFIER: Dict[str, str] = { +FILE_EXTENSION_TO_FORMAT_IDENTIFIER: dict[str, str] = { ".srt": "srt", ".ass": "ass", ".ssa": "ssa", @@ -28,7 +29,7 @@ FILE_EXTENSION_TO_FORMAT_IDENTIFIER: Dict[str, str] = { } #: Dict mapping format identifiers to implementations (FormatBase subclasses). -FORMAT_IDENTIFIER_TO_FORMAT_CLASS: Dict[str, Type[FormatBase]] = { +FORMAT_IDENTIFIER_TO_FORMAT_CLASS: dict[str, Type[FormatBase]] = { "srt": SubripFormat, "ass": SubstationFormat, "ssa": SubstationFormat, diff --git a/libs/pysubs2/formats/sami.py b/libs/pysubs2/formats/sami.py index 4362086b1..bb73027f4 100644 --- a/libs/pysubs2/formats/sami.py +++ b/libs/pysubs2/formats/sami.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from html.parser import HTMLParser -from typing import List, Optional, TextIO, Any, Tuple +from typing import Optional, TextIO, Any from .base import FormatBase from ..ssaevent import SSAEvent @@ -67,7 +67,7 @@ class SyncElement: class SAMIParser(HTMLParser): def __init__(self) -> None: super().__init__() - self.sync_elements: List[SyncElement] = [] + self.sync_elements: list[SyncElement] = [] self.current_sync_element: Optional[SyncElement] = None def begin_sync_element(self, start_ms: int) -> None: @@ -84,7 +84,7 @@ class SAMIParser(HTMLParser): if self.current_sync_element is not None: self.current_sync_element.text += text - def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None: + def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None: if tag == "sync": start_ms = int(dict(attrs)["start"] or 0) self.begin_sync_element(start_ms) diff --git a/libs/pysubs2/formats/subrip.py b/libs/pysubs2/formats/subrip.py index 0acbd9682..7d9385bb8 100644 --- a/libs/pysubs2/formats/subrip.py +++ b/libs/pysubs2/formats/subrip.py @@ -1,6 +1,6 @@ import re import warnings -from typing import List, Sequence, Optional, TextIO, Any, Tuple +from typing import Sequence, Optional, TextIO, Any from .base import FormatBase from ..ssaevent import SSAEvent @@ -77,8 +77,8 @@ class SubripFormat(FormatBase): If False, these other HTML tags will be stripped from output (in the previous example, you would get only ``example {\\i1}text{\\i0}``). """ - timestamps: List[Tuple[int, int]] = [] # (start, end) - following_lines: List[List[str]] = [] # contains lists of lines following each timestamp + timestamps: list[tuple[int, int]] = [] # (start, end) + following_lines: list[list[str]] = [] # contains lists of lines following each timestamp for line in fp: stamps = cls.TIMESTAMP.findall(line) @@ -90,7 +90,7 @@ class SubripFormat(FormatBase): if timestamps: following_lines[-1].append(line) - def prepare_text(lines: List[str]) -> str: + def prepare_text(lines: list[str]) -> str: # Handle the "happy" empty subtitle case, which is timestamp line followed by blank line(s) # followed by number line and timestamp line of the next subtitle. Fixes issue #11. if (len(lines) >= 2 @@ -175,5 +175,5 @@ class SubripFormat(FormatBase): lineno += 1 @classmethod - def _get_visible_lines(cls, subs: "SSAFile") -> List[SSAEvent]: + def _get_visible_lines(cls, subs: "SSAFile") -> list[SSAEvent]: return subs.get_text_events() diff --git a/libs/pysubs2/formats/substation.py b/libs/pysubs2/formats/substation.py index c9e1078ca..6464a830c 100644 --- a/libs/pysubs2/formats/substation.py +++ b/libs/pysubs2/formats/substation.py @@ -1,7 +1,7 @@ import logging import re import warnings -from typing import Any, Union, Optional, Dict, Tuple, List, TextIO +from typing import Any, Union, Optional, TextIO from .base import FormatBase from ..ssaevent import SSAEvent @@ -86,8 +86,8 @@ def is_valid_field_content(s: str) -> bool: def parse_tags(text: str, style: SSAStyle = SSAStyle.DEFAULT_STYLE, - styles: Optional[Dict[str, SSAStyle]] = None, - skip_empty_fragments: bool = False) -> List[Tuple[str, SSAStyle]]: + styles: Optional[dict[str, SSAStyle]] = None, + skip_empty_fragments: bool = False) -> list[tuple[str, SSAStyle]]: """ Split text into fragments with computed SSAStyles. diff --git a/libs/pysubs2/formats/tmp.py b/libs/pysubs2/formats/tmp.py index e9db38a39..e3e0b7bfd 100644 --- a/libs/pysubs2/formats/tmp.py +++ b/libs/pysubs2/formats/tmp.py @@ -104,7 +104,7 @@ class TmpFormat(FormatBase): fragment = f"{fragment}" body.append(fragment) - return re.sub("\n+", "\n", "".join(body).strip()) + return re.sub("\n+", "|", "".join(body).strip()) for line in subs.get_text_events(): start = cls.ms_to_timestamp(line.start) diff --git a/libs/pysubs2/formats/ttml.py b/libs/pysubs2/formats/ttml.py index fa36e689a..2e6b617ce 100644 --- a/libs/pysubs2/formats/ttml.py +++ b/libs/pysubs2/formats/ttml.py @@ -1,6 +1,6 @@ import re from enum import Enum -from typing import Optional, TextIO, Any, Dict, List, Union +from typing import Optional, TextIO, Any, Union import xml.etree.ElementTree as ET from .base import FormatBase @@ -42,11 +42,11 @@ class TTMLFormat(FormatBase): return make_time(**{metric: count}) # type: ignore[arg-type] # clock-time (not supported: frames, subframes) - m = re.fullmatch(r"(\d{2,}):(\d{2}):(\d{2}(?:\.\d+)?)", expr) + m = re.fullmatch(r"(((?P\d+):)?(?P\d{1,2}):)?(?P\d{2}(?:\.\d+)?)", expr) if m is not None: - hours = int(m.group(1)) - minutes = int(m.group(2)) - seconds = int(m.group(3)) if m.group(3).isnumeric() else float(m.group(3)) + hours = int(m.group("h")) if m.group("h") else 0 + minutes = int(m.group("m")) if m.group("m") else 0 + seconds = int(m.group("s")) if m.group("s").isnumeric() else float(m.group("s")) return make_time(h=hours, m=minutes, s=seconds) raise NotImplementedError(f"Unsupported time expression: {expr}") @@ -60,11 +60,25 @@ class TTMLFormat(FormatBase): return None @classmethod - def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: + def from_file( + cls, + subs: "SSAFile", + fp: TextIO, + format_: str, + ignore_par_time_offset: bool = False, + **kwargs: Any + ) -> None: """ Rudimentary TTML parser. No formatting/styling apart from newlines is supported. Frame-based time expressions are not supported. + Keyword Args: + ignore_par_time_offset: Interpret all "begin" and "end" attributes of ``

`` elements + as absolute times, instead of relative to their parent (note that we only support "par" + time containers, for which this logic should be applied). This violates TTML1 spec + (https://www.w3.org/TR/2018/REC-ttml1-20181108/#timing-attribute-timeContainer), + but is used in Apple Music lyrics. + """ tree = ET.parse(fp) root = tree.getroot() @@ -73,61 +87,8 @@ class TTMLFormat(FormatBase): if body_elem is None: return - cls._parse_body(subs, body_elem) - - @classmethod - def _parse_body(cls, subs: "SSAFile", body_elem: ET.Element) -> None: - begin_ms = cls.timestamp_to_ms(body_elem.attrib.get("begin", "0s")) - time_container = TimeContainer(body_elem.attrib.get("timeContainer", "par")) - if time_container != TimeContainer.PAR: - raise NotImplementedError("Only 'par' timeContainer is supported") - - for div_elem in body_elem.iterfind(f"{TT_NS}div"): - cls._parse_div(subs, div_elem, begin_ms) - - @classmethod - def _parse_div(cls, subs: "SSAFile", div_elem: ET.Element, parent_begin_ms: int) -> None: - begin_ms = cls.timestamp_to_ms(div_elem.attrib.get("begin", "0s")) + parent_begin_ms - time_container = TimeContainer(div_elem.attrib.get("timeContainer", "par")) - if time_container != TimeContainer.PAR: - raise NotImplementedError("Only 'par' timeContainer is supported") - - for p_elem in div_elem.iterfind(f"{TT_NS}p"): - cls._parse_p(subs, p_elem, begin_ms) - - @classmethod - def _parse_p(cls, subs: "SSAFile", p_elem: ET.Element, parent_begin_ms: int) -> None: - begin_ms = cls.timestamp_to_ms(p_elem.attrib.get("begin", "0s")) + parent_begin_ms - if "duration" in p_elem.attrib: - end_ms = begin_ms + cls.timestamp_to_ms(p_elem.attrib["duration"]) - else: - end_ms = cls.timestamp_to_ms(p_elem.attrib["end"]) + parent_begin_ms - time_container = TimeContainer(p_elem.attrib.get("timeContainer", "par")) - if time_container != TimeContainer.PAR: - raise NotImplementedError("Only 'par' timeContainer is supported") - - event = SSAEvent(start=begin_ms, end=end_ms) - subs.events.append(event) - - for node in etree_iter_child_nodes(p_elem): - if isinstance(node, str): - event.text += node.strip().replace("\n", " ") - else: - if node.tag == f"{TT_NS}br": - event.text += "\\N" - elif node.tag == f"{TT_NS}span": - cls._parse_span(event, node) - - @classmethod - def _parse_span(cls, event: SSAEvent, span_elem: ET.Element) -> None: - for node in etree_iter_child_nodes(span_elem): - if isinstance(node, str): - event.text += node.strip().replace("\n", " ") - else: - if node.tag == f"{TT_NS}br": - event.text += "\\N" - elif node.tag == f"{TT_NS}span": - cls._parse_span(event, node) + parser = _TTMLParser(subs, ignore_par_time_offset) + parser.parse_body(body_elem) @classmethod def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: @@ -189,7 +150,7 @@ class TTMLFormat(FormatBase): print(output_xml, file=fp) @classmethod - def ssastyle_to_tts(cls, style: SSAStyle, base_style: Optional[SSAStyle] = None) -> Dict[str, str]: + def ssastyle_to_tts(cls, style: SSAStyle, base_style: Optional[SSAStyle] = None) -> dict[str, str]: """ Convert `SSAStyle` (or its difference to base style) into dictionary of XML attributes @@ -225,10 +186,72 @@ class TTMLFormat(FormatBase): def _append_text(cls, elem: ET.Element, text: str) -> None: text = text.replace("\\h", " ") chunks = re.split(r"\\[Nn]", text) - nodes: List[Union[ET.Element, str]] = [] + nodes: list[Union[ET.Element, str]] = [] for i, chunk in enumerate(chunks): if i > 0: nodes.append(ET.Element(f"{TT_NS}br")) nodes.append(chunk) etree_append_child_nodes(elem, nodes) + + +class _TTMLParser: + def __init__(self, subs: "SSAFile", ignore_par_time_offset: bool = False) -> None: + self.subs = subs + self.ignore_par_time_offset = ignore_par_time_offset + + def parse_body(self, body_elem: ET.Element) -> None: + begin_ms = TTMLFormat.timestamp_to_ms(body_elem.attrib.get("begin", "0s")) + time_container = TimeContainer(body_elem.attrib.get("timeContainer", "par")) + if time_container != TimeContainer.PAR: + raise NotImplementedError("Only 'par' timeContainer is supported") + + for div_elem in body_elem.iterfind(f"{TT_NS}div"): + self.parse_div(div_elem, begin_ms) + + def parse_div(self, div_elem: ET.Element, parent_begin_ms: int) -> None: + begin_ms = TTMLFormat.timestamp_to_ms(div_elem.attrib.get("begin", "0s")) + parent_begin_ms + time_container = TimeContainer(div_elem.attrib.get("timeContainer", "par")) + if time_container != TimeContainer.PAR: + raise NotImplementedError("Only 'par' timeContainer is supported") + + for p_elem in div_elem.iterfind(f"{TT_NS}p"): + self.parse_p(p_elem, begin_ms) + + def parse_p(self, p_elem: ET.Element, parent_begin_ms: int) -> None: + if self.ignore_par_time_offset: + parent_begin_ms = 0 + + begin_ms = TTMLFormat.timestamp_to_ms(p_elem.attrib.get("begin", "0s")) + parent_begin_ms + if "duration" in p_elem.attrib: + end_ms = begin_ms + TTMLFormat.timestamp_to_ms(p_elem.attrib["duration"]) + else: + end_ms = TTMLFormat.timestamp_to_ms(p_elem.attrib["end"]) + parent_begin_ms + time_container = TimeContainer(p_elem.attrib.get("timeContainer", "par")) + if time_container != TimeContainer.PAR: + raise NotImplementedError("Only 'par' timeContainer is supported") + + event = SSAEvent(start=begin_ms, end=end_ms) + self.subs.events.append(event) + + for node in etree_iter_child_nodes(p_elem): + if isinstance(node, str): + event.text += re.sub(r"\s+", " ", node) + else: + if node.tag == f"{TT_NS}br": + event.text += "\\N" + elif node.tag == f"{TT_NS}span": + self.parse_span(event, node) + + event.text = event.text.strip() + event.text = re.sub(r"\s*\\N\s*", r"\\N", event.text) + + def parse_span(self, event: SSAEvent, span_elem: ET.Element) -> None: + for node in etree_iter_child_nodes(span_elem): + if isinstance(node, str): + event.text += re.sub(r"\s+", " ", node) + else: + if node.tag == f"{TT_NS}br": + event.text += "\\N" + elif node.tag == f"{TT_NS}span": + self.parse_span(event, node) diff --git a/libs/pysubs2/formats/webvtt.py b/libs/pysubs2/formats/webvtt.py index 88f088a0a..fce6b3e7c 100644 --- a/libs/pysubs2/formats/webvtt.py +++ b/libs/pysubs2/formats/webvtt.py @@ -1,5 +1,5 @@ import re -from typing import List, Sequence, Optional, TextIO, Any +from typing import Sequence, Optional, TextIO, Any from ..ssaevent import SSAEvent from .subrip import SubripFormat @@ -48,7 +48,7 @@ class WebVTTFormat(SubripFormat): subs=subs, fp=fp, format_=format_, **kwargs) @classmethod - def _get_visible_lines(cls, subs: "SSAFile") -> List[SSAEvent]: + def _get_visible_lines(cls, subs: "SSAFile") -> list[SSAEvent]: visible_lines = super()._get_visible_lines(subs) visible_lines.sort(key=lambda e: e.start) return visible_lines diff --git a/libs/pysubs2/formats/whisper.py b/libs/pysubs2/formats/whisper.py index 7fac1f3aa..c8ed4dfd0 100644 --- a/libs/pysubs2/formats/whisper.py +++ b/libs/pysubs2/formats/whisper.py @@ -8,11 +8,11 @@ from .base import FormatBase from ..ssaevent import SSAEvent from ..ssafile import SSAFile from ..time import make_time, timestamp_to_ms -from typing import Union, List, Dict, Any, Optional, Sequence, TextIO +from typing import Union, Any, Optional, Sequence, TextIO import re -def load_from_whisper(result_or_segments: Union[Dict[str, Any], List[Dict[str, Any]]]) -> SSAFile: +def load_from_whisper(result_or_segments: Union[dict[str, Any], list[dict[str, Any]]]) -> SSAFile: """ Load subtitle file from OpenAI Whisper transcript diff --git a/libs/pysubs2/ssaevent.py b/libs/pysubs2/ssaevent.py index a33d2e308..4a8517309 100644 --- a/libs/pysubs2/ssaevent.py +++ b/libs/pysubs2/ssaevent.py @@ -1,6 +1,6 @@ import re import warnings -from typing import Optional, Dict, Any, ClassVar, FrozenSet +from typing import Optional, Any, ClassVar, Literal import dataclasses from .common import IntOrFloat @@ -24,24 +24,38 @@ class SSAEvent: >>> ev = SSAEvent(start=make_time(s=1), end=make_time(s=2.5), text="Hello World!") - """ - OVERRIDE_SEQUENCE: ClassVar = re.compile(r"{[^}]*}") + Attributes: + start: Subtitle start time (in milliseconds) + end: Subtitle end time (in milliseconds) + text: Text of subtitle (with SubStation override tags) + marked: (SSA only) + layer: Layer number, 0 is the lowest layer (ASS only) + style: Style name + name: Actor name + marginl: Left margin + marginr: Right margin + marginv: Vertical margin + effect: Line effect + type: Line type (Dialogue/Comment) - start: int = 0 #: Subtitle start time (in milliseconds) - end: int = 10000 #: Subtitle end time (in milliseconds) - text: str = "" #: Text of subtitle (with SubStation override tags) - marked: bool = False #: (SSA only) - layer: int = 0 #: Layer number, 0 is the lowest layer (ASS only) - style: str = "Default" #: Style name - name: str = "" #: Actor name - marginl: int = 0 #: Left margin - marginr: int = 0 #: Right margin - marginv: int = 0 #: Vertical margin - effect: str = "" #: Line effect - type: str = "Dialogue" #: Line type (Dialogue/Comment) + """ + OVERRIDE_SEQUENCE: ClassVar[re.Pattern[str]] = re.compile(r"{[^}]*}") + + start: int = 0 + end: int = 10000 + text: str = "" + marked: bool = False + layer: int = 0 + style: str = "Default" + name: str = "" + marginl: int = 0 + marginr: int = 0 + marginv: int = 0 + effect: str = "" + type: Literal["Dialogue", "Comment"] = "Dialogue" @property - def FIELDS(self) -> FrozenSet[str]: + def FIELDS(self) -> frozenset[str]: """All fields in SSAEvent.""" warnings.warn("Deprecated in 1.2.0 - it's a dataclass now", DeprecationWarning) return frozenset(field.name for field in dataclasses.fields(self)) @@ -130,7 +144,7 @@ class SSAEvent: """Return a copy of the SSAEvent.""" return SSAEvent(**self.as_dict()) - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: # dataclasses.asdict() would recursively dictify Color objects, which we don't want return {field.name: getattr(self, field.name) for field in dataclasses.fields(self)} diff --git a/libs/pysubs2/ssafile.py b/libs/pysubs2/ssafile.py index bda20b8c3..5aac97489 100644 --- a/libs/pysubs2/ssafile.py +++ b/libs/pysubs2/ssafile.py @@ -1,10 +1,10 @@ import io from itertools import chain -import os.path +from pathlib import Path import logging -from typing import Optional, List, Dict, Iterable, Any, overload, Iterator, TextIO, Tuple, MutableSequence +from typing import Optional, Iterable, Any, overload, Iterator, TextIO, MutableSequence -from .common import IntOrFloat +from .common import IntOrFloat, PathOrStr from .ssaevent import SSAEvent from .ssastyle import SSAStyle from .time import make_time, ms_to_str @@ -26,30 +26,48 @@ class SSAFile(MutableSequence[SSAEvent]): del subs[0] + Attributes: + events: List of :class:`SSAEvent` instances, ie. individual subtitles. + styles: Dict of :class:`SSAStyle` instances. + info: Dict with script metadata, ie. ``[Script Info]``. + aegisub_project: Dict with Aegisub project, ie. ``[Aegisub Project Garbage]``. + fonts_opaque: Dict with embedded fonts, ie. ``[Fonts]``. + graphics_opaque: Dict with embedded images, ie. ``[Graphics]``. + fps: Framerate used when reading the file, if applicable. + format: Format of source subtitle file, if applicable, eg. ``"srt"``. + """ - DEFAULT_INFO: Dict[str, str] = { + DEFAULT_INFO: dict[str, str] = { "WrapStyle": "0", "ScaledBorderAndShadow": "yes", "Collisions": "Normal" } + events: list[SSAEvent] + styles: dict[str, SSAStyle] + info: dict[str, str] + aegisub_project: dict[str, str] + fonts_opaque: dict[str, Any] + graphics_opaque: dict[str, Any] + fps: Optional[float] + format: Optional[str] def __init__(self) -> None: - self.events: List[SSAEvent] = [] #: List of :class:`SSAEvent` instances, ie. individual subtitles. - self.styles: Dict[str, SSAStyle] = {"Default": SSAStyle.DEFAULT_STYLE.copy()} #: Dict of :class:`SSAStyle` instances. - self.info: Dict[str, str] = self.DEFAULT_INFO.copy() #: Dict with script metadata, ie. ``[Script Info]``. - self.aegisub_project: Dict[str, str] = {} #: Dict with Aegisub project, ie. ``[Aegisub Project Garbage]``. - self.fonts_opaque: Dict[str, Any] = {} #: Dict with embedded fonts, ie. ``[Fonts]``. - self.graphics_opaque: Dict[str, Any] = {} #: Dict with embedded images, ie. ``[Graphics]``. - self.fps: Optional[float] = None #: Framerate used when reading the file, if applicable. - self.format: Optional[str] = None #: Format of source subtitle file, if applicable, eg. ``"srt"``. + self.events = [] + self.styles = {"Default": SSAStyle.DEFAULT_STYLE.copy()} + self.info = self.DEFAULT_INFO.copy() + self.aegisub_project = {} + self.fonts_opaque = {} + self.graphics_opaque = {} + self.fps = None + self.format = None # ------------------------------------------------------------------------ # I/O methods # ------------------------------------------------------------------------ @classmethod - def load(cls, path: str, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None, + def load(cls, path: PathOrStr, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None, errors: Optional[str] = None, **kwargs: Any) -> "SSAFile": """ Load subtitle file from given path. @@ -62,7 +80,7 @@ class SSAFile(MutableSequence[SSAEvent]): (eg. :meth:`pysubs2.formats.subrip.SubripFormat.from_file()`) Arguments: - path (str): Path to subtitle file. + path (Path | str): Path to subtitle file. encoding (str): Character encoding of input file. Defaults to UTF-8, you may need to change this. errors (Optional[str]): Error handling for character encoding @@ -104,11 +122,11 @@ class SSAFile(MutableSequence[SSAEvent]): Example: >>> subs1 = pysubs2.load("subrip-subtitles.srt") - >>> subs2 = pysubs2.load("microdvd-subtitles.sub",fps=23.976) - >>> subs3 = pysubs2.load("subrip-subtitles-with-fancy-tags.srt",keep_unknown_html_tags=True) + >>> subs2 = pysubs2.load("microdvd-subtitles.sub", fps=23.976) + >>> subs3 = pysubs2.load("subrip-subtitles-with-fancy-tags.srt", keep_unknown_html_tags=True) """ - with open(path, encoding=encoding, errors=errors) as fp: + with Path(path).open(encoding=encoding, errors=errors) as fp: return cls.from_file(fp, format_, fps=fps, **kwargs) @classmethod @@ -191,7 +209,7 @@ class SSAFile(MutableSequence[SSAEvent]): impl.from_file(subs, fp, format_, fps=fps, **kwargs) return subs - def save(self, path: str, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None, + def save(self, path: PathOrStr, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None, errors: Optional[str] = None, **kwargs: Any) -> None: """ Save subtitle file to given path. @@ -204,7 +222,7 @@ class SSAFile(MutableSequence[SSAEvent]): (eg. :meth:`pysubs2.formats.subrip.SubripFormat.to_file()`) Arguments: - path (str): Path to subtitle file. + path (Path | str): Path to subtitle file. encoding (str): Character encoding of output file. Defaults to UTF-8, which should be fine for most purposes. format_ (str): Optional, specifies desired subtitle format @@ -240,11 +258,12 @@ class SSAFile(MutableSequence[SSAEvent]): pysubs2.exceptions.UnknownFileExtensionError """ + outpath = Path(path) if format_ is None: - ext = os.path.splitext(path)[1].lower() + ext = outpath.suffix.lower() format_ = get_format_identifier(ext) - with open(path, "w", encoding=encoding, errors=errors) as fp: + with outpath.open("w", encoding=encoding, errors=errors) as fp: self.to_file(fp, format_, fps=fps, **kwargs) def to_string(self, format_: str, fps: Optional[float] = None, **kwargs: Any) -> str: @@ -309,9 +328,9 @@ class SSAFile(MutableSequence[SSAEvent]): line.start += delta line.end += delta - def transform_framerate(self, in_fps: float, out_fps: float, keep_duration: bool = False) -> None: + def transform_framerate(self, in_fps: float, out_fps: float) -> None: """ - Rescale all timestamps by ratio of in_fps/out_fps. Force keep duration/display time of subtitles. + Rescale all timestamps by ratio of in_fps/out_fps. Can be used to fix files converted from frame-based to time-based with wrongly assumed framerate. @@ -319,7 +338,6 @@ class SSAFile(MutableSequence[SSAEvent]): Arguments: in_fps (float) out_fps (float) - keep_duration (bool) Raises: ValueError: Non-positive framerate given. @@ -330,14 +348,8 @@ class SSAFile(MutableSequence[SSAEvent]): ratio = in_fps / out_fps for line in self: - duration = line.duration line.start = int(round(line.start * ratio)) - if keep_duration: - line.duration = duration - else: - line.end = int(round(line.end * ratio)) - - + line.end = int(round(line.end * ratio)) # ------------------------------------------------------------------------ # Working with styles @@ -405,10 +417,11 @@ class SSAFile(MutableSequence[SSAEvent]): - Less than two characters of text - Duplicated text with identical time interval (only the first event is kept) """ - new_events = [] + new_events: list[SSAEvent] = [] + + duplicate_text_ids: set[int] = set() + times_to_texts: dict[tuple[int, int], list[str]] = {} - duplicate_text_ids = set() - times_to_texts: Dict[Tuple[int, int], List[str]] = {} for i, e in enumerate(self): tmp = times_to_texts.setdefault((e.start, e.end), []) if tmp.count(e.plaintext) > 0: @@ -427,7 +440,7 @@ class SSAFile(MutableSequence[SSAEvent]): self.events = new_events - def get_text_events(self) -> List[SSAEvent]: + def get_text_events(self) -> list[SSAEvent]: """ Return list of events excluding SSA comment lines and lines with SSA drawing tags """ @@ -538,7 +551,7 @@ class SSAFile(MutableSequence[SSAEvent]): pass @overload - def __getitem__(self, s: slice) -> List[SSAEvent]: + def __getitem__(self, s: slice) -> list[SSAEvent]: pass def __getitem__(self, item: Any) -> Any: diff --git a/libs/pysubs2/ssastyle.py b/libs/pysubs2/ssastyle.py index b0df57106..d7e91988a 100644 --- a/libs/pysubs2/ssastyle.py +++ b/libs/pysubs2/ssastyle.py @@ -1,9 +1,10 @@ import warnings -from typing import Dict, Any, ClassVar, FrozenSet +from typing import Any, ClassVar import dataclasses from .common import Color, Alignment + @dataclasses.dataclass(repr=False) class SSAStyle: """ @@ -19,50 +20,79 @@ class SSAStyle: This class defines equality (equality of all fields). + Attributes: + fontname: Font name + fontsize: Font size (in pixels) + primarycolor: Primary color (:class:`pysubs2.Color` instance) + secondarycolor: Secondary color (:class:`pysubs2.Color` instance) + tertiarycolor: Tertiary color (:class:`pysubs2.Color` instance) + outlinecolor: Outline color (:class:`pysubs2.Color` instance) + backcolor: Back, ie. shadow color (:class:`pysubs2.Color` instance) + bold: Bold + italic: Italic + underline: Underline (ASS only) + strikeout: Strikeout (ASS only) + scalex: Horizontal scaling (ASS only) + scaley: Vertical scaling (ASS only) + spacing: Letter spacing (ASS only) + angle: Rotation (ASS only) + borderstyle: Border style (1=outline, 3=box) + outline: Outline width (in pixels) + shadow: Shadow depth (in pixels) + alignment: Text alignment (:class:`pysubs2.Alignment` instance); the underlying integer + uses numpad-style alignment, eg. 7 is "top left" (that is, ASS alignment semantics). + You can also use ``int`` here, though it is discouraged. + marginl: Left margin (in pixels) + marginr: Right margin (in pixels) + marginv: Vertical margin (in pixels) + alphalevel: Old, unused SSA-only field + encoding: Charset + drawing: Indicates that text span is a SSA vector drawing, see :func:`pysubs2.substation.parse_tags()` + """ DEFAULT_STYLE: ClassVar["SSAStyle"] = None # type: ignore[assignment] @property - def FIELDS(self) -> FrozenSet[str]: + def FIELDS(self) -> frozenset[str]: """All fields in SSAStyle.""" warnings.warn("Deprecated in 1.2.0 - it's a dataclass now", DeprecationWarning) return frozenset(field.name for field in dataclasses.fields(self)) - fontname: str = "Arial" #: Font name - fontsize: float = 20.0 #: Font size (in pixels) - primarycolor: Color = dataclasses.field(default_factory=lambda: Color(255, 255, 255, 0)) #: Primary color (:class:`pysubs2.Color` instance) - secondarycolor: Color = dataclasses.field(default_factory=lambda: Color(255, 0, 0, 0)) #: Secondary color (:class:`pysubs2.Color` instance) - tertiarycolor: Color = dataclasses.field(default_factory=lambda: Color(0, 0, 0, 0)) #: Tertiary color (:class:`pysubs2.Color` instance) - outlinecolor: Color = dataclasses.field(default_factory=lambda: Color(0, 0, 0, 0)) #: Outline color (:class:`pysubs2.Color` instance) - backcolor: Color = dataclasses.field(default_factory=lambda: Color(0, 0, 0, 0)) #: Back, ie. shadow color (:class:`pysubs2.Color` instance) - bold: bool = False #: Bold - italic: bool = False #: Italic - underline: bool = False #: Underline (ASS only) - strikeout: bool = False #: Strikeout (ASS only) - scalex: float = 100.0 #: Horizontal scaling (ASS only) - scaley: float = 100.0 #: Vertical scaling (ASS only) - spacing: float = 0.0 #: Letter spacing (ASS only) - angle: float = 0.0 #: Rotation (ASS only) - borderstyle: int = 1 #: Border style (1=outline, 3=box) - outline: float = 2.0 #: Outline width (in pixels) - shadow: float = 2.0 #: Shadow depth (in pixels) - alignment: Alignment = Alignment.BOTTOM_CENTER #: Text alignment (:class:`pysubs2.Alignment` instance); the underlying integer uses numpad-style alignment, eg. 7 is "top left" (that is, ASS alignment semantics). You can also use ``int`` here, though it is discouraged. - marginl: int = 10 #: Left margin (in pixels) - marginr: int = 10 #: Right margin (in pixels) - marginv: int = 10 #: Vertical margin (in pixels) - alphalevel: int = 0 #: Old, unused SSA-only field - encoding: int = 1 #: Charset + fontname: str = "Arial" + fontsize: float = 20.0 + primarycolor: Color = dataclasses.field(default_factory=lambda: Color(255, 255, 255, 0)) + secondarycolor: Color = dataclasses.field(default_factory=lambda: Color(255, 0, 0, 0)) + tertiarycolor: Color = dataclasses.field(default_factory=lambda: Color(0, 0, 0, 0)) + outlinecolor: Color = dataclasses.field(default_factory=lambda: Color(0, 0, 0, 0)) + backcolor: Color = dataclasses.field(default_factory=lambda: Color(0, 0, 0, 0)) + bold: bool = False + italic: bool = False + underline: bool = False + strikeout: bool = False + scalex: float = 100.0 + scaley: float = 100.0 + spacing: float = 0.0 + angle: float = 0.0 + borderstyle: int = 1 + outline: float = 2.0 + shadow: float = 2.0 + alignment: Alignment = Alignment.BOTTOM_CENTER + marginl: int = 10 + marginr: int = 10 + marginv: int = 10 + alphalevel: int = 0 + encoding: int = 1 # The following attributes cannot be defined for SSA styles themselves, # but can be used in override tags and thus are useful to keep here # for the `pysubs2.substation.parse_tags()` interface which returns # SSAStyles for text fragments. - drawing: bool = False #: Indicates that text span is a SSA vector drawing, see :func:`pysubs2.substation.parse_tags()` + drawing: bool = False def copy(self) -> "SSAStyle": return SSAStyle(**self.as_dict()) - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: # dataclasses.asdict() would recursively dictify Color objects, which we don't want return {field.name: getattr(self, field.name) for field in dataclasses.fields(self)} diff --git a/libs/python_dateutil-2.9.0.dist-info/INSTALLER b/libs/python_dateutil-2.9.0.post0.dist-info/INSTALLER similarity index 100% rename from libs/python_dateutil-2.9.0.dist-info/INSTALLER rename to libs/python_dateutil-2.9.0.post0.dist-info/INSTALLER diff --git a/libs/python_dateutil-2.9.0.dist-info/METADATA b/libs/python_dateutil-2.9.0.post0.dist-info/METADATA similarity index 99% rename from libs/python_dateutil-2.9.0.dist-info/METADATA rename to libs/python_dateutil-2.9.0.post0.dist-info/METADATA index ab2feebaa..c0f24aef1 100644 --- a/libs/python_dateutil-2.9.0.dist-info/METADATA +++ b/libs/python_dateutil-2.9.0.post0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: python-dateutil -Version: 2.9.0 +Version: 2.9.0.post0 Summary: Extensions to the standard Python datetime module Home-page: https://github.com/dateutil/dateutil Author: Gustavo Niemeyer diff --git a/libs/python_dateutil-2.9.0.dist-info/RECORD b/libs/python_dateutil-2.9.0.post0.dist-info/RECORD similarity index 58% rename from libs/python_dateutil-2.9.0.dist-info/RECORD rename to libs/python_dateutil-2.9.0.post0.dist-info/RECORD index b1326097e..b3542130f 100644 --- a/libs/python_dateutil-2.9.0.dist-info/RECORD +++ b/libs/python_dateutil-2.9.0.post0.dist-info/RECORD @@ -1,6 +1,6 @@ dateutil/__init__.py,sha256=Mqam67WO9IkTmUFyI66vS6IoSXTp9G388DadH2LCMLY,620 dateutil/_common.py,sha256=77w0yytkrxlYbSn--lDVPUMabUXRR9I3lBv_vQRUqUY,932 -dateutil/_version.py,sha256=24D27WSWcO2HaMUGCrVCG6Zbd76BrLDh2qQ1KbGX6Hc,704 +dateutil/_version.py,sha256=BV031OxDDAmy58neUg5yyqLkLaqIw7ibK9As3jiMib0,166 dateutil/easter.py,sha256=dyBi-lKvimH1u_k6p7Z0JJK72QhqVtVBsqByvpEPKvc,2678 dateutil/parser/__init__.py,sha256=wWk6GFuxTpjoggCGtgkceJoti4pVjl4_fHQXpNOaSYg,1766 dateutil/parser/_parser.py,sha256=7klDdyicksQB_Xgl-3UAmBwzCYor1AIZqklIcT6dH_8,58796 @@ -15,13 +15,13 @@ dateutil/tz/win.py,sha256=xJszWgSwE1xPx_HJj4ZkepyukC_hNy016WMcXhbRaB8,12935 dateutil/tzwin.py,sha256=7Ar4vdQCnnM0mKR3MUjbIKsZrBVfHgdwsJZc_mGYRew,59 dateutil/utils.py,sha256=dKCchEw8eObi0loGTx91unBxm_7UGlU3v_FjFMdqwYM,1965 dateutil/zoneinfo/__init__.py,sha256=KYg0pthCMjcp5MXSEiBJn3nMjZeNZav7rlJw5-tz1S4,5889 -dateutil/zoneinfo/dateutil-zoneinfo.tar.gz,sha256=Y25Djz4f3ZwC5TcTaM4UJMhWRNZSciMe0zOF9XsDv4I,156385 +dateutil/zoneinfo/dateutil-zoneinfo.tar.gz,sha256=0-pS57bpaN4NiE3xKIGTWW-pW4A9tPkqGCeac5gARHU,156400 dateutil/zoneinfo/rebuild.py,sha256=MiqYzCIHvNbMH-LdRYLv-4T0EIA7hDKt5GLR0IRTLdI,2392 -python_dateutil-2.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -python_dateutil-2.9.0.dist-info/METADATA,sha256=0onxRgklOi6tNqMWcNgBo8Nm08eafsZO5srBQbdFd5g,8390 -python_dateutil-2.9.0.dist-info/RECORD,, -python_dateutil-2.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -python_dateutil-2.9.0.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 -python_dateutil-2.9.0.dist-info/licenses/LICENSE,sha256=ugD1Gg2SgjtaHN4n2LW50jIeZ-2NqbwWPv-W1eF-V34,2889 -python_dateutil-2.9.0.dist-info/top_level.txt,sha256=4tjdWkhRZvF7LA_BYe_L9gB2w_p2a-z5y6ArjaRkot8,9 -python_dateutil-2.9.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +python_dateutil-2.9.0.post0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +python_dateutil-2.9.0.post0.dist-info/METADATA,sha256=pGHFOnG-ugFSX5O42DnhMjZPxS1jXrJlq0qbYVZfK1w,8396 +python_dateutil-2.9.0.post0.dist-info/RECORD,, +python_dateutil-2.9.0.post0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +python_dateutil-2.9.0.post0.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 +python_dateutil-2.9.0.post0.dist-info/licenses/LICENSE,sha256=ugD1Gg2SgjtaHN4n2LW50jIeZ-2NqbwWPv-W1eF-V34,2889 +python_dateutil-2.9.0.post0.dist-info/top_level.txt,sha256=4tjdWkhRZvF7LA_BYe_L9gB2w_p2a-z5y6ArjaRkot8,9 +python_dateutil-2.9.0.post0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 diff --git a/libs/python_dateutil-2.9.0.dist-info/REQUESTED b/libs/python_dateutil-2.9.0.post0.dist-info/REQUESTED similarity index 100% rename from libs/python_dateutil-2.9.0.dist-info/REQUESTED rename to libs/python_dateutil-2.9.0.post0.dist-info/REQUESTED diff --git a/libs/python_dateutil-2.9.0.dist-info/WHEEL b/libs/python_dateutil-2.9.0.post0.dist-info/WHEEL similarity index 100% rename from libs/python_dateutil-2.9.0.dist-info/WHEEL rename to libs/python_dateutil-2.9.0.post0.dist-info/WHEEL diff --git a/libs/python_dateutil-2.9.0.dist-info/licenses/LICENSE b/libs/python_dateutil-2.9.0.post0.dist-info/licenses/LICENSE similarity index 100% rename from libs/python_dateutil-2.9.0.dist-info/licenses/LICENSE rename to libs/python_dateutil-2.9.0.post0.dist-info/licenses/LICENSE diff --git a/libs/python_dateutil-2.9.0.dist-info/top_level.txt b/libs/python_dateutil-2.9.0.post0.dist-info/top_level.txt similarity index 100% rename from libs/python_dateutil-2.9.0.dist-info/top_level.txt rename to libs/python_dateutil-2.9.0.post0.dist-info/top_level.txt diff --git a/libs/python_dateutil-2.9.0.dist-info/zip-safe b/libs/python_dateutil-2.9.0.post0.dist-info/zip-safe similarity index 100% rename from libs/python_dateutil-2.9.0.dist-info/zip-safe rename to libs/python_dateutil-2.9.0.post0.dist-info/zip-safe diff --git a/libs/python_engineio-4.13.1.dist-info/INSTALLER b/libs/python_engineio-4.13.2.dist-info/INSTALLER similarity index 100% rename from libs/python_engineio-4.13.1.dist-info/INSTALLER rename to libs/python_engineio-4.13.2.dist-info/INSTALLER diff --git a/libs/python_engineio-4.13.1.dist-info/METADATA b/libs/python_engineio-4.13.2.dist-info/METADATA similarity index 99% rename from libs/python_engineio-4.13.1.dist-info/METADATA rename to libs/python_engineio-4.13.2.dist-info/METADATA index f5765d585..5a43c84da 100644 --- a/libs/python_engineio-4.13.1.dist-info/METADATA +++ b/libs/python_engineio-4.13.2.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: python-engineio -Version: 4.13.1 +Version: 4.13.2 Summary: Engine.IO server and client for Python Author-email: Miguel Grinberg License: MIT diff --git a/libs/python_engineio-4.13.1.dist-info/RECORD b/libs/python_engineio-4.13.2.dist-info/RECORD similarity index 62% rename from libs/python_engineio-4.13.1.dist-info/RECORD rename to libs/python_engineio-4.13.2.dist-info/RECORD index 12d185449..e4b34f033 100644 --- a/libs/python_engineio-4.13.1.dist-info/RECORD +++ b/libs/python_engineio-4.13.2.dist-info/RECORD @@ -2,32 +2,32 @@ engineio/__init__.py,sha256=0R2PY1EXu3sicP7mkA0_QxEVGRlFlgvsxfhByqREE1A,481 engineio/async_client.py,sha256=EQzirWwJ_gUinklogHvHN1vz5Ux0zUt-RhGkCureJbE,29976 engineio/async_drivers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 engineio/async_drivers/_websocket_wsgi.py,sha256=LuOEfKhbAw8SplB5PMpYKIUqfCPEadQEpqeiq_leOIA,949 -engineio/async_drivers/aiohttp.py,sha256=fgio4z82i93vTojINDLSBiYCXNcAHTSqcqyFa4yqyHs,3624 -engineio/async_drivers/asgi.py,sha256=VxkQg3r-vD7W3X4v2QdB_C5bNS55FJYT2SNBRNaHgds,11383 +engineio/async_drivers/aiohttp.py,sha256=SOU9KOoru_OzxLioSKCYNIZQZLoyf-oxw6-bXc6-_L8,3697 +engineio/async_drivers/asgi.py,sha256=9qAPXs4wO4TxTyF-Se-nhilqzufj9on63MWM66D4j0g,11535 engineio/async_drivers/eventlet.py,sha256=n1y4OjPdj4J2GIep5N56O29oa5NQgFJVcTBjyO1C-Gs,1735 engineio/async_drivers/gevent.py,sha256=hnJHeWdDQE2jfoLCP5DnwVPzsQlcTLJUMA5EVf1UL-k,2962 engineio/async_drivers/gevent_uwsgi.py,sha256=m6ay5dov9FDQl0fbeiKeE-Orh5LiF6zLlYQ64Oa3T5g,5954 engineio/async_drivers/sanic.py,sha256=GYX8YWR1GbRm-GkMTAQkfkWbY12MOT1IV2DzH0Xx8Ns,4495 engineio/async_drivers/threading.py,sha256=ywmG59d4H6OHZjKarBN97-9BHEsRxFEz9YN-E9QAu_I,463 engineio/async_drivers/tornado.py,sha256=v9uuGk8_HS6DjhoEKXfjHpki1FnydN4wi7eyBMsUTJA,5908 -engineio/async_server.py,sha256=ONjhNee8oSncqBrWDaaihWG7fhoFasluAmdOh8NYnqI,27557 -engineio/async_socket.py,sha256=nHY0DPPk0FtI9djUnQWtzZ3ce2OD184Tu-Dop7JLg9I,10715 +engineio/async_server.py,sha256=7oQClUwwSiPlFFUSEBeE6VuOYnbK2z8FCmRqeWoiiVE,27558 +engineio/async_socket.py,sha256=2agIDyicauuHuDHf_2WtUWreq1BP5iu5FQu9tSHWdtY,10827 engineio/base_client.py,sha256=oOdq-zR7kg8QpvAGti0zBIiFBQGyEELwVMZNtcoJ2ig,5827 engineio/base_server.py,sha256=tTCqc65Vzf-0Be1uunt1FuGxPOvcpOUCxnT5V2mZCWo,14763 -engineio/base_socket.py,sha256=sQqbNSfGhMQG3xzwar6IXMal28C7Q5TIAQRGp74Wt2o,399 +engineio/base_socket.py,sha256=PWiJC1msNo-0ctlP3JAwWsngKvnYM-ZjS6B6zzfu-eM,420 engineio/client.py,sha256=-9f8kLYOw86QXrFx5PoyI56uHZ1cUKbhVYolTEj-2t8,27784 engineio/exceptions.py,sha256=FyuMb5qhX9CUYP3fEoe1m-faU96ApdQTSbblaaoo8LA,292 engineio/json.py,sha256=SG5FTojqd1ix6u0dKXJsZVqqdYioZLO4S2GPL7BKl3U,405 engineio/middleware.py,sha256=5NKBXz-ftuFErUB_V9IDvRHaSOsjhtW-NnuJtquB1nc,3750 engineio/packet.py,sha256=YO3gmeKUoKyIsUq2e1z7Zifr5PGeS3gnaCeqR7wj5hQ,3198 engineio/payload.py,sha256=GIWu0Vnay4WNZlDxHqVgP34tKTBXX58OArJ-mO5zD3E,1539 -engineio/server.py,sha256=47pqjDob0XBwS_2gKRHn0DwAf4NHXHHVuYceZ6pFkD8,23087 -engineio/socket.py,sha256=Oaw1E7ZDyOCaS7KV151g1u9rqOf1JJHh5gttEU0cSeA,10342 +engineio/server.py,sha256=P6Pa53MRJVoXL_n5tEONuIQoBtOvsdFR5GylKgu5oUE,23088 +engineio/socket.py,sha256=WUwGT9MByp04KjHiEnDaESosNsxYxp6AG1tB3pei9eQ,10454 engineio/static_files.py,sha256=pwez9LQFaSQXMbtI0vLyD6UDiokQ4rNfmRYgVLKOthc,2064 -python_engineio-4.13.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -python_engineio-4.13.1.dist-info/METADATA,sha256=jWP1NgC-iIVq_nIphGPFUG-vGPVgYuLoxrC6UgAFlmA,2314 -python_engineio-4.13.1.dist-info/RECORD,, -python_engineio-4.13.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -python_engineio-4.13.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -python_engineio-4.13.1.dist-info/licenses/LICENSE,sha256=yel9Pbwfu82094CLKCzWRtuIev9PUxP-a76NTDFAWpw,1082 -python_engineio-4.13.1.dist-info/top_level.txt,sha256=u8PmNisCZLwRYcWrNLe9wutQ2tt4zNi8IH362c-HWuA,9 +python_engineio-4.13.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +python_engineio-4.13.2.dist-info/METADATA,sha256=XanaByNEwDBrXOcmqz5fT8CJ2S4qW3voMSpa4Mrchd0,2314 +python_engineio-4.13.2.dist-info/RECORD,, +python_engineio-4.13.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +python_engineio-4.13.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +python_engineio-4.13.2.dist-info/licenses/LICENSE,sha256=yel9Pbwfu82094CLKCzWRtuIev9PUxP-a76NTDFAWpw,1082 +python_engineio-4.13.2.dist-info/top_level.txt,sha256=u8PmNisCZLwRYcWrNLe9wutQ2tt4zNi8IH362c-HWuA,9 diff --git a/libs/python_engineio-4.13.1.dist-info/REQUESTED b/libs/python_engineio-4.13.2.dist-info/REQUESTED similarity index 100% rename from libs/python_engineio-4.13.1.dist-info/REQUESTED rename to libs/python_engineio-4.13.2.dist-info/REQUESTED diff --git a/libs/mako-1.3.10.dist-info/WHEEL b/libs/python_engineio-4.13.2.dist-info/WHEEL similarity index 100% rename from libs/mako-1.3.10.dist-info/WHEEL rename to libs/python_engineio-4.13.2.dist-info/WHEEL diff --git a/libs/python_engineio-4.13.1.dist-info/licenses/LICENSE b/libs/python_engineio-4.13.2.dist-info/licenses/LICENSE similarity index 100% rename from libs/python_engineio-4.13.1.dist-info/licenses/LICENSE rename to libs/python_engineio-4.13.2.dist-info/licenses/LICENSE diff --git a/libs/python_engineio-4.13.1.dist-info/top_level.txt b/libs/python_engineio-4.13.2.dist-info/top_level.txt similarity index 100% rename from libs/python_engineio-4.13.1.dist-info/top_level.txt rename to libs/python_engineio-4.13.2.dist-info/top_level.txt diff --git a/libs/python_socketio-5.16.1.dist-info/INSTALLER b/libs/python_socketio-5.16.3.dist-info/INSTALLER similarity index 100% rename from libs/python_socketio-5.16.1.dist-info/INSTALLER rename to libs/python_socketio-5.16.3.dist-info/INSTALLER diff --git a/libs/python_socketio-5.16.1.dist-info/METADATA b/libs/python_socketio-5.16.3.dist-info/METADATA similarity index 98% rename from libs/python_socketio-5.16.1.dist-info/METADATA rename to libs/python_socketio-5.16.3.dist-info/METADATA index 5389f6c64..143c87190 100644 --- a/libs/python_socketio-5.16.1.dist-info/METADATA +++ b/libs/python_socketio-5.16.3.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: python-socketio -Version: 5.16.1 +Version: 5.16.3 Summary: Socket.IO server and client for Python Author-email: Miguel Grinberg License: MIT @@ -14,7 +14,7 @@ Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: bidict>=0.21.0 -Requires-Dist: python-engineio>=4.11.0 +Requires-Dist: python-engineio>=4.13.2 Provides-Extra: client Requires-Dist: requests>=2.21.0; extra == "client" Requires-Dist: websocket-client>=0.54.0; extra == "client" diff --git a/libs/python_socketio-5.16.1.dist-info/RECORD b/libs/python_socketio-5.16.3.dist-info/RECORD similarity index 69% rename from libs/python_socketio-5.16.1.dist-info/RECORD rename to libs/python_socketio-5.16.3.dist-info/RECORD index 37f3b224b..1cf6f2205 100644 --- a/libs/python_socketio-5.16.1.dist-info/RECORD +++ b/libs/python_socketio-5.16.3.dist-info/RECORD @@ -1,21 +1,21 @@ -python_socketio-5.16.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -python_socketio-5.16.1.dist-info/METADATA,sha256=aKRBDNDXnim5rhevWwXO_7swzXof2LRiSJr3zGEeqDM,3281 -python_socketio-5.16.1.dist-info/RECORD,, -python_socketio-5.16.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -python_socketio-5.16.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -python_socketio-5.16.1.dist-info/licenses/LICENSE,sha256=yel9Pbwfu82094CLKCzWRtuIev9PUxP-a76NTDFAWpw,1082 -python_socketio-5.16.1.dist-info/top_level.txt,sha256=xWd-HVUanhys_VzQQTRTRZBX8W448ayFytYf1Zffivs,9 +python_socketio-5.16.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +python_socketio-5.16.3.dist-info/METADATA,sha256=Sh2h28lDLOA2zYOvS6I-_7Bn4hEFvMIfn1rPSrdyJ98,3281 +python_socketio-5.16.3.dist-info/RECORD,, +python_socketio-5.16.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +python_socketio-5.16.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +python_socketio-5.16.3.dist-info/licenses/LICENSE,sha256=yel9Pbwfu82094CLKCzWRtuIev9PUxP-a76NTDFAWpw,1082 +python_socketio-5.16.3.dist-info/top_level.txt,sha256=xWd-HVUanhys_VzQQTRTRZBX8W448ayFytYf1Zffivs,9 socketio/__init__.py,sha256=DXxtwPIqHFIqV4BGTgJ86OvCXD6Mth3PxBYhFoJ1_7g,1269 socketio/admin.py,sha256=Ag0fz2QRVq5mprjkBe9txNjGPvQEpBai6-VJ-rJsxmA,16260 socketio/asgi.py,sha256=NaJtYhOswVVcwHU0zcMM5H5TrSzXq9K-CAYaeSNTZRY,2192 socketio/async_admin.py,sha256=4DFriPI3lwjgV80a81x1tndmQcRuYJGIu0wwkNS9ayY,16471 -socketio/async_aiopika_manager.py,sha256=X3PQrPMpJdu0yF8EoqvjB7lTb1wyAe4-EI1ToSuHiaA,5892 +socketio/async_aiopika_manager.py,sha256=zvP5FqHMgp3PZEHmtjKQm_LzhP1DWle_1M1EmLYS-n8,6037 socketio/async_client.py,sha256=Xa9R2LJz6enF94TbfzPl7m2BMikLcqZl6AAZDyIHlZo,28124 socketio/async_manager.py,sha256=G7TLqCDVozbHt0ODG3jU09h1ku6ahk87_Sz4Cm5tL90,4518 socketio/async_namespace.py,sha256=BxSUs_iQzPHFo6JyoRZm2Yy0uRyizvS2PMCIzsVzk_o,12043 socketio/async_pubsub_manager.py,sha256=UwU_i_cDvJ4vRzB6RnefEFuWKoiL35WgXHR0y2-CXas,12474 -socketio/async_redis_manager.py,sha256=S_S09BCeyr8N_Ma6ickbDuKESvPaKSYTSXAjd73OMPo,7848 -socketio/async_server.py,sha256=7_AVMhfZFPaJ5t3nZxaoBT_-Sl1FmDA8A46oXOrXE74,36669 +socketio/async_redis_manager.py,sha256=E0w4gDgrZnEUu5jCatf-P5F-wQvjr2673viEf8zr3pQ,7555 +socketio/async_server.py,sha256=nU-QMFiTk1i2WrLaxiUGJuHmfFnXPvpxHhMFcqC2JuM,36968 socketio/async_simple_client.py,sha256=eZUgU9fz7c9lXhsbm4Ln95dinM9zxT3Ps2sML4HtT6Y,9016 socketio/base_client.py,sha256=LJzYynrxPuId7xbHAWiTPFlEAROQCLi8cOmiIDuc1Lk,11673 socketio/base_manager.py,sha256=w4JZrUSpQ8Unsy0ZwQmEEXwvSzeCGT3AfR0ma0Tmczo,6116 @@ -24,15 +24,15 @@ socketio/base_server.py,sha256=JtHtmxFjtclcdORg7FIBoMtMxiaCFnuwulXrpLUSjUE,10637 socketio/client.py,sha256=ttcjNZ-Qb1yUKjb2Eiylw02wt6ghTXWalfw_y7ODNRo,26355 socketio/exceptions.py,sha256=c8yKss_oJl-fkL52X_AagyJecL-9Mxlgb5xDRqSz5tA,975 socketio/kafka_manager.py,sha256=QrgTh94Ft2YmQbJ_uVX9HMotKWZYBfWh6Y66RYrSwtM,3114 -socketio/kombu_manager.py,sha256=EAwW-_L-FCQTWeZjpjE0xqJt5RZSUBdQwU7ASORyaNg,6468 +socketio/kombu_manager.py,sha256=3nLyVPrJyeKLi0E2jp0I33W3EYXDkR89mCmW0FtrBmk,6581 socketio/manager.py,sha256=RPYPcVBFAjN-fEtLfcsPlk6SOW_SBATvw0Tkq_PkGZw,3861 socketio/middleware.py,sha256=P8wOgSzy3YKOcRVI-r3KNKsEejBz_f5p2wdV8ZqW12E,1591 socketio/msgpack_packet.py,sha256=gSLKgYP88QAJ1J8sOmgGFvtRBmNH8QWPtHImWaXIqzs,1765 socketio/namespace.py,sha256=80y8BN2FFlHK8JKF1TirWvvE4pn9FkGKk14IVFkCLEs,9488 socketio/packet.py,sha256=N7jEjcqOUg2PPEooCoEglxVxhjRqJF-FZCPyDF-UDKQ,7120 socketio/pubsub_manager.py,sha256=3xUqRnHiFVf_YI68OWRakJTa2LGHPpyMZNSAQN43GZY,11775 -socketio/redis_manager.py,sha256=_I8n5GJcYWlSnmYZnADDfvn4FP2BMfllHqN4JYuuYeE,8923 -socketio/server.py,sha256=ciEtM1mmXP99zOs5i8kHEPe3uXytYSFNQPRnfTE466s,35042 +socketio/redis_manager.py,sha256=5MhbV4DWJtazxvEgAF_fmAVj-a_F6xhFhq9cpX-_V2I,8734 +socketio/server.py,sha256=cNDOBJ8u01D0ttZmcrUM0vN-1rT8Z1D_-oMMOnEWGWU,35341 socketio/simple_client.py,sha256=aSMTRO8LHBAWO2IV9uetL6_zwZgPGR-ts73x_4k3624,8434 socketio/tornado.py,sha256=R82JCqz-E1ibZAQX708h7FX3sguCHQ1OLYpnMag-LY8,295 socketio/zmq_manager.py,sha256=PkY5aP7-JesCJjumRht90pf3dqLGZV1WrGZ5lA8jQ-Y,4232 diff --git a/libs/python_socketio-5.16.1.dist-info/REQUESTED b/libs/python_socketio-5.16.3.dist-info/REQUESTED similarity index 100% rename from libs/python_socketio-5.16.1.dist-info/REQUESTED rename to libs/python_socketio-5.16.3.dist-info/REQUESTED diff --git a/libs/pysubs2-1.8.0.dist-info/WHEEL b/libs/python_socketio-5.16.3.dist-info/WHEEL similarity index 100% rename from libs/pysubs2-1.8.0.dist-info/WHEEL rename to libs/python_socketio-5.16.3.dist-info/WHEEL diff --git a/libs/python_socketio-5.16.1.dist-info/licenses/LICENSE b/libs/python_socketio-5.16.3.dist-info/licenses/LICENSE similarity index 100% rename from libs/python_socketio-5.16.1.dist-info/licenses/LICENSE rename to libs/python_socketio-5.16.3.dist-info/licenses/LICENSE diff --git a/libs/python_socketio-5.16.1.dist-info/top_level.txt b/libs/python_socketio-5.16.3.dist-info/top_level.txt similarity index 100% rename from libs/python_socketio-5.16.1.dist-info/top_level.txt rename to libs/python_socketio-5.16.3.dist-info/top_level.txt diff --git a/libs/requests-2.32.5.dist-info/RECORD b/libs/requests-2.32.5.dist-info/RECORD deleted file mode 100644 index 6b6cd668f..000000000 --- a/libs/requests-2.32.5.dist-info/RECORD +++ /dev/null @@ -1,25 +0,0 @@ -requests-2.32.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -requests-2.32.5.dist-info/METADATA,sha256=ZbWgjagfSRVRPnYJZf8Ut1GPZbe7Pv4NqzZLvMTUDLA,4945 -requests-2.32.5.dist-info/RECORD,, -requests-2.32.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -requests-2.32.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -requests-2.32.5.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142 -requests-2.32.5.dist-info/top_level.txt,sha256=fMSVmHfb5rbGOo6xv-O_tUX6j-WyixssE-SnwcDRxNQ,9 -requests/__init__.py,sha256=4xaAERmPDIBPsa2PsjpU9r06yooK-2mZKHTZAhWRWts,5072 -requests/__version__.py,sha256=QKDceK8K_ujqwDDc3oYrR0odOBYgKVOQQ5vFap_G_cg,435 -requests/_internal_utils.py,sha256=nMQymr4hs32TqVo5AbCrmcJEhvPUh7xXlluyqwslLiQ,1495 -requests/adapters.py,sha256=8nX113gbb123aUtx2ETkAN_6IsYX-M2fRoLGluTEcRk,26285 -requests/api.py,sha256=_Zb9Oa7tzVIizTKwFrPjDEY9ejtm_OnSRERnADxGsQs,6449 -requests/auth.py,sha256=kF75tqnLctZ9Mf_hm9TZIj4cQWnN5uxRz8oWsx5wmR0,10186 -requests/certs.py,sha256=Z9Sb410Anv6jUFTyss0jFFhU6xst8ctELqfy8Ev23gw,429 -requests/compat.py,sha256=J7sIjR6XoDGp5JTVzOxkK5fSoUVUa_Pjc7iRZhAWGmI,2142 -requests/cookies.py,sha256=bNi-iqEj4NPZ00-ob-rHvzkvObzN3lEpgw3g6paS3Xw,18590 -requests/exceptions.py,sha256=jJPS1UWATs86ShVUaLorTiJb1SaGuoNEWgICJep-VkY,4260 -requests/help.py,sha256=gPX5d_H7Xd88aDABejhqGgl9B1VFRTt5BmiYvL3PzIQ,3875 -requests/hooks.py,sha256=CiuysiHA39V5UfcCBXFIx83IrDpuwfN9RcTUgv28ftQ,733 -requests/models.py,sha256=MjZdZ4k7tnw-1nz5PKShjmPmqyk0L6DciwnFngb_Vk4,35510 -requests/packages.py,sha256=_g0gZ681UyAlKHRjH6kanbaoxx2eAb6qzcXiODyTIoc,904 -requests/sessions.py,sha256=Cl1dpEnOfwrzzPbku-emepNeN4Rt_0_58Iy2x-JGTm8,30503 -requests/status_codes.py,sha256=iJUAeA25baTdw-6PfD0eF4qhpINDJRJI-yaMqxs4LEI,4322 -requests/structures.py,sha256=-IbmhVz06S-5aPSZuUthZ6-6D9XOjRuTXHOabY041XM,2912 -requests/utils.py,sha256=WqU86rZ3wvhC-tQjWcjtH_HEKZwWB3iWCZV6SW5DEdQ,33213 diff --git a/libs/requests-2.32.5.dist-info/INSTALLER b/libs/requests-2.34.2.dist-info/INSTALLER similarity index 100% rename from libs/requests-2.32.5.dist-info/INSTALLER rename to libs/requests-2.34.2.dist-info/INSTALLER diff --git a/libs/requests-2.32.5.dist-info/METADATA b/libs/requests-2.34.2.dist-info/METADATA similarity index 77% rename from libs/requests-2.32.5.dist-info/METADATA rename to libs/requests-2.34.2.dist-info/METADATA index b31773e3e..cfc3f948e 100644 --- a/libs/requests-2.32.5.dist-info/METADATA +++ b/libs/requests-2.34.2.dist-info/METADATA @@ -1,10 +1,9 @@ Metadata-Version: 2.4 Name: requests -Version: 2.32.5 +Version: 2.34.2 Summary: Python HTTP for Humans. -Home-page: https://requests.readthedocs.io -Author: Kenneth Reitz -Author-email: me@kennethreitz.org +Author-email: Kenneth Reitz +Maintainer-email: Ian Stapleton Cordasco , Nate Prewitt License: Apache-2.0 Project-URL: Documentation, https://requests.readthedocs.io Project-URL: Source, https://github.com/psf/requests @@ -16,45 +15,41 @@ Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Software Development :: Libraries -Requires-Python: >=3.9 +Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE +License-File: NOTICE Requires-Dist: charset_normalizer<4,>=2 Requires-Dist: idna<4,>=2.5 -Requires-Dist: urllib3<3,>=1.21.1 -Requires-Dist: certifi>=2017.4.17 +Requires-Dist: urllib3<3,>=1.26 +Requires-Dist: certifi>=2023.5.7 Provides-Extra: security Provides-Extra: socks Requires-Dist: PySocks!=1.5.7,>=1.5.6; extra == "socks" Provides-Extra: use-chardet-on-py3 -Requires-Dist: chardet<6,>=3.0.2; extra == "use-chardet-on-py3" -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: description-content-type -Dynamic: home-page -Dynamic: license +Requires-Dist: chardet<8,>=3.0.2; extra == "use-chardet-on-py3" Dynamic: license-file -Dynamic: project-url -Dynamic: provides-extra -Dynamic: requires-dist -Dynamic: requires-python -Dynamic: summary # Requests +[![Version](https://img.shields.io/pypi/v/requests.svg?maxAge=86400)](https://pypi.org/project/requests/) +[![Supported Versions](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests) +[![Downloads](https://static.pepy.tech/badge/requests/month)](https://pepy.tech/project/requests) +[![Contributors](https://img.shields.io/github/contributors/psf/requests.svg)](https://github.com/psf/requests/graphs/contributors) +[![Documentation](https://readthedocs.org/projects/requests/badge/?version=latest)](https://requests.readthedocs.io) + **Requests** is a simple, yet elegant, HTTP library. ```python @@ -74,11 +69,7 @@ Dynamic: summary Requests allows you to send HTTP/1.1 requests extremely easily. There’s no need to manually add query strings to your URLs, or to form-encode your `PUT` & `POST` data — but nowadays, just use the `json` method! -Requests is one of the most downloaded Python packages today, pulling in around `30M downloads / week`— according to GitHub, Requests is currently [depended upon](https://github.com/psf/requests/network/dependents?package_id=UGFja2FnZS01NzA4OTExNg%3D%3D) by `1,000,000+` repositories. You may certainly put your trust in this code. - -[![Downloads](https://static.pepy.tech/badge/requests/month)](https://pepy.tech/project/requests) -[![Supported Versions](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests) -[![Contributors](https://img.shields.io/github/contributors/psf/requests.svg)](https://github.com/psf/requests/graphs/contributors) +Requests is one of the most downloaded Python packages today, pulling in around `300M downloads / week` — according to GitHub, Requests is currently [depended upon](https://github.com/psf/requests/network/dependents?package_id=UGFja2FnZS01NzA4OTExNg%3D%3D) by `4,000,000+` repositories. ## Installing Requests and Supported Versions @@ -88,7 +79,7 @@ Requests is available on PyPI: $ python -m pip install requests ``` -Requests officially supports Python 3.9+. +Requests officially supports Python 3.10+. ## Supported Features & Best–Practices @@ -108,10 +99,6 @@ Requests is ready for the demands of building robust and reliable HTTP–speakin - Automatic honoring of `.netrc` - Chunked HTTP Requests -## API Reference and User Guide available on [Read the Docs](https://requests.readthedocs.io) - -[![Read the Docs](https://raw.githubusercontent.com/psf/requests/main/ext/ss.png)](https://requests.readthedocs.io) - ## Cloning the repository When cloning the Requests repository, you may need to add the `-c diff --git a/libs/requests-2.34.2.dist-info/RECORD b/libs/requests-2.34.2.dist-info/RECORD new file mode 100644 index 000000000..a0bdad36f --- /dev/null +++ b/libs/requests-2.34.2.dist-info/RECORD @@ -0,0 +1,28 @@ +requests-2.34.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +requests-2.34.2.dist-info/METADATA,sha256=jDhLo-l5SA-q4oWdPF5sEnbdLDYW4yLhJNUsjPxVbyc,4806 +requests-2.34.2.dist-info/RECORD,, +requests-2.34.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +requests-2.34.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +requests-2.34.2.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142 +requests-2.34.2.dist-info/licenses/NOTICE,sha256=9REJct7a0rTp0xRRja87fXLW4C5Jms2AIYHeb3RXHcw,38 +requests-2.34.2.dist-info/top_level.txt,sha256=fMSVmHfb5rbGOo6xv-O_tUX6j-WyixssE-SnwcDRxNQ,9 +requests/__init__.py,sha256=MRFX5_pKqRZsgn1SN-b9aU7rXKVJ7kDi6Q-J8hq81W4,5922 +requests/__version__.py,sha256=q7qQZwsDcKPcQNl8LepGo11L7JQf8Q18PPMJjvBKF_k,435 +requests/_internal_utils.py,sha256=TH2NEyyYmPx9cV5HPzrHR4XdxKuW0skkD4eDXcbZgf8,1542 +requests/_types.py,sha256=0G33n1SiecDMP67FlsRGJG1a0U7f1SnrMo3vvWKkSok,5838 +requests/adapters.py,sha256=ZH6r1EU94jKqQLVf8DPW8Zd4kQt-cGnFH99jB2_HdJM,28064 +requests/api.py,sha256=TRVICsBG8Ikgl5joZQR270oo6-b4G0AHWPjvQuxrVQk,7152 +requests/auth.py,sha256=6TlRpVLUw8X9m4Sl4dSbA8qrpHI8T_ES2mBTYA7IWmU,12233 +requests/certs.py,sha256=_ZxrgzWc75D_bE7uq43MI4jaOC68p9AKSZs8C0NKh-Q,430 +requests/compat.py,sha256=XXm6KJaQtQFQRDQaH3vH0kE0_BOi2z78-FJRW_mqGLQ,2465 +requests/cookies.py,sha256=ChzmFA8rlCBSLFCA41bmifp5bCUi9VyhCCb17Y4Kcjw,21549 +requests/exceptions.py,sha256=xeGPM1JFSWjjN7swVvCGffLzfAoNLBp33AXn4LPAQRs,4564 +requests/help.py,sha256=cjUZuxiE2hjYT2svt49-v3-1f3MgcLYCWuBx2sai0Xk,4210 +requests/hooks.py,sha256=69igJHXTGg5HOo9VPpUB_0NkW5VjiFrVKETnpj8Ndqs,1138 +requests/models.py,sha256=0bwNmQq_XV6-4F-JCRG0Nj-t8tUmS2hqlj30fFKbas4,41783 +requests/packages.py,sha256=_g0gZ681UyAlKHRjH6kanbaoxx2eAb6qzcXiODyTIoc,904 +requests/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +requests/sessions.py,sha256=lvuzC7vwalmlJo0TtXiFFJdWqj8xaVtcFeQd17svZ6Y,34248 +requests/status_codes.py,sha256=GVD0fInPGAGXh-B9jOSPZtazjmIvfZTXCpAkf-5sBA4,4351 +requests/structures.py,sha256=upRgw5B48l5vHSokrJQaxvjS7pcZf6jI0MJi2KHmegI,4134 +requests/utils.py,sha256=ZX_QI0OyWGu9E4pOwPA87rdhDCcS-WrFTyinCL99B5w,36322 diff --git a/libs/requests-2.32.5.dist-info/REQUESTED b/libs/requests-2.34.2.dist-info/REQUESTED similarity index 100% rename from libs/requests-2.32.5.dist-info/REQUESTED rename to libs/requests-2.34.2.dist-info/REQUESTED diff --git a/libs/python_engineio-4.13.1.dist-info/WHEEL b/libs/requests-2.34.2.dist-info/WHEEL similarity index 100% rename from libs/python_engineio-4.13.1.dist-info/WHEEL rename to libs/requests-2.34.2.dist-info/WHEEL diff --git a/libs/requests-2.32.5.dist-info/licenses/LICENSE b/libs/requests-2.34.2.dist-info/licenses/LICENSE similarity index 100% rename from libs/requests-2.32.5.dist-info/licenses/LICENSE rename to libs/requests-2.34.2.dist-info/licenses/LICENSE diff --git a/libs/requests-2.34.2.dist-info/licenses/NOTICE b/libs/requests-2.34.2.dist-info/licenses/NOTICE new file mode 100644 index 000000000..1ff62db68 --- /dev/null +++ b/libs/requests-2.34.2.dist-info/licenses/NOTICE @@ -0,0 +1,2 @@ +Requests +Copyright 2019 Kenneth Reitz diff --git a/libs/requests-2.32.5.dist-info/top_level.txt b/libs/requests-2.34.2.dist-info/top_level.txt similarity index 100% rename from libs/requests-2.32.5.dist-info/top_level.txt rename to libs/requests-2.34.2.dist-info/top_level.txt diff --git a/libs/requests/__init__.py b/libs/requests/__init__.py index 051cda134..ad842400b 100644 --- a/libs/requests/__init__.py +++ b/libs/requests/__init__.py @@ -38,6 +38,8 @@ is at . :license: Apache 2.0, see LICENSE for more details. """ +from __future__ import annotations + import warnings import urllib3 @@ -50,21 +52,25 @@ except ImportError: charset_normalizer_version = None try: - from chardet import __version__ as chardet_version + from chardet import __version__ as chardet_version # type: ignore[import-not-found] except ImportError: chardet_version = None -def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version): - urllib3_version = urllib3_version.split(".") - assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git. +def check_compatibility( + urllib3_version: str, + chardet_version: str | None, + charset_normalizer_version: str | None, +) -> None: + urllib3_version_list = urllib3_version.split(".")[:3] + assert urllib3_version_list != ["dev"] # Verify urllib3 isn't installed from git. # Sometimes, urllib3 only reports its version as 16.1. - if len(urllib3_version) == 2: - urllib3_version.append("0") + if len(urllib3_version_list) == 2: + urllib3_version_list.append("0") # Check urllib3 for compatibility. - major, minor, patch = urllib3_version # noqa: F811 + major, minor, patch = urllib3_version_list # noqa: F811 major, minor, patch = int(major), int(minor), int(patch) # urllib3 >= 1.21.1 assert major >= 1 @@ -75,8 +81,8 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver if chardet_version: major, minor, patch = chardet_version.split(".")[:3] major, minor, patch = int(major), int(minor), int(patch) - # chardet_version >= 3.0.2, < 6.0.0 - assert (3, 0, 2) <= (major, minor, patch) < (6, 0, 0) + # chardet_version >= 3.0.2, < 8.0.0 + assert (3, 0, 2) <= (major, minor, patch) < (8, 0, 0) elif charset_normalizer_version: major, minor, patch = charset_normalizer_version.split(".")[:3] major, minor, patch = int(major), int(minor), int(patch) @@ -90,31 +96,30 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver ) -def _check_cryptography(cryptography_version): +def _check_cryptography(cryptography_version: str) -> None: # cryptography < 1.3.4 try: - cryptography_version = list(map(int, cryptography_version.split("."))) + cryptography_version_list = list(map(int, cryptography_version.split("."))) except ValueError: return - if cryptography_version < [1, 3, 4]: - warning = "Old version of cryptography ({}) may cause slowdown.".format( - cryptography_version - ) + if cryptography_version_list < [1, 3, 4]: + warning = f"Old version of cryptography ({cryptography_version_list}) may cause slowdown." warnings.warn(warning, RequestsDependencyWarning) # Check imported dependencies for compatibility. try: check_compatibility( - urllib3.__version__, chardet_version, charset_normalizer_version + urllib3.__version__, # type: ignore[reportPrivateImportUsage] + chardet_version, # type: ignore[reportUnknownArgumentType] + charset_normalizer_version, ) except (AssertionError, ValueError): warnings.warn( - "urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported " - "version!".format( - urllib3.__version__, chardet_version, charset_normalizer_version - ), + f"urllib3 ({urllib3.__version__}) or chardet " # type: ignore[reportPrivateImportUsage] + f"({chardet_version})/charset_normalizer ({charset_normalizer_version}) " + "doesn't match a supported version!", RequestsDependencyWarning, ) @@ -133,9 +138,11 @@ try: pyopenssl.inject_into_urllib3() # Check cryptography version - from cryptography import __version__ as cryptography_version + from cryptography import ( # type: ignore[reportMissingImports] + __version__ as cryptography_version, # type: ignore[reportUnknownVariableType] + ) - _check_cryptography(cryptography_version) + _check_cryptography(cryptography_version) # type: ignore[reportUnknownArgumentType] except ImportError: pass @@ -178,6 +185,34 @@ from .models import PreparedRequest, Request, Response from .sessions import Session, session from .status_codes import codes +__all__ = ( + "ConnectionError", + "ConnectTimeout", + "HTTPError", + "JSONDecodeError", + "PreparedRequest", + "ReadTimeout", + "Request", + "RequestException", + "Response", + "Session", + "Timeout", + "TooManyRedirects", + "URLRequired", + "codes", + "delete", + "get", + "head", + "options", + "packages", + "patch", + "post", + "put", + "request", + "session", + "utils", +) + logging.getLogger(__name__).addHandler(NullHandler()) # FileModeWarnings go off per the default. diff --git a/libs/requests/__version__.py b/libs/requests/__version__.py index effdd98cf..fea814e58 100644 --- a/libs/requests/__version__.py +++ b/libs/requests/__version__.py @@ -5,8 +5,8 @@ __title__ = "requests" __description__ = "Python HTTP for Humans." __url__ = "https://requests.readthedocs.io" -__version__ = "2.32.5" -__build__ = 0x023205 +__version__ = "2.34.2" +__build__ = 0x023402 __author__ = "Kenneth Reitz" __author_email__ = "me@kennethreitz.org" __license__ = "Apache-2.0" diff --git a/libs/requests/_internal_utils.py b/libs/requests/_internal_utils.py index f2cf635e2..0466a7d34 100644 --- a/libs/requests/_internal_utils.py +++ b/libs/requests/_internal_utils.py @@ -5,14 +5,15 @@ requests._internal_utils Provides utility functions that are consumed internally by Requests which depend on extremely few external helpers (such as compat) """ + import re from .compat import builtin_str -_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*$") -_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$") -_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$") -_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$") +_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*\Z") +_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*\Z") +_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*\Z|^\Z") +_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*\Z|^\Z") _HEADER_VALIDATORS_STR = (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR) _HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE) @@ -22,7 +23,7 @@ HEADER_VALIDATORS = { } -def to_native_string(string, encoding="ascii"): +def to_native_string(string: str | bytes, encoding: str = "ascii") -> str: """Given a string object, regardless of type, returns a representation of that string in the native string type, encoding and decoding where necessary. This assumes ASCII unless told otherwise. @@ -35,7 +36,7 @@ def to_native_string(string, encoding="ascii"): return out -def unicode_is_ascii(u_string): +def unicode_is_ascii(u_string: str) -> bool: """Determine if unicode string only contains ASCII characters. :param str u_string: unicode string to check. Must be unicode diff --git a/libs/requests/_types.py b/libs/requests/_types.py new file mode 100644 index 000000000..b2273d612 --- /dev/null +++ b/libs/requests/_types.py @@ -0,0 +1,183 @@ +""" +requests._types +~~~~~~~~~~~~~~~ + +This module contains type aliases used internally by the Requests library. +These types are not part of the public API and must not be relied upon +by external code. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Protocol, + TypeAlias, + TypeVar, + runtime_checkable, +) + +_T_co = TypeVar("_T_co", covariant=True) +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) + + +@runtime_checkable +class SupportsRead(Protocol[_T_co]): + def read(self, length: int = ..., /) -> _T_co: ... + + +@runtime_checkable +class SupportsItems(Protocol[_KT_co, _VT_co]): + def items(self) -> Iterable[tuple[_KT_co, _VT_co]]: ... + + +# These are needed at runtime for default_hooks() return type +HookType: TypeAlias = Callable[["Response"], Any] +HooksInputType: TypeAlias = Mapping[str, Iterable[HookType] | HookType] + + +def is_prepared(request: PreparedRequest) -> TypeIs[_ValidatedRequest]: + """Verify a PreparedRequest has been fully prepared.""" + if TYPE_CHECKING: + return request.url is not None and request.method is not None + # noop at runtime to avoid AssertionError + return True + + +if TYPE_CHECKING: + from http.cookiejar import CookieJar + from typing import TypeAlias, TypedDict + + from typing_extensions import ( + Buffer, # TODO: move to collections.abc when Python >= 3.12 + TypeIs, # TODO: move to typing when Python >= 3.13 + ) + + from .auth import AuthBase + from .cookies import RequestsCookieJar + from .models import PreparedRequest, Response + from .structures import CaseInsensitiveDict + + class _ValidatedRequest(PreparedRequest): + """Subtype asserting a PreparedRequest has been fully prepared before calling. + + The override suppression is required because mutable attribute types are + invariant (Liskov), but we only narrow after preparation is complete. This + is the explicit contract for Requests but Python's typing doesn't have a + better way to represent the requirement. + """ + + url: str # type: ignore[reportIncompatibleVariableOverride] + method: str # type: ignore[reportIncompatibleVariableOverride] + + # Type aliases for core API concepts (ordered by request() signature) + UriType: TypeAlias = str | bytes + + _ParamsMappingKeyType: TypeAlias = str | bytes | int | float + _ParamsMappingValueType: TypeAlias = ( + str | bytes | int | float | Iterable[str | bytes | int | float] | None + ) + ParamsType: TypeAlias = ( + SupportsItems[_ParamsMappingKeyType, _ParamsMappingValueType] + | tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...] + | Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]] + | str + | bytes + | None + ) + + KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | SupportsItems[Any, Any] + + RawDataType: TypeAlias = KVDataType | str | bytes + StreamDataType: TypeAlias = SupportsRead[str | bytes] + EncodableDataType: TypeAlias = RawDataType | StreamDataType + + DataType: TypeAlias = ( + KVDataType + | Iterable[bytes | str] + | str + | bytes + | Buffer + | SupportsRead[str | bytes] + | None + ) + + BodyType: TypeAlias = ( + bytes | str | Iterable[bytes | str] | SupportsRead[bytes | str] | None + ) + + HeadersType: TypeAlias = Mapping[str, str | bytes] | None + + CookiesType: TypeAlias = RequestsCookieJar | Mapping[str, str] + + # Building blocks for FilesType + _FileName: TypeAlias = str | None + _FileContent: TypeAlias = SupportsRead[str | bytes] | str | bytes + _FileSpecBasic: TypeAlias = tuple[_FileName, _FileContent] + _FileSpecWithContentType: TypeAlias = tuple[_FileName, _FileContent, str] + _FileSpecWithHeaders: TypeAlias = tuple[ + _FileName, _FileContent, str, CaseInsensitiveDict[str] | Mapping[str, str] + ] + _FileSpec: TypeAlias = ( + _FileContent | _FileSpecBasic | _FileSpecWithContentType | _FileSpecWithHeaders + ) + FilesType: TypeAlias = ( + Mapping[str, _FileSpec] | Iterable[tuple[str, _FileSpec]] | None + ) + + AuthType: TypeAlias = ( + tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] | None + ) + + TimeoutType: TypeAlias = float | tuple[float | None, float | None] | None + ProxiesType: TypeAlias = MutableMapping[str, str] + HooksType: TypeAlias = dict[str, list[HookType]] | None + VerifyType: TypeAlias = bool | str + CertType: TypeAlias = str | tuple[str, str] | None + JsonType: TypeAlias = ( + None + | bool + | int + | float + | str + | Sequence["JsonType"] + | Mapping[str, "JsonType"] + ) + + # TypedDicts for Unpack kwargs (PEP 692) + + class BaseRequestKwargs(TypedDict, total=False): + headers: HeadersType + cookies: RequestsCookieJar | CookieJar | dict[str, str] | None + files: FilesType + auth: AuthType + timeout: TimeoutType + allow_redirects: bool + proxies: dict[str, str] | None + hooks: HooksInputType | None + stream: bool | None + verify: VerifyType | None + cert: CertType + + class RequestKwargs(BaseRequestKwargs, total=False): + """kwargs for request(), options(), head(), delete().""" + + params: ParamsType + data: DataType + json: JsonType + + class GetKwargs(BaseRequestKwargs, total=False): + data: DataType + json: JsonType + + class PostKwargs(BaseRequestKwargs, total=False): + params: ParamsType + + class DataKwargs(BaseRequestKwargs, total=False): + """kwargs for put(), patch().""" + + params: ParamsType + json: JsonType diff --git a/libs/requests/adapters.py b/libs/requests/adapters.py index 670c92767..40fe8a6d5 100644 --- a/libs/requests/adapters.py +++ b/libs/requests/adapters.py @@ -6,29 +6,34 @@ This module contains the transport adapters that Requests uses to define and maintain connections. """ +from __future__ import annotations + import os.path -import socket # noqa: F401 +import socket # noqa: F401 # type: ignore[reportUnusedImport] import typing import warnings +from typing import Any -from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError -from urllib3.exceptions import HTTPError as _HTTPError -from urllib3.exceptions import InvalidHeader as _InvalidHeader from urllib3.exceptions import ( + ClosedPoolError, + ConnectTimeoutError, LocationValueError, MaxRetryError, NewConnectionError, ProtocolError, + ReadTimeoutError, + ResponseError, ) +from urllib3.exceptions import HTTPError as _HTTPError +from urllib3.exceptions import InvalidHeader as _InvalidHeader from urllib3.exceptions import ProxyError as _ProxyError -from urllib3.exceptions import ReadTimeoutError, ResponseError from urllib3.exceptions import SSLError as _SSLError from urllib3.poolmanager import PoolManager, proxy_from_url from urllib3.util import Timeout as TimeoutSauce from urllib3.util import parse_url from urllib3.util.retry import Retry -from .auth import _basic_auth_str +from .auth import _basic_auth_str # type: ignore[reportPrivateUsage] from .compat import basestring, urlparse from .cookies import extract_cookies_to_jar from .exceptions import ( @@ -47,7 +52,6 @@ from .models import Response from .structures import CaseInsensitiveDict from .utils import ( DEFAULT_CA_BUNDLE_PATH, - extract_zipped_paths, get_auth_from_url, get_encoding_from_headers, prepend_scheme_if_needed, @@ -56,16 +60,21 @@ from .utils import ( ) try: - from urllib3.contrib.socks import SOCKSProxyManager + from urllib3.contrib.socks import SOCKSProxyManager # type: ignore[assignment] except ImportError: - def SOCKSProxyManager(*args, **kwargs): + def SOCKSProxyManager(*args: Any, **kwargs: Any) -> None: raise InvalidSchema("Missing dependencies for SOCKS support.") if typing.TYPE_CHECKING: + from urllib3.connectionpool import HTTPConnectionPool + from urllib3.poolmanager import PoolManager as _PoolManager + + from . import _types as _t from .models import PreparedRequest +from ._types import is_prepared as _is_prepared DEFAULT_POOLBLOCK = False DEFAULT_POOLSIZE = 10 @@ -74,13 +83,13 @@ DEFAULT_POOL_TIMEOUT = None def _urllib3_request_context( - request: "PreparedRequest", - verify: "bool | str | None", - client_cert: "typing.Tuple[str, str] | str | None", - poolmanager: "PoolManager", -) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])": - host_params = {} - pool_kwargs = {} + request: PreparedRequest, + verify: bool | str | None, + client_cert: tuple[str, str] | str | None, + poolmanager: PoolManager, +) -> tuple[dict[str, Any], dict[str, Any]]: + host_params: dict[str, Any] = {} + pool_kwargs: dict[str, Any] = {} parsed_request_url = urlparse(request.url) scheme = parsed_request_url.scheme.lower() port = parsed_request_url.port @@ -113,12 +122,18 @@ def _urllib3_request_context( class BaseAdapter: """The Base Transport Adapter""" - def __init__(self): + def __init__(self) -> None: super().__init__() def send( - self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None - ): + self, + request: PreparedRequest, + stream: bool = False, + timeout: _t.TimeoutType = None, + verify: _t.VerifyType = True, + cert: _t.CertType = None, + proxies: dict[str, str] | None = None, + ) -> Response: """Sends PreparedRequest object. Returns Response object. :param request: The :class:`PreparedRequest ` being sent. @@ -135,7 +150,7 @@ class BaseAdapter: """ raise NotImplementedError - def close(self): + def close(self) -> None: """Cleans up adapter specific items.""" raise NotImplementedError @@ -167,7 +182,7 @@ class HTTPAdapter(BaseAdapter): >>> s.mount('http://', a) """ - __attrs__ = [ + __attrs__: list[str] = [ "max_retries", "config", "_pool_connections", @@ -175,13 +190,21 @@ class HTTPAdapter(BaseAdapter): "_pool_block", ] + max_retries: Retry + config: dict[str, Any] + proxy_manager: dict[str, Any] + _pool_connections: int + _pool_maxsize: int + _pool_block: bool + poolmanager: _PoolManager + def __init__( self, - pool_connections=DEFAULT_POOLSIZE, - pool_maxsize=DEFAULT_POOLSIZE, - max_retries=DEFAULT_RETRIES, - pool_block=DEFAULT_POOLBLOCK, - ): + pool_connections: int = DEFAULT_POOLSIZE, + pool_maxsize: int = DEFAULT_POOLSIZE, + max_retries: int | Retry = DEFAULT_RETRIES, + pool_block: bool = DEFAULT_POOLBLOCK, + ) -> None: if max_retries == DEFAULT_RETRIES: self.max_retries = Retry(0, read=False) else: @@ -197,10 +220,10 @@ class HTTPAdapter(BaseAdapter): self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block) - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: return {attr: getattr(self, attr, None) for attr in self.__attrs__} - def __setstate__(self, state): + def __setstate__(self, state: dict[str, Any]) -> None: # Can't handle by adding 'proxy_manager' to self.__attrs__ because # self.poolmanager uses a lambda function, which isn't pickleable. self.proxy_manager = {} @@ -214,8 +237,12 @@ class HTTPAdapter(BaseAdapter): ) def init_poolmanager( - self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs - ): + self, + connections: int, + maxsize: int, + block: bool = DEFAULT_POOLBLOCK, + **pool_kwargs: Any, + ) -> None: """Initializes a urllib3 PoolManager. This method should not be called from user code, and is only @@ -239,7 +266,7 @@ class HTTPAdapter(BaseAdapter): **pool_kwargs, ) - def proxy_manager_for(self, proxy, **proxy_kwargs): + def proxy_manager_for(self, proxy: str, **proxy_kwargs: Any) -> Any: """Return urllib3 ProxyManager for the given proxy. This method should not be called from user code, and is only @@ -277,7 +304,9 @@ class HTTPAdapter(BaseAdapter): return manager - def cert_verify(self, conn, url, verify, cert): + def cert_verify( + self, conn: Any, url: str, verify: _t.VerifyType, cert: _t.CertType + ) -> None: """Verify a SSL certificate. This method should not be called from user code, and is only exposed for use when subclassing the :class:`HTTPAdapter `. @@ -297,7 +326,7 @@ class HTTPAdapter(BaseAdapter): cert_loc = verify if not cert_loc: - cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) + cert_loc = DEFAULT_CA_BUNDLE_PATH if not cert_loc or not os.path.exists(cert_loc): raise OSError( @@ -333,7 +362,7 @@ class HTTPAdapter(BaseAdapter): f"Could not find the TLS key file, invalid path: {conn.key_file}" ) - def build_response(self, req, resp): + def build_response(self, req: PreparedRequest, resp: Any) -> Response: """Builds a :class:`Response ` object from a urllib3 response. This should not be called from user code, and is only exposed for use when subclassing the @@ -343,10 +372,11 @@ class HTTPAdapter(BaseAdapter): :param resp: The urllib3 response object. :rtype: requests.Response """ + assert _is_prepared(req) response = Response() # Fallback to None if there's no status_code, for whatever reason. - response.status_code = getattr(resp, "status", None) + response.status_code = getattr(resp, "status", None) # type: ignore[assignment] # Make headers case-insensitive. response.headers = CaseInsensitiveDict(getattr(resp, "headers", {})) @@ -370,7 +400,9 @@ class HTTPAdapter(BaseAdapter): return response - def build_connection_pool_key_attributes(self, request, verify, cert=None): + def build_connection_pool_key_attributes( + self, request: PreparedRequest, verify: _t.VerifyType, cert: _t.CertType = None + ) -> tuple[dict[str, Any], dict[str, Any]]: """Build the PoolKey attributes used by urllib3 to return a connection. This looks at the PreparedRequest, the user-specified verify value, @@ -400,7 +432,7 @@ class HTTPAdapter(BaseAdapter): alter the other keys to ensure the desired behaviour. :param request: - The PreparedReqest being sent over the connection. + The PreparedRequest being sent over the connection. :type request: :class:`~requests.models.PreparedRequest` :param verify: @@ -420,7 +452,13 @@ class HTTPAdapter(BaseAdapter): """ return _urllib3_request_context(request, verify, cert, self.poolmanager) - def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None): + def get_connection_with_tls_context( + self, + request: PreparedRequest, + verify: _t.VerifyType, + proxies: dict[str, str] | None = None, + cert: _t.CertType = None, + ) -> HTTPConnectionPool: """Returns a urllib3 connection for the given request and TLS settings. This should not be called from user code, and is only exposed for use when subclassing the :class:`HTTPAdapter `. @@ -438,8 +476,10 @@ class HTTPAdapter(BaseAdapter): (optional) Any user-provided SSL certificate to be used for client authentication (a.k.a., mTLS). :rtype: - urllib3.ConnectionPool + urllib3.HTTPConnectionPool """ + assert _is_prepared(request) + proxy = select_proxy(request.url, proxies) try: host_params, pool_kwargs = self.build_connection_pool_key_attributes( @@ -469,7 +509,9 @@ class HTTPAdapter(BaseAdapter): return conn - def get_connection(self, url, proxies=None): + def get_connection( + self, url: str, proxies: dict[str, str] | None = None + ) -> HTTPConnectionPool: """DEPRECATED: Users should move to `get_connection_with_tls_context` for all subclasses of HTTPAdapter using Requests>=2.32.2. @@ -479,7 +521,7 @@ class HTTPAdapter(BaseAdapter): :param url: The URL to connect to. :param proxies: (optional) A Requests-style dictionary of proxies used on this request. - :rtype: urllib3.ConnectionPool + :rtype: urllib3.HTTPConnectionPool """ warnings.warn( ( @@ -510,7 +552,7 @@ class HTTPAdapter(BaseAdapter): return conn - def close(self): + def close(self) -> None: """Disposes of any internal state. Currently, this closes the PoolManager and any active ProxyManager, @@ -520,7 +562,9 @@ class HTTPAdapter(BaseAdapter): for proxy in self.proxy_manager.values(): proxy.clear() - def request_url(self, request, proxies): + def request_url( + self, request: PreparedRequest, proxies: dict[str, str] | None + ) -> str: """Obtain the url to use when making the final request. If the message is being sent through a HTTP proxy, the full URL has to @@ -534,6 +578,8 @@ class HTTPAdapter(BaseAdapter): :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs. :rtype: str """ + assert _is_prepared(request) + proxy = select_proxy(request.url, proxies) scheme = urlparse(request.url).scheme @@ -544,15 +590,13 @@ class HTTPAdapter(BaseAdapter): using_socks_proxy = proxy_scheme.startswith("socks") url = request.path_url - if url.startswith("//"): # Don't confuse urllib3 - url = f"/{url.lstrip('/')}" if is_proxied_http_request and not using_socks_proxy: url = urldefragauth(request.url) return url - def add_headers(self, request, **kwargs): + def add_headers(self, request: PreparedRequest, **kwargs: Any) -> None: """Add any headers needed by the connection. As of v2.0 this does nothing by default, but is left for overriding by users that subclass the :class:`HTTPAdapter `. @@ -566,7 +610,7 @@ class HTTPAdapter(BaseAdapter): """ pass - def proxy_headers(self, proxy): + def proxy_headers(self, proxy: str) -> dict[str, str]: """Returns a dictionary of the headers to add to any request sent through a proxy. This works with urllib3 magic to ensure that they are correctly sent to the proxy, rather than in a tunnelled request if @@ -579,7 +623,7 @@ class HTTPAdapter(BaseAdapter): :param proxy: The url of the proxy being used for this request. :rtype: dict """ - headers = {} + headers: dict[str, str] = {} username, password = get_auth_from_url(proxy) if username: @@ -588,8 +632,14 @@ class HTTPAdapter(BaseAdapter): return headers def send( - self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None - ): + self, + request: PreparedRequest, + stream: bool = False, + timeout: _t.TimeoutType = None, + verify: _t.VerifyType = True, + cert: _t.CertType = None, + proxies: dict[str, str] | None = None, + ) -> Response: """Sends PreparedRequest object. Returns Response object. :param request: The :class:`PreparedRequest ` being sent. @@ -606,6 +656,8 @@ class HTTPAdapter(BaseAdapter): :rtype: requests.Response """ + assert _is_prepared(request) + try: conn = self.get_connection_with_tls_context( request, verify, proxies=proxies, cert=cert @@ -629,29 +681,29 @@ class HTTPAdapter(BaseAdapter): if isinstance(timeout, tuple): try: connect, read = timeout - timeout = TimeoutSauce(connect=connect, read=read) + resolved_timeout = TimeoutSauce(connect=connect, read=read) except ValueError: raise ValueError( f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, " f"or a single float to set both timeouts to the same value." ) elif isinstance(timeout, TimeoutSauce): - pass + resolved_timeout = timeout else: - timeout = TimeoutSauce(connect=timeout, read=timeout) + resolved_timeout = TimeoutSauce(connect=timeout, read=timeout) try: resp = conn.urlopen( method=request.method, url=url, - body=request.body, - headers=request.headers, + body=request.body, # type: ignore[arg-type] # urllib3 stubs don't accept Iterable[bytes | str] + headers=request.headers, # type: ignore[arg-type] # urllib3#3072 redirect=False, assert_same_host=False, preload_content=False, decode_content=False, retries=self.max_retries, - timeout=timeout, + timeout=resolved_timeout, chunked=chunked, ) diff --git a/libs/requests/api.py b/libs/requests/api.py index 596074455..eeb3b54d7 100644 --- a/libs/requests/api.py +++ b/libs/requests/api.py @@ -8,10 +8,22 @@ This module implements the Requests API. :license: Apache2, see LICENSE for more details. """ +from __future__ import annotations + +from typing import TYPE_CHECKING + from . import sessions +from .models import Response + +if TYPE_CHECKING: + from typing_extensions import Unpack + + from . import _types as _t -def request(method, url, **kwargs): +def request( + method: str, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs] +) -> Response: """Constructs and sends a :class:`Request `. :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``. @@ -59,7 +71,9 @@ def request(method, url, **kwargs): return session.request(method=method, url=url, **kwargs) -def get(url, params=None, **kwargs): +def get( + url: _t.UriType, params: _t.ParamsType = None, **kwargs: Unpack[_t.GetKwargs] +) -> Response: r"""Sends a GET request. :param url: URL for the new :class:`Request` object. @@ -73,7 +87,7 @@ def get(url, params=None, **kwargs): return request("get", url, params=params, **kwargs) -def options(url, **kwargs): +def options(url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: r"""Sends an OPTIONS request. :param url: URL for the new :class:`Request` object. @@ -85,7 +99,7 @@ def options(url, **kwargs): return request("options", url, **kwargs) -def head(url, **kwargs): +def head(url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: r"""Sends a HEAD request. :param url: URL for the new :class:`Request` object. @@ -100,7 +114,12 @@ def head(url, **kwargs): return request("head", url, **kwargs) -def post(url, data=None, json=None, **kwargs): +def post( + url: _t.UriType, + data: _t.DataType = None, + json: _t.JsonType = None, + **kwargs: Unpack[_t.PostKwargs], +) -> Response: r"""Sends a POST request. :param url: URL for the new :class:`Request` object. @@ -115,7 +134,9 @@ def post(url, data=None, json=None, **kwargs): return request("post", url, data=data, json=json, **kwargs) -def put(url, data=None, **kwargs): +def put( + url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs] +) -> Response: r"""Sends a PUT request. :param url: URL for the new :class:`Request` object. @@ -130,7 +151,9 @@ def put(url, data=None, **kwargs): return request("put", url, data=data, **kwargs) -def patch(url, data=None, **kwargs): +def patch( + url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs] +) -> Response: r"""Sends a PATCH request. :param url: URL for the new :class:`Request` object. @@ -145,7 +168,7 @@ def patch(url, data=None, **kwargs): return request("patch", url, data=data, **kwargs) -def delete(url, **kwargs): +def delete(url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: r"""Sends a DELETE request. :param url: URL for the new :class:`Request` object. diff --git a/libs/requests/auth.py b/libs/requests/auth.py index 4a7ce6dc1..2af481dbf 100644 --- a/libs/requests/auth.py +++ b/libs/requests/auth.py @@ -5,6 +5,8 @@ requests.auth This module contains the authentication handlers for Requests. """ +from __future__ import annotations + import hashlib import os import re @@ -12,17 +14,24 @@ import threading import time import warnings from base64 import b64encode +from typing import TYPE_CHECKING, Any, Final, cast, overload from ._internal_utils import to_native_string from .compat import basestring, str, urlparse from .cookies import extract_cookies_to_jar from .utils import parse_dict_header -CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded" -CONTENT_TYPE_MULTI_PART = "multipart/form-data" +if TYPE_CHECKING: + from http.cookiejar import CookieJar + from typing import Any + + from .models import PreparedRequest, Response + +CONTENT_TYPE_FORM_URLENCODED: Final = "application/x-www-form-urlencoded" +CONTENT_TYPE_MULTI_PART: Final = "multipart/form-data" -def _basic_auth_str(username, password): +def _basic_auth_str(username: bytes | str, password: bytes | str) -> str: """Returns a Basic Auth string.""" # "I want us to put a big-ol' comment on top of it that @@ -32,22 +41,22 @@ def _basic_auth_str(username, password): # # These are here solely to maintain backwards compatibility # for things like ints. This will be removed in 3.0.0. - if not isinstance(username, basestring): + if not isinstance(username, basestring): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for non-str/bytes warnings.warn( "Non-string usernames will no longer be supported in Requests " - "3.0.0. Please convert the object you've passed in ({!r}) to " + f"3.0.0. Please convert the object you've passed in ({username!r}) to " "a string or bytes object in the near future to avoid " - "problems.".format(username), + "problems.", category=DeprecationWarning, ) username = str(username) - if not isinstance(password, basestring): + if not isinstance(password, basestring): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for non-str/bytes warnings.warn( "Non-string passwords will no longer be supported in Requests " - "3.0.0. Please convert the object you've passed in ({!r}) to " + f"3.0.0. Please convert the object you've passed in ({type(password)!r}) to " "a string or bytes object in the near future to avoid " - "problems.".format(type(password)), + "problems.", category=DeprecationWarning, ) password = str(password) @@ -69,18 +78,26 @@ def _basic_auth_str(username, password): class AuthBase: """Base class that all auth implementations derive from""" - def __call__(self, r): + def __call__(self, r: PreparedRequest) -> PreparedRequest: raise NotImplementedError("Auth hooks must be callable.") class HTTPBasicAuth(AuthBase): """Attaches HTTP Basic Authentication to the given Request object.""" - def __init__(self, username, password): + username: bytes | str + password: bytes | str + + @overload + def __init__(self, username: str, password: str) -> None: ... + @overload + def __init__(self, username: bytes, password: bytes) -> None: ... + + def __init__(self, username: bytes | str, password: bytes | str) -> None: self.username = username self.password = password - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return all( [ self.username == getattr(other, "username", None), @@ -88,10 +105,10 @@ class HTTPBasicAuth(AuthBase): ] ) - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return not self == other - def __call__(self, r): + def __call__(self, r: PreparedRequest) -> PreparedRequest: r.headers["Authorization"] = _basic_auth_str(self.username, self.password) return r @@ -99,7 +116,7 @@ class HTTPBasicAuth(AuthBase): class HTTPProxyAuth(HTTPBasicAuth): """Attaches HTTP Proxy Authentication to a given Request object.""" - def __call__(self, r): + def __call__(self, r: PreparedRequest) -> PreparedRequest: r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password) return r @@ -107,13 +124,27 @@ class HTTPProxyAuth(HTTPBasicAuth): class HTTPDigestAuth(AuthBase): """Attaches HTTP Digest Authentication to the given Request object.""" - def __init__(self, username, password): + username: bytes | str + password: bytes | str + _thread_local: threading.local + last_nonce: str + nonce_count: int + chal: dict[str, str] + pos: int | None + num_401_calls: int | None + + @overload + def __init__(self, username: str, password: str) -> None: ... + @overload + def __init__(self, username: bytes, password: bytes) -> None: ... + + def __init__(self, username: bytes | str, password: bytes | str) -> None: self.username = username self.password = password # Keep state in per-thread local storage self._thread_local = threading.local() - def init_per_thread_state(self): + def init_per_thread_state(self) -> None: # Ensure state is initialized just once per-thread if not hasattr(self._thread_local, "init"): self._thread_local.init = True @@ -123,7 +154,7 @@ class HTTPDigestAuth(AuthBase): self._thread_local.pos = None self._thread_local.num_401_calls = None - def build_digest_header(self, method, url): + def build_digest_header(self, method: str, url: str) -> str | None: """ :rtype: str """ @@ -142,42 +173,43 @@ class HTTPDigestAuth(AuthBase): # lambdas assume digest modules are imported at the top level if _algorithm == "MD5" or _algorithm == "MD5-SESS": - def md5_utf8(x): + def md5_utf8(x: str | bytes) -> str: if isinstance(x, str): x = x.encode("utf-8") - return hashlib.md5(x).hexdigest() + return hashlib.md5(x, usedforsecurity=False).hexdigest() hash_utf8 = md5_utf8 elif _algorithm == "SHA": - def sha_utf8(x): + def sha_utf8(x: str | bytes) -> str: if isinstance(x, str): x = x.encode("utf-8") - return hashlib.sha1(x).hexdigest() + return hashlib.sha1(x, usedforsecurity=False).hexdigest() hash_utf8 = sha_utf8 elif _algorithm == "SHA-256": - def sha256_utf8(x): + def sha256_utf8(x: str | bytes) -> str: if isinstance(x, str): x = x.encode("utf-8") - return hashlib.sha256(x).hexdigest() + return hashlib.sha256(x, usedforsecurity=False).hexdigest() hash_utf8 = sha256_utf8 elif _algorithm == "SHA-512": - def sha512_utf8(x): + def sha512_utf8(x: str | bytes) -> str: if isinstance(x, str): x = x.encode("utf-8") - return hashlib.sha512(x).hexdigest() + return hashlib.sha512(x, usedforsecurity=False).hexdigest() hash_utf8 = sha512_utf8 - KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731 - if hash_utf8 is None: return None + def KD(s: str, d: str) -> str: + return hash_utf8(f"{s}:{d}") + # XXX not implemented yet entdig = None p_parsed = urlparse(url) @@ -202,9 +234,9 @@ class HTTPDigestAuth(AuthBase): s += time.ctime().encode("utf-8") s += os.urandom(8) - cnonce = hashlib.sha1(s).hexdigest()[:16] + cnonce = hashlib.sha1(s, usedforsecurity=False).hexdigest()[:16] if _algorithm == "MD5-SESS": - HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}") + HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}") # type: ignore[reportConstantRedefinition] # RFC 2617 terminology if not qop: respdig = KD(HA1, f"{nonce}:{HA2}") @@ -233,12 +265,12 @@ class HTTPDigestAuth(AuthBase): return f"Digest {base}" - def handle_redirect(self, r, **kwargs): + def handle_redirect(self, r: Response, **kwargs: Any) -> None: """Reset num_401_calls counter on redirects.""" if r.is_redirect: self._thread_local.num_401_calls = 1 - def handle_401(self, r, **kwargs): + def handle_401(self, r: Response, **kwargs: Any) -> Response: """ Takes the given response and tries digest-auth, if needed. @@ -254,7 +286,8 @@ class HTTPDigestAuth(AuthBase): if self._thread_local.pos is not None: # Rewind the file position indicator of the body to where # it was to resend the request. - r.request.body.seek(self._thread_local.pos) + if (seek := getattr(r.request.body, "seek", None)) is not None: + seek(self._thread_local.pos) s_auth = r.headers.get("www-authenticate", "") if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2: @@ -267,12 +300,15 @@ class HTTPDigestAuth(AuthBase): r.content r.close() prep = r.request.copy() - extract_cookies_to_jar(prep._cookies, r.request, r.raw) - prep.prepare_cookies(prep._cookies) + cookie_jar = cast("CookieJar", prep._cookies) # type: ignore[reportPrivateUsage] + extract_cookies_to_jar(cookie_jar, r.request, r.raw) + prep.prepare_cookies(cookie_jar) - prep.headers["Authorization"] = self.build_digest_header( - prep.method, prep.url + _digest_auth = self.build_digest_header( + cast(str, prep.method), cast(str, prep.url) ) + if _digest_auth: + prep.headers["Authorization"] = _digest_auth _r = r.connection.send(prep, **kwargs) _r.history.append(r) _r.request = prep @@ -282,15 +318,19 @@ class HTTPDigestAuth(AuthBase): self._thread_local.num_401_calls = 1 return r - def __call__(self, r): + def __call__(self, r: PreparedRequest) -> PreparedRequest: # Initialize per-thread state, if needed self.init_per_thread_state() # If we have a saved nonce, skip the 401 if self._thread_local.last_nonce: - r.headers["Authorization"] = self.build_digest_header(r.method, r.url) - try: - self._thread_local.pos = r.body.tell() - except AttributeError: + _digest_auth = self.build_digest_header( + cast(str, r.method), cast(str, r.url) + ) + if _digest_auth: + r.headers["Authorization"] = _digest_auth + if (tell := getattr(r.body, "tell", None)) is not None: + self._thread_local.pos = tell() + else: # In the case of HTTPDigestAuth being reused and the body of # the previous request was a file-like object, pos has the # file position of the previous body. Ensure it's set to @@ -302,7 +342,7 @@ class HTTPDigestAuth(AuthBase): return r - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return all( [ self.username == getattr(other, "username", None), @@ -310,5 +350,5 @@ class HTTPDigestAuth(AuthBase): ] ) - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return not self == other diff --git a/libs/requests/certs.py b/libs/requests/certs.py index be422c3e9..4f85ac070 100644 --- a/libs/requests/certs.py +++ b/libs/requests/certs.py @@ -11,6 +11,7 @@ If you are packaging Requests, e.g., for a Linux distribution or a managed environment, you can change the definition of where() to return a separately packaged CA bundle. """ + from certifi import where if __name__ == "__main__": diff --git a/libs/requests/compat.py b/libs/requests/compat.py index 7f9d75435..deab3c091 100644 --- a/libs/requests/compat.py +++ b/libs/requests/compat.py @@ -7,13 +7,20 @@ between Python 2 and Python 3. It remains for backwards compatibility until the next major version. """ +# pyright: reportUnusedImport=false + +from __future__ import annotations + import importlib import sys +from types import ModuleType # ------- # urllib3 # ------- -from urllib3 import __version__ as urllib3_version +from urllib3 import ( + __version__ as urllib3_version, # type: ignore[reportPrivateImportUsage] +) # Detect which major version of urllib3 is being used. try: @@ -27,7 +34,7 @@ except (TypeError, AttributeError): # ------------------- -def _resolve_char_detection(): +def _resolve_char_detection() -> ModuleType | None: """Find supported character detection libraries.""" chardet = None for lib in ("chardet", "charset_normalizer"): @@ -57,14 +64,14 @@ is_py3 = _ver[0] == 3 # json/simplejson module import resolution has_simplejson = False try: - import simplejson as json + import simplejson as json # type: ignore[import-not-found] has_simplejson = True except ImportError: import json if has_simplejson: - from simplejson import JSONDecodeError + from simplejson import JSONDecodeError # type: ignore[import-not-found] else: from json import JSONDecodeError @@ -95,7 +102,7 @@ from urllib.request import ( getproxies_environment, parse_http_list, proxy_bypass, - proxy_bypass_environment, + proxy_bypass_environment, # type: ignore[attr-defined] # https://github.com/python/cpython/issues/145331 ) builtin_str = str diff --git a/libs/requests/cookies.py b/libs/requests/cookies.py index f69d0cda9..2e3fc2150 100644 --- a/libs/requests/cookies.py +++ b/libs/requests/cookies.py @@ -7,21 +7,29 @@ Compatibility code to be able to use `http.cookiejar.CookieJar` with requests. requests.utils imports from here, so be careful with imports. """ +from __future__ import annotations + import calendar import copy import time +from collections.abc import Iterator, MutableMapping +from http.cookiejar import Cookie, CookieJar, CookiePolicy +from typing import TYPE_CHECKING, Any, TypeVar, overload from ._internal_utils import to_native_string -from .compat import Morsel, MutableMapping, cookielib, urlparse, urlunparse +from ._types import is_prepared as _is_prepared +from .compat import Morsel, cookielib, urlparse, urlunparse -try: - import threading -except ImportError: - import dummy_threading as threading +if TYPE_CHECKING: + from _typeshed import SupportsKeysAndGetItem + + from .models import PreparedRequest + +import threading class MockRequest: - """Wraps a `requests.Request` to mimic a `urllib2.Request`. + """Wraps a `requests.PreparedRequest` to mimic a `urllib2.Request`. The code in `http.cookiejar.CookieJar` expects this interface in order to correctly manage cookie policies, i.e., determine whether a cookie can be set, given the @@ -32,21 +40,24 @@ class MockRequest: probably want `get_cookie_header`, defined below. """ - def __init__(self, request): + type: str + + def __init__(self, request: PreparedRequest) -> None: + assert _is_prepared(request) self._r = request - self._new_headers = {} + self._new_headers: dict[str, str] = {} self.type = urlparse(self._r.url).scheme - def get_type(self): + def get_type(self) -> str: return self.type - def get_host(self): + def get_host(self) -> str: return urlparse(self._r.url).netloc - def get_origin_req_host(self): + def get_origin_req_host(self) -> str: return self.get_host() - def get_full_url(self): + def get_full_url(self) -> str: # Only return the response's URL if the user hadn't set the Host # header if not self._r.headers.get("Host"): @@ -66,37 +77,37 @@ class MockRequest: ] ) - def is_unverifiable(self): + def is_unverifiable(self) -> bool: return True - def has_header(self, name): + def has_header(self, name: str) -> bool: return name in self._r.headers or name in self._new_headers - def get_header(self, name, default=None): - return self._r.headers.get(name, self._new_headers.get(name, default)) + def get_header(self, name: str, default: str | None = None) -> str | None: + return self._r.headers.get(name, self._new_headers.get(name, default)) # type: ignore[return-value] - def add_header(self, key, val): + def add_header(self, key: str, val: str) -> None: """cookiejar has no legitimate use for this method; add it back if you find one.""" raise NotImplementedError( "Cookie headers should be added with add_unredirected_header()" ) - def add_unredirected_header(self, name, value): + def add_unredirected_header(self, name: str, value: str) -> None: self._new_headers[name] = value - def get_new_headers(self): + def get_new_headers(self) -> dict[str, str]: return self._new_headers @property - def unverifiable(self): + def unverifiable(self) -> bool: return self.is_unverifiable() @property - def origin_req_host(self): + def origin_req_host(self) -> str: return self.get_origin_req_host() @property - def host(self): + def host(self) -> str: return self.get_host() @@ -107,21 +118,23 @@ class MockResponse: the way `http.cookiejar` expects to see them. """ - def __init__(self, headers): + def __init__(self, headers: Any) -> None: """Make a MockResponse for `cookiejar` to read. :param headers: a httplib.HTTPMessage or analogous carrying the headers """ self._headers = headers - def info(self): + def info(self) -> Any: return self._headers - def getheaders(self, name): + def getheaders(self, name: str) -> Any: self._headers.getheaders(name) -def extract_cookies_to_jar(jar, request, response): +def extract_cookies_to_jar( + jar: CookieJar, request: PreparedRequest, response: Any +) -> None: """Extract the cookies from the response into a CookieJar. :param jar: http.cookiejar.CookieJar (not necessarily a RequestsCookieJar) @@ -134,26 +147,28 @@ def extract_cookies_to_jar(jar, request, response): req = MockRequest(request) # pull out the HTTPMessage with the headers and put it in the mock: res = MockResponse(response._original_response.msg) - jar.extract_cookies(res, req) + jar.extract_cookies(res, req) # type: ignore[arg-type] -def get_cookie_header(jar, request): +def get_cookie_header(jar: CookieJar, request: PreparedRequest) -> str | None: """ Produce an appropriate Cookie header string to be sent with `request`, or None. :rtype: str """ r = MockRequest(request) - jar.add_cookie_header(r) + jar.add_cookie_header(r) # type: ignore[arg-type] return r.get_new_headers().get("Cookie") -def remove_cookie_by_name(cookiejar, name, domain=None, path=None): +def remove_cookie_by_name( + cookiejar: CookieJar, name: str, domain: str | None = None, path: str | None = None +) -> None: """Unsets a cookie by name, by default over all domains and paths. Wraps CookieJar.clear(), is O(n). """ - clearables = [] + clearables: list[tuple[str, str, str]] = [] for cookie in cookiejar: if cookie.name != name: continue @@ -173,7 +188,7 @@ class CookieConflictError(RuntimeError): """ -class RequestsCookieJar(cookielib.CookieJar, MutableMapping): +class RequestsCookieJar(CookieJar, MutableMapping[str, str | None]): # type: ignore[misc] """Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict interface. @@ -191,7 +206,15 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): .. warning:: dictionary operations that are normally O(1) may be O(n). """ - def get(self, name, default=None, domain=None, path=None): + _policy: CookiePolicy + + def get( # type: ignore[override] + self, + name: str, + default: str | None = None, + domain: str | None = None, + path: str | None = None, + ) -> str | None: """Dict-like get() that also supports optional domain and path args in order to resolve naming collisions from using one cookie jar over multiple domains. @@ -203,7 +226,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): except KeyError: return default - def set(self, name, value, **kwargs): + def set( + self, name: str, value: str | Morsel[dict[str, str]] | None, **kwargs: Any + ) -> Cookie | None: """Dict-like set() that also supports optional domain and path args in order to resolve naming collisions from using one cookie jar over multiple domains. @@ -222,7 +247,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): self.set_cookie(c) return c - def iterkeys(self): + def iterkeys(self) -> Iterator[str]: """Dict-like iterkeys() that returns an iterator of names of cookies from the jar. @@ -231,7 +256,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): for cookie in iter(self): yield cookie.name - def keys(self): + def keys(self) -> list[str]: # type: ignore[override] """Dict-like keys() that returns a list of names of cookies from the jar. @@ -239,7 +264,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): """ return list(self.iterkeys()) - def itervalues(self): + def itervalues(self) -> Iterator[str | None]: """Dict-like itervalues() that returns an iterator of values of cookies from the jar. @@ -248,7 +273,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): for cookie in iter(self): yield cookie.value - def values(self): + def values(self) -> list[str | None]: # type: ignore[override] """Dict-like values() that returns a list of values of cookies from the jar. @@ -256,7 +281,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): """ return list(self.itervalues()) - def iteritems(self): + def iteritems(self) -> Iterator[tuple[str, str | None]]: """Dict-like iteritems() that returns an iterator of name-value tuples from the jar. @@ -265,7 +290,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): for cookie in iter(self): yield cookie.name, cookie.value - def items(self): + def items(self) -> list[tuple[str, str | None]]: # type: ignore[override] """Dict-like items() that returns a list of name-value tuples from the jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a vanilla python dict of key value pairs. @@ -274,43 +299,45 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): """ return list(self.iteritems()) - def list_domains(self): + def list_domains(self) -> list[str]: """Utility method to list all the domains in the jar.""" - domains = [] + domains: list[str] = [] for cookie in iter(self): if cookie.domain not in domains: domains.append(cookie.domain) return domains - def list_paths(self): + def list_paths(self) -> list[str]: """Utility method to list all the paths in the jar.""" - paths = [] + paths: list[str] = [] for cookie in iter(self): if cookie.path not in paths: paths.append(cookie.path) return paths - def multiple_domains(self): + def multiple_domains(self) -> bool: """Returns True if there are multiple domains in the jar. Returns False otherwise. :rtype: bool """ - domains = [] + domains: list[str] = [] for cookie in iter(self): - if cookie.domain is not None and cookie.domain in domains: + if cookie.domain is not None and cookie.domain in domains: # type: ignore[reportUnnecessaryComparison] # defensive check return True domains.append(cookie.domain) return False # there is only one domain in jar - def get_dict(self, domain=None, path=None): + def get_dict( + self, domain: str | None = None, path: str | None = None + ) -> dict[str, str | None]: """Takes as an argument an optional domain and path and returns a plain old Python dict of name-value pairs of cookies that meet the requirements. :rtype: dict """ - dictionary = {} + dictionary: dict[str, str | None] = {} for cookie in iter(self): if (domain is None or cookie.domain == domain) and ( path is None or cookie.path == path @@ -318,13 +345,17 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): dictionary[cookie.name] = cookie.value return dictionary - def __contains__(self, name): + def __iter__(self) -> Iterator[Cookie]: # type: ignore[override] + """RequestCookieJar's __iter__ comes from CookieJar not MutableMapping.""" + return super().__iter__() + + def __contains__(self, name: object) -> bool: try: return super().__contains__(name) except CookieConflictError: return True - def __getitem__(self, name): + def __getitem__(self, name: str) -> str | None: """Dict-like __getitem__() for compatibility with client code. Throws exception if there are more than one cookie with name. In that case, use the more explicit get() method instead. @@ -333,29 +364,33 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): """ return self._find_no_duplicates(name) - def __setitem__(self, name, value): + def __setitem__( + self, name: str, value: str | Morsel[dict[str, str]] | None + ) -> None: """Dict-like __setitem__ for compatibility with client code. Throws exception if there is already a cookie of that name in the jar. In that case, use the more explicit set() method instead. """ self.set(name, value) - def __delitem__(self, name): + def __delitem__(self, name: str) -> None: """Deletes a cookie given a name. Wraps ``http.cookiejar.CookieJar``'s ``remove_cookie_by_name()``. """ remove_cookie_by_name(self, name) - def set_cookie(self, cookie, *args, **kwargs): + def set_cookie(self, cookie: Cookie, *args: Any, **kwargs: Any) -> None: if ( - hasattr(cookie.value, "startswith") - and cookie.value.startswith('"') - and cookie.value.endswith('"') + (value := cookie.value) is not None + and value.startswith('"') + and value.endswith('"') ): - cookie.value = cookie.value.replace('\\"', "") + cookie.value = value.replace('\\"', "") return super().set_cookie(cookie, *args, **kwargs) - def update(self, other): + def update( # type: ignore[override] + self, other: CookieJar | SupportsKeysAndGetItem[str, str] + ) -> None: """Updates this jar with cookies from another CookieJar or dict-like""" if isinstance(other, cookielib.CookieJar): for cookie in other: @@ -363,7 +398,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): else: super().update(other) - def _find(self, name, domain=None, path=None): + def _find( + self, name: str, domain: str | None = None, path: str | None = None + ) -> str | None: """Requests uses this method internally to get cookie values. If there are conflicting cookies, _find arbitrarily chooses one. @@ -383,7 +420,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}") - def _find_no_duplicates(self, name, domain=None, path=None): + def _find_no_duplicates( + self, name: str, domain: str | None = None, path: str | None = None + ) -> str: """Both ``__get_item__`` and ``get`` call this function: it's never used elsewhere in Requests. @@ -408,42 +447,42 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping): # we will eventually return this as long as no cookie conflict toReturn = cookie.value - if toReturn: + if toReturn is not None: return toReturn raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}") - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: """Unlike a normal CookieJar, this class is pickleable.""" state = self.__dict__.copy() # remove the unpickleable RLock object state.pop("_cookies_lock") return state - def __setstate__(self, state): + def __setstate__(self, state: dict[str, Any]) -> None: """Unlike a normal CookieJar, this class is pickleable.""" self.__dict__.update(state) if "_cookies_lock" not in self.__dict__: self._cookies_lock = threading.RLock() - def copy(self): + def copy(self) -> RequestsCookieJar: """Return a copy of this RequestsCookieJar.""" new_cj = RequestsCookieJar() new_cj.set_policy(self.get_policy()) new_cj.update(self) return new_cj - def get_policy(self): + def get_policy(self) -> CookiePolicy: """Return the CookiePolicy instance used.""" return self._policy -def _copy_cookie_jar(jar): +def _copy_cookie_jar(jar: CookieJar | None) -> CookieJar | None: # type: ignore[reportUnusedFunction] # cross-module usage in models.py if jar is None: return None - if hasattr(jar, "copy"): + if copy_method := getattr(jar, "copy", None): # We're dealing with an instance of RequestsCookieJar - return jar.copy() + return copy_method() # We're dealing with a generic CookieJar instance new_jar = copy.copy(jar) new_jar.clear() @@ -452,13 +491,13 @@ def _copy_cookie_jar(jar): return new_jar -def create_cookie(name, value, **kwargs): +def create_cookie(name: str, value: str, **kwargs: Any) -> Cookie: """Make a cookie from underspecified parameters. By default, the pair of `name` and `value` will be set for the domain '' and sent on every request (this is sometimes called a "supercookie"). """ - result = { + result: dict[str, Any] = { "version": 0, "name": name, "value": value, @@ -489,10 +528,10 @@ def create_cookie(name, value, **kwargs): return cookielib.Cookie(**result) -def morsel_to_cookie(morsel): +def morsel_to_cookie(morsel: Morsel[Any]) -> Cookie: """Convert a Morsel object into a Cookie containing the one k/v pair.""" - expires = None + expires: int | None = None if morsel["max-age"]: try: expires = int(time.time() + int(morsel["max-age"])) @@ -518,7 +557,30 @@ def morsel_to_cookie(morsel): ) -def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True): +_CookieJarT = TypeVar("_CookieJarT", bound=CookieJar) + + +@overload +def cookiejar_from_dict( + cookie_dict: dict[str, str] | None, + cookiejar: None = None, + overwrite: bool = True, +) -> RequestsCookieJar: ... + + +@overload +def cookiejar_from_dict( + cookie_dict: dict[str, str] | None, + cookiejar: _CookieJarT, + overwrite: bool = True, +) -> _CookieJarT: ... + + +def cookiejar_from_dict( + cookie_dict: dict[str, str] | None, + cookiejar: CookieJar | None = None, + overwrite: bool = True, +) -> CookieJar: """Returns a CookieJar from a key/value dictionary. :param cookie_dict: Dict of key/values to insert into CookieJar. @@ -539,22 +601,24 @@ def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True): return cookiejar -def merge_cookies(cookiejar, cookies): +def merge_cookies( + cookiejar: CookieJar, cookies: dict[str, str] | CookieJar | None +) -> CookieJar: """Add cookies to cookiejar and returns a merged CookieJar. :param cookiejar: CookieJar object to add the cookies to. :param cookies: Dictionary or CookieJar object to be added. :rtype: CookieJar """ - if not isinstance(cookiejar, cookielib.CookieJar): + if not isinstance(cookiejar, cookielib.CookieJar): # type: ignore[reportUnnecessaryIsInstance] # runtime guard raise ValueError("You can only merge into CookieJar") if isinstance(cookies, dict): cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False) elif isinstance(cookies, cookielib.CookieJar): - try: - cookiejar.update(cookies) - except AttributeError: + if update_method := getattr(cookiejar, "update", None): + update_method(cookies) + else: for cookie_in_jar in cookies: cookiejar.set_cookie(cookie_in_jar) diff --git a/libs/requests/exceptions.py b/libs/requests/exceptions.py index 83986b489..cb5e9510e 100644 --- a/libs/requests/exceptions.py +++ b/libs/requests/exceptions.py @@ -4,23 +4,34 @@ requests.exceptions This module contains the set of Requests' exceptions. """ + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from urllib3.exceptions import HTTPError as BaseHTTPError from .compat import JSONDecodeError as CompatJSONDecodeError +if TYPE_CHECKING: + from .models import PreparedRequest, Request, Response + class RequestException(IOError): """There was an ambiguous exception that occurred while handling your request. """ - def __init__(self, *args, **kwargs): + response: Response | None + request: Request | PreparedRequest | None + + def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize RequestException with `request` and `response` objects.""" - response = kwargs.pop("response", None) + response: Response | None = kwargs.pop("response", None) self.response = response self.request = kwargs.pop("request", None) if response is not None and not self.request and hasattr(response, "request"): - self.request = self.response.request + self.request = response.request super().__init__(*args, **kwargs) @@ -31,7 +42,7 @@ class InvalidJSONError(RequestException): class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError): """Couldn't decode the text into json""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """ Construct the JSONDecodeError instance first with all args. Then use it's args to construct the IOError so that @@ -41,7 +52,7 @@ class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError): CompatJSONDecodeError.__init__(self, *args) InvalidJSONError.__init__(self, *self.args, **kwargs) - def __reduce__(self): + def __reduce__(self) -> tuple[Any, ...] | str: """ The __reduce__ method called when pickling the object must be the one from the JSONDecodeError (be it json/simplejson) diff --git a/libs/requests/help.py b/libs/requests/help.py index 8fbcd6560..9269cc712 100644 --- a/libs/requests/help.py +++ b/libs/requests/help.py @@ -1,9 +1,12 @@ """Module containing bug report helper(s).""" +# pyright: reportUnknownMemberType=false + import json import platform import ssl import sys +from typing import Any import idna import urllib3 @@ -16,7 +19,7 @@ except ImportError: charset_normalizer = None try: - import chardet + import chardet # type: ignore[import-not-found] except ImportError: chardet = None @@ -27,8 +30,8 @@ except ImportError: OpenSSL = None cryptography = None else: - import cryptography - import OpenSSL + import cryptography # type: ignore[import-not-found] + import OpenSSL # type: ignore[import-not-found] def _implementation(): @@ -47,14 +50,11 @@ def _implementation(): if implementation == "CPython": implementation_version = platform.python_version() elif implementation == "PyPy": - implementation_version = "{}.{}.{}".format( - sys.pypy_version_info.major, - sys.pypy_version_info.minor, - sys.pypy_version_info.micro, - ) - if sys.pypy_version_info.releaselevel != "final": + pypy = sys.pypy_version_info # type: ignore[attr-defined] + implementation_version = f"{pypy.major}.{pypy.minor}.{pypy.micro}" + if sys.pypy_version_info.releaselevel != "final": # type: ignore[attr-defined] implementation_version = "".join( - [implementation_version, sys.pypy_version_info.releaselevel] + [implementation_version, sys.pypy_version_info.releaselevel] # type: ignore[attr-defined] ) elif implementation == "Jython": implementation_version = platform.python_version() # Complete Guess @@ -66,7 +66,7 @@ def _implementation(): return {"name": implementation, "version": implementation_version} -def info(): +def info() -> dict[str, Any]: """Generate information for a bug report.""" try: platform_info = { @@ -80,15 +80,15 @@ def info(): } implementation_info = _implementation() - urllib3_info = {"version": urllib3.__version__} + urllib3_info = {"version": urllib3.__version__} # type: ignore[reportPrivateImportUsage] charset_normalizer_info = {"version": None} - chardet_info = {"version": None} + chardet_info: dict[str, str | None] = {"version": None} if charset_normalizer: charset_normalizer_info = {"version": charset_normalizer.__version__} if chardet: chardet_info = {"version": chardet.__version__} - pyopenssl_info = { + pyopenssl_info: dict[str, str | None] = { "version": None, "openssl_version": "", } @@ -105,7 +105,7 @@ def info(): } system_ssl = ssl.OPENSSL_VERSION_NUMBER - system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""} + system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""} # type: ignore[reportUnnecessaryComparison] return { "platform": platform_info, diff --git a/libs/requests/hooks.py b/libs/requests/hooks.py index d181ba2ec..11ff9e9f2 100644 --- a/libs/requests/hooks.py +++ b/libs/requests/hooks.py @@ -9,24 +9,39 @@ Available hooks: ``response``: The response generated from a Request. """ -HOOKS = ["response"] + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from . import _types as _t + from .models import Response + +HOOKS: list[str] = ["response"] -def default_hooks(): +def default_hooks() -> dict[str, list[_t.HookType]]: return {event: [] for event in HOOKS} # TODO: response is the only one -def dispatch_hook(key, hooks, hook_data, **kwargs): +def dispatch_hook( + key: str, + hooks: _t.HooksInputType | None, + hook_data: Response, + **kwargs: Any, +) -> Response: """Dispatches a hook dictionary on a given piece of data.""" - hooks = hooks or {} - hooks = hooks.get(key) - if hooks: - if hasattr(hooks, "__call__"): - hooks = [hooks] - for hook in hooks: + hooks_dict = hooks or {} + hook_list: Iterable[_t.HookType] | _t.HookType | None = hooks_dict.get(key) + if hook_list: + if isinstance(hook_list, Callable): + hook_list = [hook_list] + for hook in hook_list: _hook_data = hook(hook_data, **kwargs) if _hook_data is not None: hook_data = _hook_data diff --git a/libs/requests/models.py b/libs/requests/models.py index c4b25fa07..59b561596 100644 --- a/libs/requests/models.py +++ b/libs/requests/models.py @@ -5,13 +5,24 @@ requests.models This module contains the primary objects that power Requests. """ +from __future__ import annotations + import datetime # Import encoding now, to avoid implicit import later. # Implicit import within threads may cause LookupError when standard library is in a ZIP, # such as in Embedded Python. See https://github.com/psf/requests/issues/3578. -import encodings.idna # noqa: F401 +import encodings.idna # noqa: F401 # type: ignore[reportUnusedImport] +from collections.abc import Callable, Generator, Iterable, Iterator, Mapping from io import UnsupportedOperation +from typing import ( + TYPE_CHECKING, + Any, + Final, + Literal, + cast, + overload, +) from urllib3.exceptions import ( DecodeError, @@ -25,19 +36,24 @@ from urllib3.filepost import encode_multipart_formdata from urllib3.util import parse_url from ._internal_utils import to_native_string, unicode_is_ascii +from ._types import SupportsRead as _SupportsRead from .auth import HTTPBasicAuth from .compat import ( - Callable, JSONDecodeError, - Mapping, basestring, builtin_str, chardet, cookielib, + urlencode, + urlsplit, + urlunparse, ) from .compat import json as complexjson -from .compat import urlencode, urlsplit, urlunparse -from .cookies import _copy_cookie_jar, cookiejar_from_dict, get_cookie_header +from .cookies import ( + _copy_cookie_jar, # type: ignore[reportPrivateUsage] + cookiejar_from_dict, + get_cookie_header, +) from .exceptions import ( ChunkedEncodingError, ConnectionError, @@ -45,11 +61,11 @@ from .exceptions import ( HTTPError, InvalidJSONError, InvalidURL, + MissingSchema, + StreamConsumedError, ) from .exceptions import JSONDecodeError as RequestsJSONDecodeError -from .exceptions import MissingSchema from .exceptions import SSLError as RequestsSSLError -from .exceptions import StreamConsumedError from .hooks import default_hooks from .status_codes import codes from .structures import CaseInsensitiveDict @@ -66,9 +82,18 @@ from .utils import ( to_key_val_list, ) +if TYPE_CHECKING: + from http.cookiejar import CookieJar + + from typing_extensions import Self + + from . import _types as _t + from .adapters import HTTPAdapter + from .cookies import RequestsCookieJar + #: The set of HTTP status codes that indicate an automatically #: processable redirect. -REDIRECT_STATI = ( +REDIRECT_STATI: Final[tuple[int, ...]] = ( # type: ignore[assignment] codes.moved, # 301 codes.found, # 302 codes.other, # 303 @@ -76,19 +101,21 @@ REDIRECT_STATI = ( codes.permanent_redirect, # 308 ) -DEFAULT_REDIRECT_LIMIT = 30 -CONTENT_CHUNK_SIZE = 10 * 1024 -ITER_CHUNK_SIZE = 512 +DEFAULT_REDIRECT_LIMIT: int = 30 +CONTENT_CHUNK_SIZE: int = 10 * 1024 +ITER_CHUNK_SIZE: int = 512 class RequestEncodingMixin: + url: str | None + @property - def path_url(self): + def path_url(self) -> str: """Build the path URL to use.""" - url = [] + url: list[str] = [] - p = urlsplit(self.url) + p = urlsplit(cast(str, self.url)) path = p.path if not path: @@ -103,8 +130,28 @@ class RequestEncodingMixin: return "".join(url) + @overload @staticmethod - def _encode_params(data): + def _encode_params(data: str) -> str: ... + + @overload + @staticmethod + def _encode_params(data: bytes) -> bytes: ... + + @overload + @staticmethod + def _encode_params( + data: _t.SupportsRead[str | bytes], + ) -> _t.SupportsRead[str | bytes]: ... + + @overload + @staticmethod + def _encode_params(data: _t.KVDataType) -> str: ... + + @staticmethod + def _encode_params( + data: _t.EncodableDataType, + ) -> str | bytes | _t.SupportsRead[str | bytes]: """Encode parameters in a piece of data. Will successfully encode parameters when passed as a dict or a list of @@ -114,10 +161,10 @@ class RequestEncodingMixin: if isinstance(data, (str, bytes)): return data - elif hasattr(data, "read"): + elif isinstance(data, _SupportsRead): return data elif hasattr(data, "__iter__"): - result = [] + result: list[tuple[bytes, bytes]] = [] for k, vs in to_key_val_list(data): if isinstance(vs, basestring) or not hasattr(vs, "__iter__"): vs = [vs] @@ -131,10 +178,12 @@ class RequestEncodingMixin: ) return urlencode(result, doseq=True) else: - return data + return data # type: ignore[return-value] # unreachable for valid _t.DataType @staticmethod - def _encode_files(files, data): + def _encode_files( + files: _t.FilesType, data: _t.RawDataType | None + ) -> tuple[bytes, str]: """Build the body for a multipart/form-data request. Will successfully encode files when passed as a dict or a list of @@ -148,7 +197,7 @@ class RequestEncodingMixin: elif isinstance(data, basestring): raise ValueError("Data must not be a string.") - new_fields = [] + new_fields: list[RequestField | tuple[str, bytes]] = [] fields = to_key_val_list(data or {}) files = to_key_val_list(files or {}) @@ -187,9 +236,9 @@ class RequestEncodingMixin: if isinstance(fp, (str, bytes, bytearray)): fdata = fp - elif hasattr(fp, "read"): + elif isinstance(fp, _SupportsRead): # type: ignore[reportUnnecessaryIsInstance] # defensive check for untyped callers fdata = fp.read() - elif fp is None: + elif fp is None: # type: ignore[reportUnnecessaryComparison] # defensive check for untyped callers continue else: fdata = fp @@ -204,7 +253,11 @@ class RequestEncodingMixin: class RequestHooksMixin: - def register_hook(self, event, hook): + hooks: dict[str, list[_t.HookType]] + + def register_hook( + self, event: str, hook: Iterable[_t.HookType] | _t.HookType + ) -> None: """Properly register a hook.""" if event not in self.hooks: @@ -213,9 +266,9 @@ class RequestHooksMixin: if isinstance(hook, Callable): self.hooks[event].append(hook) elif hasattr(hook, "__iter__"): - self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) + self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) # type: ignore[reportUnnecessaryIsInstance] # defensive runtime filter - def deregister_hook(self, event, hook): + def deregister_hook(self, event: str, hook: _t.HookType) -> bool: """Deregister a previously registered hook. Returns True if the hook existed, False if not. """ @@ -255,19 +308,29 @@ class Request(RequestHooksMixin): """ + method: str | None + url: _t.UriType | None + headers: Mapping[str, str | bytes] + files: _t.FilesType + data: _t.DataType + json: _t.JsonType + params: _t.ParamsType + auth: _t.AuthType + cookies: RequestsCookieJar | CookieJar | dict[str, str] | None + def __init__( self, - method=None, - url=None, - headers=None, - files=None, - data=None, - params=None, - auth=None, - cookies=None, - hooks=None, - json=None, - ): + method: str | None = None, + url: _t.UriType | None = None, + headers: _t.HeadersType = None, + files: _t.FilesType = None, + data: _t.DataType = None, + params: _t.ParamsType = None, + auth: _t.AuthType = None, + cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None, + hooks: _t.HooksInputType | None = None, + json: _t.JsonType = None, + ) -> None: # Default empty dicts for dict params. data = [] if data is None else data files = [] if files is None else files @@ -289,10 +352,10 @@ class Request(RequestHooksMixin): self.auth = auth self.cookies = cookies - def __repr__(self): + def __repr__(self) -> str: return f"" - def prepare(self): + def prepare(self) -> PreparedRequest: """Constructs a :class:`PreparedRequest ` for transmission and returns it.""" p = PreparedRequest() p.prepare( @@ -331,13 +394,21 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): """ - def __init__(self): + method: str | None + url: str | None + headers: CaseInsensitiveDict[str | bytes] + _cookies: RequestsCookieJar | CookieJar | None + body: _t.BodyType + hooks: dict[str, list[_t.HookType]] + _body_position: int | object | None + + def __init__(self) -> None: #: HTTP verb to send to the server. self.method = None #: HTTP URL to send the request to. self.url = None #: dictionary of HTTP headers. - self.headers = None + self.headers = None # type: ignore[assignment] # The `CookieJar` used to create the Cookie header will be stored here # after prepare_cookies is called self._cookies = None @@ -350,19 +421,20 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): def prepare( self, - method=None, - url=None, - headers=None, - files=None, - data=None, - params=None, - auth=None, - cookies=None, - hooks=None, - json=None, - ): + method: str | None = None, + url: _t.UriType | None = None, + headers: Mapping[str, str | bytes] | None = None, + files: _t.FilesType = None, + data: _t.DataType = None, + params: _t.ParamsType = None, + auth: _t.AuthType = None, + cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None, + hooks: _t.HooksInputType | None = None, + json: _t.JsonType = None, + ) -> None: """Prepares the entire request with the given parameters.""" + url = cast("_t.UriType", url) self.prepare_method(method) self.prepare_url(url, params) self.prepare_headers(headers) @@ -376,28 +448,28 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): # This MUST go after prepare_auth. Authenticators could add a hook self.prepare_hooks(hooks) - def __repr__(self): + def __repr__(self) -> str: return f"" - def copy(self): + def copy(self) -> PreparedRequest: p = PreparedRequest() p.method = self.method p.url = self.url - p.headers = self.headers.copy() if self.headers is not None else None + p.headers = self.headers.copy() if self.headers is not None else None # type: ignore[assignment] p._cookies = _copy_cookie_jar(self._cookies) p.body = self.body p.hooks = self.hooks p._body_position = self._body_position return p - def prepare_method(self, method): + def prepare_method(self, method: str | None) -> None: """Prepares the given HTTP method.""" self.method = method if self.method is not None: self.method = to_native_string(self.method.upper()) @staticmethod - def _get_idna_encoded_host(host): + def _get_idna_encoded_host(host: str) -> str: import idna try: @@ -406,7 +478,11 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): raise UnicodeError return host - def prepare_url(self, url, params): + def prepare_url( + self, + url: _t.UriType, + params: _t.ParamsType, + ) -> None: """Prepares the given HTTP URL.""" #: Accept objects that have string representations. #: We're unable to blindly call unicode/str functions @@ -470,17 +546,21 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): if isinstance(params, (str, bytes)): params = to_native_string(params) - enc_params = self._encode_params(params) + if params is not None: + enc_params = self._encode_params(params) + else: + enc_params = "" + if enc_params: if query: query = f"{query}&{enc_params}" else: query = enc_params - url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment])) + url = requote_uri(urlunparse((scheme, netloc, path, "", query, fragment))) self.url = url - def prepare_headers(self, headers): + def prepare_headers(self, headers: Mapping[str, str | bytes] | None) -> None: """Prepares the given HTTP headers.""" self.headers = CaseInsensitiveDict() @@ -491,7 +571,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): name, value = header self.headers[to_native_string(name)] = value - def prepare_body(self, data, files, json=None): + def prepare_body( + self, data: _t.DataType, files: _t.FilesType, json: _t.JsonType = None + ) -> None: """Prepares the given HTTP body data.""" # Check if file, fo, generator, iterator. @@ -514,14 +596,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): if not isinstance(body, bytes): body = body.encode("utf-8") - is_stream = all( - [ - hasattr(data, "__iter__"), - not isinstance(data, (basestring, list, tuple, Mapping)), - ] - ) - - if is_stream: + # data that proxies attributes to underlying objects needs hasattr + is_iterable = isinstance(data, Iterable) or hasattr(data, "__iter__") + if is_iterable and not isinstance(data, (str, bytes, list, tuple, Mapping)): try: length = super_len(data) except (TypeError, AttributeError, UnsupportedOperation): @@ -534,7 +611,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): # This will allow us to rewind a file in the event # of a redirect. try: - self._body_position = body.tell() + self._body_position = body.tell() # type: ignore[union-attr] # guarded by getattr check except OSError: # This differentiates from None, allowing us to catch # a failed `tell()` later when trying to rewind the body @@ -550,13 +627,16 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): else: self.headers["Transfer-Encoding"] = "chunked" else: + # After is_stream filtering, remaining data is raw (not streamed) + raw_data = cast("_t.RawDataType | None", data) + # Multi-part file uploads. if files: - (body, content_type) = self._encode_files(files, data) + (body, content_type) = self._encode_files(files, raw_data) else: - if data: - body = self._encode_params(data) - if isinstance(data, basestring) or hasattr(data, "read"): + if raw_data: + body = self._encode_params(raw_data) + if isinstance(data, basestring) or isinstance(data, _SupportsRead): content_type = None else: content_type = "application/x-www-form-urlencoded" @@ -567,9 +647,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): if content_type and ("content-type" not in self.headers): self.headers["Content-Type"] = content_type - self.body = body + self.body = body # type: ignore[assignment] # body transforms from DataType to BodyType - def prepare_content_length(self, body): + def prepare_content_length(self, body: _t.BodyType) -> None: """Prepare Content-Length header based on request method and body""" if body is not None: length = super_len(body) @@ -585,21 +665,28 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): # but don't provide one. (i.e. not GET or HEAD) self.headers["Content-Length"] = "0" - def prepare_auth(self, auth, url=""): + def prepare_auth( + self, + auth: _t.AuthType, + url: _t.UriType = "", + ) -> None: """Prepares the given HTTP auth data.""" # If no Auth is explicitly provided, extract it from the URL first. if auth is None: - url_auth = get_auth_from_url(self.url) + url_auth = get_auth_from_url(cast(str, self.url)) auth = url_auth if any(url_auth) else None if auth: - if isinstance(auth, tuple) and len(auth) == 2: + if isinstance(auth, tuple) and len(auth) == 2: # type: ignore[arg-type] # pyright widens tuple from Callable in AuthType # special-case basic HTTP auth - auth = HTTPBasicAuth(*auth) + auth_handler = HTTPBasicAuth(*auth) # type: ignore[arg-type] # pyright widens tuple from Callable in AuthType + else: + # TODO: can be fixed by flipping the conditionals + auth_handler = cast("Callable[..., PreparedRequest]", auth) # Allow auth to make its changes. - r = auth(self) + r = auth_handler(self) # Update self to reflect the auth changes. self.__dict__.update(r.__dict__) @@ -607,7 +694,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): # Recompute Content-Length self.prepare_content_length(self.body) - def prepare_cookies(self, cookies): + def prepare_cookies( + self, cookies: RequestsCookieJar | CookieJar | dict[str, str] | None + ) -> None: """Prepares the given HTTP cookie data. This function eventually generates a ``Cookie`` header from the @@ -623,16 +712,17 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): else: self._cookies = cookiejar_from_dict(cookies) - cookie_header = get_cookie_header(self._cookies, self) + cookies_jar = cast("CookieJar", self._cookies) + cookie_header = get_cookie_header(cookies_jar, self) if cookie_header is not None: self.headers["Cookie"] = cookie_header - def prepare_hooks(self, hooks): + def prepare_hooks(self, hooks: _t.HooksInputType | None) -> None: """Prepares the given hooks.""" # hooks can be passed as None to the prepare method and to this # method. To prevent iterating over None, simply use an empty list # if hooks is False-y - hooks = hooks or [] + hooks = hooks or {} for event in hooks: self.register_hook(event, hooks[event]) @@ -642,7 +732,22 @@ class Response: server's response to an HTTP request. """ - __attrs__ = [ + _content: bytes | Literal[False] | None + _content_consumed: bool + _next: PreparedRequest | None + status_code: int + headers: CaseInsensitiveDict[str] + raw: Any + url: str + encoding: str | None + history: list[Response] + reason: str + cookies: RequestsCookieJar + elapsed: datetime.timedelta + request: PreparedRequest + connection: HTTPAdapter + + __attrs__: list[str] = [ "_content", "status_code", "headers", @@ -655,13 +760,13 @@ class Response: "request", ] - def __init__(self): + def __init__(self) -> None: self._content = False self._content_consumed = False self._next = None #: Integer Code of responded HTTP Status, e.g. 404 or 200. - self.status_code = None + self.status_code = None # type: ignore[assignment] #: Case-insensitive Dictionary of Response Headers. #: For example, ``headers['content-encoding']`` will return the @@ -674,7 +779,7 @@ class Response: self.raw = None #: Final URL location of Response. - self.url = None + self.url = None # type: ignore[assignment] #: Encoding to decode with when accessing r.text. self.encoding = None @@ -685,7 +790,7 @@ class Response: self.history = [] #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK". - self.reason = None + self.reason = None # type: ignore[assignment] #: A CookieJar of Cookies the server sent back. self.cookies = cookiejar_from_dict({}) @@ -700,15 +805,15 @@ class Response: #: The :class:`PreparedRequest ` object to which this #: is a response. - self.request = None + self.request = None # type: ignore[assignment] - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, *args): + def __exit__(self, *args: Any) -> None: self.close() - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: # Consume everything; accessing the content attribute makes # sure the content has been fully read. if not self._content_consumed: @@ -716,7 +821,7 @@ class Response: return {attr: getattr(self, attr, None) for attr in self.__attrs__} - def __setstate__(self, state): + def __setstate__(self, state: dict[str, Any]) -> None: for name, value in state.items(): setattr(self, name, value) @@ -724,10 +829,10 @@ class Response: setattr(self, "_content_consumed", True) setattr(self, "raw", None) - def __repr__(self): + def __repr__(self) -> str: return f"" - def __bool__(self): + def __bool__(self) -> bool: """Returns True if :attr:`status_code` is less than 400. This attribute checks if the status code of the response is between @@ -737,7 +842,7 @@ class Response: """ return self.ok - def __nonzero__(self): + def __nonzero__(self) -> bool: """Returns True if :attr:`status_code` is less than 400. This attribute checks if the status code of the response is between @@ -747,12 +852,12 @@ class Response: """ return self.ok - def __iter__(self): + def __iter__(self) -> Iterator[bytes]: """Allows you to use a response as an iterator.""" return self.iter_content(128) @property - def ok(self): + def ok(self) -> bool: """Returns True if :attr:`status_code` is less than 400, False if not. This attribute checks if the status code of the response is between @@ -767,14 +872,14 @@ class Response: return True @property - def is_redirect(self): + def is_redirect(self) -> bool: """True if this Response is a well-formed HTTP redirect that could have been processed automatically (by :meth:`Session.resolve_redirects`). """ return "location" in self.headers and self.status_code in REDIRECT_STATI @property - def is_permanent_redirect(self): + def is_permanent_redirect(self) -> bool: """True if this Response one of the permanent versions of redirect.""" return "location" in self.headers and self.status_code in ( codes.moved_permanently, @@ -782,12 +887,12 @@ class Response: ) @property - def next(self): + def next(self) -> PreparedRequest | None: """Returns a PreparedRequest for the next request in a redirect chain, if there is one.""" return self._next @property - def apparent_encoding(self): + def apparent_encoding(self) -> str | None: """The apparent encoding, provided by the charset_normalizer or chardet libraries.""" if chardet is not None: return chardet.detect(self.content)["encoding"] @@ -796,7 +901,17 @@ class Response: # to a standard Python utf-8 str. return "utf-8" - def iter_content(self, chunk_size=1, decode_unicode=False): + @overload + def iter_content( + self, chunk_size: int | None = 1, decode_unicode: Literal[False] = False + ) -> Iterator[bytes]: ... + @overload + def iter_content( + self, chunk_size: int | None = 1, *, decode_unicode: Literal[True] + ) -> Iterator[str | bytes]: ... + def iter_content( + self, chunk_size: int | None = 1, decode_unicode: bool = False + ) -> Iterator[str | bytes]: """Iterates over the response data. When stream=True is set on the request, this avoids reading the content at once into memory for large responses. The chunk size is the number of bytes it should @@ -809,11 +924,13 @@ class Response: chunks are received. If stream=False, data is returned as a single chunk. - If decode_unicode is True, content will be decoded using the best - available encoding based on the response. + If decode_unicode is True, content will be decoded using encoding + information from the response. If no encoding information is available, + bytes will be returned. This can be bypassed by manually setting + `encoding` on the response. """ - def generate(): + def generate() -> Generator[bytes, None, None]: # Special case for urllib3. if hasattr(self.raw, "stream"): try: @@ -838,42 +955,65 @@ class Response: if self._content_consumed and isinstance(self._content, bool): raise StreamConsumedError() - elif chunk_size is not None and not isinstance(chunk_size, int): + elif chunk_size is not None and not isinstance(chunk_size, int): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for untyped callers raise TypeError( f"chunk_size must be an int, it is instead a {type(chunk_size)}." ) - # simulate reading small chunks of the content - reused_chunks = iter_slices(self._content, chunk_size) - stream_chunks = generate() - - chunks = reused_chunks if self._content_consumed else stream_chunks + if self._content_consumed: + # simulate reading small chunks of the content + content = cast(bytes, self._content) + chunks = iter_slices(content, chunk_size) + else: + chunks = generate() if decode_unicode: chunks = stream_decode_response_unicode(chunks, self) return chunks + @overload def iter_lines( - self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None - ): + self, + chunk_size: int = ITER_CHUNK_SIZE, + decode_unicode: Literal[False] = False, + delimiter: bytes | None = None, + ) -> Iterator[bytes]: ... + @overload + def iter_lines( + self, + chunk_size: int = ITER_CHUNK_SIZE, + *, + decode_unicode: Literal[True], + delimiter: str | bytes | None = None, + ) -> Iterator[str | bytes]: ... + def iter_lines( + self, + chunk_size: int = ITER_CHUNK_SIZE, + decode_unicode: bool = False, + delimiter: str | bytes | None = None, + ) -> Iterator[str | bytes]: """Iterates over the response data, one line at a time. When stream=True is set on the request, this avoids reading the content at once into memory for large responses. + The decode_unicode param works the same as in `iter_content`, with the + same caveats. + .. note:: This method is not reentrant safe. """ - pending = None + pending: str | bytes | None = None for chunk in self.iter_content( chunk_size=chunk_size, decode_unicode=decode_unicode ): if pending is not None: - chunk = pending + chunk + # TODO: remove cast after iter_lines rewrite + chunk = cast("str | bytes", pending + chunk) # type: ignore[operator] if delimiter: - lines = chunk.split(delimiter) + lines = chunk.split(delimiter) # type: ignore[arg-type] else: lines = chunk.splitlines() @@ -888,7 +1028,7 @@ class Response: yield pending @property - def content(self): + def content(self) -> bytes: """Content of the response, in bytes.""" if self._content is False: @@ -904,10 +1044,10 @@ class Response: self._content_consumed = True # don't need to release the connection; that's been handled by urllib3 # since we exhausted the data. - return self._content + return self._content # type: ignore[return-value] @property - def text(self): + def text(self) -> str: """Content of the response, in unicode. If Response.encoding is None, encoding will be guessed using @@ -932,7 +1072,7 @@ class Response: # Decode unicode from given encoding. try: - content = str(self.content, encoding, errors="replace") + content = str(self.content, encoding or "utf-8", errors="replace") except (LookupError, TypeError): # A LookupError is raised if the encoding was not found which could # indicate a misspelling or similar mistake. @@ -944,7 +1084,7 @@ class Response: return content - def json(self, **kwargs): + def json(self, **kwargs: Any) -> Any: r"""Decodes the JSON response body (if any) as a Python object. This may return a dictionary, list, etc. depending on what is in the response. @@ -980,23 +1120,24 @@ class Response: raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) @property - def links(self): + def links(self) -> dict[str, dict[str, str]]: """Returns the parsed header links of the response, if any.""" header = self.headers.get("link") - resolved_links = {} + resolved_links: dict[str, dict[str, str]] = {} if header: links = parse_header_links(header) for link in links: key = link.get("rel") or link.get("url") - resolved_links[key] = link + if key is not None: + resolved_links[key] = link return resolved_links - def raise_for_status(self): + def raise_for_status(self) -> None: """Raises :class:`HTTPError`, if one occurred.""" http_error_msg = "" @@ -1025,7 +1166,7 @@ class Response: if http_error_msg: raise HTTPError(http_error_msg, response=self) - def close(self): + def close(self) -> None: """Releases the connection back to the pool. Once this method has been called the underlying ``raw`` object must not be accessed again. diff --git a/libs/rich-14.3.3.dist-info/REQUESTED b/libs/requests/py.typed similarity index 100% rename from libs/rich-14.3.3.dist-info/REQUESTED rename to libs/requests/py.typed diff --git a/libs/requests/sessions.py b/libs/requests/sessions.py index 731550de8..feaf34053 100644 --- a/libs/requests/sessions.py +++ b/libs/requests/sessions.py @@ -5,16 +5,22 @@ requests.sessions This module provides a Session object to manage and persist settings across requests (cookies, auth, proxies). """ + +from __future__ import annotations + import os import sys import time from collections import OrderedDict +from collections.abc import Generator, Mapping, MutableMapping from datetime import timedelta +from typing import TYPE_CHECKING, Any, cast from ._internal_utils import to_native_string +from ._types import is_prepared as _is_prepared from .adapters import HTTPAdapter -from .auth import _basic_auth_str -from .compat import Mapping, cookielib, urljoin, urlparse +from .auth import _basic_auth_str # type: ignore[reportPrivateUsage] +from .compat import cookielib, urljoin, urlparse from .cookies import ( RequestsCookieJar, cookiejar_from_dict, @@ -32,9 +38,10 @@ from .hooks import default_hooks, dispatch_hook # formerly defined here, reexposed here for backward compatibility from .models import ( # noqa: F401 DEFAULT_REDIRECT_LIMIT, - REDIRECT_STATI, + REDIRECT_STATI, # type: ignore[reportUnusedImport] PreparedRequest, Request, + Response, ) from .status_codes import codes from .structures import CaseInsensitiveDict @@ -47,10 +54,18 @@ from .utils import ( # noqa: F401 requote_uri, resolve_proxies, rewind_body, - should_bypass_proxies, + should_bypass_proxies, # type: ignore[reportUnusedImport] # re-export for external consumers to_key_val_list, ) +if TYPE_CHECKING: + from http.cookiejar import CookieJar + + from typing_extensions import Self, Unpack + + from . import _types as _t + from .adapters import BaseAdapter + # Preferred clock, based on which one is more accurate on a given system. if sys.platform == "win32": preferred_clock = time.perf_counter @@ -58,7 +73,9 @@ else: preferred_clock = time.time -def merge_setting(request_setting, session_setting, dict_class=OrderedDict): +def merge_setting( + request_setting: Any, session_setting: Any, dict_class: type = OrderedDict +) -> Any: """Determines appropriate setting for a given request, taking into account the explicit setting on that request, and the setting in the session. If a setting is a dictionary, they will be merged together using `dict_class` @@ -76,8 +93,8 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict): ): return request_setting - merged_setting = dict_class(to_key_val_list(session_setting)) - merged_setting.update(to_key_val_list(request_setting)) + merged_setting = dict_class(to_key_val_list(session_setting)) # type: ignore[arg-type] # isinstance narrows Any to Mapping[Unknown] + merged_setting.update(to_key_val_list(request_setting)) # type: ignore[arg-type] # Remove keys that are set to None. Extract keys first to avoid altering # the dictionary during iteration. @@ -88,7 +105,11 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict): return merged_setting -def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict): +def merge_hooks( + request_hooks: _t.HooksType, + session_hooks: _t.HooksType, + dict_class: type = OrderedDict, +) -> _t.HooksType: """Properly merges both requests and session hooks. This is necessary because when request_hooks == {'response': []}, the @@ -104,7 +125,13 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict): class SessionRedirectMixin: - def get_redirect_target(self, resp): + max_redirects: int + trust_env: bool + cookies: RequestsCookieJar + + def send(self, request: PreparedRequest, **kwargs: Any) -> Response: ... + + def get_redirect_target(self, resp: Response) -> str | None: """Receives a Response. Returns a redirect URI or ``None``""" # Due to the nature of how requests processes redirects this method will # be called at least once upon the original response and at least twice @@ -124,7 +151,7 @@ class SessionRedirectMixin: return to_native_string(location, "utf8") return None - def should_strip_auth(self, old_url, new_url): + def should_strip_auth(self, old_url: str, new_url: str) -> bool: """Decide whether Authorization header should be removed when redirecting""" old_parsed = urlparse(old_url) new_parsed = urlparse(new_url) @@ -158,19 +185,19 @@ class SessionRedirectMixin: def resolve_redirects( self, - resp, - req, - stream=False, - timeout=None, - verify=True, - cert=None, - proxies=None, - yield_requests=False, - **adapter_kwargs, - ): + resp: Response, + req: PreparedRequest, + stream: bool = False, + timeout: _t.TimeoutType = None, + verify: _t.VerifyType = True, + cert: _t.CertType = None, + proxies: dict[str, str] | None = None, + yield_requests: bool = False, + **adapter_kwargs: Any, + ) -> Generator[Response, None, None]: """Receives a Response. Returns a generator of Responses or Requests.""" - hist = [] # keep track of history + hist: list[Response] = [] # keep track of history url = self.get_redirect_target(resp) previous_fragment = urlparse(req.url).fragment @@ -178,9 +205,8 @@ class SessionRedirectMixin: prepared_request = req.copy() # Update history and keep track of redirects. - # resp.history must ignore the original request in this loop + resp.history = hist[:] hist.append(resp) - resp.history = hist[1:] try: resp.content # Consume socket so it can be released @@ -237,9 +263,10 @@ class SessionRedirectMixin: # Extract any cookies sent on the response to the cookiejar # in the new request. Because we've mutated our copied prepared # request, use the old one that we haven't yet touched. - extract_cookies_to_jar(prepared_request._cookies, req, resp.raw) - merge_cookies(prepared_request._cookies, self.cookies) - prepared_request.prepare_cookies(prepared_request._cookies) + cookie_jar = cast("CookieJar", prepared_request._cookies) # type: ignore[reportPrivateUsage] + extract_cookies_to_jar(cookie_jar, req, resp.raw) + merge_cookies(cookie_jar, self.cookies) + prepared_request.prepare_cookies(cookie_jar) # Rebuild auth and proxy information. proxies = self.rebuild_proxies(prepared_request, proxies) @@ -248,7 +275,7 @@ class SessionRedirectMixin: # A failed tell() sets `_body_position` to `object()`. This non-None # value ensures `rewindable` will be True, allowing us to raise an # UnrewindableBodyError, instead of hanging the connection. - rewindable = prepared_request._body_position is not None and ( + rewindable = prepared_request._body_position is not None and ( # type: ignore[reportPrivateUsage] "Content-Length" in headers or "Transfer-Encoding" in headers ) @@ -260,7 +287,7 @@ class SessionRedirectMixin: req = prepared_request if yield_requests: - yield req + yield req # type: ignore[misc] # Internal use only, returns PreparedRequest else: resp = self.send( req, @@ -279,17 +306,22 @@ class SessionRedirectMixin: url = self.get_redirect_target(resp) yield resp - def rebuild_auth(self, prepared_request, response): + def rebuild_auth( + self, prepared_request: PreparedRequest, response: Response + ) -> None: """When being redirected we may want to strip authentication from the request to avoid leaking credentials. This method intelligently removes and reapplies authentication where possible to avoid credential loss. """ + original_request = response.request + assert _is_prepared(original_request) + assert _is_prepared(prepared_request) + headers = prepared_request.headers + original_url = original_request.url url = prepared_request.url - if "Authorization" in headers and self.should_strip_auth( - response.request.url, url - ): + if "Authorization" in headers and self.should_strip_auth(original_url, url): # If we get redirected to a new host, we should strip out any # authentication headers. del headers["Authorization"] @@ -299,7 +331,11 @@ class SessionRedirectMixin: if new_auth is not None: prepared_request.prepare_auth(new_auth) - def rebuild_proxies(self, prepared_request, proxies): + def rebuild_proxies( + self, + prepared_request: PreparedRequest, + proxies: dict[str, str] | None, + ) -> dict[str, str]: """This method re-evaluates the proxy configuration by considering the environment variables. If we are redirected to a URL covered by NO_PROXY, we strip the proxy configuration. Otherwise, we set missing @@ -311,6 +347,7 @@ class SessionRedirectMixin: :rtype: dict """ + assert _is_prepared(prepared_request) headers = prepared_request.headers scheme = urlparse(prepared_request.url).scheme new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env) @@ -330,7 +367,9 @@ class SessionRedirectMixin: return new_proxies - def rebuild_method(self, prepared_request, response): + def rebuild_method( + self, prepared_request: PreparedRequest, response: Response + ) -> None: """When being redirected we may want to change the method of the request based on certain specs or browser behavior. """ @@ -372,7 +411,20 @@ class Session(SessionRedirectMixin): """ - __attrs__ = [ + headers: CaseInsensitiveDict[str] + auth: _t.AuthType + proxies: dict[str, str] + hooks: dict[str, list[_t.HookType]] + params: MutableMapping[str, Any] + stream: bool + verify: _t.VerifyType + cert: _t.CertType + max_redirects: int + trust_env: bool + cookies: RequestsCookieJar + adapters: MutableMapping[str, BaseAdapter] + + __attrs__: list[str] = [ "headers", "cookies", "auth", @@ -387,7 +439,7 @@ class Session(SessionRedirectMixin): "max_redirects", ] - def __init__(self): + def __init__(self) -> None: #: A case-insensitive dictionary of headers to be sent on each #: :class:`Request ` sent from this #: :class:`Session `. @@ -421,6 +473,8 @@ class Session(SessionRedirectMixin): #: expired certificates, which will make your application vulnerable to #: man-in-the-middle (MitM) attacks. #: Only set this to `False` for testing. + #: If verify is set to a string, it must be the path to a CA bundle file + #: that will be used to verify the TLS certificate. self.verify = True #: SSL client certificate default, if String, path to ssl client @@ -448,13 +502,13 @@ class Session(SessionRedirectMixin): self.mount("https://", HTTPAdapter()) self.mount("http://", HTTPAdapter()) - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, *args): + def __exit__(self, *args: Any) -> None: self.close() - def prepare_request(self, request): + def prepare_request(self, request: Request) -> PreparedRequest: """Constructs a :class:`PreparedRequest ` for transmission and returns it. The :class:`PreparedRequest` has settings merged from the :class:`Request ` instance and those of the @@ -464,6 +518,9 @@ class Session(SessionRedirectMixin): session's settings. :rtype: requests.PreparedRequest """ + url = cast("_t.UriType", request.url) + method = cast(str, request.method) + cookies = request.cookies or {} # Bootstrap CookieJar. @@ -478,12 +535,12 @@ class Session(SessionRedirectMixin): # Set environment's basic authentication if not explicitly set. auth = request.auth if self.trust_env and not auth and not self.auth: - auth = get_netrc_auth(request.url) + auth = get_netrc_auth(url) p = PreparedRequest() p.prepare( - method=request.method.upper(), - url=request.url, + method=method.upper(), + url=url, files=request.files, data=request.data, json=request.json, @@ -499,23 +556,23 @@ class Session(SessionRedirectMixin): def request( self, - method, - url, - params=None, - data=None, - headers=None, - cookies=None, - files=None, - auth=None, - timeout=None, - allow_redirects=True, - proxies=None, - hooks=None, - stream=None, - verify=None, - cert=None, - json=None, - ): + method: str, + url: _t.UriType, + params: _t.ParamsType = None, + data: _t.DataType = None, + headers: _t.HeadersType = None, + cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None, + files: _t.FilesType = None, + auth: _t.AuthType = None, + timeout: _t.TimeoutType = None, + allow_redirects: bool = True, + proxies: dict[str, str] | None = None, + hooks: _t.HooksInputType | None = None, + stream: bool | None = None, + verify: _t.VerifyType | None = None, + cert: _t.CertType = None, + json: _t.JsonType = None, + ) -> Response: """Constructs a :class:`Request `, prepares it and sends it. Returns :class:`Response ` object. @@ -559,6 +616,9 @@ class Session(SessionRedirectMixin): If Tuple, ('cert', 'key') pair. :rtype: requests.Response """ + if isinstance(url, bytes): + url = url.decode("utf-8") + # Create the Request. req = Request( method=method.upper(), @@ -574,6 +634,8 @@ class Session(SessionRedirectMixin): ) prep = self.prepare_request(req) + assert _is_prepared(prep) + proxies = proxies or {} settings = self.merge_environment_settings( @@ -590,18 +652,25 @@ class Session(SessionRedirectMixin): return resp - def get(self, url, **kwargs): + def get( + self, + url: _t.UriType, + params: _t.ParamsType = None, + **kwargs: Unpack[_t.GetKwargs], + ) -> Response: r"""Sends a GET request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary, list of tuples or bytes to send + in the query string for the :class:`Request`. :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ kwargs.setdefault("allow_redirects", True) - return self.request("GET", url, **kwargs) + return self.request("GET", url, params=params, **kwargs) - def options(self, url, **kwargs): + def options(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: r"""Sends a OPTIONS request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -612,7 +681,7 @@ class Session(SessionRedirectMixin): kwargs.setdefault("allow_redirects", True) return self.request("OPTIONS", url, **kwargs) - def head(self, url, **kwargs): + def head(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: r"""Sends a HEAD request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -623,7 +692,13 @@ class Session(SessionRedirectMixin): kwargs.setdefault("allow_redirects", False) return self.request("HEAD", url, **kwargs) - def post(self, url, data=None, json=None, **kwargs): + def post( + self, + url: _t.UriType, + data: _t.DataType = None, + json: _t.JsonType = None, + **kwargs: Unpack[_t.PostKwargs], + ) -> Response: r"""Sends a POST request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -636,7 +711,9 @@ class Session(SessionRedirectMixin): return self.request("POST", url, data=data, json=json, **kwargs) - def put(self, url, data=None, **kwargs): + def put( + self, url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs] + ) -> Response: r"""Sends a PUT request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -648,7 +725,9 @@ class Session(SessionRedirectMixin): return self.request("PUT", url, data=data, **kwargs) - def patch(self, url, data=None, **kwargs): + def patch( + self, url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs] + ) -> Response: r"""Sends a PATCH request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -660,7 +739,7 @@ class Session(SessionRedirectMixin): return self.request("PATCH", url, data=data, **kwargs) - def delete(self, url, **kwargs): + def delete(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: r"""Sends a DELETE request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. @@ -670,7 +749,7 @@ class Session(SessionRedirectMixin): return self.request("DELETE", url, **kwargs) - def send(self, request, **kwargs): + def send(self, request: PreparedRequest, **kwargs: Any) -> Response: """Send a given PreparedRequest. :rtype: requests.Response @@ -688,6 +767,8 @@ class Session(SessionRedirectMixin): if isinstance(request, Request): raise ValueError("You can only send PreparedRequests.") + assert _is_prepared(request) + # Set up variables needed for resolve_redirects and dispatching of hooks allow_redirects = kwargs.pop("allow_redirects", True) stream = kwargs.get("stream") @@ -736,7 +817,7 @@ class Session(SessionRedirectMixin): # If redirects aren't being followed, store the response on the Request for Response.next(). if not allow_redirects: try: - r._next = next( + r._next = next( # type: ignore[assignment] # yield_requests=True returns PreparedRequest self.resolve_redirects(r, request, yield_requests=True, **kwargs) ) except StopIteration: @@ -747,7 +828,14 @@ class Session(SessionRedirectMixin): return r - def merge_environment_settings(self, url, proxies, stream, verify, cert): + def merge_environment_settings( + self, + url: str, + proxies: dict[str, str] | None, + stream: bool | None, + verify: _t.VerifyType | None, + cert: _t.CertType, + ) -> dict[str, Any]: """ Check the environment and merge it with some settings. @@ -758,8 +846,9 @@ class Session(SessionRedirectMixin): # Set environment's proxies. no_proxy = proxies.get("no_proxy") if proxies is not None else None env_proxies = get_environ_proxies(url, no_proxy=no_proxy) - for k, v in env_proxies.items(): - proxies.setdefault(k, v) + if proxies is not None: + for k, v in env_proxies.items(): + proxies.setdefault(k, v) # Look for requests environment configuration # and be compatible with cURL. @@ -778,7 +867,7 @@ class Session(SessionRedirectMixin): return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert} - def get_adapter(self, url): + def get_adapter(self, url: str) -> BaseAdapter: """ Returns the appropriate connection adapter for the given URL. @@ -791,12 +880,12 @@ class Session(SessionRedirectMixin): # Nothing matches :-/ raise InvalidSchema(f"No connection adapters were found for {url!r}") - def close(self): + def close(self) -> None: """Closes all adapters and as such the session""" for v in self.adapters.values(): v.close() - def mount(self, prefix, adapter): + def mount(self, prefix: str, adapter: BaseAdapter) -> None: """Registers a connection adapter to a prefix. Adapters are sorted in descending order by prefix length. @@ -807,16 +896,16 @@ class Session(SessionRedirectMixin): for key in keys_to_move: self.adapters[key] = self.adapters.pop(key) - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: state = {attr: getattr(self, attr, None) for attr in self.__attrs__} return state - def __setstate__(self, state): + def __setstate__(self, state: dict[str, Any]) -> None: for attr, value in state.items(): setattr(self, attr, value) -def session(): +def session() -> Session: """ Returns a :class:`Session` for context-management. diff --git a/libs/requests/status_codes.py b/libs/requests/status_codes.py index c7945a2f0..6c59d6bae 100644 --- a/libs/requests/status_codes.py +++ b/libs/requests/status_codes.py @@ -103,7 +103,7 @@ _codes = { 511: ("network_authentication_required", "network_auth", "network_authentication"), } -codes = LookupDict(name="status_codes") +codes: LookupDict[int] = LookupDict(name="status_codes") def _init(): @@ -113,7 +113,7 @@ def _init(): if not title.startswith(("\\", "/")): setattr(codes, title.upper(), code) - def doc(code): + def doc(code: int) -> str: names = ", ".join(f"``{n}``" for n in _codes[code]) return "* %d: %s" % (code, names) diff --git a/libs/requests/structures.py b/libs/requests/structures.py index 188e13e48..7675eaf15 100644 --- a/libs/requests/structures.py +++ b/libs/requests/structures.py @@ -5,12 +5,19 @@ requests.structures Data structures that power Requests. """ +from __future__ import annotations + from collections import OrderedDict +from collections.abc import Iterable, Iterator, Mapping +from typing import Any, Generic, TypeVar, overload -from .compat import Mapping, MutableMapping +from .compat import MutableMapping + +_VT = TypeVar("_VT") +_D = TypeVar("_D") -class CaseInsensitiveDict(MutableMapping): +class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]): """A case-insensitive ``dict``-like object. Implements all methods and operations of @@ -37,63 +44,87 @@ class CaseInsensitiveDict(MutableMapping): behavior is undefined. """ - def __init__(self, data=None, **kwargs): + _store: OrderedDict[str, tuple[str, _VT]] + + def __init__( + self, + data: Mapping[str, _VT] | Iterable[tuple[str, _VT]] | None = None, + **kwargs: _VT, + ) -> None: self._store = OrderedDict() if data is None: data = {} self.update(data, **kwargs) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: _VT) -> None: # Use the lowercased key for lookups, but store the actual # key alongside the value. self._store[key.lower()] = (key, value) - def __getitem__(self, key): + def __getitem__(self, key: str) -> _VT: return self._store[key.lower()][1] - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: del self._store[key.lower()] - def __iter__(self): - return (casedkey for casedkey, mappedvalue in self._store.values()) + def __iter__(self) -> Iterator[str]: + return (casedkey for casedkey, _ in self._store.values()) - def __len__(self): + def __len__(self) -> int: return len(self._store) - def lower_items(self): + def lower_items(self) -> Iterator[tuple[str, _VT]]: """Like iteritems(), but with all lowercase keys.""" return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items()) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, Mapping): - other = CaseInsensitiveDict(other) + other_dict: CaseInsensitiveDict[Any] = CaseInsensitiveDict(other) # type: ignore[reportUnknownArgumentType] else: return NotImplemented # Compare insensitively - return dict(self.lower_items()) == dict(other.lower_items()) + return dict(self.lower_items()) == dict(other_dict.lower_items()) # Copy is required - def copy(self): + def copy(self) -> CaseInsensitiveDict[_VT]: return CaseInsensitiveDict(self._store.values()) - def __repr__(self): + def __repr__(self) -> str: return str(dict(self.items())) -class LookupDict(dict): +class LookupDict(dict[str, _VT]): """Dictionary lookup object.""" - def __init__(self, name=None): + name: Any + + def __init__(self, name: Any = None) -> None: self.name = name super().__init__() - def __repr__(self): + def __repr__(self) -> str: return f"" - def __getitem__(self, key): + def __getattr__(self, key: str) -> _VT | None: + # We need this for type checkers to infer typing + # on attribute access with status_codes.py + if key in self.__dict__: + return self.__dict__[key] + else: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}'" + ) + + def __getitem__(self, key: str) -> _VT | None: # type: ignore[override] # We allow fall-through here, so values default to None return self.__dict__.get(key, None) - def get(self, key, default=None): + @overload + def get(self, key: str, default: None = None) -> _VT | None: ... + + @overload + def get(self, key: str, default: _D | _VT) -> _D | _VT: ... + + def get(self, key: str, default: _D | None = None) -> _VT | _D | None: return self.__dict__.get(key, default) diff --git a/libs/requests/utils.py b/libs/requests/utils.py index 8ab55852c..120336ddc 100644 --- a/libs/requests/utils.py +++ b/libs/requests/utils.py @@ -6,6 +6,8 @@ This module provides utility functions that are used within Requests that are also useful for external consumption. """ +from __future__ import annotations + import codecs import contextlib import io @@ -18,6 +20,15 @@ import tempfile import warnings import zipfile from collections import OrderedDict +from collections.abc import Generator, Iterable +from typing import ( + TYPE_CHECKING, + Any, + Final, + TypeVar, + cast, + overload, +) from urllib3.util import make_headers, parse_url @@ -26,30 +37,28 @@ from .__version__ import __version__ # to_native_string is unused here, but imported here for backwards compatibility from ._internal_utils import ( # noqa: F401 - _HEADER_VALIDATORS_BYTE, - _HEADER_VALIDATORS_STR, - HEADER_VALIDATORS, - to_native_string, + _HEADER_VALIDATORS_BYTE, # type: ignore[reportPrivateUsage] + _HEADER_VALIDATORS_STR, # type: ignore[reportPrivateUsage] + HEADER_VALIDATORS, # type: ignore[reportUnusedImport] + to_native_string, # type: ignore[reportUnusedImport] ) +from ._types import SupportsItems as _SupportsItems from .compat import ( Mapping, - basestring, bytes, getproxies, getproxies_environment, integer_types, is_urllib3_1, -) -from .compat import parse_http_list as _parse_list_header -from .compat import ( proxy_bypass, - proxy_bypass_environment, + proxy_bypass_environment, # type: ignore[attr-defined] # https://github.com/python/cpython/issues/145331 quote, str, unquote, urlparse, urlunparse, ) +from .compat import parse_http_list as _parse_list_header from .cookies import cookiejar_from_dict from .exceptions import ( FileModeWarning, @@ -59,14 +68,27 @@ from .exceptions import ( ) from .structures import CaseInsensitiveDict -NETRC_FILES = (".netrc", "_netrc") +if TYPE_CHECKING: + from http.cookiejar import CookieJar + from io import BufferedWriter -DEFAULT_CA_BUNDLE_PATH = certs.where() + from . import _types as _t + from .models import PreparedRequest, Request, Response -DEFAULT_PORTS = {"http": 80, "https": 443} +NETRC_FILES: Final = (".netrc", "_netrc") + + +# Certificate is extracted by certifi when needed. +DEFAULT_CA_BUNDLE_PATH: str = certs.where() + + +DEFAULT_PORTS: Final = {"http": 80, "https": 443} + +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") # Ensure that ', ' is used to preserve previous delimiter behavior. -DEFAULT_ACCEPT_ENCODING = ", ".join( +DEFAULT_ACCEPT_ENCODING: Final = ", ".join( re.split(r",\s*", make_headers(accept_encoding=True)["accept-encoding"]) ) @@ -74,7 +96,7 @@ DEFAULT_ACCEPT_ENCODING = ", ".join( if sys.platform == "win32": # provide a proxy_bypass version on Windows without DNS lookups - def proxy_bypass_registry(host): + def proxy_bypass_registry(host: str) -> bool: try: import winreg except ImportError: @@ -112,7 +134,7 @@ if sys.platform == "win32": return True return False - def proxy_bypass(host): # noqa + def proxy_bypass(host: str) -> bool: # noqa """Return True, if the host should be bypassed. Checks proxy settings gathered from the environment, if specified, @@ -124,16 +146,18 @@ if sys.platform == "win32": return proxy_bypass_registry(host) -def dict_to_sequence(d): +def dict_to_sequence( + d: _t.SupportsItems[Any, Any] | Iterable[tuple[Any, Any]], +) -> Iterable[tuple[Any, Any]]: """Returns an internal sequence dictionary update.""" - if hasattr(d, "items"): - d = d.items() + if isinstance(d, _SupportsItems): + return d.items() return d -def super_len(o): +def super_len(o: Any) -> int: total_length = None current_position = 0 @@ -204,9 +228,14 @@ def super_len(o): return max(0, total_length - current_position) -def get_netrc_auth(url, raise_errors=False): +def get_netrc_auth( + url: _t.UriType, raise_errors: bool = False +) -> tuple[str, str] | None: """Returns the Requests tuple auth for a given url from netrc.""" + if isinstance(url, bytes): + url = url.decode("utf-8") + netrc_file = os.environ.get("NETRC") if netrc_file is not None: netrc_locations = (netrc_file,) @@ -231,12 +260,15 @@ def get_netrc_auth(url, raise_errors=False): ri = urlparse(url) host = ri.hostname + if host is None: + return + try: _netrc = netrc(netrc_path).authenticators(host) - if _netrc: + if _netrc and any(_netrc): # Return with login / password login_i = 0 if _netrc[0] else 1 - return (_netrc[login_i], _netrc[2]) + return (_netrc[login_i] or "", _netrc[2] or "") except (NetrcParseError, OSError): # If there was a parsing error or a permissions issue reading the file, # we'll just skip netrc auth unless explicitly asked to raise errors. @@ -248,14 +280,14 @@ def get_netrc_auth(url, raise_errors=False): pass -def guess_filename(obj): +def guess_filename(obj: Any) -> str | None: """Tries to guess the filename of the given object.""" name = getattr(obj, "name", None) - if name and isinstance(name, basestring) and name[0] != "<" and name[-1] != ">": - return os.path.basename(name) + if name and isinstance(name, (str, bytes)) and name[0] != "<" and name[-1] != ">": + return os.path.basename(name) # type: ignore[return-value] # urllib3 accepts bytes but types str only -def extract_zipped_paths(path): +def extract_zipped_paths(path: str) -> str: """Replace nonexistent paths that look like they refer to a member of a zip archive with the location of an extracted copy of the target, or else just return the provided path unchanged. @@ -283,17 +315,18 @@ def extract_zipped_paths(path): return path # we have a valid zip archive and a valid member of that archive - tmp = tempfile.gettempdir() - extracted_path = os.path.join(tmp, member.split("/")[-1]) - if not os.path.exists(extracted_path): - # use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition - with atomic_open(extracted_path) as file_handler: - file_handler.write(zip_file.read(member)) + suffix = os.path.splitext(member.split("/")[-1])[-1] + fd, extracted_path = tempfile.mkstemp(suffix=suffix) + try: + os.write(fd, zip_file.read(member)) + finally: + os.close(fd) + return extracted_path @contextlib.contextmanager -def atomic_open(filename): +def atomic_open(filename: str) -> Generator[BufferedWriter, None, None]: """Write a file to the disk in an atomic fashion""" tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename)) try: @@ -305,7 +338,9 @@ def atomic_open(filename): raise -def from_key_val_list(value): +def from_key_val_list( + value: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None, +) -> dict[Any, Any] | None: """Take an object and test to see if it can be represented as a dictionary. Unless it can not be represented as such, return an OrderedDict, e.g., @@ -332,7 +367,15 @@ def from_key_val_list(value): return OrderedDict(value) -def to_key_val_list(value): +@overload +def to_key_val_list(value: None) -> None: ... +@overload +def to_key_val_list( + value: _t.SupportsItems[_KT, _VT] | Iterable[tuple[_KT, _VT]], +) -> list[tuple[_KT, _VT]]: ... +def to_key_val_list( + value: _t.SupportsItems[_KT, _VT] | Iterable[tuple[_KT, _VT]] | None, +) -> list[tuple[_KT, _VT]] | None: """Take an object and test to see if it can be represented as a dictionary. If it can be, return a list of tuples, e.g., @@ -355,14 +398,14 @@ def to_key_val_list(value): if isinstance(value, (str, bytes, bool, int)): raise ValueError("cannot encode objects that are not 2-tuples") - if isinstance(value, Mapping): - value = value.items() + if isinstance(value, _SupportsItems): + return list(value.items()) return list(value) # From mitsuhiko/werkzeug (used with permission). -def parse_list_header(value): +def parse_list_header(value: str) -> list[str]: """Parse lists as described by RFC 2068 Section 2. In particular, parse comma-separated lists where the elements of @@ -385,7 +428,7 @@ def parse_list_header(value): :return: :class:`list` :rtype: list """ - result = [] + result: list[str] = [] for item in _parse_list_header(value): if item[:1] == item[-1:] == '"': item = unquote_header_value(item[1:-1]) @@ -394,7 +437,7 @@ def parse_list_header(value): # From mitsuhiko/werkzeug (used with permission). -def parse_dict_header(value): +def parse_dict_header(value: str) -> dict[str, str | None]: """Parse lists of key, value pairs as described by RFC 2068 Section 2 and convert them into a python dict: @@ -416,7 +459,7 @@ def parse_dict_header(value): :return: :class:`dict` :rtype: dict """ - result = {} + result: dict[str, str | None] = {} for item in _parse_list_header(value): if "=" not in item: result[item] = None @@ -429,7 +472,7 @@ def parse_dict_header(value): # From mitsuhiko/werkzeug (used with permission). -def unquote_header_value(value, is_filename=False): +def unquote_header_value(value: str, is_filename: bool = False) -> str: r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). This does not use the real unquoting but what browsers are actually using for quoting. @@ -454,7 +497,7 @@ def unquote_header_value(value, is_filename=False): return value -def dict_from_cookiejar(cj): +def dict_from_cookiejar(cj: CookieJar) -> dict[str, str | None]: """Returns a key/value dictionary from a CookieJar. :param cj: CookieJar object to extract cookies from. @@ -465,7 +508,7 @@ def dict_from_cookiejar(cj): return cookie_dict -def add_dict_to_cookiejar(cj, cookie_dict): +def add_dict_to_cookiejar(cj: CookieJar, cookie_dict: dict[str, str]) -> CookieJar: """Returns a CookieJar from a key/value dictionary. :param cj: CookieJar to insert cookies into. @@ -476,7 +519,7 @@ def add_dict_to_cookiejar(cj, cookie_dict): return cookiejar_from_dict(cookie_dict, cj) -def get_encodings_from_content(content): +def get_encodings_from_content(content: str) -> list[str]: """Returns encodings from given content string. :param content: bytestring to extract encodings from. @@ -501,32 +544,29 @@ def get_encodings_from_content(content): ) -def _parse_content_type_header(header): - """Returns content type and parameters from given header +def _parse_content_type_header(header: str) -> tuple[str, dict[str, Any]]: + """Returns content type and parameters from given header. :param header: string :return: tuple containing content type and dictionary of - parameters + parameters. """ tokens = header.split(";") content_type, params = tokens[0].strip(), tokens[1:] - params_dict = {} - items_to_strip = "\"' " + params_dict: dict[str, str | bool] = {} + strip_chars = "\"' " for param in params: param = param.strip() - if param: - key, value = param, True - index_of_equals = param.find("=") - if index_of_equals != -1: - key = param[:index_of_equals].strip(items_to_strip) - value = param[index_of_equals + 1 :].strip(items_to_strip) + if param and (idx := param.find("=")) != -1: + key = param[:idx].strip(strip_chars) + value = param[idx + 1 :].strip(strip_chars) params_dict[key.lower()] = value return content_type, params_dict -def get_encoding_from_headers(headers): +def get_encoding_from_headers(headers: CaseInsensitiveDict[str]) -> str | None: """Returns encodings from given HTTP Header Dict. :param headers: dictionary to extract encoding from. @@ -551,7 +591,9 @@ def get_encoding_from_headers(headers): return "utf-8" -def stream_decode_response_unicode(iterator, r): +def stream_decode_response_unicode( + iterator: Iterable[bytes], r: Response +) -> Generator[str | bytes, None, None]: """Stream decodes an iterator.""" if r.encoding is None: @@ -568,7 +610,17 @@ def stream_decode_response_unicode(iterator, r): yield rv -def iter_slices(string, slice_length): +@overload +def iter_slices( + string: bytes, slice_length: int | None +) -> Generator[bytes, None, None]: ... +@overload +def iter_slices( + string: str, slice_length: int | None +) -> Generator[str, None, None]: ... +def iter_slices( + string: bytes | str, slice_length: int | None +) -> Generator[bytes | str, None, None]: """Iterate over slices of a string.""" pos = 0 if slice_length is None or slice_length <= 0: @@ -578,7 +630,7 @@ def iter_slices(string, slice_length): pos += slice_length -def get_unicode_from_response(r): +def get_unicode_from_response(r: Response) -> str | bytes | None: """Returns the requested content back in unicode. :param r: Response object to get unicode content from. @@ -598,8 +650,10 @@ def get_unicode_from_response(r): ), DeprecationWarning, ) + if r.content is None: # type: ignore[reportUnnecessaryComparison] + return None - tried_encodings = [] + tried_encodings: list[str] = [] # Try charset from content-type encoding = get_encoding_from_headers(r.headers) @@ -612,18 +666,18 @@ def get_unicode_from_response(r): # Fall back: try: - return str(r.content, encoding, errors="replace") + return str(r.content, encoding or "utf-8", errors="replace") except TypeError: return r.content # The unreserved URI characters (RFC 3986) -UNRESERVED_SET = frozenset( +UNRESERVED_SET: Final = frozenset( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~" ) -def unquote_unreserved(uri): +def unquote_unreserved(uri: str) -> str: """Un-escape any percent-escape sequences in a URI that are unreserved characters. This leaves all reserved, illegal and non-ASCII bytes encoded. @@ -647,7 +701,7 @@ def unquote_unreserved(uri): return "".join(parts) -def requote_uri(uri): +def requote_uri(uri: str) -> str: """Re-quote the given URI. This function passes the given URI through an unquote/quote cycle to @@ -669,7 +723,7 @@ def requote_uri(uri): return quote(uri, safe=safe_without_percent) -def address_in_network(ip, net): +def address_in_network(ip: str, net: str) -> bool: """This function allows you to check if an IP belongs to a network subnet Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24 @@ -684,7 +738,7 @@ def address_in_network(ip, net): return (ipaddr & netmask) == (network & netmask) -def dotted_netmask(mask): +def dotted_netmask(mask: int) -> str: """Converts mask from /xx format to xxx.xxx.xxx.xxx Example: if mask is 24 function returns 255.255.255.0 @@ -695,7 +749,7 @@ def dotted_netmask(mask): return socket.inet_ntoa(struct.pack(">I", bits)) -def is_ipv4_address(string_ip): +def is_ipv4_address(string_ip: str) -> bool: """ :rtype: bool """ @@ -706,7 +760,7 @@ def is_ipv4_address(string_ip): return True -def is_valid_cidr(string_network): +def is_valid_cidr(string_network: str) -> bool: """ Very simple check of the cidr format in no_proxy variable. @@ -731,7 +785,7 @@ def is_valid_cidr(string_network): @contextlib.contextmanager -def set_environ(env_name, value): +def set_environ(env_name: str, value: str | None) -> Generator[None, None, None]: """Set the environment variable 'env_name' to 'value' Save previous value, yield, and then restore the previous value stored in @@ -739,6 +793,7 @@ def set_environ(env_name, value): If 'value' is None, do nothing""" value_changed = value is not None + old_value: str | None = None if value_changed: old_value = os.environ.get(env_name) os.environ[env_name] = value @@ -752,7 +807,7 @@ def set_environ(env_name, value): os.environ[env_name] = old_value -def should_bypass_proxies(url, no_proxy): +def should_bypass_proxies(url: str, no_proxy: str | None) -> bool: """ Returns whether we should bypass proxies or not. @@ -761,7 +816,7 @@ def should_bypass_proxies(url, no_proxy): # Prioritize lowercase environment variables over uppercase # to keep a consistent behaviour with other http projects (curl, wget). - def get_proxy(key): + def get_proxy(key: str) -> str | None: return os.environ.get(key) or os.environ.get(key.upper()) # First check whether no_proxy is defined. If it is, check that the URL @@ -770,40 +825,42 @@ def should_bypass_proxies(url, no_proxy): if no_proxy is None: no_proxy = get_proxy("no_proxy") parsed = urlparse(url) + hostname = parsed.hostname - if parsed.hostname is None: + if hostname is None: # URLs don't always have hostnames, e.g. file:/// urls. return True if no_proxy: # We need to check whether we match here. We need to see if we match # the end of the hostname, both with and without the port. - no_proxy = (host for host in no_proxy.replace(" ", "").split(",") if host) + no_proxy_hosts = (host for host in no_proxy.replace(" ", "").split(",") if host) - if is_ipv4_address(parsed.hostname): - for proxy_ip in no_proxy: + if is_ipv4_address(hostname): + for proxy_ip in no_proxy_hosts: if is_valid_cidr(proxy_ip): - if address_in_network(parsed.hostname, proxy_ip): + if address_in_network(hostname, proxy_ip): return True - elif parsed.hostname == proxy_ip: + elif hostname == proxy_ip: # If no_proxy ip was defined in plain IP notation instead of cidr notation & # matches the IP of the index return True else: - host_with_port = parsed.hostname + host_with_port = hostname if parsed.port: host_with_port += f":{parsed.port}" - for host in no_proxy: - if parsed.hostname.endswith(host) or host_with_port.endswith(host): - # The URL does match something in no_proxy, so we don't want - # to apply the proxies on this URL. + for host in no_proxy_hosts: + host = host.lstrip(".") + if hostname == host or host_with_port == host: + return True + host = "." + host + if hostname.endswith(host) or host_with_port.endswith(host): return True with set_environ("no_proxy", no_proxy_arg): - # parsed.hostname can be `None` in cases such as a file URI. try: - bypass = proxy_bypass(parsed.hostname) + bypass = proxy_bypass(hostname) except (TypeError, socket.gaierror): bypass = False @@ -813,7 +870,7 @@ def should_bypass_proxies(url, no_proxy): return False -def get_environ_proxies(url, no_proxy=None): +def get_environ_proxies(url: str, no_proxy: str | None = None) -> dict[str, str]: """ Return a dict of environment proxies. @@ -825,7 +882,7 @@ def get_environ_proxies(url, no_proxy=None): return getproxies() -def select_proxy(url, proxies): +def select_proxy(url: str, proxies: dict[str, str] | None) -> str | None: """Select a proxy for the url, if applicable. :param url: The url being for the request @@ -851,7 +908,11 @@ def select_proxy(url, proxies): return proxy -def resolve_proxies(request, proxies, trust_env=True): +def resolve_proxies( + request: Request | PreparedRequest, + proxies: dict[str, str] | None, + trust_env: bool = True, +) -> dict[str, str]: """This method takes proxy information from a request and configuration input to resolve a mapping of target proxies. This will consider settings such as NO_PROXY to strip proxy configurations. @@ -863,7 +924,7 @@ def resolve_proxies(request, proxies, trust_env=True): :rtype: dict """ proxies = proxies if proxies is not None else {} - url = request.url + url = cast(str, request.url) scheme = urlparse(url).scheme no_proxy = proxies.get("no_proxy") new_proxies = proxies.copy() @@ -878,7 +939,7 @@ def resolve_proxies(request, proxies, trust_env=True): return new_proxies -def default_user_agent(name="python-requests"): +def default_user_agent(name: str = "python-requests") -> str: """ Return a string representing the default user agent. @@ -887,7 +948,7 @@ def default_user_agent(name="python-requests"): return f"{name}/{__version__}" -def default_headers(): +def default_headers() -> CaseInsensitiveDict[str]: """ :rtype: requests.structures.CaseInsensitiveDict """ @@ -901,7 +962,7 @@ def default_headers(): ) -def parse_header_links(value): +def parse_header_links(value: str) -> list[dict[str, str]]: """Return a list of parsed link headers proxies. i.e. Link: ; rel=front; type="image/jpeg",; rel=back;type="image/jpeg" @@ -909,7 +970,7 @@ def parse_header_links(value): :rtype: list """ - links = [] + links: list[dict[str, str]] = [] replace_chars = " '\"" @@ -923,7 +984,7 @@ def parse_header_links(value): except ValueError: url, params = val, "" - link = {"url": url.strip("<> '\"")} + link: dict[str, str] = {"url": url.strip("<> '\"")} for param in params.split(";"): try: @@ -944,7 +1005,7 @@ _null2 = _null * 2 _null3 = _null * 3 -def guess_json_utf(data): +def guess_json_utf(data: bytes) -> str | None: """ :rtype: str """ @@ -976,14 +1037,14 @@ def guess_json_utf(data): return None -def prepend_scheme_if_needed(url, new_scheme): +def prepend_scheme_if_needed(url: str, new_scheme: str) -> str: """Given a URL that may or may not have a scheme, prepend the given scheme. Does not replace a present scheme with the one provided as an argument. :rtype: str """ parsed = parse_url(url) - scheme, auth, host, port, path, query, fragment = parsed + scheme, auth, _host, _port, path, query, fragment = parsed # A defect in urlparse determines that there isn't a netloc present in some # urls. We previously assumed parsing was overly cautious, and swapped the @@ -996,6 +1057,7 @@ def prepend_scheme_if_needed(url, new_scheme): if auth: # parse_url doesn't provide the netloc with auth # so we'll add it ourselves. + netloc = cast(str, netloc) netloc = "@".join([auth, netloc]) if scheme is None: scheme = new_scheme @@ -1005,7 +1067,7 @@ def prepend_scheme_if_needed(url, new_scheme): return urlunparse((scheme, netloc, path, "", query, fragment)) -def get_auth_from_url(url): +def get_auth_from_url(url: str) -> tuple[str, str]: """Given a url with authentication components, extract them into a tuple of username,password. @@ -1014,14 +1076,15 @@ def get_auth_from_url(url): parsed = urlparse(url) try: - auth = (unquote(parsed.username), unquote(parsed.password)) + # except handles parsed.username/password being None + auth = (unquote(parsed.username), unquote(parsed.password)) # type: ignore[arg-type] except (AttributeError, TypeError): auth = ("", "") return auth -def check_header_validity(header): +def check_header_validity(header: tuple[str | bytes, str | bytes]) -> None: """Verifies that header parts don't contain leading whitespace reserved characters, or return characters. @@ -1032,10 +1095,15 @@ def check_header_validity(header): _validate_header_part(header, value, 1) -def _validate_header_part(header, header_part, header_validator_index): +def _validate_header_part( + header: tuple[str | bytes, str | bytes], + header_part: str | bytes, + header_validator_index: int, +) -> None: if isinstance(header_part, str): validator = _HEADER_VALIDATORS_STR[header_validator_index] - elif isinstance(header_part, bytes): + elif isinstance(header_part, bytes): # type: ignore[reportUnnecessaryIsInstance] + # runtime guard for non-str/bytes input validator = _HEADER_VALIDATORS_BYTE[header_validator_index] else: raise InvalidHeader( @@ -1043,7 +1111,7 @@ def _validate_header_part(header, header_part, header_validator_index): f"must be of type str or bytes, not {type(header_part)}" ) - if not validator.match(header_part): + if not validator.match(header_part): # type: ignore[arg-type] header_kind = "name" if header_validator_index == 0 else "value" raise InvalidHeader( f"Invalid leading whitespace, reserved character(s), or return " @@ -1051,13 +1119,13 @@ def _validate_header_part(header, header_part, header_validator_index): ) -def urldefragauth(url): +def urldefragauth(url: str) -> str: """ Given a url remove the fragment and the authentication part. :rtype: str """ - scheme, netloc, path, params, query, fragment = urlparse(url) + scheme, netloc, path, params, query, _fragment = urlparse(url) # see func:`prepend_scheme_if_needed` if not netloc: @@ -1068,16 +1136,17 @@ def urldefragauth(url): return urlunparse((scheme, netloc, path, params, query, "")) -def rewind_body(prepared_request): +def rewind_body(prepared_request: PreparedRequest) -> None: """Move file pointer back to its recorded starting position so it can be read again on redirect. """ body_seek = getattr(prepared_request.body, "seek", None) if body_seek is not None and isinstance( - prepared_request._body_position, integer_types + prepared_request._body_position, # type: ignore[reportPrivateUsage] + integer_types, ): try: - body_seek(prepared_request._body_position) + body_seek(prepared_request._body_position) # type: ignore[reportPrivateUsage] except OSError: raise UnrewindableBodyError( "An error occurred when rewinding request body for redirect." diff --git a/libs/rich-14.3.3.dist-info/WHEEL b/libs/rich-14.3.3.dist-info/WHEEL deleted file mode 100644 index 7894e8861..000000000 --- a/libs/rich-14.3.3.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: poetry-core 2.3.1 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/libs/rich-14.3.3.dist-info/INSTALLER b/libs/rich-15.0.0.dist-info/INSTALLER similarity index 100% rename from libs/rich-14.3.3.dist-info/INSTALLER rename to libs/rich-15.0.0.dist-info/INSTALLER diff --git a/libs/rich-14.3.3.dist-info/METADATA b/libs/rich-15.0.0.dist-info/METADATA similarity index 99% rename from libs/rich-14.3.3.dist-info/METADATA rename to libs/rich-15.0.0.dist-info/METADATA index 9f6bc96c6..574ddfb0e 100644 --- a/libs/rich-14.3.3.dist-info/METADATA +++ b/libs/rich-15.0.0.dist-info/METADATA @@ -1,12 +1,12 @@ Metadata-Version: 2.4 Name: rich -Version: 14.3.3 +Version: 15.0.0 Summary: Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal License: MIT License-File: LICENSE Author: Will McGugan Author-email: willmcgugan@gmail.com -Requires-Python: >=3.8.0 +Requires-Python: >=3.9.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Framework :: IPython @@ -16,7 +16,6 @@ Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 diff --git a/libs/rich-14.3.3.dist-info/RECORD b/libs/rich-15.0.0.dist-info/RECORD similarity index 82% rename from libs/rich-14.3.3.dist-info/RECORD rename to libs/rich-15.0.0.dist-info/RECORD index fea25aa7e..b97bc1f7e 100644 --- a/libs/rich-14.3.3.dist-info/RECORD +++ b/libs/rich-15.0.0.dist-info/RECORD @@ -1,13 +1,13 @@ -rich-14.3.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -rich-14.3.3.dist-info/METADATA,sha256=9kOnJPejl6vTwavl35qTJmtKu218p9Ob-z-jDoz1ATo,18495 -rich-14.3.3.dist-info/RECORD,, -rich-14.3.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -rich-14.3.3.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88 -rich-14.3.3.dist-info/licenses/LICENSE,sha256=3u18F6QxgVgZCj6iOcyHmlpQJxzruYrnAl9I--WNyhU,1056 +rich-15.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +rich-15.0.0.dist-info/METADATA,sha256=-tNPxgP6xEgb-B-N1ilJL8MCwOwxkJyGZEdRUSr7Ljs,18445 +rich-15.0.0.dist-info/RECORD,, +rich-15.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +rich-15.0.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88 +rich-15.0.0.dist-info/licenses/LICENSE,sha256=3u18F6QxgVgZCj6iOcyHmlpQJxzruYrnAl9I--WNyhU,1056 rich/__init__.py,sha256=j7AA9_z_PANQbmRTwTkWZc_DVdqtBBHxBqGrMsz21CI,6131 rich/__main__.py,sha256=kcy8BtBEUkYLCogRkwhCdJs0VNx6KF1n8kFU2g-7plg,7725 rich/_emoji_codes.py,sha256=hu1VL9nbVdppJrVoijVshRlcRRe_v3dju3Mmd2sKZdY,140235 -rich/_emoji_replace.py,sha256=n-kcetsEUx2ZUmhQrfeMNc-teeGhpuSQ5F8VPBsyvDo,1064 +rich/_emoji_replace.py,sha256=ysSn8smSqQW22XEfCH0lJ80vogUYZKr9dPImfUG2e6Y,1067 rich/_export_format.py,sha256=RI08pSrm5tBSzPMvnbTqbD9WIalaOoN5d4M1RTmLq1Y,2128 rich/_extension.py,sha256=G66PkbH_QdTJh6jD-J228O76CmAnr2hLQv72CgPPuzE,241 rich/_fileno.py,sha256=HWZxP5C2ajMbHryvAQZseflVfQoGzsKOHzKGsLD8ynQ,799 @@ -50,22 +50,22 @@ rich/_windows_renderer.py,sha256=d799xOnxLbCCCzGu9-U7YLmIQkxtxQIBFQQ6iu4veSc,275 rich/_wrap.py,sha256=FlSsom5EX0LVkA3KWy34yHnCfLtqX-ZIepXKh-70rpc,3404 rich/abc.py,sha256=dALMOGfKVNeAbvqq66IpTQxQUerxD7AE4FKwqd0eQKk,878 rich/align.py,sha256=a8MbP-iJjoOFonxqgoP3YketKMWU3WFZaxjqAv2Qc5E,10726 -rich/ansi.py,sha256=Avs1LHbSdcyOvDOdpELZUoULcBiYewY76eNBp6uFBhs,6921 +rich/ansi.py,sha256=TQ7pdksq0b3XPcw_tSGvL_QthPxNLKOYQImPQYWp6pM,6947 rich/bar.py,sha256=ldbVHOzKJOnflVNuv1xS7g6dLX2E3wMnXkdPbpzJTcs,3263 rich/box.py,sha256=SSolg8_pzHzY9QvJQo-qp0tbPsnj8O_2W4hmi1l-Zo0,10650 rich/cells.py,sha256=fPRSnquYlCO4THgmUokOJOhNlcx8C4NZ2Xv9JM_7GbE,12335 rich/color.py,sha256=3HSULVDj7qQkXUdFWv78JOiSZzfy5y1nkcYhna296V0,18211 rich/color_triplet.py,sha256=3lhQkdJbvWPoLDO-AnYImAWmJvV5dlgYNCVZ97ORaN4,1054 rich/columns.py,sha256=HUX0KcMm9dsKNi11fTbiM_h2iDtl8ySCaVcxlalEzq8,7131 -rich/console.py,sha256=doKNi4BLSKZCt6Ra-6AXiQK-FaWAL-7fClG2ZO5w7uQ,101009 +rich/console.py,sha256=RTP8ZpY2I6KiFEe1Adb1H4z-5-FHdReAk6Xm62HcE1o,101361 rich/constrain.py,sha256=1VIPuC8AgtKWrcncQrjBdYqA3JVWysu6jZo1rrh7c7Q,1288 rich/containers.py,sha256=c_56TxcedGYqDepHBMTuZdUIijitAQgnox-Qde0Z1qo,5502 rich/control.py,sha256=HnsraFTzBaUQDzKJWXsfPv-PPmgGypSgSv7oANackqs,6475 -rich/default_styles.py,sha256=Tk1pZ9bOZ40EVX7cfBbYDEo7hrpwBvZGsiUCXdh0P0M,8340 +rich/default_styles.py,sha256=DqEUiGrj4N0eAcgDwjGeUSe8BLMxZw57L9RXnTCroTs,8401 rich/diagnose.py,sha256=1RWnQoppPXjC_49AB4vtV048DK3ksQSq671C83Y6f-g,977 -rich/emoji.py,sha256=_bTf1Y3JqiMk6Nfn4V_YOhq1wAPAHNODhGLJj95R3uI,2343 +rich/emoji.py,sha256=z7ABu0_61WJPWOM_dxhUHNaA0B6RQuZX3reljs1iWE8,2388 rich/errors.py,sha256=5pP3Kc5d4QJ_c0KFsxrfyhjiPVe7J1zOqSFbFAzcV-Y,642 -rich/file_proxy.py,sha256=Tl9THMDZ-Pk5Wm8sI1gGg_U5DhusmxD-FZ0fUbcU0W0,1683 +rich/file_proxy.py,sha256=MvlB3tSsuVmH4qCWXK9SK3S_OohVRIczUw_Fufmsdt8,1750 rich/filesize.py,sha256=_iz9lIpRgvW7MNSeCZnLg-HwzbP4GETg543WqD8SFs0,2484 rich/highlighter.py,sha256=MIapWwjR8ahUcsDicgN4xLRbanbh5x_Qzh5kfdDrKKk,9729 rich/json.py,sha256=omC2WHTgURxEosna1ftoSJCne2EX7MDuQtCdswS3qsk,5019 @@ -73,8 +73,8 @@ rich/jupyter.py,sha256=G9pOJmR4ESIFYSd4MKGqmHqCtstx0oRWpyeTgv54-Xc,3228 rich/layout.py,sha256=WR8PCSroYnteIT3zawxQ3k3ad1sQO5wGG1SZOoeBuBM,13944 rich/live.py,sha256=UKvqLSuSzNHIBz5pxRrU7SPsnG48vJJx2TMHiaHlmnI,15317 rich/live_render.py,sha256=jQO3X_p2wemGI2bdCAY_e5j9xXNTwDxuujbpoSRsH6c,3803 -rich/logging.py,sha256=UL6TZNlaptYKHNhQ45LREy-29Pl-tQsBh7q3HSnWIAA,12456 -rich/markdown.py,sha256=mDDJmUbWRYm7_vKenVq76K6aG43KTJJ4tiYMzxXO7ns,26177 +rich/logging.py,sha256=7vhk7r_LhWTcwteyjLLM5Nfc839fwBuIiISvBZcGwzo,12622 +rich/markdown.py,sha256=MmbNh8IhWgdGNQkeNTHWNR470KQVACt_9aJu6TMourA,26549 rich/markup.py,sha256=btpr271BLhiCR1jNglRnv2BpIzVcNefYwSMeW9teDbc,8427 rich/measure.py,sha256=HmrIJX8sWRTHbgh8MxEay_83VkqNW_70s8aKP5ZcYI8,5305 rich/padding.py,sha256=h8XnIivLrNtlxI3vQPKHXh4hAwjOJqZx0slM0z3g1_M,4896 @@ -85,23 +85,23 @@ rich/pretty.py,sha256=lxW_bhBfDTKe-grtzP1KQhQsqPZxfZN14th4JRVoLzQ,36349 rich/progress.py,sha256=Z6Yz6pYO26LFXx24cXKvk2bEX6FkjK2Z5e2fy0Bt78s,60393 rich/progress_bar.py,sha256=mZTPpJUwcfcdgQCTTz3kyY-fc79ddLwtx6Ghhxfo064,8162 rich/prompt.py,sha256=1SIuBEelovvwCG2wp22PZwskA3IkmAsOxf0ruFkOhOk,12448 -rich/protocol.py,sha256=Wt-2HZd67OYiopUkCTOz7lM38vyo5r3HEQZ9TOPDl5Q,1367 +rich/protocol.py,sha256=9OHzfZXAnidZqaZgSFsX5KsJg8JHHNUc8jk9dsxa1ik,1348 rich/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 rich/region.py,sha256=rNT9xZrVZTYIXZC0NYn41CJQwYNbR-KecPOxTgQvB8Y,166 -rich/repr.py,sha256=HIsurPLZK9Gray75l3_vQx7S27AzTpAj4ChXSfe1Fes,4419 +rich/repr.py,sha256=mFexGTVledesZiMD3jBBBBoICoWh-XPFAUq3Hu7iX4U,4436 rich/rule.py,sha256=umO21Wjw0FcYAeTB3UumNLCsDWhejzxnjlf2VwiXiDI,4590 rich/scope.py,sha256=x-jkKIE-j7ZxA9I07lya5iVAuDAyUKM58HWtNZQUzRc,3239 rich/screen.py,sha256=rL_j2wX-4SeuIOI2oOlc418QP9EAvD59GInUmEAE6jQ,1579 -rich/segment.py,sha256=aUh9qRteTyo6guePUn8cU7S1ccXRsfzLZ-VMDXXeNug,25795 +rich/segment.py,sha256=17JAw4bmsV_LrJ8PnhI9XH2DaN-fOEPF-8mgx7joLDM,25740 rich/spinner.py,sha256=onIhpKlljRHppTZasxO8kXgtYyCHUkpSgKglRJ3o51g,4214 rich/status.py,sha256=kkPph3YeAZBo-X-4wPp8gTqZyU466NLwZBA4PZTTewo,4424 -rich/style.py,sha256=W9Ccy8Py8lNICtlfcp-ryzMTuQaGxAU3av7-g5fHu0s,26990 +rich/style.py,sha256=yp4TsNKNPUxBytoFWfy7kzE2Gh_9TYA7fs2ULXfbm00,27068 rich/styled.py,sha256=wljVsVTXbABMMZvkzkO43ZEk_-irzEtvUiQ-sNnikQ8,1234 -rich/syntax.py,sha256=5ZBNxjIj3C1FC92vLwBVN-C5YAdKjPHfH6SqCzFaOYE,36263 +rich/syntax.py,sha256=ytgeGUt91fdr8LaHKOdkaNMewrPRxgoci5mMd3MPE4I,36305 rich/table.py,sha256=6yv7wMLXZgOsGgzEDpKXpecXQOwcLBHNOn-MYcbo1Zk,40033 rich/terminal_theme.py,sha256=1j5-ufJfnvlAo5Qsi_ACZiXDmwMXzqgmFByObT9-yJY,3370 rich/text.py,sha256=t2J8iMquwR0BUUggm-cVNp30Wxw5M8fIF_p0jioYRV0,47655 -rich/theme.py,sha256=oNyhXhGagtDlbDye3tVu3esWOWk0vNkuxFw-_unlaK0,3771 +rich/theme.py,sha256=biYZWd_Wu1KW8VtT7UsTQDrDU89RxAK5_Vz1y3ztGYI,3780 rich/themes.py,sha256=0xgTLozfabebYtcJtDdC5QkX5IVUEaviqDUJJh4YVFk,102 rich/traceback.py,sha256=TT03eLKEj5XapXFp9kgf0bBwNupIFrHquKxZtwBhz_8,37535 rich/tree.py,sha256=QoOwg424FkdwGfR8K0tZ6Q7qtzWNAUP_m4sFaYuG6nw,9391 diff --git a/libs/soupsieve-2.3.2.post1.dist-info/REQUESTED b/libs/rich-15.0.0.dist-info/REQUESTED similarity index 100% rename from libs/soupsieve-2.3.2.post1.dist-info/REQUESTED rename to libs/rich-15.0.0.dist-info/REQUESTED diff --git a/libs/rich-15.0.0.dist-info/WHEEL b/libs/rich-15.0.0.dist-info/WHEEL new file mode 100644 index 000000000..179c4dac6 --- /dev/null +++ b/libs/rich-15.0.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 2.4.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/rich-14.3.3.dist-info/licenses/LICENSE b/libs/rich-15.0.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/rich-14.3.3.dist-info/licenses/LICENSE rename to libs/rich-15.0.0.dist-info/licenses/LICENSE diff --git a/libs/rich/_emoji_replace.py b/libs/rich/_emoji_replace.py index bb2cafa18..fdaff320d 100644 --- a/libs/rich/_emoji_replace.py +++ b/libs/rich/_emoji_replace.py @@ -1,8 +1,5 @@ -from typing import Callable, Match, Optional import re - -from ._emoji_codes import EMOJI - +from typing import Callable, Match, Optional _ReStringMatch = Match[str] # regex match object _ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub @@ -15,8 +12,10 @@ def _emoji_replace( _emoji_sub: _EmojiSubMethod = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub, ) -> str: """Replace emoji code in text.""" + from ._emoji_codes import EMOJI + get_emoji = EMOJI.__getitem__ - variants = {"text": "\uFE0E", "emoji": "\uFE0F"} + variants = {"text": "\ufe0e", "emoji": "\ufe0f"} get_variant = variants.get default_variant_code = variants.get(default_variant, "") if default_variant else "" diff --git a/libs/rich/ansi.py b/libs/rich/ansi.py index 7de86ce50..7bbcb0bfc 100644 --- a/libs/rich/ansi.py +++ b/libs/rich/ansi.py @@ -127,13 +127,13 @@ class AnsiDecoder: """Decode ANSI codes in an iterable of lines. Args: - lines (Iterable[str]): An iterable of lines of terminal output. + terminal_text: Output potentially containing ANSI escape sequences. Yields: Text: Marked up Text. """ - for line in terminal_text.splitlines(): - yield self.decode_line(line) + for line in re.split(r"(?<=\n)", terminal_text): + yield self.decode_line(line.rstrip("\n")) def decode_line(self, line: str) -> Text: """Decode a line containing ansi codes. diff --git a/libs/rich/console.py b/libs/rich/console.py index ad92d529c..0bdce7698 100644 --- a/libs/rich/console.py +++ b/libs/rich/console.py @@ -1,17 +1,13 @@ -import inspect import os import sys import threading -import zlib from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime from functools import wraps -from getpass import getpass -from html import escape -from inspect import isclass from itertools import islice from math import ceil +from os import PathLike from time import monotonic from types import FrameType, ModuleType, TracebackType from typing import ( @@ -50,10 +46,8 @@ from .highlighter import NullHighlighter, ReprHighlighter from .markup import render as render_markup from .measure import Measurement, measure_renderables from .pager import Pager, SystemPager -from .pretty import Pretty, is_expandable from .protocol import rich_cast from .region import Region -from .scope import render_scope from .screen import Screen from .segment import Segment from .style import Style, StyleType @@ -1321,7 +1315,7 @@ class Console: render_iterable: RenderResult renderable = rich_cast(renderable) - if hasattr(renderable, "__rich_console__") and not isclass(renderable): + if hasattr(renderable, "__rich_console__") and not isinstance(renderable, type): render_iterable = renderable.__rich_console__(self, _options) elif isinstance(renderable, str): text_renderable = self.render_str( @@ -1528,6 +1522,14 @@ class Console: Returns: List[ConsoleRenderable]: A list of things to render. """ + + def is_expandable(obj: object) -> bool: + """Check if an object is expandable by pretty printer.""" + # Permit lazy loading + from .pretty import is_expandable as _is_expandable + + return _is_expandable(obj) + renderables: List[ConsoleRenderable] = [] _append = renderables.append text: List[Text] = [] @@ -1570,6 +1572,8 @@ class Console: append(renderable) elif is_expandable(renderable): check_text() + from .pretty import Pretty + append(Pretty(renderable, highlighter=_highlighter)) else: append_text(_highlighter(str(renderable))) @@ -1683,7 +1687,10 @@ class Console: new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``. """ if not objects: - objects = (NewLine(),) + if end == "\n": + objects = (NewLine(),) + else: + objects = ("",) if soft_wrap is None: soft_wrap = self.soft_wrap @@ -1900,15 +1907,14 @@ class Console: @staticmethod def _caller_frame_info( - offset: int, - currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe, + offset: int, currentframe: Optional[Callable[[], Optional[FrameType]]] = None ) -> Tuple[str, int, Dict[str, Any]]: """Get caller frame information. Args: offset (int): the caller offset within the current frame stack. currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to - retrieve the current frame. Defaults to ``inspect.currentframe``. + retrieve the current frame. Defaults to None, which will use ``inspect.currentframe()``. Returns: Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and @@ -1920,17 +1926,22 @@ class Console: # Ignore the frame of this local helper offset += 1 - frame = currentframe() + if currentframe is None: + import inspect + + frame = inspect.currentframe() + else: + frame = currentframe() if frame is not None: - # Use the faster currentframe where implemented while offset and frame is not None: frame = frame.f_back offset -= 1 assert frame is not None return frame.f_code.co_filename, frame.f_lineno, frame.f_locals else: - # Fallback to the slower stack - frame_info = inspect.stack()[offset] + from inspect import stack + + frame_info = stack()[offset] return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals def log( @@ -1983,6 +1994,8 @@ class Console: link_path = None if filename.startswith("<") else os.path.abspath(filename) path = filename.rpartition(os.sep)[-1] if log_locals: + from .scope import render_scope + locals_map = { key: value for key, value in locals.items() @@ -2166,7 +2179,9 @@ class Console: if prompt: self.print(prompt, markup=markup, emoji=emoji, end="") if password: - result = getpass("", stream=stream) + import getpass as _getpass_mod + + result = _getpass_mod.getpass("", stream=stream) else: if stream: result = stream.readline() @@ -2206,11 +2221,17 @@ class Console: del self._record_buffer[:] return text - def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None: + def save_text( + self, + path: Union[str, PathLike[str]], + *, + clear: bool = True, + styles: bool = False, + ) -> None: """Generate text from console and save to a given location (requires record=True argument in constructor). Args: - path (str): Path to write text files. + path (Union[str, PathLike[str]]): Path to write text files. clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text. Defaults to ``False``. @@ -2242,6 +2263,8 @@ class Console: Returns: str: String containing console contents as HTML. """ + from html import escape + assert ( self.record ), "To export console contents set record=True in the constructor or instance" @@ -2297,7 +2320,7 @@ class Console: def save_html( self, - path: str, + path: Union[str, PathLike[str]], *, theme: Optional[TerminalTheme] = None, clear: bool = True, @@ -2307,7 +2330,7 @@ class Console: """Generate HTML from console contents and write to a file (requires record=True argument in constructor). Args: - path (str): Path to write html file. + path (Union[str, PathLike[str]]): Path to write html file. theme (TerminalTheme, optional): TerminalTheme object containing console colors. clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. code_format (str, optional): Format string to render HTML. In addition to '{foreground}', @@ -2353,6 +2376,9 @@ class Console: ids). If not set, this defaults to a computed value based on the recorded content. """ + import zlib + from html import escape + from rich.cells import cell_len style_cache: Dict[Style, str] = {} @@ -2579,7 +2605,7 @@ class Console: def save_svg( self, - path: str, + path: Union[str, PathLike[str]], *, title: str = "Rich", theme: Optional[TerminalTheme] = None, @@ -2591,7 +2617,7 @@ class Console: """Generate an SVG file from the console contents (requires record=True in Console constructor). Args: - path (str): The path to write the SVG to. + path (Union[str, PathLike[str]]): The path to write the SVG to. title (str, optional): The title of the tab in the output image theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` @@ -2616,18 +2642,6 @@ class Console: write_file.write(svg) -def _svg_hash(svg_main_code: str) -> str: - """Returns a unique hash for the given SVG main code. - - Args: - svg_main_code (str): The content we're going to inject in the SVG envelope. - - Returns: - str: a hash of the given content - """ - return str(zlib.adler32(svg_main_code.encode())) - - if __name__ == "__main__": # pragma: no cover console = Console(record=True) diff --git a/libs/rich/default_styles.py b/libs/rich/default_styles.py index c18b6095e..f2e4cd1b9 100644 --- a/libs/rich/default_styles.py +++ b/libs/rich/default_styles.py @@ -165,6 +165,7 @@ DEFAULT_STYLES: Dict[str, Style] = { "markdown.s": Style(strike=True), "markdown.table.border": Style(color="cyan"), "markdown.table.header": Style(color="cyan", bold=False), + "markdown.kbd": Style(bold=True, color="bright_yellow"), "iso8601.date": Style(color="blue"), "iso8601.time": Style(color="magenta"), "iso8601.timezone": Style(color="yellow"), diff --git a/libs/rich/emoji.py b/libs/rich/emoji.py index 9433e6fb3..067f93ce3 100644 --- a/libs/rich/emoji.py +++ b/libs/rich/emoji.py @@ -1,12 +1,10 @@ import sys -from typing import TYPE_CHECKING, Optional, Union, Literal +from typing import TYPE_CHECKING, Literal, Optional, Union +from ._emoji_replace import _emoji_replace from .jupyter import JupyterMixin from .segment import Segment from .style import Style -from ._emoji_codes import EMOJI -from ._emoji_replace import _emoji_replace - if TYPE_CHECKING: from .console import Console, ConsoleOptions, RenderResult @@ -22,7 +20,7 @@ class NoEmoji(Exception): class Emoji(JupyterMixin): __slots__ = ["name", "style", "_char", "variant"] - VARIANTS = {"text": "\uFE0E", "emoji": "\uFE0F"} + VARIANTS = {"text": "\ufe0e", "emoji": "\ufe0f"} def __init__( self, @@ -39,6 +37,8 @@ class Emoji(JupyterMixin): Raises: NoEmoji: If the emoji doesn't exist. """ + from ._emoji_codes import EMOJI + self.name = name self.style = style self.variant = variant @@ -81,8 +81,10 @@ if __name__ == "__main__": # pragma: no cover console = Console(record=True) + from ._emoji_codes import EMOJI + columns = Columns( - (f":{name}: {name}" for name in sorted(EMOJI.keys()) if "\u200D" not in name), + (f":{name}: {name}" for name in sorted(EMOJI.keys()) if "\u200d" not in name), column_first=True, ) diff --git a/libs/rich/file_proxy.py b/libs/rich/file_proxy.py index 4b0b0da6c..e32523bd6 100644 --- a/libs/rich/file_proxy.py +++ b/libs/rich/file_proxy.py @@ -55,3 +55,6 @@ class FileProxy(io.TextIOBase): def fileno(self) -> int: return self.__file.fileno() + + def isatty(self) -> bool: + return self.__file.isatty() diff --git a/libs/rich/logging.py b/libs/rich/logging.py index c3e7a5f67..e572f078c 100644 --- a/libs/rich/logging.py +++ b/libs/rich/logging.py @@ -1,18 +1,24 @@ +from __future__ import annotations + import logging +import os from datetime import datetime from logging import Handler, LogRecord -from pathlib import Path from types import ModuleType -from typing import ClassVar, Iterable, List, Optional, Type, Union +from typing import TYPE_CHECKING, ClassVar, Iterable, List, Optional, Type, Union + +if TYPE_CHECKING: + from ._log_render import FormatTimeCallable + from .console import Console, ConsoleRenderable + from .highlighter import Highlighter + from .traceback import Traceback from rich._null_file import NullFile from . import get_console -from ._log_render import FormatTimeCallable, LogRender -from .console import Console, ConsoleRenderable -from .highlighter import Highlighter, ReprHighlighter +from ._log_render import LogRender +from .highlighter import ReprHighlighter from .text import Text -from .traceback import Traceback class RichHandler(Handler): @@ -141,6 +147,8 @@ class RichHandler(Handler): exc_type, exc_value, exc_traceback = record.exc_info assert exc_type is not None assert exc_value is not None + from .traceback import Traceback + traceback = Traceback.from_exception( exc_type, exc_value, @@ -179,7 +187,7 @@ class RichHandler(Handler): except Exception: self.handleError(record) - def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable": + def render_message(self, record: LogRecord, message: str) -> ConsoleRenderable: """Render message text in to Text. Args: @@ -209,8 +217,8 @@ class RichHandler(Handler): *, record: LogRecord, traceback: Optional[Traceback], - message_renderable: "ConsoleRenderable", - ) -> "ConsoleRenderable": + message_renderable: ConsoleRenderable, + ) -> ConsoleRenderable: """Render log for display. Args: @@ -221,7 +229,7 @@ class RichHandler(Handler): Returns: ConsoleRenderable: Renderable to display log. """ - path = Path(record.pathname).name + path = os.path.basename(record.pathname) level = self.get_level_text(record) time_format = None if self.formatter is None else self.formatter.datefmt log_time = datetime.fromtimestamp(record.created) diff --git a/libs/rich/markdown.py b/libs/rich/markdown.py index 5db183b0a..abbbb07d6 100644 --- a/libs/rich/markdown.py +++ b/libs/rich/markdown.py @@ -331,9 +331,10 @@ class TableDataElement(MarkdownElement): self.justify = justify def on_text(self, context: MarkdownContext, text: TextType) -> None: - text = Text(text) if isinstance(text, str) else text - text.stylize(context.current_style) - self.content.append_text(text) + if isinstance(text, str): + self.content.append(text, context.current_style) + else: + self.content.append_text(text) class ListElement(MarkdownElement): @@ -615,6 +616,14 @@ class Markdown(JupyterMixin): context.enter_style(link_style) else: context.stack.push(Link.create(self, token)) + elif node_type == "html_inline": + if token.content == "": + kbd_style = console.get_style("markdown.kbd", default="bold") + context.enter_style(kbd_style) + elif token.content == "": + context.leave_style() + else: + continue elif node_type == "link_close": if self.hyperlinks: context.leave_style() diff --git a/libs/rich/protocol.py b/libs/rich/protocol.py index c6923dd7d..ae13856f3 100644 --- a/libs/rich/protocol.py +++ b/libs/rich/protocol.py @@ -1,5 +1,4 @@ from typing import Any, cast, Set, TYPE_CHECKING -from inspect import isclass if TYPE_CHECKING: from rich.console import RenderableType @@ -28,7 +27,7 @@ def rich_cast(renderable: object) -> "RenderableType": from rich.console import RenderableType rich_visited_set: Set[type] = set() # Prevent potential infinite loop - while hasattr(renderable, "__rich__") and not isclass(renderable): + while hasattr(renderable, "__rich__") and not isinstance(renderable, type): # Detect object which claim to have all the attributes if hasattr(renderable, _GIBBERISH): return repr(renderable) diff --git a/libs/rich/repr.py b/libs/rich/repr.py index 95331006e..0e9bc0e53 100644 --- a/libs/rich/repr.py +++ b/libs/rich/repr.py @@ -1,4 +1,3 @@ -import inspect from functools import partial from typing import ( Any, @@ -68,6 +67,8 @@ def auto( def auto_rich_repr(self: Type[T]) -> Result: """Auto generate __rich_rep__ from signature of __init__""" try: + import inspect + signature = inspect.signature(self.__init__) for name, param in signature.parameters.items(): if param.kind == param.POSITIONAL_ONLY: diff --git a/libs/rich/segment.py b/libs/rich/segment.py index 0df63fdef..dd1e8c2a4 100644 --- a/libs/rich/segment.py +++ b/libs/rich/segment.py @@ -1,7 +1,6 @@ from enum import IntEnum from functools import lru_cache from itertools import filterfalse -from logging import getLogger from operator import attrgetter from typing import ( TYPE_CHECKING, @@ -29,8 +28,6 @@ from .style import Style if TYPE_CHECKING: from .console import Console, ConsoleOptions, RenderResult -log = getLogger("rich") - class ControlType(IntEnum): """Non-printable control codes which typically translate to ANSI codes.""" diff --git a/libs/rich/style.py b/libs/rich/style.py index 529424189..3806a8c4b 100644 --- a/libs/rich/style.py +++ b/libs/rich/style.py @@ -1,8 +1,9 @@ import sys from functools import lru_cache +from itertools import count from operator import attrgetter from pickle import dumps, loads -from random import randint +from random import getrandbits from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast from . import errors @@ -18,6 +19,9 @@ _hash_getter = attrgetter( StyleType = Union[str, "Style"] +_id_generator = count(getrandbits(24)) + + class _Bit: """A descriptor to get/set a style attribute bit.""" @@ -195,7 +199,7 @@ class Style: self._link = link self._meta = None if meta is None else dumps(meta) self._link_id = ( - f"{randint(0, 999999)}{hash(self._meta)}" if (link or meta) else "" + f"{next(_id_generator)}{hash(self._meta)}" if (link or meta) else "" ) self._hash: Optional[int] = None self._null = not (self._set_attributes or color or bgcolor or link or meta) @@ -245,7 +249,7 @@ class Style: style._attributes = 0 style._link = None style._meta = dumps(meta) - style._link_id = f"{randint(0, 999999)}{hash(style._meta)}" + style._link_id = f"{next(_id_generator)}{hash(style._meta)}" style._hash = None style._null = not (meta) return style @@ -483,7 +487,7 @@ class Style: style._attributes = self._attributes style._set_attributes = self._set_attributes style._link = self._link - style._link_id = f"{randint(0, 999999)}" if self._link else "" + style._link_id = f"{next(_id_generator)}" if self._link else "" style._null = False style._meta = None style._hash = None @@ -635,7 +639,7 @@ class Style: style._attributes = self._attributes style._set_attributes = self._set_attributes style._link = self._link - style._link_id = f"{randint(0, 999999)}" if self._link else "" + style._link_id = f"{next(_id_generator)}" if self._link else "" style._hash = self._hash style._null = False style._meta = self._meta @@ -681,7 +685,7 @@ class Style: style._attributes = self._attributes style._set_attributes = self._set_attributes style._link = link - style._link_id = f"{randint(0, 999999)}" if link else "" + style._link_id = f"{next(_id_generator)}" if link else "" style._hash = None style._null = False style._meta = self._meta diff --git a/libs/rich/syntax.py b/libs/rich/syntax.py index 5e17b48f1..8c8f8315e 100644 --- a/libs/rich/syntax.py +++ b/libs/rich/syntax.py @@ -7,6 +7,7 @@ import textwrap from abc import ABC, abstractmethod from pathlib import Path from typing import ( + TYPE_CHECKING, Any, Dict, Iterable, @@ -38,13 +39,15 @@ from pygments.token import ( ) from pygments.util import ClassNotFound +if TYPE_CHECKING: + from .console import Console, ConsoleOptions, JustifyMethod, RenderResult + from rich.containers import Lines from rich.padding import Padding, PaddingDimensions from ._loop import loop_first from .cells import cell_len from .color import Color, blend_rgb -from .console import Console, ConsoleOptions, JustifyMethod, RenderResult from .jupyter import JupyterMixin from .measure import Measurement from .segment import Segment, Segments diff --git a/libs/rich/theme.py b/libs/rich/theme.py index 227f1d863..6b3c8c0ee 100644 --- a/libs/rich/theme.py +++ b/libs/rich/theme.py @@ -1,4 +1,3 @@ -import configparser from typing import IO, Dict, List, Mapping, Optional from .default_styles import DEFAULT_STYLES @@ -49,6 +48,8 @@ class Theme: Returns: Theme: A New theme instance. """ + import configparser + config = configparser.ConfigParser() config.read_file(config_file, source=source) styles = {name: Style.parse(value) for name, value in config.items("styles")} diff --git a/libs/socketio/async_aiopika_manager.py b/libs/socketio/async_aiopika_manager.py index 626158bfd..b6d5e336c 100644 --- a/libs/socketio/async_aiopika_manager.py +++ b/libs/socketio/async_aiopika_manager.py @@ -97,18 +97,20 @@ class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover ), routing_key='*', ) break - except aio_pika.AMQPException: - if retry: - self._get_logger().error('Cannot publish to rabbitmq... ' - 'retrying') - retry = False - else: - self._get_logger().error( - 'Cannot publish to rabbitmq... giving up') - break except aio_pika.exceptions.ChannelInvalidStateError: # aio_pika raises this exception when the task is cancelled raise asyncio.CancelledError() + except Exception as exc: + if retry: + self._get_logger().error( + 'Cannot publish to rabbitmq... retrying', + extra={"rabbitmq_exception": str(exc)}) + retry = False + else: + self._get_logger().error( + 'Cannot publish to rabbitmq... giving up', + extra={"rabbitmq_exception": str(exc)}) + break async def _listen(self): retry_sleep = 1 @@ -125,12 +127,13 @@ class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover async with message.process(): yield message.body retry_sleep = 1 - except aio_pika.AMQPException: - self._get_logger().error( - 'Cannot receive from rabbitmq... ' - 'retrying in {} secs'.format(retry_sleep)) - await asyncio.sleep(retry_sleep) - retry_sleep = min(retry_sleep * 2, 60) except aio_pika.exceptions.ChannelInvalidStateError: # aio_pika raises this exception when the task is cancelled raise asyncio.CancelledError() + except Exception as exc: + self._get_logger().error( + 'Cannot receive from rabbotmq... retrying in ' + f'{retry_sleep} secs', + extra={"rabbitmq_exception": str(exc)}) + await asyncio.sleep(retry_sleep) + retry_sleep = min(retry_sleep * 2, 60) diff --git a/libs/socketio/async_redis_manager.py b/libs/socketio/async_redis_manager.py index 862e61045..52b30f40e 100644 --- a/libs/socketio/async_redis_manager.py +++ b/libs/socketio/async_redis_manager.py @@ -75,7 +75,7 @@ class AsyncRedisManager(AsyncPubSubManager): self.redis = None self.pubsub = None - def _get_redis_module_and_error(self): + def _get_redis_module(self): parsed_url = urlparse(self.redis_url) scheme = parsed_url.scheme.split('+', 1)[0].lower() if scheme in ['redis', 'rediss']: @@ -83,13 +83,13 @@ class AsyncRedisManager(AsyncPubSubManager): raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" ' 'in your virtualenv).') - return aioredis, RedisError + return aioredis if scheme in ['valkey', 'valkeys']: if aiovalkey is None or ValkeyError is None: raise RuntimeError('Valkey package is not installed ' '(Run "pip install valkey" ' 'in your virtualenv).') - return aiovalkey, ValkeyError + return aiovalkey if scheme == 'unix': if aioredis is None or RedisError is None: if aiovalkey is None or ValkeyError is None: @@ -98,14 +98,14 @@ class AsyncRedisManager(AsyncPubSubManager): 'or "pip install valkey" ' 'in your virtualenv).') else: - return aiovalkey, ValkeyError + return aiovalkey else: - return aioredis, RedisError + return aioredis error_msg = f'Unsupported Redis URL scheme: {scheme}' raise ValueError(error_msg) def _redis_connect(self): - module, _ = self._get_redis_module_and_error() + module = self._get_redis_module() parsed_url = urlparse(self.redis_url) if parsed_url.scheme in {"redis+sentinel", "valkey+sentinel"}: sentinels, service_name, connection_kwargs = \ @@ -121,30 +121,25 @@ class AsyncRedisManager(AsyncPubSubManager): self.connected = True async def _publish(self, data): # pragma: no cover - _, error = self._get_redis_module_and_error() for retries_left in range(1, -1, -1): # 2 attempts try: if not self.connected: self._redis_connect() return await self.redis.publish( self.channel, self.json.dumps(data)) - except error as exc: + except Exception as exc: if retries_left > 0: self._get_logger().error( - 'Cannot publish to redis... ' - 'retrying', + 'Cannot publish to redis... retrying', extra={"redis_exception": str(exc)}) self.connected = False else: self._get_logger().error( - 'Cannot publish to redis... ' - 'giving up', + 'Cannot publish to redis... giving up', extra={"redis_exception": str(exc)}) - break async def _redis_listen_with_retries(self): # pragma: no cover - _, error = self._get_redis_module_and_error() retry_sleep = 1 subscribed = False while True: @@ -155,11 +150,11 @@ class AsyncRedisManager(AsyncPubSubManager): retry_sleep = 1 async for message in self.pubsub.listen(): yield message - except error as exc: - self._get_logger().error('Cannot receive from redis... ' - 'retrying in ' - f'{retry_sleep} secs', - extra={"redis_exception": str(exc)}) + except Exception as exc: + self._get_logger().error( + 'Cannot receive from redis... retrying in ' + f'{retry_sleep} secs', + extra={"redis_exception": str(exc)}) subscribed = False await asyncio.sleep(retry_sleep) retry_sleep *= 2 diff --git a/libs/socketio/async_server.py b/libs/socketio/async_server.py index 733d6e94c..7e9c87257 100644 --- a/libs/socketio/async_server.py +++ b/libs/socketio/async_server.py @@ -427,6 +427,8 @@ class AsyncServer(base_server.BaseServer): if delete_it: self.logger.info('Disconnecting %s [%s]', sid, namespace) eio_sid = self.manager.pre_disconnect(sid, namespace=namespace) + if eio_sid in self._binary_packet: + del self._binary_packet[eio_sid] await self._send_packet(eio_sid, self.packet_class( packet.DISCONNECT, namespace=namespace)) await self._trigger_event('disconnect', namespace, sid, @@ -702,6 +704,9 @@ class AsyncServer(base_server.BaseServer): pkt.data) elif pkt.packet_type == packet.BINARY_EVENT or \ pkt.packet_type == packet.BINARY_ACK: + if not self.manager.sid_from_eio_sid(eio_sid, + pkt.namespace or '/'): + raise ValueError('Unexpected binary packet') self._binary_packet[eio_sid] = pkt elif pkt.packet_type == packet.CONNECT_ERROR: raise ValueError('Unexpected CONNECT_ERROR packet.') diff --git a/libs/socketio/kombu_manager.py b/libs/socketio/kombu_manager.py index d17505e8e..797dac476 100644 --- a/libs/socketio/kombu_manager.py +++ b/libs/socketio/kombu_manager.py @@ -114,14 +114,16 @@ class KombuManager(PubSubManager): # pragma: no cover self.publisher_connection) producer_publish(self.json.dumps(data)) break - except (OSError, kombu.exceptions.KombuError): + except Exception as exc: if retry: - self._get_logger().error('Cannot publish to rabbitmq... ' - 'retrying') + self._get_logger().error( + 'Cannot publish to rabbitmq... retrying', + extra={"rabbitmq_exception": str(exc)}) retry = False else: self._get_logger().error( - 'Cannot publish to rabbitmq... giving up') + 'Cannot publish to rabbitmq... giving up', + extra={"rabbitmq_exception": str(exc)}) break def _listen(self): @@ -136,9 +138,10 @@ class KombuManager(PubSubManager): # pragma: no cover message.ack() yield message.payload retry_sleep = 1 - except (OSError, kombu.exceptions.KombuError): + except Exception as exc: self._get_logger().error( - 'Cannot receive from rabbitmq... ' - 'retrying in {} secs'.format(retry_sleep)) + 'Cannot receive from rabbotmq... retrying in ' + f'{retry_sleep} secs', + extra={"rabbitmq_exception": str(exc)}) time.sleep(retry_sleep) retry_sleep = min(retry_sleep * 2, 60) diff --git a/libs/socketio/redis_manager.py b/libs/socketio/redis_manager.py index fb2e04004..a2edfaca9 100644 --- a/libs/socketio/redis_manager.py +++ b/libs/socketio/redis_manager.py @@ -1,4 +1,3 @@ -import logging import time from urllib.parse import urlparse @@ -18,8 +17,6 @@ except ImportError: # pragma: no cover from .pubsub_manager import PubSubManager -logger = logging.getLogger('socketio') - def parse_redis_sentinel_url(url): """Parse a Redis Sentinel URL with the format: @@ -112,7 +109,7 @@ class RedisManager(PubSubManager): 'Redis requires a monkey patched socket library to work ' 'with ' + self.server.async_mode) - def _get_redis_module_and_error(self): + def _get_redis_module(self): parsed_url = urlparse(self.redis_url) scheme = parsed_url.scheme.split('+', 1)[0].lower() if scheme in ['redis', 'rediss']: @@ -120,13 +117,13 @@ class RedisManager(PubSubManager): raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" ' 'in your virtualenv).') - return redis, RedisError + return redis if scheme in ['valkey', 'valkeys']: if valkey is None or ValkeyError is None: raise RuntimeError('Valkey package is not installed ' '(Run "pip install valkey" ' 'in your virtualenv).') - return valkey, ValkeyError + return valkey if scheme == 'unix': if redis is None or RedisError is None: if valkey is None or ValkeyError is None: @@ -135,14 +132,14 @@ class RedisManager(PubSubManager): 'or "pip install valkey" ' 'in your virtualenv).') else: - return valkey, ValkeyError + return valkey else: - return redis, RedisError + return redis error_msg = f'Unsupported Redis URL scheme: {scheme}' raise ValueError(error_msg) def _redis_connect(self): - module, _ = self._get_redis_module_and_error() + module = self._get_redis_module() parsed_url = urlparse(self.redis_url) if parsed_url.scheme in {"redis+sentinel", "valkey+sentinel"}: sentinels, service_name, connection_kwargs = \ @@ -158,28 +155,26 @@ class RedisManager(PubSubManager): self.connected = True def _publish(self, data): # pragma: no cover - _, error = self._get_redis_module_and_error() for retries_left in range(1, -1, -1): # 2 attempts try: if not self.connected: self._redis_connect() return self.redis.publish(self.channel, self.json.dumps(data)) - except error as exc: + except Exception as exc: if retries_left > 0: - logger.error( + self._get_logger().error( 'Cannot publish to redis... retrying', extra={"redis_exception": str(exc)} ) self.connected = False else: - logger.error( + self._get_logger().error( 'Cannot publish to redis... giving up', extra={"redis_exception": str(exc)} ) break def _redis_listen_with_retries(self): # pragma: no cover - _, error = self._get_redis_module_and_error() retry_sleep = 1 subscribed = False while True: @@ -189,10 +184,11 @@ class RedisManager(PubSubManager): self.pubsub.subscribe(self.channel) retry_sleep = 1 yield from self.pubsub.listen() - except error as exc: - logger.error('Cannot receive from redis... ' - f'retrying in {retry_sleep} secs', - extra={"redis_exception": str(exc)}) + except Exception as exc: + self._get_logger().error( + 'Cannot receive from redis... ' + f'retrying in {retry_sleep} secs', + extra={"redis_exception": str(exc)}) subscribed = False time.sleep(retry_sleep) retry_sleep *= 2 diff --git a/libs/socketio/server.py b/libs/socketio/server.py index 6a1a2026d..143d27eac 100644 --- a/libs/socketio/server.py +++ b/libs/socketio/server.py @@ -402,6 +402,8 @@ class Server(base_server.BaseServer): if delete_it: self.logger.info('Disconnecting %s [%s]', sid, namespace) eio_sid = self.manager.pre_disconnect(sid, namespace=namespace) + if eio_sid in self._binary_packet: + del self._binary_packet[eio_sid] self._send_packet(eio_sid, self.packet_class( packet.DISCONNECT, namespace=namespace)) self._trigger_event('disconnect', namespace, sid, @@ -663,6 +665,9 @@ class Server(base_server.BaseServer): self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data) elif pkt.packet_type == packet.BINARY_EVENT or \ pkt.packet_type == packet.BINARY_ACK: + if not self.manager.sid_from_eio_sid(eio_sid, + pkt.namespace or '/'): + raise ValueError('Unexpected binary packet') self._binary_packet[eio_sid] = pkt elif pkt.packet_type == packet.CONNECT_ERROR: raise ValueError('Unexpected CONNECT_ERROR packet.') diff --git a/libs/soupsieve-2.3.2.post1.dist-info/RECORD b/libs/soupsieve-2.3.2.post1.dist-info/RECORD deleted file mode 100644 index dea2e43af..000000000 --- a/libs/soupsieve-2.3.2.post1.dist-info/RECORD +++ /dev/null @@ -1,14 +0,0 @@ -soupsieve-2.3.2.post1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -soupsieve-2.3.2.post1.dist-info/METADATA,sha256=T564kvfwmPwbUqz2rEgA2HW1Zozp1WZbMFKxUNGRBoo,5700 -soupsieve-2.3.2.post1.dist-info/RECORD,, -soupsieve-2.3.2.post1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -soupsieve-2.3.2.post1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 -soupsieve-2.3.2.post1.dist-info/licenses/LICENSE.md,sha256=vGYjTK3T9ueq9xYPrxZfP4af_IoRtrDWxEouKoMxTdA,1096 -soupsieve/__init__.py,sha256=_7J_eiqFvRzoGHZ9AW63f23OmTYLy9YsBQ53XsDbPgE,4717 -soupsieve/__meta__.py,sha256=l4sqBM7pDLTSCbaqZ8p_7FQn6RB5DcFRBeJKpAh5epQ,6817 -soupsieve/css_match.py,sha256=ca8giQr305Wb5rOlAu4z7GSR1xX9mQnnyJVCuE_C-PQ,58167 -soupsieve/css_parser.py,sha256=CxLhmTdri-Fu8AI9JtEW6uCl2HqMB2iJvrbw8kJjZBs,47907 -soupsieve/css_types.py,sha256=7PYNMJRORxJYBn77S6o7MsC9nPj1lIfXb-b5DBzISk8,10479 -soupsieve/pretty.py,sha256=ZW1L09BiepANk0qDpUlUM-E5bOGPiA-b100mFk1L8cg,4018 -soupsieve/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -soupsieve/util.py,sha256=N0LC-E6w_4GXAUEbAB5xelOoab7VIN-IXskBToQbxx4,3371 diff --git a/libs/soupsieve-2.3.2.post1.dist-info/WHEEL b/libs/soupsieve-2.3.2.post1.dist-info/WHEEL deleted file mode 100644 index ae8ec1bda..000000000 --- a/libs/soupsieve-2.3.2.post1.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.28.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/libs/soupsieve-2.3.2.post1.dist-info/INSTALLER b/libs/soupsieve-2.8.4.dist-info/INSTALLER similarity index 100% rename from libs/soupsieve-2.3.2.post1.dist-info/INSTALLER rename to libs/soupsieve-2.8.4.dist-info/INSTALLER diff --git a/libs/soupsieve-2.3.2.post1.dist-info/METADATA b/libs/soupsieve-2.8.4.dist-info/METADATA similarity index 70% rename from libs/soupsieve-2.3.2.post1.dist-info/METADATA rename to libs/soupsieve-2.8.4.dist-info/METADATA index 3e4d93d91..fcdc11850 100644 --- a/libs/soupsieve-2.3.2.post1.dist-info/METADATA +++ b/libs/soupsieve-2.8.4.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: soupsieve -Version: 2.3.2.post1 +Version: 2.8.4 Summary: A modern CSS selector implementation for Beautiful Soup. Project-URL: Homepage, https://github.com/facelessuser/soupsieve Author-email: Isaac Muse @@ -13,24 +13,25 @@ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Typing :: Typed -Requires-Python: >=3.6 -Requires-Dist: backports-functools-lru-cache; python_version < '3' +Requires-Python: >=3.9 Description-Content-Type: text/markdown [![Donate via PayPal][donate-image]][donate-link] -[![Discord][discord-image]][discord-link] [![Build][github-ci-image]][github-ci-link] [![Coverage Status][codecov-image]][codecov-link] [![PyPI Version][pypi-image]][pypi-link] +[![PyPI Downloads][pypi-down]][pypi-link] [![PyPI - Python Version][python-image]][pypi-link] -![License][license-image-mit] +[![License][license-image-mit]][license-link] # Soup Sieve @@ -87,7 +88,7 @@ Then navigate to the root of the project and build the wheel and install (replac ``` python -m build -w -pip install dist/soupsive--py3-none-any.whl +pip install dist/soupsieve--py3-none-any.whl ``` ## Documentation @@ -96,34 +97,19 @@ Documentation is found here: https://facelessuser.github.io/soupsieve/. ## License -MIT License - -Copyright (c) 2018 - 2022 Isaac Muse - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT [bs4]: https://beautiful-soup-4.readthedocs.io/en/latest/# -[github-ci-image]: https://github.com/facelessuser/soupsieve/workflows/build/badge.svg?branch=master&event=push -[github-ci-link]: https://github.com/facelessuser/soupsieve/actions?query=workflow%3Abuild+branch%3Amaster -[discord-image]: https://img.shields.io/discord/678289859768745989?logo=discord&logoColor=aaaaaa&color=mediumpurple&labelColor=333333 -[discord-link]:https://discord.gg/XBnPUZF +[github-ci-image]: https://github.com/facelessuser/soupsieve/workflows/build/badge.svg +[github-ci-link]: https://github.com/facelessuser/soupsieve/actions?query=workflow%3Abuild+branch%3Amain [codecov-image]: https://img.shields.io/codecov/c/github/facelessuser/soupsieve/master.svg?logo=codecov&logoColor=aaaaaa&labelColor=333333 [codecov-link]: https://codecov.io/github/facelessuser/soupsieve [pypi-image]: https://img.shields.io/pypi/v/soupsieve.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333 +[pypi-down]: https://img.shields.io/pypi/dm/soupsieve.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333 [pypi-link]: https://pypi.python.org/pypi/soupsieve [python-image]: https://img.shields.io/pypi/pyversions/soupsieve?logo=python&logoColor=aaaaaa&labelColor=333333 [license-image-mit]: https://img.shields.io/badge/license-MIT-blue.svg?labelColor=333333 +[license-link]: https://github.com/facelessuser/soupsieve/blob/main/LICENSE.md [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal [donate-link]: https://www.paypal.me/facelessuser diff --git a/libs/soupsieve-2.8.4.dist-info/RECORD b/libs/soupsieve-2.8.4.dist-info/RECORD new file mode 100644 index 000000000..a90ded254 --- /dev/null +++ b/libs/soupsieve-2.8.4.dist-info/RECORD @@ -0,0 +1,14 @@ +soupsieve-2.8.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +soupsieve-2.8.4.dist-info/METADATA,sha256=T49dhV2r2LPBBwbLwB11F71fAnzEe1kYO2BKIIDmtiY,4635 +soupsieve-2.8.4.dist-info/RECORD,, +soupsieve-2.8.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +soupsieve-2.8.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +soupsieve-2.8.4.dist-info/licenses/LICENSE.md,sha256=dqCs25jDpjaB4gtvCWaZ74ytX7NdVu74OH4Ppx15epo,1096 +soupsieve/__init__.py,sha256=FV4Ol2FRldy7rG94nQHNHoJTZIdQbUJdZkGhXkI3tiE,4581 +soupsieve/__meta__.py,sha256=GMGkjRHL-JvTiRigfSCpZ4j8vo0wdKM4bfEBZ1EX8l8,6766 +soupsieve/css_match.py,sha256=QmLmHPDYS8ye_YnRmVOK_Ei1BX_JrCFGiygW2RV9coE,61404 +soupsieve/css_parser.py,sha256=bJaTncV2Ydp7zr5PzvRmUMsJgbTAHINZ7DwO84qdVpU,49081 +soupsieve/css_types.py,sha256=OxTs1p2bZK4nvPjJu-TVgPYVrMXnQwQ7RGq0cfzZwC0,10014 +soupsieve/pretty.py,sha256=iSM-NKCzruJQCwEriQT72LrF4slxsib7TB552H2bsqA,4362 +soupsieve/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +soupsieve/util.py,sha256=Q0MVH77cUBXMtKV0D2f8Syf90remPe0EfLqe6-msAeI,3352 diff --git a/libs/sqlalchemy-2.0.48.dist-info/REQUESTED b/libs/soupsieve-2.8.4.dist-info/REQUESTED similarity index 100% rename from libs/sqlalchemy-2.0.48.dist-info/REQUESTED rename to libs/soupsieve-2.8.4.dist-info/REQUESTED diff --git a/libs/soupsieve-2.8.4.dist-info/WHEEL b/libs/soupsieve-2.8.4.dist-info/WHEEL new file mode 100644 index 000000000..bd8422674 --- /dev/null +++ b/libs/soupsieve-2.8.4.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/soupsieve-2.3.2.post1.dist-info/licenses/LICENSE.md b/libs/soupsieve-2.8.4.dist-info/licenses/LICENSE.md similarity index 94% rename from libs/soupsieve-2.3.2.post1.dist-info/licenses/LICENSE.md rename to libs/soupsieve-2.8.4.dist-info/licenses/LICENSE.md index 38adcc8d3..128c5342c 100644 --- a/libs/soupsieve-2.3.2.post1.dist-info/licenses/LICENSE.md +++ b/libs/soupsieve-2.8.4.dist-info/licenses/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 - 2022 Isaac Muse +Copyright (c) 2018 - 2026 Isaac Muse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/libs/soupsieve/__init__.py b/libs/soupsieve/__init__.py index c89b7002c..e170c71c5 100644 --- a/libs/soupsieve/__init__.py +++ b/libs/soupsieve/__init__.py @@ -25,13 +25,14 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations from .__meta__ import __version__, __version_info__ # noqa: F401 from . import css_parser as cp from . import css_match as cm from . import css_types as ct from .util import DEBUG, SelectorSyntaxError # noqa: F401 -import bs4 # type: ignore[import] -from typing import Dict, Optional, Any, List, Iterator, Iterable +import bs4 +from typing import Any, Iterator, Iterable __all__ = ( 'DEBUG', 'SelectorSyntaxError', 'SoupSieve', @@ -44,17 +45,14 @@ SoupSieve = cm.SoupSieve def compile( # noqa: A001 pattern: str, - namespaces: Optional[Dict[str, str]] = None, + namespaces: dict[str, str] | None = None, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any ) -> cm.SoupSieve: """Compile CSS pattern.""" - ns = ct.Namespaces(namespaces) if namespaces is not None else namespaces # type: Optional[ct.Namespaces] - cs = ct.CustomSelectors(custom) if custom is not None else custom # type: Optional[ct.CustomSelectors] - if isinstance(pattern, SoupSieve): if flags: raise ValueError("Cannot process 'flags' argument on a compiled selector list") @@ -64,7 +62,12 @@ def compile( # noqa: A001 raise ValueError("Cannot process 'custom' argument on a compiled selector list") return pattern - return cp._cached_css_compile(pattern, ns, cs, flags) + return cp._cached_css_compile( + pattern, + ct.Namespaces(namespaces) if namespaces is not None else namespaces, + ct.CustomSelectors(custom) if custom is not None else custom, + flags + ) def purge() -> None: @@ -75,13 +78,13 @@ def purge() -> None: def closest( select: str, - tag: 'bs4.Tag', - namespaces: Optional[Dict[str, str]] = None, + tag: bs4.Tag, + namespaces: dict[str, str] | None = None, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any -) -> 'bs4.Tag': +) -> bs4.Tag | None: """Match closest ancestor.""" return compile(select, namespaces, flags, **kwargs).closest(tag) @@ -89,11 +92,11 @@ def closest( def match( select: str, - tag: 'bs4.Tag', - namespaces: Optional[Dict[str, str]] = None, + tag: bs4.Tag, + namespaces: dict[str, str] | None = None, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any ) -> bool: """Match node.""" @@ -103,13 +106,13 @@ def match( def filter( # noqa: A001 select: str, - iterable: Iterable['bs4.Tag'], - namespaces: Optional[Dict[str, str]] = None, + iterable: Iterable[bs4.Tag], + namespaces: dict[str, str] | None = None, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any -) -> List['bs4.Tag']: +) -> list[bs4.Tag]: """Filter list of nodes.""" return compile(select, namespaces, flags, **kwargs).filter(iterable) @@ -117,13 +120,13 @@ def filter( # noqa: A001 def select_one( select: str, - tag: 'bs4.Tag', - namespaces: Optional[Dict[str, str]] = None, + tag: bs4.Tag, + namespaces: dict[str, str] | None = None, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any -) -> 'bs4.Tag': +) -> bs4.Tag | None: """Select a single tag.""" return compile(select, namespaces, flags, **kwargs).select_one(tag) @@ -131,14 +134,14 @@ def select_one( def select( select: str, - tag: 'bs4.Tag', - namespaces: Optional[Dict[str, str]] = None, + tag: bs4.Tag, + namespaces: dict[str, str] | None = None, limit: int = 0, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any -) -> List['bs4.Tag']: +) -> list[bs4.Tag]: """Select the specified tags.""" return compile(select, namespaces, flags, **kwargs).select(tag, limit) @@ -146,18 +149,17 @@ def select( def iselect( select: str, - tag: 'bs4.Tag', - namespaces: Optional[Dict[str, str]] = None, + tag: bs4.Tag, + namespaces: dict[str, str] | None = None, limit: int = 0, flags: int = 0, *, - custom: Optional[Dict[str, str]] = None, + custom: dict[str, str] | None = None, **kwargs: Any -) -> Iterator['bs4.Tag']: +) -> Iterator[bs4.Tag]: """Iterate the specified tags.""" - for el in compile(select, namespaces, flags, **kwargs).iselect(tag, limit): - yield el + yield from compile(select, namespaces, flags, **kwargs).iselect(tag, limit) def escape(ident: str) -> str: diff --git a/libs/soupsieve/__meta__.py b/libs/soupsieve/__meta__.py index 3bd6607f1..e0868e5fa 100644 --- a/libs/soupsieve/__meta__.py +++ b/libs/soupsieve/__meta__.py @@ -1,4 +1,5 @@ """Meta related things.""" +from __future__ import annotations from collections import namedtuple import re @@ -83,7 +84,7 @@ class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre" cls, major: int, minor: int, micro: int, release: str = "final", pre: int = 0, post: int = 0, dev: int = 0 - ) -> "Version": + ) -> Version: """Validate version info.""" # Ensure all parts are positive integers. @@ -92,7 +93,7 @@ class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre" raise ValueError("All version parts except 'release' should be integers.") if release not in REL_MAP: - raise ValueError("'{}' is not a valid release type.".format(release)) + raise ValueError(f"'{release}' is not a valid release type.") # Ensure valid pre-release (we do not allow implicit pre-releases). if ".dev-candidate" < release < "final": @@ -117,7 +118,7 @@ class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre" elif dev: raise ValueError("Version is not a development release.") - return super(Version, cls).__new__(cls, major, minor, micro, release, pre, post, dev) + return super().__new__(cls, major, minor, micro, release, pre, post, dev) def _is_pre(self) -> bool: """Is prerelease.""" @@ -144,15 +145,15 @@ class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre" # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed.. if self.micro == 0: - ver = "{}.{}".format(self.major, self.minor) + ver = f"{self.major}.{self.minor}" else: - ver = "{}.{}.{}".format(self.major, self.minor, self.micro) + ver = f"{self.major}.{self.minor}.{self.micro}" if self._is_pre(): - ver += '{}{}'.format(REL_MAP[self.release], self.pre) + ver += f'{REL_MAP[self.release]}{self.pre}' if self._is_post(): - ver += ".post{}".format(self.post) + ver += f".post{self.post}" if self._is_dev(): - ver += ".dev{}".format(self.dev) + ver += f".dev{self.dev}" return ver @@ -163,7 +164,7 @@ def parse_version(ver: str) -> Version: m = RE_VER.match(ver) if m is None: - raise ValueError("'{}' is not a valid version".format(ver)) + raise ValueError(f"'{ver}' is not a valid version") # Handle major, minor, micro major = int(m.group('major')) @@ -192,5 +193,5 @@ def parse_version(ver: str) -> Version: return Version(major, minor, micro, release, pre, post, dev) -__version_info__ = Version(2, 3, 2, "final", post=1) +__version_info__ = Version(2, 8, 4, "final") __version__ = __version_info__._get_canonical() diff --git a/libs/soupsieve/css_match.py b/libs/soupsieve/css_match.py index 49e5f070e..15e3917cb 100644 --- a/libs/soupsieve/css_match.py +++ b/libs/soupsieve/css_match.py @@ -1,11 +1,12 @@ """CSS matcher.""" +from __future__ import annotations from datetime import datetime from . import util import re from . import css_types as ct import unicodedata -import bs4 # type: ignore[import] -from typing import Iterator, Iterable, List, Any, Optional, Tuple, Union, Dict, Callable, Sequence, cast +import bs4 +from typing import Iterator, Iterable, Any, Callable, Sequence, Any, cast # noqa: F401, F811 # Empty tag pattern (whitespace okay) RE_NOT_EMPTY = re.compile('[^ \t\r\n\f]') @@ -64,12 +65,12 @@ class _FakeParent: fake parent so we can traverse the root element as a child. """ - def __init__(self, element: 'bs4.Tag') -> None: + def __init__(self, element: bs4.Tag) -> None: """Initialize.""" self.contents = [element] - def __len__(self) -> 'bs4.PageElement': + def __len__(self) -> int: """Length.""" return len(self.contents) @@ -84,70 +85,73 @@ class _DocumentNav: # Fail on unexpected types. if not cls.is_tag(tag): - raise TypeError("Expected a BeautifulSoup 'Tag', but instead received type {}".format(type(tag))) + raise TypeError(f"Expected a BeautifulSoup 'Tag', but instead received type {type(tag)}") @staticmethod - def is_doc(obj: 'bs4.Tag') -> bool: + def is_doc(obj: bs4.element.PageElement | None) -> bool: """Is `BeautifulSoup` object.""" return isinstance(obj, bs4.BeautifulSoup) @staticmethod - def is_tag(obj: 'bs4.PageElement') -> bool: + def is_tag(obj: bs4.element.PageElement | None) -> bool: """Is tag.""" return isinstance(obj, bs4.Tag) @staticmethod - def is_declaration(obj: 'bs4.PageElement') -> bool: # pragma: no cover + def is_declaration(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover """Is declaration.""" return isinstance(obj, bs4.Declaration) @staticmethod - def is_cdata(obj: 'bs4.PageElement') -> bool: + def is_cdata(obj: bs4.element.PageElement | None) -> bool: """Is CDATA.""" return isinstance(obj, bs4.CData) @staticmethod - def is_processing_instruction(obj: 'bs4.PageElement') -> bool: # pragma: no cover + def is_processing_instruction(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover """Is processing instruction.""" return isinstance(obj, bs4.ProcessingInstruction) @staticmethod - def is_navigable_string(obj: 'bs4.PageElement') -> bool: + def is_navigable_string(obj: bs4.element.PageElement | None) -> bool: """Is navigable string.""" - return isinstance(obj, bs4.NavigableString) + return isinstance(obj, bs4.element.NavigableString) @staticmethod - def is_special_string(obj: 'bs4.PageElement') -> bool: + def is_special_string(obj: bs4.element.PageElement | None) -> bool: """Is special string.""" return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype)) @classmethod - def is_content_string(cls, obj: 'bs4.PageElement') -> bool: + def is_content_string(cls, obj: bs4.element.PageElement | None) -> bool: """Check if node is content string.""" return cls.is_navigable_string(obj) and not cls.is_special_string(obj) @staticmethod - def create_fake_parent(el: 'bs4.Tag') -> _FakeParent: + def create_fake_parent(el: bs4.Tag) -> _FakeParent: """Create fake parent for a given element.""" return _FakeParent(el) @staticmethod - def is_xml_tree(el: 'bs4.Tag') -> bool: + def is_xml_tree(el: bs4.Tag | None) -> bool: """Check if element (or document) is from a XML tree.""" - return bool(el._is_xml) + return el is not None and bool(el._is_xml) - def is_iframe(self, el: 'bs4.Tag') -> bool: + def is_iframe(self, el: bs4.Tag | None) -> bool: """Check if element is an `iframe`.""" + if el is None: # pragma: no cover + return False + return bool( ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'iframe') and self.is_html_tag(el) # type: ignore[attr-defined] ) - def is_root(self, el: 'bs4.Tag') -> bool: + def is_root(self, el: bs4.Tag) -> bool: """ Return whether element is a root element. @@ -161,23 +165,35 @@ class _DocumentNav: root = parent is not None and self.is_html and self.is_iframe(parent) # type: ignore[attr-defined] return root - def get_contents(self, el: 'bs4.Tag', no_iframe: bool = False) -> Iterator['bs4.PageElement']: + def get_contents(self, el: bs4.Tag | None, no_iframe: bool = False) -> Iterator[bs4.element.PageElement]: """Get contents or contents in reverse.""" - if not no_iframe or not self.is_iframe(el): - for content in el.contents: - yield content + + if el is not None: + if not no_iframe or not self.is_iframe(el): + yield from el.contents + + def get_tag_children( + self, + el: bs4.Tag | None, + start: int | None = None, + reverse: bool = False, + no_iframe: bool = False + ) -> Iterator[bs4.Tag]: + """Get tag children.""" + + return self.get_children(el, start, reverse, True, no_iframe) # type: ignore[return-value] def get_children( self, - el: 'bs4.Tag', - start: Optional[int] = None, + el: bs4.Tag | None, + start: int | None = None, reverse: bool = False, - tags: bool = True, + tags: bool = False, no_iframe: bool = False - ) -> Iterator['bs4.PageElement']: + ) -> Iterator[bs4.element.PageElement]: """Get children.""" - if not no_iframe or not self.is_iframe(el): + if el is not None and (not no_iframe or not self.is_iframe(el)): last = len(el.contents) - 1 if start is None: index = last if reverse else 0 @@ -193,15 +209,24 @@ class _DocumentNav: if not tags or self.is_tag(node): yield node + def get_tag_descendants( + self, + el: bs4.Tag | None, + no_iframe: bool = False + ) -> Iterator[bs4.Tag]: + """Specifically get tag descendants.""" + + yield from self.get_descendants(el, tags=True, no_iframe=no_iframe) # type: ignore[misc] + def get_descendants( self, - el: 'bs4.Tag', - tags: bool = True, + el: bs4.Tag | None, + tags: bool = False, no_iframe: bool = False - ) -> Iterator['bs4.PageElement']: + ) -> Iterator[bs4.element.PageElement]: """Get descendants.""" - if not no_iframe or not self.is_iframe(el): + if el is not None and (not no_iframe or not self.is_iframe(el)): next_good = None for child in el.descendants: @@ -210,71 +235,91 @@ class _DocumentNav: continue next_good = None - is_tag = self.is_tag(child) - - if no_iframe and is_tag and self.is_iframe(child): - if child.next_sibling is not None: - next_good = child.next_sibling - else: - last_child = child - while self.is_tag(last_child) and last_child.contents: - last_child = last_child.contents[-1] - next_good = last_child.next_element - yield child - if next_good is None: - break - # Coverage isn't seeing this even though it's executed - continue # pragma: no cover - - if not tags or is_tag: + if isinstance(child, bs4.Tag): + if no_iframe and self.is_iframe(child): + if child.next_sibling is not None: + next_good = child.next_sibling + else: + last_child = child # type: bs4.element.PageElement + while isinstance(last_child, bs4.Tag) and last_child.contents: + last_child = last_child.contents[-1] + next_good = last_child.next_element + yield child + if next_good is None: + break + # Coverage isn't seeing this even though it's executed + continue # pragma: no cover yield child - def get_parent(self, el: 'bs4.Tag', no_iframe: bool = False) -> 'bs4.Tag': + elif not tags: + yield child + + def get_parent(self, el: bs4.Tag | None, no_iframe: bool = False) -> bs4.Tag | None: """Get parent.""" - parent = el.parent - if no_iframe and parent is not None and self.is_iframe(parent): + parent = el.parent if el is not None else None + if no_iframe and parent is not None and self.is_iframe(parent): # pragma: no cover parent = None return parent @staticmethod - def get_tag_name(el: 'bs4.Tag') -> Optional[str]: + def get_tag_name(el: bs4.Tag | None) -> str | None: """Get tag.""" - return cast(Optional[str], el.name) + return el.name if el is not None else None @staticmethod - def get_prefix_name(el: 'bs4.Tag') -> Optional[str]: + def get_prefix_name(el: bs4.Tag) -> str | None: """Get prefix.""" - return cast(Optional[str], el.prefix) + return el.prefix @staticmethod - def get_uri(el: 'bs4.Tag') -> Optional[str]: + def get_uri(el: bs4.Tag | None) -> str | None: """Get namespace `URI`.""" - return cast(Optional[str], el.namespace) + return el.namespace if el is not None else None @classmethod - def get_next(cls, el: 'bs4.Tag', tags: bool = True) -> 'bs4.PageElement': + def get_next_tag(cls, el: bs4.Tag) -> bs4.Tag | None: + """Get next sibling tag.""" + + return cls.get_next(el, tags=True) # type: ignore[return-value] + + @classmethod + def get_next(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None: """Get next sibling tag.""" sibling = el.next_sibling - while tags and not cls.is_tag(sibling) and sibling is not None: + while tags and not isinstance(sibling, bs4.Tag) and sibling is not None: sibling = sibling.next_sibling + + if tags and not isinstance(sibling, bs4.Tag): + sibling = None + return sibling @classmethod - def get_previous(cls, el: 'bs4.Tag', tags: bool = True) -> 'bs4.PageElement': + def get_previous_tag(cls, el: bs4.Tag, tags: bool = True) -> bs4.Tag | None: + """Get previous sibling tag.""" + + return cls.get_previous(el, True) # type: ignore[return-value] + + @classmethod + def get_previous(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None: """Get previous sibling tag.""" sibling = el.previous_sibling - while tags and not cls.is_tag(sibling) and sibling is not None: + while tags and not isinstance(sibling, bs4.Tag) and sibling is not None: sibling = sibling.previous_sibling + + if tags and not isinstance(sibling, bs4.Tag): + sibling = None + return sibling @staticmethod - def has_html_ns(el: 'bs4.Tag') -> bool: + def has_html_ns(el: bs4.Tag | None) -> bool: """ Check if element has an HTML namespace. @@ -282,17 +327,20 @@ class _DocumentNav: like we do in the case of `is_html_tag`. """ - ns = getattr(el, 'namespace') if el else None + ns = getattr(el, 'namespace') if el is not None else None # noqa: B009 return bool(ns and ns == NS_XHTML) @staticmethod - def split_namespace(el: 'bs4.Tag', attr_name: str) -> Tuple[Optional[str], Optional[str]]: + def split_namespace(el: bs4.Tag | None, attr_name: str) -> tuple[str | None, str | None]: """Return namespace and attribute name without the prefix.""" + if el is None: # pragma: no cover + return None, None + return getattr(attr_name, 'namespace', None), getattr(attr_name, 'name', None) @classmethod - def normalize_value(cls, value: Any) -> Union[str, Sequence[str]]: + def normalize_value(cls, value: Any) -> str | Sequence[str]: """Normalize the value to be a string or list of strings.""" # Treat `None` as empty string. @@ -327,10 +375,10 @@ class _DocumentNav: @classmethod def get_attribute_by_name( cls, - el: 'bs4.Tag', + el: bs4.Tag, name: str, - default: Optional[Union[str, Sequence[str]]] = None - ) -> Optional[Union[str, Sequence[str]]]: + default: str | Sequence[str] | None = None + ) -> str | Sequence[str] | None: """Get attribute by name.""" value = default @@ -347,14 +395,15 @@ class _DocumentNav: return value @classmethod - def iter_attributes(cls, el: 'bs4.Tag') -> Iterator[Tuple[str, Optional[Union[str, Sequence[str]]]]]: + def iter_attributes(cls, el: bs4.Tag | None) -> Iterator[tuple[str, str | Sequence[str] | None]]: """Iterate attributes.""" - for k, v in el.attrs.items(): - yield k, cls.normalize_value(v) + if el is not None: + for k, v in el.attrs.items(): + yield k, cls.normalize_value(v) @classmethod - def get_classes(cls, el: 'bs4.Tag') -> Sequence[str]: + def get_classes(cls, el: bs4.Tag) -> Sequence[str]: """Get classes.""" classes = cls.get_attribute_by_name(el, 'class', []) @@ -362,17 +411,22 @@ class _DocumentNav: classes = RE_NOT_WS.findall(classes) return cast(Sequence[str], classes) - def get_text(self, el: 'bs4.Tag', no_iframe: bool = False) -> str: + def get_text(self, el: bs4.Tag, no_iframe: bool = False) -> str: """Get text.""" return ''.join( - [node for node in self.get_descendants(el, tags=False, no_iframe=no_iframe) if self.is_content_string(node)] + [ + node for node in self.get_descendants(el, no_iframe=no_iframe) # type: ignore[misc] + if self.is_content_string(node) + ] ) - def get_own_text(self, el: 'bs4.Tag', no_iframe: bool = False) -> List[str]: + def get_own_text(self, el: bs4.Tag, no_iframe: bool = False) -> list[str]: """Get Own Text.""" - return [node for node in self.get_contents(el, no_iframe=no_iframe) if self.is_content_string(node)] + return [ + node for node in self.get_contents(el, no_iframe=no_iframe) if self.is_content_string(node) # type: ignore[misc] + ] class Inputs: @@ -393,9 +447,18 @@ class Inputs: def validate_week(year: int, week: int) -> bool: """Validate week.""" - max_week = datetime.strptime("{}-{}-{}".format(12, 31, year), "%m-%d-%Y").isocalendar()[1] - if max_week == 1: - max_week = 53 + # Validate an ISO week number for `year`. + # + # Per ISO 8601 rules, the last ISO week of a year is the week + # containing Dec 28. Using Dec 28 guarantees we obtain the + # correct ISO week-number for the final week of `year`, even in + # years where Dec 31 falls in ISO week 01 of the following year. + # + # Example: if Dec 31 is a Thursday the year's last ISO week will + # be week 53; if Dec 31 is a Monday and that week is counted as + # week 1 of the next year, Dec 28 still belongs to the final + # week of the current ISO year and yields the correct max week. + max_week = datetime(year, 12, 28).isocalendar()[1] return 1 <= week <= max_week @staticmethod @@ -423,10 +486,10 @@ class Inputs: return 0 <= minutes <= 59 @classmethod - def parse_value(cls, itype: str, value: Optional[str]) -> Optional[Tuple[float, ...]]: + def parse_value(cls, itype: str, value: str | None) -> tuple[float, ...] | None: """Parse the input value.""" - parsed = None # type: Optional[Tuple[float, ...]] + parsed = None # type: tuple[float, ...] | None if value is None: return value if itype == "date": @@ -484,19 +547,19 @@ class CSSMatch(_DocumentNav): def __init__( self, selectors: ct.SelectorList, - scope: 'bs4.Tag', - namespaces: Optional[ct.Namespaces], + scope: bs4.Tag | None, + namespaces: ct.Namespaces | None, flags: int ) -> None: """Initialize.""" self.assert_valid_input(scope) self.tag = scope - self.cached_meta_lang = [] # type: List[Tuple[str, str]] - self.cached_default_forms = [] # type: List[Tuple['bs4.Tag', 'bs4.Tag']] - self.cached_indeterminate_forms = [] # type: List[Tuple['bs4.Tag', str, bool]] + self.cached_meta_lang = [] # type: list[tuple[str, str]] + self.cached_default_forms = [] # type: list[tuple[bs4.Tag, bs4.Tag]] + self.cached_indeterminate_forms = [] # type: list[tuple[bs4.Tag, str, bool]] self.selectors = selectors - self.namespaces = {} if namespaces is None else namespaces # type: Union[ct.Namespaces, Dict[str, str]] + self.namespaces = {} if namespaces is None else namespaces # type: ct.Namespaces | dict[str, str] self.flags = flags self.iframe_restrict = False @@ -506,11 +569,11 @@ class CSSMatch(_DocumentNav): while parent: doc = parent parent = self.get_parent(doc) - root = None + root = None # type: bs4.Tag | None if not self.is_doc(doc): root = doc else: - for child in self.get_children(doc): + for child in self.get_tag_children(doc): root = child break @@ -527,11 +590,14 @@ class CSSMatch(_DocumentNav): return self.is_xml or self.has_html_namespace - def get_tag_ns(self, el: 'bs4.Tag') -> str: + def get_tag_ns(self, el: bs4.Tag | None) -> str: """Get tag namespace.""" + namespace = '' + if el is None: # pragma: no cover + return namespace + if self.supports_namespaces(): - namespace = '' ns = self.get_uri(el) if ns: namespace = ns @@ -539,42 +605,43 @@ class CSSMatch(_DocumentNav): namespace = NS_XHTML return namespace - def is_html_tag(self, el: 'bs4.Tag') -> bool: + def is_html_tag(self, el: bs4.Tag | None) -> bool: """Check if tag is in HTML namespace.""" return self.get_tag_ns(el) == NS_XHTML - def get_tag(self, el: 'bs4.Tag') -> Optional[str]: + def get_tag(self, el: bs4.Tag | None) -> str | None: """Get tag.""" name = self.get_tag_name(el) return util.lower(name) if name is not None and not self.is_xml else name - def get_prefix(self, el: 'bs4.Tag') -> Optional[str]: + def get_prefix(self, el: bs4.Tag) -> str | None: """Get prefix.""" prefix = self.get_prefix_name(el) return util.lower(prefix) if prefix is not None and not self.is_xml else prefix - def find_bidi(self, el: 'bs4.Tag') -> Optional[int]: + def find_bidi(self, el: bs4.Tag) -> int | None: """Get directionality from element text.""" - for node in self.get_children(el, tags=False): + for node in self.get_children(el): # Analyze child text nodes if self.is_tag(node): # Avoid analyzing certain elements specified in the specification. - direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) + direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) # type: ignore[arg-type] + name = self.get_tag(node) # type: ignore[arg-type] if ( - self.get_tag(node) in ('bdi', 'script', 'style', 'textarea', 'iframe') or - not self.is_html_tag(node) or + (name and name in ('bdi', 'script', 'style', 'textarea', 'iframe')) or + not self.is_html_tag(node) or # type: ignore[arg-type] direction is not None ): continue # pragma: no cover # Check directionality of this node's text - value = self.find_bidi(node) + value = self.find_bidi(node) # type: ignore[arg-type] if value is not None: return value @@ -586,7 +653,7 @@ class CSSMatch(_DocumentNav): continue # Analyze text nodes for directionality. - for c in node: + for c in node: # type: ignore[attr-defined] bidi = unicodedata.bidirectional(c) if bidi in ('AL', 'R', 'L'): return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL @@ -600,13 +667,18 @@ class CSSMatch(_DocumentNav): ranges = lang_range.split('-') subtags = lang_tag.lower().split('-') length = len(ranges) + slength = len(subtags) rindex = 0 sindex = 0 r = ranges[rindex] s = subtags[sindex] + # Empty specified language should match unspecified language attributes + if length == 1 and slength == 1 and not r and r == s: + return True + # Primary tag needs to match - if r != '*' and r != s: + if (r != '*' and r != s) or (r == '*' and slength == 1 and not s): match = False rindex += 1 @@ -645,10 +717,10 @@ class CSSMatch(_DocumentNav): def match_attribute_name( self, - el: 'bs4.Tag', + el: bs4.Tag, attr: str, - prefix: Optional[str] - ) -> Optional[Union[str, Sequence[str]]]: + prefix: str | None + ) -> str | Sequence[str] | None: """Match attribute name and return value if it exists.""" value = None @@ -679,7 +751,7 @@ class CSSMatch(_DocumentNav): continue # pragma: no cover # We can't match our desired prefix attribute as the attribute doesn't have a prefix - if namespace is None or ns != namespace and prefix != '*': + if namespace is None or (ns != namespace and prefix != '*'): continue # The attribute doesn't match. @@ -696,7 +768,7 @@ class CSSMatch(_DocumentNav): break return value - def match_namespace(self, el: 'bs4.Tag', tag: ct.SelectorTag) -> bool: + def match_namespace(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool: """Match the namespace of the element.""" match = True @@ -717,7 +789,7 @@ class CSSMatch(_DocumentNav): match = False return match - def match_attributes(self, el: 'bs4.Tag', attributes: Tuple[ct.SelectorAttribute, ...]) -> bool: + def match_attributes(self, el: bs4.Tag, attributes: tuple[ct.SelectorAttribute, ...]) -> bool: """Match attributes.""" match = True @@ -736,7 +808,7 @@ class CSSMatch(_DocumentNav): break return match - def match_tagname(self, el: 'bs4.Tag', tag: ct.SelectorTag) -> bool: + def match_tagname(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool: """Match tag name.""" name = (util.lower(tag.name) if not self.is_xml and tag.name is not None else tag.name) @@ -745,7 +817,7 @@ class CSSMatch(_DocumentNav): name not in (self.get_tag(el), '*') ) - def match_tag(self, el: 'bs4.Tag', tag: Optional[ct.SelectorTag]) -> bool: + def match_tag(self, el: bs4.Tag, tag: ct.SelectorTag | None) -> bool: """Match the tag.""" match = True @@ -757,7 +829,7 @@ class CSSMatch(_DocumentNav): match = False return match - def match_past_relations(self, el: 'bs4.Tag', relation: ct.SelectorList) -> bool: + def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool: """Match past relationship.""" found = False @@ -775,31 +847,31 @@ class CSSMatch(_DocumentNav): if parent: found = self.match_selectors(parent, relation) elif relation[0].rel_type == REL_SIBLING: - sibling = self.get_previous(el) + sibling = self.get_previous_tag(el) while not found and sibling: found = self.match_selectors(sibling, relation) - sibling = self.get_previous(sibling) + sibling = self.get_previous_tag(sibling) elif relation[0].rel_type == REL_CLOSE_SIBLING: - sibling = self.get_previous(el) + sibling = self.get_previous_tag(el) if sibling and self.is_tag(sibling): found = self.match_selectors(sibling, relation) return found - def match_future_child(self, parent: 'bs4.Tag', relation: ct.SelectorList, recursive: bool = False) -> bool: + def match_future_child(self, parent: bs4.Tag, relation: ct.SelectorList, recursive: bool = False) -> bool: """Match future child.""" match = False if recursive: - children = self.get_descendants # type: Callable[..., Iterator['bs4.Tag']] + children = self.get_tag_descendants # type: Callable[..., Iterator[bs4.Tag]] else: - children = self.get_children + children = self.get_tag_children for child in children(parent, no_iframe=self.iframe_restrict): match = self.match_selectors(child, relation) if match: break return match - def match_future_relations(self, el: 'bs4.Tag', relation: ct.SelectorList) -> bool: + def match_future_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool: """Match future relationship.""" found = False @@ -812,17 +884,17 @@ class CSSMatch(_DocumentNav): elif relation[0].rel_type == REL_HAS_CLOSE_PARENT: found = self.match_future_child(el, relation) elif relation[0].rel_type == REL_HAS_SIBLING: - sibling = self.get_next(el) + sibling = self.get_next_tag(el) while not found and sibling: found = self.match_selectors(sibling, relation) - sibling = self.get_next(sibling) + sibling = self.get_next_tag(sibling) elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING: - sibling = self.get_next(el) + sibling = self.get_next_tag(el) if sibling and self.is_tag(sibling): found = self.match_selectors(sibling, relation) return found - def match_relations(self, el: 'bs4.Tag', relation: ct.SelectorList) -> bool: + def match_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool: """Match relationship to other elements.""" found = False @@ -837,7 +909,7 @@ class CSSMatch(_DocumentNav): return found - def match_id(self, el: 'bs4.Tag', ids: Tuple[str, ...]) -> bool: + def match_id(self, el: bs4.Tag, ids: tuple[str, ...]) -> bool: """Match element's ID.""" found = True @@ -847,7 +919,7 @@ class CSSMatch(_DocumentNav): break return found - def match_classes(self, el: 'bs4.Tag', classes: Tuple[str, ...]) -> bool: + def match_classes(self, el: bs4.Tag, classes: tuple[str, ...]) -> bool: """Match element's classes.""" current_classes = self.get_classes(el) @@ -858,12 +930,12 @@ class CSSMatch(_DocumentNav): break return found - def match_root(self, el: 'bs4.Tag') -> bool: + def match_root(self, el: bs4.Tag) -> bool: """Match element as root.""" is_root = self.is_root(el) if is_root: - sibling = self.get_previous(el, tags=False) + sibling = self.get_previous(el) # type: Any while is_root and sibling is not None: if ( self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or @@ -871,9 +943,9 @@ class CSSMatch(_DocumentNav): ): is_root = False else: - sibling = self.get_previous(sibling, tags=False) + sibling = self.get_previous(sibling) if is_root: - sibling = self.get_next(el, tags=False) + sibling = self.get_next(el) while is_root and sibling is not None: if ( self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or @@ -881,23 +953,23 @@ class CSSMatch(_DocumentNav): ): is_root = False else: - sibling = self.get_next(sibling, tags=False) + sibling = self.get_next(sibling) return is_root - def match_scope(self, el: 'bs4.Tag') -> bool: + def match_scope(self, el: bs4.Tag) -> bool: """Match element as scope.""" return self.scope is el - def match_nth_tag_type(self, el: 'bs4.Tag', child: 'bs4.Tag') -> bool: + def match_nth_tag_type(self, el: bs4.Tag, child: bs4.Tag) -> bool: """Match tag type for `nth` matches.""" - return( + return ( (self.get_tag(child) == self.get_tag(el)) and (self.get_tag_ns(child) == self.get_tag_ns(el)) ) - def match_nth(self, el: 'bs4.Tag', nth: 'bs4.Tag') -> bool: + def match_nth(self, el: bs4.Tag, nth: tuple[ct.SelectorNth, ...]) -> bool: """Match `nth` elements.""" matched = True @@ -906,9 +978,9 @@ class CSSMatch(_DocumentNav): matched = False if n.selectors and not self.match_selectors(el, n.selectors): break - parent = self.get_parent(el) + parent = self.get_parent(el) # type: bs4.Tag | None if parent is None: - parent = self.create_fake_parent(el) + parent = cast('bs4.Tag', self.create_fake_parent(el)) last = n.last last_index = len(parent) - 1 index = last_index if last else 0 @@ -964,11 +1036,11 @@ class CSSMatch(_DocumentNav): # Evaluate elements while our calculated nth index is still in range while 1 <= idx <= last_index + 1: - child = None + child = None # type: bs4.element.PageElement | None # Evaluate while our child index is still in range. - for child in self.get_children(parent, start=index, reverse=factor < 0, tags=False): + for child in self.get_children(parent, start=index, reverse=factor < 0): index += factor - if not self.is_tag(child): + if not isinstance(child, bs4.Tag): continue # Handle `of S` in `nth-child` if n.selectors and not self.match_selectors(child, n.selectors): @@ -998,20 +1070,20 @@ class CSSMatch(_DocumentNav): break return matched - def match_empty(self, el: 'bs4.Tag') -> bool: + def match_empty(self, el: bs4.Tag) -> bool: """Check if element is empty (if requested).""" is_empty = True - for child in self.get_children(el, tags=False): + for child in self.get_children(el): if self.is_tag(child): is_empty = False break - elif self.is_content_string(child) and RE_NOT_EMPTY.search(child): + elif self.is_content_string(child) and RE_NOT_EMPTY.search(child): # type: ignore[call-overload] is_empty = False break return is_empty - def match_subselectors(self, el: 'bs4.Tag', selectors: Tuple[ct.SelectorList, ...]) -> bool: + def match_subselectors(self, el: bs4.Tag, selectors: tuple[ct.SelectorList, ...]) -> bool: """Match selectors.""" match = True @@ -1020,11 +1092,11 @@ class CSSMatch(_DocumentNav): match = False return match - def match_contains(self, el: 'bs4.Tag', contains: Tuple[ct.SelectorContains, ...]) -> bool: + def match_contains(self, el: bs4.Tag, contains: tuple[ct.SelectorContains, ...]) -> bool: """Match element if it contains text.""" match = True - content = None # type: Optional[Union[str, Sequence[str]]] + content = None # type: str | Sequence[str] | None for contain_list in contains: if content is None: if contain_list.own: @@ -1048,13 +1120,13 @@ class CSSMatch(_DocumentNav): match = False return match - def match_default(self, el: 'bs4.Tag') -> bool: + def match_default(self, el: bs4.Tag) -> bool: """Match default.""" match = False # Find this input's form - form = None + form = None # type: bs4.Tag | None parent = self.get_parent(el, no_iframe=True) while parent and form is None: if self.get_tag(parent) == 'form' and self.is_html_tag(parent): @@ -1062,38 +1134,39 @@ class CSSMatch(_DocumentNav): else: parent = self.get_parent(parent, no_iframe=True) - # Look in form cache to see if we've already located its default button - found_form = False - for f, t in self.cached_default_forms: - if f is form: - found_form = True - if t is el: - match = True - break - - # We didn't have the form cached, so look for its default button - if not found_form: - for child in self.get_descendants(form, no_iframe=True): - name = self.get_tag(child) - # Can't do nested forms (haven't figured out why we never hit this) - if name == 'form': # pragma: no cover + if form is not None: + # Look in form cache to see if we've already located its default button + found_form = False + for f, t in self.cached_default_forms: + if f is form: + found_form = True + if t is el: + match = True break - if name in ('input', 'button'): - v = self.get_attribute_by_name(child, 'type', '') - if v and util.lower(v) == 'submit': - self.cached_default_forms.append((form, child)) - if el is child: - match = True + + # We didn't have the form cached, so look for its default button + if not found_form: + for child in self.get_tag_descendants(form, no_iframe=True): + name = self.get_tag(child) + # Can't do nested forms (haven't figured out why we never hit this) + if name == 'form': # pragma: no cover break + if name in ('input', 'button'): + v = self.get_attribute_by_name(child, 'type', '') + if v and util.lower(v) == 'submit': + self.cached_default_forms.append((form, child)) + if el is child: + match = True + break return match - def match_indeterminate(self, el: 'bs4.Tag') -> bool: + def match_indeterminate(self, el: bs4.Tag) -> bool: """Match default.""" match = False name = cast(str, self.get_attribute_by_name(el, 'name')) - def get_parent_form(el: 'bs4.Tag') -> Optional['bs4.Tag']: + def get_parent_form(el: bs4.Tag) -> bs4.Tag | None: """Find this input's form.""" form = None parent = self.get_parent(el, no_iframe=True) @@ -1111,44 +1184,45 @@ class CSSMatch(_DocumentNav): form = get_parent_form(el) # Look in form cache to see if we've already evaluated that its fellow radio buttons are indeterminate - found_form = False - for f, n, i in self.cached_indeterminate_forms: - if f is form and n == name: - found_form = True - if i is True: - match = True - break - - # We didn't have the form cached, so validate that the radio button is indeterminate - if not found_form: - checked = False - for child in self.get_descendants(form, no_iframe=True): - if child is el: - continue - tag_name = self.get_tag(child) - if tag_name == 'input': - is_radio = False - check = False - has_name = False - for k, v in self.iter_attributes(child): - if util.lower(k) == 'type' and util.lower(v) == 'radio': - is_radio = True - elif util.lower(k) == 'name' and v == name: - has_name = True - elif util.lower(k) == 'checked': - check = True - if is_radio and check and has_name and get_parent_form(child) is form: - checked = True - break - if checked: + if form is not None: + found_form = False + for f, n, i in self.cached_indeterminate_forms: + if f is form and n == name: + found_form = True + if i is True: + match = True break - if not checked: - match = True - self.cached_indeterminate_forms.append((form, name, match)) + + # We didn't have the form cached, so validate that the radio button is indeterminate + if not found_form: + checked = False + for child in self.get_tag_descendants(form, no_iframe=True): + if child is el: + continue + tag_name = self.get_tag(child) + if tag_name == 'input': + is_radio = False + check = False + has_name = False + for k, v in self.iter_attributes(child): + if util.lower(k) == 'type' and util.lower(v) == 'radio': + is_radio = True + elif util.lower(k) == 'name' and v == name: + has_name = True + elif util.lower(k) == 'checked': + check = True + if is_radio and check and has_name and get_parent_form(child) is form: + checked = True + break + if checked: + break + if not checked: + match = True + self.cached_indeterminate_forms.append((form, name, match)) return match - def match_lang(self, el: 'bs4.Tag', langs: Tuple[ct.SelectorLang, ...]) -> bool: + def match_lang(self, el: bs4.Tag, langs: tuple[ct.SelectorLang, ...]) -> bool: """Match languages.""" match = False @@ -1157,7 +1231,7 @@ class CSSMatch(_DocumentNav): has_html_namespace = self.has_html_namespace # Walk parents looking for `lang` (HTML) or `xml:lang` XML property. - parent = el + parent = el # type: bs4.Tag | None found_lang = None last = None while not found_lang: @@ -1183,18 +1257,18 @@ class CSSMatch(_DocumentNav): break # Use cached meta language. - if not found_lang and self.cached_meta_lang: + if found_lang is None and self.cached_meta_lang: for cache in self.cached_meta_lang: - if root is cache[0]: + if root is not None and cast(str, root) is cache[0]: found_lang = cache[1] # If we couldn't find a language, and the document is HTML, look to meta to determine language. - if found_lang is None and (not self.is_xml or (has_html_namespace and root.name == 'html')): + if found_lang is None and (not self.is_xml or (has_html_namespace and root and root.name == 'html')): # Find head found = False for tag in ('html', 'head'): found = False - for child in self.get_children(parent, no_iframe=self.is_html): + for child in self.get_tag_children(parent, no_iframe=self.is_html): if self.get_tag(child) == tag and self.is_html_tag(child): found = True parent = child @@ -1203,12 +1277,12 @@ class CSSMatch(_DocumentNav): break # Search meta tags - if found: - for child in parent: - if self.is_tag(child) and self.get_tag(child) == 'meta' and self.is_html_tag(parent): + if found and parent is not None: + for child2 in parent: + if isinstance(child2, bs4.Tag) and self.get_tag(child2) == 'meta' and self.is_html_tag(parent): c_lang = False content = None - for k, v in self.iter_attributes(child): + for k, v in self.iter_attributes(child2): if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language': c_lang = True if util.lower(k) == 'content': @@ -1217,13 +1291,13 @@ class CSSMatch(_DocumentNav): found_lang = content self.cached_meta_lang.append((cast(str, root), cast(str, found_lang))) break - if found_lang: + if found_lang is not None: break - if not found_lang: + if found_lang is None: self.cached_meta_lang.append((cast(str, root), '')) # If we determined a language, compare. - if found_lang: + if found_lang is not None: for patterns in langs: match = False for pattern in patterns: @@ -1234,7 +1308,7 @@ class CSSMatch(_DocumentNav): return match - def match_dir(self, el: 'bs4.Tag', directionality: int) -> bool: + def match_dir(self, el: bs4.Tag | None, directionality: int) -> bool: """Check directionality.""" # If we have to match both left and right, we can't match either. @@ -1266,11 +1340,7 @@ class CSSMatch(_DocumentNav): # Auto handling for text inputs if ((is_input and itype in ('text', 'search', 'tel', 'url', 'email')) or is_textarea) and direction == 0: if is_textarea: - temp = [] - for node in self.get_contents(el, no_iframe=True): - if self.is_content_string(node): - temp.append(node) - value = ''.join(temp) + value = ''.join(node for node in self.get_contents(el, no_iframe=True) if self.is_content_string(node)) # type: ignore[misc] else: value = cast(str, self.get_attribute_by_name(el, 'value', '')) if value: @@ -1297,7 +1367,7 @@ class CSSMatch(_DocumentNav): # Match parents direction return self.match_dir(self.get_parent(el, no_iframe=True), directionality) - def match_range(self, el: 'bs4.Tag', condition: int) -> bool: + def match_range(self, el: bs4.Tag, condition: int) -> bool: """ Match range. @@ -1337,7 +1407,7 @@ class CSSMatch(_DocumentNav): return not out_of_range if condition & ct.SEL_IN_RANGE else out_of_range - def match_defined(self, el: 'bs4.Tag') -> bool: + def match_defined(self, el: bs4.Tag) -> bool: """ Match defined. @@ -1360,7 +1430,7 @@ class CSSMatch(_DocumentNav): ) ) - def match_placeholder_shown(self, el: 'bs4.Tag') -> bool: + def match_placeholder_shown(self, el: bs4.Tag) -> bool: """ Match placeholder shown according to HTML spec. @@ -1375,7 +1445,7 @@ class CSSMatch(_DocumentNav): return match - def match_selectors(self, el: 'bs4.Tag', selectors: ct.SelectorList) -> bool: + def match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> bool: """Check if element matches one of the selectors.""" match = False @@ -1459,12 +1529,12 @@ class CSSMatch(_DocumentNav): return match - def select(self, limit: int = 0) -> Iterator['bs4.Tag']: + def select(self, limit: int = 0) -> Iterator[bs4.Tag]: """Match all tags under the targeted tag.""" lim = None if limit < 1 else limit - for child in self.get_descendants(self.tag): + for child in self.get_tag_descendants(self.tag): if self.match(child): yield child if lim is not None: @@ -1472,10 +1542,10 @@ class CSSMatch(_DocumentNav): if lim < 1: break - def closest(self) -> Optional['bs4.Tag']: + def closest(self) -> bs4.Tag | None: """Match closest ancestor.""" - current = self.tag + current = self.tag # type: bs4.Tag | None closest = None while closest is None and current is not None: if self.match(current): @@ -1484,12 +1554,15 @@ class CSSMatch(_DocumentNav): current = self.get_parent(current) return closest - def filter(self) -> List['bs4.Tag']: # noqa A001 + def filter(self) -> list[bs4.Tag]: # noqa A001 """Filter tag's children.""" - return [tag for tag in self.get_contents(self.tag) if not self.is_navigable_string(tag) and self.match(tag)] + return [ + tag for tag in self.get_contents(self.tag) + if isinstance(tag, bs4.Tag) and self.match(tag) + ] - def match(self, el: 'bs4.Tag') -> bool: + def match(self, el: bs4.Tag) -> bool: """Match.""" return not self.is_doc(el) and self.is_tag(el) and self.match_selectors(el, self.selectors) @@ -1500,8 +1573,8 @@ class SoupSieve(ct.Immutable): pattern: str selectors: ct.SelectorList - namespaces: Optional[ct.Namespaces] - custom: Dict[str, str] + namespaces: ct.Namespaces | None + custom: dict[str, str] flags: int __slots__ = ("pattern", "selectors", "namespaces", "custom", "flags", "_hash") @@ -1510,8 +1583,8 @@ class SoupSieve(ct.Immutable): self, pattern: str, selectors: ct.SelectorList, - namespaces: Optional[ct.Namespaces], - custom: Optional[ct.CustomSelectors], + namespaces: ct.Namespaces | None, + custom: ct.CustomSelectors | None, flags: int ): """Initialize.""" @@ -1524,17 +1597,17 @@ class SoupSieve(ct.Immutable): flags=flags ) - def match(self, tag: 'bs4.Tag') -> bool: + def match(self, tag: bs4.Tag) -> bool: """Match.""" return CSSMatch(self.selectors, tag, self.namespaces, self.flags).match(tag) - def closest(self, tag: 'bs4.Tag') -> 'bs4.Tag': + def closest(self, tag: bs4.Tag) -> bs4.Tag | None: """Match closest ancestor.""" return CSSMatch(self.selectors, tag, self.namespaces, self.flags).closest() - def filter(self, iterable: Iterable['bs4.Tag']) -> List['bs4.Tag']: # noqa A001 + def filter(self, iterable: Iterable[bs4.Tag]) -> list[bs4.Tag]: # noqa A001 """ Filter. @@ -1546,36 +1619,33 @@ class SoupSieve(ct.Immutable): so for those, we use a new `CSSMatch` for each item in the iterable. """ - if CSSMatch.is_tag(iterable): + if isinstance(iterable, bs4.Tag): return CSSMatch(self.selectors, iterable, self.namespaces, self.flags).filter() else: return [node for node in iterable if not CSSMatch.is_navigable_string(node) and self.match(node)] - def select_one(self, tag: 'bs4.Tag') -> 'bs4.Tag': + def select_one(self, tag: bs4.Tag) -> bs4.Tag | None: """Select a single tag.""" tags = self.select(tag, limit=1) return tags[0] if tags else None - def select(self, tag: 'bs4.Tag', limit: int = 0) -> List['bs4.Tag']: + def select(self, tag: bs4.Tag, limit: int = 0) -> list[bs4.Tag]: """Select the specified tags.""" return list(self.iselect(tag, limit)) - def iselect(self, tag: 'bs4.Tag', limit: int = 0) -> Iterator['bs4.Tag']: + def iselect(self, tag: bs4.Tag, limit: int = 0) -> Iterator[bs4.Tag]: """Iterate the specified tags.""" - for el in CSSMatch(self.selectors, tag, self.namespaces, self.flags).select(limit): - yield el + yield from CSSMatch(self.selectors, tag, self.namespaces, self.flags).select(limit) def __repr__(self) -> str: # pragma: no cover """Representation.""" - return "SoupSieve(pattern={!r}, namespaces={!r}, custom={!r}, flags={!r})".format( - self.pattern, - self.namespaces, - self.custom, - self.flags + return ( + f"SoupSieve(pattern={self.pattern!r}, namespaces={self.namespaces!r}, " + f"custom={self.custom!r}, flags={self.flags!r})" ) __str__ = __repr__ diff --git a/libs/soupsieve/css_parser.py b/libs/soupsieve/css_parser.py index d77084d43..c6289cf97 100644 --- a/libs/soupsieve/css_parser.py +++ b/libs/soupsieve/css_parser.py @@ -1,4 +1,5 @@ """CSS selector parser.""" +from __future__ import annotations import re from functools import lru_cache from . import util @@ -6,10 +7,12 @@ from . import css_match as cm from . import css_types as ct from .util import SelectorSyntaxError import warnings -from typing import Optional, Dict, Match, Tuple, Type, Any, List, Union, Iterator, cast +from typing import Match, Any, Iterator, cast UNICODE_REPLACEMENT_CHAR = 0xFFFD +SELECTOR_LIMIT = 8192 + # Simple pseudo classes that take no parameters PSEUDO_SIMPLE = { ":any-link", @@ -17,6 +20,7 @@ PSEUDO_SIMPLE = { ":first-child", ":first-of-type", ":in-range", + ":open", ":out-of-range", ":last-child", ":last-of-type", @@ -35,26 +39,35 @@ PSEUDO_SIMPLE = { ':read-write', ':required', ':scope', - ':defined' + ':defined', + ':muted' } # Supported, simple pseudo classes that match nothing in the Soup Sieve environment PSEUDO_SIMPLE_NO_MATCH = { ':active', + ':autofill', + ':buffering', ':current', ':focus', ':focus-visible', ':focus-within', + ':fullscreen', ':future', ':host', ':hover', ':local-link', ':past', ':paused', + ':picture-in-picture', ':playing', + ':popover-open', + ':seeking', + ':stalled', ':target', ':target-within', ':user-invalid', + ':volume-locked', ':visited' } @@ -91,94 +104,81 @@ PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSE # Sub-patterns parts # Whitespace NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])' -WS = r'(?:[ \t]|{})'.format(NEWLINE) +WS = fr'(?:[ \t]|{NEWLINE})' # Comments COMMENTS = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' # Whitespace with comments included -WSC = r'(?:{ws}|{comments})'.format(ws=WS, comments=COMMENTS) +WSC = fr'(?:{WS}|{COMMENTS})' # CSS escapes -CSS_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$))'.format(ws=WS) -CSS_STRING_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$|{nl}))'.format(ws=WS, nl=NEWLINE) +CSS_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|[^\r\n\f]|$))' +CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|[^\r\n\f]|$|{NEWLINE}))' # CSS Identifier -IDENTIFIER = r''' -(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})+|--) -(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})*) -'''.format(esc=CSS_ESCAPES) +IDENTIFIER = fr''' +(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--) +(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*) +''' # `nth` content -NTH = r'(?:[-+])?(?:[0-9]+n?|n)(?:(?<=n){ws}*(?:[-+]){ws}*(?:[0-9]+))?'.format(ws=WSC) +NTH = fr'(?:[-+])?(?:[0-9]+n?|n)(?:(?<=n){WSC}*(?:[-+]){WSC}*(?:[0-9]+))?' # Value: quoted string or identifier -VALUE = r''' -(?:"(?:\\(?:.|{nl})|[^\\"\r\n\f]+)*?"|'(?:\\(?:.|{nl})|[^\\'\r\n\f]+)*?'|{ident}+) -'''.format(nl=NEWLINE, ident=IDENTIFIER) +VALUE = fr'''(?:"(?:\\(?:.|{NEWLINE})|[^\\"\r\n\f])*?"|'(?:\\(?:.|{NEWLINE})|[^\\'\r\n\f])*?'|{IDENTIFIER})''' # Attribute value comparison. `!=` is handled special as it is non-standard. -ATTR = r''' -(?:{ws}*(?P[!~^|*$]?=){ws}*(?P{value})(?:{ws}+(?P[is]))?)?{ws}*\] -'''.format(ws=WSC, value=VALUE) +ATTR = fr'(?:{WSC}*(?P[!~^|*$]?=){WSC}*(?P{VALUE})(?:{WSC}*(?P[is]))?)?{WSC}*' # Selector patterns # IDs (`#id`) -PAT_ID = r'\#{ident}'.format(ident=IDENTIFIER) +PAT_ID = fr'\#{IDENTIFIER}' # Classes (`.class`) -PAT_CLASS = r'\.{ident}'.format(ident=IDENTIFIER) +PAT_CLASS = fr'\.{IDENTIFIER}' # Prefix:Tag (`prefix|tag`) -PAT_TAG = r'(?P(?:{ident}|\*)?\|)?(?P{ident}|\*)'.format(ident=IDENTIFIER) +PAT_TAG = fr'(?P(?:{IDENTIFIER}|\*)?\|)?(?P{IDENTIFIER}|\*)' # Attributes (`[attr]`, `[attr=value]`, etc.) -PAT_ATTR = r''' -\[{ws}*(?P(?:{ident}|\*)?\|)?(?P{ident}){attr} -'''.format(ws=WSC, ident=IDENTIFIER, attr=ATTR) +PAT_ATTR = fr'\[{WSC}*(?P(?:{IDENTIFIER}|\*)?\|)?(?P{IDENTIFIER}){ATTR}\]' # Pseudo class (`:pseudo-class`, `:pseudo-class(`) -PAT_PSEUDO_CLASS = r'(?P:{ident})(?P\({ws}*)?'.format(ws=WSC, ident=IDENTIFIER) +PAT_PSEUDO_CLASS = fr'(?P:{IDENTIFIER})(?P\({WSC}*)?' # Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes. -PAT_PSEUDO_CLASS_SPECIAL = r'(?P:{ident})(?P\({ws}*)'.format(ws=WSC, ident=IDENTIFIER) +PAT_PSEUDO_CLASS_SPECIAL = fr'(?P:{IDENTIFIER})(?P\({WSC}*)' # Custom pseudo class (`:--custom-pseudo`) -PAT_PSEUDO_CLASS_CUSTOM = r'(?P:(?=--){ident})'.format(ident=IDENTIFIER) +PAT_PSEUDO_CLASS_CUSTOM = fr'(?P:(?=--){IDENTIFIER})' +# Nesting ampersand selector. Matches `&` +PAT_AMP = r'&' # Closing pseudo group (`)`) -PAT_PSEUDO_CLOSE = r'{ws}*\)'.format(ws=WSC) +PAT_PSEUDO_CLOSE = fr'{WSC}*\)' # Pseudo element (`::pseudo-element`) -PAT_PSEUDO_ELEMENT = r':{}'.format(PAT_PSEUDO_CLASS) +PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}' # At rule (`@page`, etc.) (not supported) -PAT_AT_RULE = r'@P{ident}'.format(ident=IDENTIFIER) +PAT_AT_RULE = fr'@P{IDENTIFIER}' # Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.) -PAT_PSEUDO_NTH_CHILD = r''' -(?P{name} -(?P{nth}|even|odd))(?:{wsc}*\)|(?P{comments}*{ws}{wsc}*of{comments}*{ws}{wsc}*)) -'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, wsc=WSC, comments=COMMENTS, ws=WS, nth=NTH) +PAT_PSEUDO_NTH_CHILD = fr''' +(?P{PAT_PSEUDO_CLASS_SPECIAL} +(?P{NTH}|even|odd))(?:{WSC}*\)|(?P{COMMENTS}*{WS}{WSC}*of{COMMENTS}*{WS}{WSC}*)) +''' # Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.) -PAT_PSEUDO_NTH_TYPE = r''' -(?P{name} -(?P{nth}|even|odd)){ws}*\) -'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, nth=NTH) +PAT_PSEUDO_NTH_TYPE = fr''' +(?P{PAT_PSEUDO_CLASS_SPECIAL} +(?P{NTH}|even|odd)){WSC}*\) +''' # Pseudo class language (`:lang("*-de", en)`) -PAT_PSEUDO_LANG = r'{name}(?P{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format( - name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE -) +PAT_PSEUDO_LANG = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P{VALUE}(?:{WSC}*,{WSC}*{VALUE})*){WSC}*\)' # Pseudo class direction (`:dir(ltr)`) -PAT_PSEUDO_DIR = r'{name}(?P

ltr|rtl){ws}*\)'.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC) +PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?Pltr|rtl){WSC}*\)' # Combining characters (`>`, `~`, ` `, `+`, `,`) -PAT_COMBINE = r'{wsc}*?(?P[,+>~]|{ws}(?![,+>~])){wsc}*'.format(ws=WS, wsc=WSC) +PAT_COMBINE = fr'{WSC}*?(?P[,+>~]|{WS}(?![,+>~])){WSC}*' # Extra: Contains (`:contains(text)`) -PAT_PSEUDO_CONTAINS = r'{name}(?P{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format( - name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE -) +PAT_PSEUDO_CONTAINS = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P{VALUE}(?:{WSC}*,{WSC}*{VALUE})*){WSC}*\)' # Regular expressions # CSS escape pattern -RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$))'.format(ws=WSC), re.I) -RE_CSS_STR_ESC = re.compile( - r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$)|(\\{nl}))'.format(ws=WS, nl=NEWLINE), re.I -) +RE_CSS_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}{WSC}?)|(\\[^\r\n\f])|(\\$))', re.I) +RE_CSS_STR_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}{WS}?)|(\\[^\r\n\f])|(\\$)|(\\{NEWLINE}))', re.I) # Pattern to break up `nth` specifiers -RE_NTH = re.compile( - r'(?P[-+])?(?P[0-9]+n?|n)(?:(?<=n){ws}*(?P[-+]){ws}*(?P[0-9]+))?'.format(ws=WSC), - re.I -) +RE_NTH = re.compile(fr'(?P[-+])?(?P[0-9]+n?|n)(?:(?<=n){WSC}*(?P[-+]){WSC}*(?P[0-9]+))?', re.I) # Pattern to iterate multiple values. -RE_VALUES = re.compile(r'(?:(?P{value})|(?P{ws}*,{ws}*))'.format(ws=WSC, value=VALUE), re.X) +RE_VALUES = re.compile(fr'(?:(?P{VALUE})|(?P{WSC}*,{WSC}*))', re.X) # Whitespace checks RE_WS = re.compile(WS) -RE_WS_BEGIN = re.compile('^{}*'.format(WSC)) -RE_WS_END = re.compile('{}*$'.format(WSC)) -RE_CUSTOM = re.compile(r'^{}$'.format(PAT_PSEUDO_CLASS_CUSTOM), re.X) +RE_WS_BEGIN = re.compile(fr'^{WSC}*') +RE_WS_END = re.compile(fr'{WSC}*$') +RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X) # Constants # List split token @@ -206,8 +206,8 @@ _MAXCACHE = 500 @lru_cache(maxsize=_MAXCACHE) def _cached_css_compile( pattern: str, - namespaces: Optional[ct.Namespaces], - custom: Optional[ct.CustomSelectors], + namespaces: ct.Namespaces | None, + custom: ct.CustomSelectors | None, flags: int ) -> cm.SoupSieve: """Cached CSS compile.""" @@ -232,7 +232,7 @@ def _purge_cache() -> None: _cached_css_compile.cache_clear() -def process_custom(custom: Optional[ct.CustomSelectors]) -> Dict[str, Union[str, ct.SelectorList]]: +def process_custom(custom: ct.CustomSelectors | None) -> dict[str, str | ct.SelectorList]: """Process custom.""" custom_selectors = {} @@ -240,9 +240,9 @@ def process_custom(custom: Optional[ct.CustomSelectors]) -> Dict[str, Union[str, for key, value in custom.items(): name = util.lower(key) if RE_CUSTOM.match(name) is None: - raise SelectorSyntaxError("The name '{}' is not a valid custom pseudo-class name".format(name)) + raise SelectorSyntaxError(f"The name '{name}' is not a valid custom pseudo-class name") if name in custom_selectors: - raise KeyError("The custom selector '{}' has already been registered".format(name)) + raise KeyError(f"The custom selector '{name}' has already been registered") custom_selectors[css_unescape(name)] = value return custom_selectors @@ -282,23 +282,23 @@ def escape(ident: str) -> str: start_dash = length > 0 and ident[0] == '-' if length == 1 and start_dash: # Need to escape identifier that is a single `-` with no other characters - string.append('\\{}'.format(ident)) + string.append(f'\\{ident}') else: for index, c in enumerate(ident): codepoint = ord(c) if codepoint == 0x00: string.append('\ufffd') elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F: - string.append('\\{:x} '.format(codepoint)) + string.append(f'\\{codepoint:x} ') elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39): - string.append('\\{:x} '.format(codepoint)) + string.append(f'\\{codepoint:x} ') elif ( codepoint in (0x2D, 0x5F) or codepoint >= 0x80 or (0x30 <= codepoint <= 0x39) or (0x30 <= codepoint <= 0x39) or (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A) ): string.append(c) else: - string.append('\\{}'.format(c)) + string.append(f'\\{c}') return ''.join(string) @@ -316,7 +316,7 @@ class SelectorPattern: return self.name - def match(self, selector: str, index: int, flags: int) -> Optional[Match[str]]: + def match(self, selector: str, index: int, flags: int) -> Match[str] | None: """Match the selector.""" return self.re_pattern.match(selector, index) @@ -325,7 +325,7 @@ class SelectorPattern: class SpecialPseudoPattern(SelectorPattern): """Selector pattern.""" - def __init__(self, patterns: Tuple[Tuple[str, Tuple[str, ...], str, Type[SelectorPattern]], ...]) -> None: + def __init__(self, patterns: tuple[tuple[str, tuple[str, ...], str, type[SelectorPattern]], ...]) -> None: """Initialize.""" self.patterns = {} @@ -335,7 +335,7 @@ class SpecialPseudoPattern(SelectorPattern): for pseudo in p[1]: self.patterns[pseudo] = pattern - self.matched_name = None # type: Optional[SelectorPattern] + self.matched_name = None # type: SelectorPattern | None self.re_pseudo_name = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U) def get_name(self) -> str: @@ -343,7 +343,7 @@ class SpecialPseudoPattern(SelectorPattern): return '' if self.matched_name is None else self.matched_name.get_name() - def match(self, selector: str, index: int, flags: int) -> Optional[Match[str]]: + def match(self, selector: str, index: int, flags: int) -> Match[str] | None: """Match the selector.""" pseudo = None @@ -371,20 +371,20 @@ class _Selector: def __init__(self, **kwargs: Any) -> None: """Initialize.""" - self.tag = kwargs.get('tag', None) # type: Optional[ct.SelectorTag] - self.ids = kwargs.get('ids', []) # type: List[str] - self.classes = kwargs.get('classes', []) # type: List[str] - self.attributes = kwargs.get('attributes', []) # type: List[ct.SelectorAttribute] - self.nth = kwargs.get('nth', []) # type: List[ct.SelectorNth] - self.selectors = kwargs.get('selectors', []) # type: List[ct.SelectorList] - self.relations = kwargs.get('relations', []) # type: List[_Selector] - self.rel_type = kwargs.get('rel_type', None) # type: Optional[str] - self.contains = kwargs.get('contains', []) # type: List[ct.SelectorContains] - self.lang = kwargs.get('lang', []) # type: List[ct.SelectorLang] + self.tag = kwargs.get('tag', None) # type: ct.SelectorTag | None + self.ids = kwargs.get('ids', []) # type: list[str] + self.classes = kwargs.get('classes', []) # type: list[str] + self.attributes = kwargs.get('attributes', []) # type: list[ct.SelectorAttribute] + self.nth = kwargs.get('nth', []) # type: list[ct.SelectorNth] + self.selectors = kwargs.get('selectors', []) # type: list[ct.SelectorList] + self.relations = kwargs.get('relations', []) # type: list[_Selector] + self.rel_type = kwargs.get('rel_type', None) # type: str | None + self.contains = kwargs.get('contains', []) # type: list[ct.SelectorContains] + self.lang = kwargs.get('lang', []) # type: list[ct.SelectorLang] self.flags = kwargs.get('flags', 0) # type: int self.no_match = kwargs.get('no_match', False) # type: bool - def _freeze_relations(self, relations: List['_Selector']) -> ct.SelectorList: + def _freeze_relations(self, relations: list[_Selector]) -> ct.SelectorList: """Freeze relation.""" if relations: @@ -394,7 +394,7 @@ class _Selector: else: return ct.SelectorList() - def freeze(self) -> Union[ct.Selector, ct.SelectorNull]: + def freeze(self) -> ct.Selector | ct.SelectorNull: """Freeze self.""" if self.no_match: @@ -418,11 +418,10 @@ class _Selector: """String representation.""" return ( - '_Selector(tag={!r}, ids={!r}, classes={!r}, attributes={!r}, nth={!r}, selectors={!r}, ' - 'relations={!r}, rel_type={!r}, contains={!r}, lang={!r}, flags={!r}, no_match={!r})' - ).format( - self.tag, self.ids, self.classes, self.attributes, self.nth, self.selectors, - self.relations, self.rel_type, self.contains, self.lang, self.flags, self.no_match + f'_Selector(tag={self.tag!r}, ids={self.ids!r}, classes={self.classes!r}, attributes={self.attributes!r}, ' + f'nth={self.nth!r}, selectors={self.selectors!r}, relations={self.relations!r}, ' + f'rel_type={self.rel_type!r}, contains={self.contains!r}, lang={self.lang!r}, flags={self.flags!r}, ' + f'no_match={self.no_match!r})' ) __repr__ = __str__ @@ -450,6 +449,7 @@ class CSSParser: SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM), SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS), SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT), + SelectorPattern("amp", PAT_AMP), SelectorPattern("at_rule", PAT_AT_RULE), SelectorPattern("id", PAT_ID), SelectorPattern("class", PAT_CLASS), @@ -461,7 +461,7 @@ class CSSParser: def __init__( self, selector: str, - custom: Optional[Dict[str, Union[str, ct.SelectorList]]] = None, + custom: dict[str, str | ct.SelectorList] | None = None, flags: int = 0 ) -> None: """Initialize.""" @@ -470,6 +470,13 @@ class CSSParser: self.flags = flags self.debug = self.flags & util.DEBUG self.custom = {} if custom is None else custom + self.count = 0 + + def check_count(self) -> None: + """Check the current selector count.""" + + if self.count > SELECTOR_LIMIT: + raise ValueError(f'Selector exceeds pseudo-class nesting limit of {SELECTOR_LIMIT}') def parse_attribute_selector(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: """Create attribute selector from the returned regex match.""" @@ -562,7 +569,7 @@ class CSSParser: selector = self.custom.get(pseudo) if selector is None: raise SelectorSyntaxError( - "Undefined custom selector '{}' found at position {}".format(pseudo, m.end(0)), + f"Undefined custom selector '{pseudo}' found at position {m.end(0)}", self.pattern, m.end(0) ) @@ -574,6 +581,9 @@ class CSSParser: ).process_selectors(flags=FLG_PSEUDO) self.custom[pseudo] = selector + self.count += selector.count + self.check_count() + sel.selectors.append(selector) has_selector = True return has_selector @@ -583,9 +593,9 @@ class CSSParser: sel: _Selector, m: Match[str], has_selector: bool, - iselector: Iterator[Tuple[str, Match[str]]], + iselector: Iterator[tuple[str, Match[str]]], is_html: bool - ) -> Tuple[bool, bool]: + ) -> tuple[bool, bool]: """Parse pseudo class.""" complex_pseudo = False @@ -605,30 +615,64 @@ class CSSParser: elif pseudo == ':empty': sel.flags |= ct.SEL_EMPTY elif pseudo in (':link', ':any-link'): + self.count += CSS_LINK.count + self.check_count() sel.selectors.append(CSS_LINK) elif pseudo == ':checked': + self.count += CSS_CHECKED.count + self.check_count() sel.selectors.append(CSS_CHECKED) elif pseudo == ':default': + self.count += CSS_DEFAULT.count + self.check_count() sel.selectors.append(CSS_DEFAULT) elif pseudo == ':indeterminate': + self.count += CSS_INDETERMINATE.count + self.check_count() sel.selectors.append(CSS_INDETERMINATE) elif pseudo == ":disabled": + self.count += CSS_DISABLED.count + self.check_count() sel.selectors.append(CSS_DISABLED) elif pseudo == ":enabled": + self.count += CSS_ENABLED.count + self.check_count() sel.selectors.append(CSS_ENABLED) elif pseudo == ":required": + self.count += CSS_REQUIRED.count + self.check_count() sel.selectors.append(CSS_REQUIRED) + elif pseudo == ":muted": + self.count += CSS_MUTED.count + self.check_count() + sel.selectors.append(CSS_MUTED) + elif pseudo == ":open": + self.count += CSS_OPEN.count + self.check_count() + sel.selectors.append(CSS_OPEN) elif pseudo == ":optional": + self.count += CSS_OPTIONAL.count + self.check_count() sel.selectors.append(CSS_OPTIONAL) elif pseudo == ":read-only": + self.count += CSS_READ_ONLY.count + self.check_count() sel.selectors.append(CSS_READ_ONLY) elif pseudo == ":read-write": + self.count += CSS_READ_WRITE.count + self.check_count() sel.selectors.append(CSS_READ_WRITE) elif pseudo == ":in-range": + self.count += CSS_IN_RANGE.count + self.check_count() sel.selectors.append(CSS_IN_RANGE) elif pseudo == ":out-of-range": + self.count += CSS_OUT_OF_RANGE.count + self.check_count() sel.selectors.append(CSS_OUT_OF_RANGE) elif pseudo == ":placeholder-shown": + self.count += CSS_PLACEHOLDER_SHOWN.count + self.check_count() sel.selectors.append(CSS_PLACEHOLDER_SHOWN) elif pseudo == ':first-child': sel.nth.append(ct.SelectorNth(1, False, 0, False, False, ct.SelectorList())) @@ -662,13 +706,16 @@ class CSSParser: has_selector = True elif pseudo in PSEUDO_SUPPORTED: raise SelectorSyntaxError( - "Invalid syntax for pseudo class '{}'".format(pseudo), + f"Invalid syntax for pseudo class '{pseudo}'", self.pattern, m.start(0) ) else: - raise NotImplementedError( - "'{}' pseudo-class is not implemented at this time".format(pseudo) + raise SelectorSyntaxError( + f"'{pseudo}' was detected as a pseudo-class and is either unsupported or invalid. " + "If the syntax was not intended to be recognized as a pseudo-class, please escape the colon.", + self.pattern, + m.start(0) ) return has_selector, is_html @@ -678,7 +725,7 @@ class CSSParser: sel: _Selector, m: Match[str], has_selector: bool, - iselector: Iterator[Tuple[str, Match[str]]] + iselector: Iterator[tuple[str, Match[str]]] ) -> bool: """Parse `nth` pseudo.""" @@ -726,6 +773,8 @@ class CSSParser: else: # Use default `*|*` for `of S`. nth_sel = CSS_NTH_OF_S_DEFAULT + self.count += nth_sel.count + self.check_count() if pseudo_sel == ':nth-child': sel.nth.append(ct.SelectorNth(s1, var, s2, False, False, nth_sel)) elif pseudo_sel == ':nth-last-child': @@ -743,7 +792,7 @@ class CSSParser: sel: _Selector, name: str, has_selector: bool, - iselector: Iterator[Tuple[str, Match[str]]], + iselector: Iterator[tuple[str, Match[str]]], index: int ) -> bool: """Parse pseudo with opening bracket.""" @@ -752,7 +801,7 @@ class CSSParser: if name == ':not': flags |= FLG_NOT elif name == ':has': - flags |= FLG_RELATIVE | FLG_FORGIVE + flags |= FLG_RELATIVE elif name in (':where', ':is'): flags |= FLG_FORGIVE @@ -766,21 +815,16 @@ class CSSParser: sel: _Selector, m: Match[str], has_selector: bool, - selectors: List[_Selector], + selectors: list[_Selector], rel_type: str, index: int - ) -> Tuple[bool, _Selector, str]: + ) -> tuple[bool, _Selector, str]: """Parse combinator tokens.""" combinator = m.group('relation').strip() if not combinator: combinator = WS_COMBINATOR if combinator == COMMA_COMBINATOR: - if not has_selector: - # If we've not captured any selector parts, the comma is either at the beginning of the pattern - # or following another comma, both of which are unexpected. But shouldn't fail the pseudo-class. - sel.no_match = True - sel.rel_type = rel_type selectors[-1].relations.append(sel) rel_type = ":" + WS_COMBINATOR @@ -797,7 +841,7 @@ class CSSParser: # multiple non-whitespace combinators. So if the current combinator is not a whitespace, # then we've hit the multiple combinator case, so we should fail. raise SelectorSyntaxError( - 'The multiple combinators at position {}'.format(index), + f'The multiple combinators at position {index}', self.pattern, index ) @@ -814,12 +858,12 @@ class CSSParser: sel: _Selector, m: Match[str], has_selector: bool, - selectors: List[_Selector], - relations: List[_Selector], + selectors: list[_Selector], + relations: list[_Selector], is_pseudo: bool, is_forgive: bool, index: int - ) -> Tuple[bool, _Selector]: + ) -> tuple[bool, _Selector]: """Parse combinator tokens.""" combinator = m.group('relation').strip() @@ -828,7 +872,7 @@ class CSSParser: if not has_selector: if not is_forgive or combinator != COMMA_COMBINATOR: raise SelectorSyntaxError( - "The combinator '{}' at position {}, must have a selector before it".format(combinator, index), + f"The combinator '{combinator}' at position {index}, must have a selector before it", self.pattern, index ) @@ -873,7 +917,7 @@ class CSSParser: pseudo = util.lower(css_unescape(m.group('name'))) if pseudo == ":contains": - warnings.warn( + warnings.warn( # noqa: B028 "The pseudo class ':contains' is deprecated, ':-soup-contains' should be used moving forward.", FutureWarning ) @@ -924,7 +968,7 @@ class CSSParser: def parse_selectors( self, - iselector: Iterator[Tuple[str, Match[str]]], + iselector: Iterator[tuple[str, Match[str]]], index: int = 0, flags: int = 0 ) -> ct.SelectorList: @@ -935,8 +979,9 @@ class CSSParser: selectors = [] has_selector = False closed = False - relations = [] # type: List[_Selector] + relations = [] # type: list[_Selector] rel_type = ":" + WS_COMBINATOR + count = self.count # Setup various flags is_open = bool(flags & FLG_OPEN) @@ -984,15 +1029,22 @@ class CSSParser: while True: key, m = next(iselector) + if key not in ('combine', 'pseudo_close'): + self.count += 1 + self.check_count() + # Handle parts if key == "at_rule": - raise NotImplementedError("At-rules found at position {}".format(m.start(0))) + raise NotImplementedError(f"At-rules found at position {m.start(0)}") + elif key == "amp": + sel.flags |= ct.SEL_SCOPE + has_selector = True elif key == 'pseudo_class_custom': has_selector = self.parse_pseudo_class_custom(sel, m, has_selector) elif key == 'pseudo_class': has_selector, is_html = self.parse_pseudo_class(sel, m, has_selector, iselector, is_html) elif key == 'pseudo_element': - raise NotImplementedError("Pseudo-element found at position {}".format(m.start(0))) + raise NotImplementedError(f"Pseudo-element found at position {m.start(0)}") elif key == 'pseudo_contains': has_selector = self.parse_pseudo_contains(sel, m, has_selector) elif key in ('pseudo_nth_type', 'pseudo_nth_child'): @@ -1007,7 +1059,7 @@ class CSSParser: if not has_selector: if not is_forgive: raise SelectorSyntaxError( - "Expected a selector at position {}".format(m.start(0)), + f"Expected a selector at position {m.start(0)}", self.pattern, m.start(0) ) @@ -1017,7 +1069,7 @@ class CSSParser: break else: raise SelectorSyntaxError( - "Unmatched pseudo-class close at position {}".format(m.start(0)), + f"Unmatched pseudo-class close at position {m.start(0)}", self.pattern, m.start(0) ) @@ -1035,7 +1087,7 @@ class CSSParser: elif key == 'tag': if has_selector: raise SelectorSyntaxError( - "Tag name found at position {} instead of at the start".format(m.start(0)), + f"Tag name found at position {m.start(0)} instead of at the start", self.pattern, m.start(0) ) @@ -1050,7 +1102,7 @@ class CSSParser: # Handle selectors that are not closed if is_open and not closed: raise SelectorSyntaxError( - "Unclosed pseudo-class at position {}".format(index), + f"Unclosed pseudo-class at position {index}", self.pattern, index ) @@ -1069,28 +1121,18 @@ class CSSParser: selectors.append(sel) # Forgive empty slots in pseudo-classes that have lists (and are forgiving) - elif is_forgive: - if is_relative: - # Handle relative selectors pseudo-classes with empty slots like `:has()` - if selectors and selectors[-1].rel_type is None and rel_type == ': ': - sel.rel_type = rel_type - sel.no_match = True - selectors[-1].relations.append(sel) - has_selector = True - else: - # Handle normal pseudo-classes with empty slots - if not selectors or not relations: - # Others like `:is()` etc. - sel.no_match = True - del relations[:] - selectors.append(sel) - has_selector = True + elif is_forgive and (not selectors or not relations): + # Handle normal pseudo-classes with empty slots like `:is()` etc. + sel.no_match = True + del relations[:] + selectors.append(sel) + has_selector = True if not has_selector: # We will always need to finish a selector when `:has()` is used as it leads with combining. # May apply to others as well. raise SelectorSyntaxError( - 'Expected a selector at position {}'.format(index), + f'Expected a selector at position {index}', self.pattern, index ) @@ -1110,9 +1152,9 @@ class CSSParser: selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN # Return selector list - return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html) + return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html, self.count - count) - def selector_iter(self, pattern: str) -> Iterator[Tuple[str, Match[str]]]: + def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]: """Iterate selector tokens.""" # Ignore whitespace and comments at start and end of pattern @@ -1122,7 +1164,7 @@ class CSSParser: end = (m.start(0) - 1) if m else (len(pattern) - 1) if self.debug: # pragma: no cover - print('## PARSING: {!r}'.format(pattern)) + print(f'## PARSING: {pattern!r}') while index <= end: m = None for v in self.css_tokens: @@ -1130,7 +1172,7 @@ class CSSParser: if m: name = v.get_name() if self.debug: # pragma: no cover - print("TOKEN: '{}' --> {!r} at position {}".format(name, m.group(0), m.start(0))) + print(f"TOKEN: '{name}' --> {m.group(0)!r} at position {m.start(0)}") index = m.end(0) yield name, m break @@ -1140,15 +1182,15 @@ class CSSParser: # throw an exception mentioning that the known selector type is in error; # otherwise, report the invalid character. if c == '[': - msg = "Malformed attribute selector at position {}".format(index) + msg = f"Malformed attribute selector at position {index}" elif c == '.': - msg = "Malformed class selector at position {}".format(index) + msg = f"Malformed class selector at position {index}" elif c == '#': - msg = "Malformed id selector at position {}".format(index) + msg = f"Malformed id selector at position {index}" elif c == ':': - msg = "Malformed pseudo-class selector at position {}".format(index) + msg = f"Malformed pseudo-class selector at position {index}" else: - msg = "Invalid character {!r} position {}".format(c, index) + msg = f"Invalid character {c!r} position {index}" raise SelectorSyntaxError(msg, self.pattern, index) if self.debug: # pragma: no cover print('## END PARSING') @@ -1308,3 +1350,18 @@ CSS_OUT_OF_RANGE = CSSParser( ) ''' ).process_selectors(flags=FLG_PSEUDO | FLG_OUT_OF_RANGE | FLG_HTML) + +# CSS pattern for :open +CSS_OPEN = CSSParser( + ''' + html|*:is(details, dialog)[open] + ''' +).process_selectors(flags=FLG_PSEUDO | FLG_HTML) + + +# CSS pattern for :muted +CSS_MUTED = CSSParser( + ''' + html|*:is(video, audio)[muted] + ''' +).process_selectors(flags=FLG_PSEUDO | FLG_HTML) diff --git a/libs/soupsieve/css_types.py b/libs/soupsieve/css_types.py index e5a6e49c8..f1af46efe 100644 --- a/libs/soupsieve/css_types.py +++ b/libs/soupsieve/css_types.py @@ -1,7 +1,8 @@ """CSS selector structure items.""" +from __future__ import annotations import copyreg from .pretty import pretty -from typing import Any, Type, Tuple, Union, Dict, Iterator, Hashable, Optional, Pattern, Iterable, Mapping +from typing import Any, Iterator, Hashable, Pattern, Iterable, Mapping __all__ = ( 'Selector', @@ -33,7 +34,7 @@ SEL_PLACEHOLDER_SHOWN = 0x400 class Immutable: """Immutable.""" - __slots__: Tuple[str, ...] = ('_hash',) + __slots__: tuple[str, ...] = ('_hash',) _hash: int @@ -44,11 +45,11 @@ class Immutable: for k, v in kwargs.items(): temp.append(type(v)) temp.append(v) - super(Immutable, self).__setattr__(k, v) - super(Immutable, self).__setattr__('_hash', hash(tuple(temp))) + super().__setattr__(k, v) + super().__setattr__('_hash', hash(tuple(temp))) @classmethod - def __base__(cls) -> "Type[Immutable]": + def __base__(cls) -> type[Immutable]: """Get base class.""" return cls @@ -58,7 +59,7 @@ class Immutable: return ( isinstance(other, self.__base__()) and - all([getattr(other, key) == getattr(self, key) for key in self.__slots__ if key != '_hash']) + all(getattr(other, key) == getattr(self, key) for key in self.__slots__ if key != '_hash') ) def __ne__(self, other: Any) -> bool: @@ -66,7 +67,7 @@ class Immutable: return ( not isinstance(other, self.__base__()) or - any([getattr(other, key) != getattr(self, key) for key in self.__slots__ if key != '_hash']) + any(getattr(other, key) != getattr(self, key) for key in self.__slots__ if key != '_hash') ) def __hash__(self) -> int: @@ -77,14 +78,13 @@ class Immutable: def __setattr__(self, name: str, value: Any) -> None: """Prevent mutability.""" - raise AttributeError("'{}' is immutable".format(self.__class__.__name__)) + raise AttributeError(f"'{self.__class__.__name__}' is immutable") def __repr__(self) -> str: # pragma: no cover """Representation.""" - return "{}({})".format( - self.__class__.__name__, ', '.join(["{}={!r}".format(k, getattr(self, k)) for k in self.__slots__[:-1]]) - ) + r = ', '.join([f"{k}={getattr(self, k)!r}" for k in self.__slots__[:-1]]) + return f"{self.__class__.__name__}({r})" __str__ = __repr__ @@ -99,7 +99,7 @@ class ImmutableDict(Mapping[Any, Any]): def __init__( self, - arg: Union[Dict[Any, Any], Iterable[Tuple[Any, Any]]] + arg: dict[Any, Any] | Iterable[tuple[Any, Any]] ) -> None: """Initialize.""" @@ -107,14 +107,14 @@ class ImmutableDict(Mapping[Any, Any]): self._d = dict(arg) self._hash = hash(tuple([(type(x), x, type(y), y) for x, y in sorted(self._d.items())])) - def _validate(self, arg: Union[Dict[Any, Any], Iterable[Tuple[Any, Any]]]) -> None: + def _validate(self, arg: dict[Any, Any] | Iterable[tuple[Any, Any]]) -> None: """Validate arguments.""" if isinstance(arg, dict): - if not all([isinstance(v, Hashable) for v in arg.values()]): - raise TypeError('{} values must be hashable'.format(self.__class__.__name__)) - elif not all([isinstance(k, Hashable) and isinstance(v, Hashable) for k, v in arg]): - raise TypeError('{} values must be hashable'.format(self.__class__.__name__)) + if not all(isinstance(v, Hashable) for v in arg.values()): + raise TypeError(f'{self.__class__.__name__} values must be hashable') + elif not all(isinstance(k, Hashable) and isinstance(v, Hashable) for k, v in arg): + raise TypeError(f'{self.__class__.__name__} values must be hashable') def __iter__(self) -> Iterator[Any]: """Iterator.""" @@ -139,7 +139,7 @@ class ImmutableDict(Mapping[Any, Any]): def __repr__(self) -> str: # pragma: no cover """Representation.""" - return "{!r}".format(self._d) + return f"{self._d!r}" __str__ = __repr__ @@ -147,37 +147,37 @@ class ImmutableDict(Mapping[Any, Any]): class Namespaces(ImmutableDict): """Namespaces.""" - def __init__(self, arg: Union[Dict[str, str], Iterable[Tuple[str, str]]]) -> None: + def __init__(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None: """Initialize.""" super().__init__(arg) - def _validate(self, arg: Union[Dict[str, str], Iterable[Tuple[str, str]]]) -> None: + def _validate(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None: """Validate arguments.""" - if isinstance(arg, dict): - if not all([isinstance(v, str) for v in arg.values()]): - raise TypeError('{} values must be hashable'.format(self.__class__.__name__)) - elif not all([isinstance(k, str) and isinstance(v, str) for k, v in arg]): - raise TypeError('{} keys and values must be Unicode strings'.format(self.__class__.__name__)) + if not all( + isinstance(k, str) and isinstance(v, str) + for k, v in (arg.items() if isinstance(arg, dict) else arg) + ): + raise TypeError(f'{self.__class__.__name__} values must be hashable') class CustomSelectors(ImmutableDict): """Custom selectors.""" - def __init__(self, arg: Union[Dict[str, str], Iterable[Tuple[str, str]]]) -> None: + def __init__(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None: """Initialize.""" super().__init__(arg) - def _validate(self, arg: Union[Dict[str, str], Iterable[Tuple[str, str]]]) -> None: + def _validate(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None: """Validate arguments.""" - if isinstance(arg, dict): - if not all([isinstance(v, str) for v in arg.values()]): - raise TypeError('{} values must be hashable'.format(self.__class__.__name__)) - elif not all([isinstance(k, str) and isinstance(v, str) for k, v in arg]): - raise TypeError('{} keys and values must be Unicode strings'.format(self.__class__.__name__)) + if not all( + isinstance(k, str) and isinstance(v, str) + for k, v in (arg.items() if isinstance(arg, dict) else arg) + ): + raise TypeError(f'{self.__class__.__name__} values must be hashable') class Selector(Immutable): @@ -188,30 +188,30 @@ class Selector(Immutable): 'relation', 'rel_type', 'contains', 'lang', 'flags', '_hash' ) - tag: Optional['SelectorTag'] - ids: Tuple[str, ...] - classes: Tuple[str, ...] - attributes: Tuple['SelectorAttribute', ...] - nth: Tuple['SelectorNth', ...] - selectors: Tuple['SelectorList', ...] - relation: 'SelectorList' - rel_type: Optional[str] - contains: Tuple['SelectorContains', ...] - lang: Tuple['SelectorLang', ...] + tag: SelectorTag | None + ids: tuple[str, ...] + classes: tuple[str, ...] + attributes: tuple[SelectorAttribute, ...] + nth: tuple[SelectorNth, ...] + selectors: tuple[SelectorList, ...] + relation: SelectorList + rel_type: str | None + contains: tuple[SelectorContains, ...] + lang: tuple[SelectorLang, ...] flags: int def __init__( self, - tag: Optional['SelectorTag'], - ids: Tuple[str, ...], - classes: Tuple[str, ...], - attributes: Tuple['SelectorAttribute', ...], - nth: Tuple['SelectorNth', ...], - selectors: Tuple['SelectorList', ...], - relation: 'SelectorList', - rel_type: Optional[str], - contains: Tuple['SelectorContains', ...], - lang: Tuple['SelectorLang', ...], + tag: SelectorTag | None, + ids: tuple[str, ...], + classes: tuple[str, ...], + attributes: tuple[SelectorAttribute, ...], + nth: tuple[SelectorNth, ...], + selectors: tuple[SelectorList, ...], + relation: SelectorList, + rel_type: str | None, + contains: tuple[SelectorContains, ...], + lang: tuple[SelectorLang, ...], flags: int ): """Initialize.""" @@ -246,9 +246,9 @@ class SelectorTag(Immutable): __slots__ = ("name", "prefix", "_hash") name: str - prefix: Optional[str] + prefix: str | None - def __init__(self, name: str, prefix: Optional[str]) -> None: + def __init__(self, name: str, prefix: str | None) -> None: """Initialize.""" super().__init__(name=name, prefix=prefix) @@ -261,15 +261,15 @@ class SelectorAttribute(Immutable): attribute: str prefix: str - pattern: Optional[Pattern[str]] - xml_type_pattern: Optional[Pattern[str]] + pattern: Pattern[str] | None + xml_type_pattern: Pattern[str] | None def __init__( self, attribute: str, prefix: str, - pattern: Optional[Pattern[str]], - xml_type_pattern: Optional[Pattern[str]] + pattern: Pattern[str] | None, + xml_type_pattern: Pattern[str] | None ) -> None: """Initialize.""" @@ -286,7 +286,7 @@ class SelectorContains(Immutable): __slots__ = ("text", "own", "_hash") - text: Tuple[str, ...] + text: tuple[str, ...] own: bool def __init__(self, text: Iterable[str], own: bool) -> None: @@ -305,9 +305,9 @@ class SelectorNth(Immutable): b: int of_type: bool last: bool - selectors: 'SelectorList' + selectors: SelectorList - def __init__(self, a: int, n: bool, b: int, of_type: bool, last: bool, selectors: 'SelectorList') -> None: + def __init__(self, a: int, n: bool, b: int, of_type: bool, last: bool, selectors: SelectorList) -> None: """Initialize.""" super().__init__( @@ -325,7 +325,7 @@ class SelectorLang(Immutable): __slots__ = ("languages", "_hash",) - languages: Tuple[str, ...] + languages: tuple[str, ...] def __init__(self, languages: Iterable[str]): """Initialize.""" @@ -351,27 +351,30 @@ class SelectorLang(Immutable): class SelectorList(Immutable): """Selector list.""" - __slots__ = ("selectors", "is_not", "is_html", "_hash") + __slots__ = ("selectors", "is_not", "is_html", "count", "_hash") - selectors: Tuple[Union['Selector', 'SelectorNull'], ...] + selectors: tuple[Selector | SelectorNull, ...] is_not: bool is_html: bool + count: int def __init__( self, - selectors: Optional[Iterable[Union['Selector', 'SelectorNull']]] = None, + selectors: Iterable[Selector | SelectorNull] | None = None, is_not: bool = False, - is_html: bool = False + is_html: bool = False, + count: int = 0, ) -> None: """Initialize.""" super().__init__( - selectors=tuple(selectors) if selectors is not None else tuple(), + selectors=tuple(selectors) if selectors is not None else (), is_not=is_not, - is_html=is_html + is_html=is_html, + count=count ) - def __iter__(self) -> Iterator[Union['Selector', 'SelectorNull']]: + def __iter__(self) -> Iterator[Selector | SelectorNull]: """Iterator.""" return iter(self.selectors) @@ -381,7 +384,7 @@ class SelectorList(Immutable): return len(self.selectors) - def __getitem__(self, index: int) -> Union['Selector', 'SelectorNull']: + def __getitem__(self, index: int) -> Selector | SelectorNull: """Get item.""" return self.selectors[index] diff --git a/libs/soupsieve/pretty.py b/libs/soupsieve/pretty.py index 57d16c970..37d51d8c2 100644 --- a/libs/soupsieve/pretty.py +++ b/libs/soupsieve/pretty.py @@ -10,7 +10,7 @@ The format and various output types is fairly known (though it hasn't been tested extensively to make sure we aren't missing corners). Example: - +------- ``` >>> import soupsieve as sv >>> sv.compile('this > that.class[name=value]').selectors.pretty() @@ -64,11 +64,13 @@ SelectorList( is_not=False, is_html=False) ``` + """ +from __future__ import annotations import re from typing import Any -RE_CLASS = re.compile(r'(?i)[a-z_][_a-z\d\.]+\(') +RE_CLASS = re.compile(r'(?i)[a-z_][_a-z\d.]+\(') RE_PARAM = re.compile(r'(?i)[_a-z][_a-z\d]+=') RE_EMPTY = re.compile(r'\(\)|\[\]|\{\}') RE_LSTRT = re.compile(r'\[') @@ -78,11 +80,12 @@ RE_LEND = re.compile(r'\]') RE_DEND = re.compile(r'\}') RE_TEND = re.compile(r'\)') RE_INT = re.compile(r'\d+') -RE_KWORD = re.compile(r'(?i)[_a-z][_a-z\d]+') +RE_KWORD = re.compile(r'(?i)[_a-z][_a-z\d.]+') RE_DQSTR = re.compile(r'"(?:\\.|[^"\\])*"') RE_SQSTR = re.compile(r"'(?:\\.|[^'\\])*'") RE_SEP = re.compile(r'\s*(,)\s*') RE_DSEP = re.compile(r'\s*(:)\s*') +RE_PSEP = re.compile(r'\s*(\|)\s*') TOKENS = { 'class': RE_CLASS, @@ -97,6 +100,7 @@ TOKENS = { 'sqstr': RE_SQSTR, 'sep': RE_SEP, 'dsep': RE_DSEP, + 'psep': RE_PSEP, 'int': RE_INT, 'kword': RE_KWORD, 'dqstr': RE_DQSTR @@ -122,16 +126,23 @@ def pretty(obj: Any) -> str: # pragma: no cover index = m.end(0) if name in ('class', 'lstrt', 'dstrt', 'tstrt'): indent += 4 - output.append('{}\n{}'.format(m.group(0), " " * indent)) + output.append(f'{m.group(0)}\n{" " * indent}') elif name in ('param', 'int', 'kword', 'sqstr', 'dqstr', 'empty'): output.append(m.group(0)) elif name in ('lend', 'dend', 'tend'): indent -= 4 output.append(m.group(0)) elif name in ('sep',): - output.append('{}\n{}'.format(m.group(1), " " * indent)) + output.append(f'{m.group(1)}\n{" " * indent}') elif name in ('dsep',): - output.append('{} '.format(m.group(1))) + output.append(f'{m.group(1)} ') + elif name in ('psep'): + output.append(f' {m.group(1)} ') break + # We shouldn't hit this, but if we do, store unrecognized character + if m is None: # pragma: no cover + output.append(sel[index]) + index += 1 + return ''.join(output) diff --git a/libs/soupsieve/util.py b/libs/soupsieve/util.py index 2b1ed24b9..9b2e64df2 100644 --- a/libs/soupsieve/util.py +++ b/libs/soupsieve/util.py @@ -1,8 +1,9 @@ """Utility.""" +from __future__ import annotations from functools import wraps, lru_cache import warnings import re -from typing import Callable, Any, Optional, Tuple, List +from typing import Callable, Any DEBUG = 0x00001 @@ -26,7 +27,7 @@ def lower(string: str) -> str: class SelectorSyntaxError(Exception): """Syntax error in a CSS selector.""" - def __init__(self, msg: str, pattern: Optional[str] = None, index: Optional[int] = None) -> None: + def __init__(self, msg: str, pattern: str | None = None, index: int | None = None) -> None: """Initialize.""" self.line = None @@ -36,7 +37,7 @@ class SelectorSyntaxError(Exception): if pattern is not None and index is not None: # Format pattern to show line and column position self.context, self.line, self.col = get_pattern_context(pattern, index) - msg = '{}\n line {}:\n{}'.format(msg, self.line, self.context) + msg = f'{msg}\n line {self.line}:\n{self.context}' super().__init__(msg) @@ -75,15 +76,15 @@ def warn_deprecated(message: str, stacklevel: int = 2) -> None: # pragma: no co ) -def get_pattern_context(pattern: str, index: int) -> Tuple[str, int, int]: +def get_pattern_context(pattern: str, index: int) -> tuple[str, int, int]: """Get the pattern context.""" last = 0 current_line = 1 col = 1 - text = [] # type: List[str] + text = [] # type: list[str] line = 1 - offset = None # type: Optional[int] + offset = None # type: int | None # Split pattern by newline and handle the text before the newline for m in RE_PATTERN_LINE_SPLIT.finditer(pattern): @@ -104,7 +105,7 @@ def get_pattern_context(pattern: str, index: int) -> Tuple[str, int, int]: # we will render the output with just `\n`. We will still log the column # correctly though. text.append('\n') - text.append('{}{}'.format(indent, linetext)) + text.append(f'{indent}{linetext}') if offset is not None: text.append('\n') text.append(' ' * (col + offset) + '^') diff --git a/libs/sqlalchemy-2.0.48.dist-info/INSTALLER b/libs/sqlalchemy-2.0.51.dist-info/INSTALLER similarity index 100% rename from libs/sqlalchemy-2.0.48.dist-info/INSTALLER rename to libs/sqlalchemy-2.0.51.dist-info/INSTALLER diff --git a/libs/sqlalchemy-2.0.48.dist-info/METADATA b/libs/sqlalchemy-2.0.51.dist-info/METADATA similarity index 99% rename from libs/sqlalchemy-2.0.48.dist-info/METADATA rename to libs/sqlalchemy-2.0.51.dist-info/METADATA index d63401ebb..8e2b6f65e 100644 --- a/libs/sqlalchemy-2.0.48.dist-info/METADATA +++ b/libs/sqlalchemy-2.0.51.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: SQLAlchemy -Version: 2.0.48 +Version: 2.0.51 Summary: Database Abstraction Library Home-page: https://www.sqlalchemy.org Author: Mike Bayer diff --git a/libs/sqlalchemy-2.0.48.dist-info/RECORD b/libs/sqlalchemy-2.0.51.dist-info/RECORD similarity index 50% rename from libs/sqlalchemy-2.0.48.dist-info/RECORD rename to libs/sqlalchemy-2.0.51.dist-info/RECORD index ef4736a57..46f55f8a2 100644 --- a/libs/sqlalchemy-2.0.48.dist-info/RECORD +++ b/libs/sqlalchemy-2.0.51.dist-info/RECORD @@ -1,151 +1,151 @@ -sqlalchemy-2.0.48.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -sqlalchemy-2.0.48.dist-info/METADATA,sha256=nBJCWxsX7QVYe9cGsQ4hR91MPdghSODauUC-kaPE4Z4,9547 -sqlalchemy-2.0.48.dist-info/RECORD,, -sqlalchemy-2.0.48.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -sqlalchemy-2.0.48.dist-info/WHEEL,sha256=DkV-ntIm-3VWD87iCIyvXSz28ISStjMGlDRnHg-QlnQ,110 -sqlalchemy-2.0.48.dist-info/licenses/LICENSE,sha256=6GK7W5BPtVE8PhQojXoloS6w7MeEegqWXTEOmpVefLk,1100 -sqlalchemy-2.0.48.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11 -sqlalchemy/__init__.py,sha256=MDNQDuSC42uiYH1uMEVPvB0l2Prw32i-j1_KRTz-Q4M,12659 +sqlalchemy-2.0.51.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +sqlalchemy-2.0.51.dist-info/METADATA,sha256=b-pF5X3FtbHErXvtaKLMoIlbZtO-ao98RR060J6Td3Y,9547 +sqlalchemy-2.0.51.dist-info/RECORD,, +sqlalchemy-2.0.51.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sqlalchemy-2.0.51.dist-info/WHEEL,sha256=DkV-ntIm-3VWD87iCIyvXSz28ISStjMGlDRnHg-QlnQ,110 +sqlalchemy-2.0.51.dist-info/licenses/LICENSE,sha256=6GK7W5BPtVE8PhQojXoloS6w7MeEegqWXTEOmpVefLk,1100 +sqlalchemy-2.0.51.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11 +sqlalchemy/__init__.py,sha256=WFqPWssZD8l4n0vPghuLU631e7wnNE9SvPj2vOyq1aU,12659 sqlalchemy/connectors/__init__.py,sha256=Gm6cQChPfkSAQckCgAcvPvxCDOFRujQefFYgccMBSpQ,476 -sqlalchemy/connectors/aioodbc.py,sha256=l486yddlVkVtC1YR2M2jP7dpUeKL3x01M6yyq0ARwfo,5577 +sqlalchemy/connectors/aioodbc.py,sha256=ZOgYop9ObJY0v1Ctc_BCQpO4BNbE7LCyOW7joltAV1g,5853 sqlalchemy/connectors/asyncio.py,sha256=RNFzIXv684F0FJuE05a-1QK_dpqYrUHwIiaUCO32n5s,13057 sqlalchemy/connectors/pyodbc.py,sha256=IHuFnhH0tb5JiWscZF84ZK4eTqQp5dZsP7EXfDz2tXU,8618 sqlalchemy/cyextension/__init__.py,sha256=ZiAVRvOcCS31zK0qNBweQu96I3YDT5LZHo-d9lA6oKE,244 -sqlalchemy/cyextension/collections.cpython-310-darwin.so,sha256=hLbUaclzvKttCv2AWpVuipVk3vuq8cbdhC-O2Fvg9y0,295120 +sqlalchemy/cyextension/collections.cpython-310-darwin.so,sha256=7Nr4dpzM1ZyRll20qY8bDxSm8tOeShwRkHzRO_yENnw,300432 sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571 -sqlalchemy/cyextension/immutabledict.cpython-310-darwin.so,sha256=6Wn4nvtegg8vCxZ9ccVYGOM_kJckKlKLN5u_QAywVSU,133408 +sqlalchemy/cyextension/immutabledict.cpython-310-darwin.so,sha256=X2XI9twwnPfkCCazrApaRlt5nkYxOcQhyqtcwt2yXkc,135168 sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291 sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535 -sqlalchemy/cyextension/processors.cpython-310-darwin.so,sha256=wGFVlhQUPpMm6YgSnKwxDU2_MahRQPN8NZOMCsC1TuI,110848 +sqlalchemy/cyextension/processors.cpython-310-darwin.so,sha256=rntxXU52D_0A07nhl_2tTqnPWm-5vKxKkQl9SmohK3Q,112016 sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792 -sqlalchemy/cyextension/resultproxy.cpython-310-darwin.so,sha256=AynbleYBU7AxnFWdOViDghu5XbHwTOumGuh6hc4CiKE,116752 +sqlalchemy/cyextension/resultproxy.cpython-310-darwin.so,sha256=DZAh3Zrh6BThCDpLb79FEFCSaM4cB-LWGABZ03etxPs,118512 sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725 -sqlalchemy/cyextension/util.cpython-310-darwin.so,sha256=MrA5zqQzTJWj2Bhb4s9U6c0OBJtJq33ypFfEy19KQuI,135736 +sqlalchemy/cyextension/util.cpython-310-darwin.so,sha256=YR2ZRSfXAyxDS6zJHgr4g78WRnKsXfohdaxv_Z0UOww,137992 sqlalchemy/cyextension/util.pyx,sha256=Tt5VwTUtO3YKQK2PHfYOLhV2Jr5GMRJcp2DzH4fjGOs,2569 sqlalchemy/dialects/__init__.py,sha256=_Pls3JOH7T5S_K8CB5TSH3gYX360SNbQgg6AGZoczBI,1798 -sqlalchemy/dialects/_typing.py,sha256=KSJ6HjSP5MTiV-TVDKm42tSkVO-HEMbXcx8laNZPZMo,971 -sqlalchemy/dialects/mssql/__init__.py,sha256=-xWcOVCiTAogF2MigpaLNJEshbYQJOC8KUSR767_Xt0,1880 +sqlalchemy/dialects/_typing.py,sha256=oUmFIxU_AVXmJyRXiE5IsDvKFh8HvAcyiGMQaWmY7MY,970 +sqlalchemy/dialects/mssql/__init__.py,sha256=evBLzJ7Q0EHJpqkJUE99l51mL5ny-LfTQXqAj3vH3rI,1879 sqlalchemy/dialects/mssql/aioodbc.py,sha256=rFbdJJ7sRx0VIyGC17wQRBBLdemZTXXo7zUQ-AZla5E,2021 -sqlalchemy/dialects/mssql/base.py,sha256=boXz0t4Oj2kDlhyHyILYlCAjkGJ1am84RV_6oFQE32k,134317 -sqlalchemy/dialects/mssql/information_schema.py,sha256=1FOgYP862FOSuy6Z9maGjfEwi-oiTRupU5soRtlAZWU,8963 +sqlalchemy/dialects/mssql/base.py,sha256=54c_7QQq9iUcRo6vqwJjwJUzJlNN_exFCTrMSE4X1bM,135146 +sqlalchemy/dialects/mssql/information_schema.py,sha256=maXUzFnv5Lt7o26li5G5mvdQ5MioeDFC0qVcPDS6_ok,8962 sqlalchemy/dialects/mssql/json.py,sha256=9CDNWSCvBMMQV_-vbqrxIgaoDK6PfvixJjPzERPkt8A,4756 sqlalchemy/dialects/mssql/provision.py,sha256=2TPp4mI_nVUBizOGjw0tXhiDrFBQwC-5oxqeyxarPeE,6480 -sqlalchemy/dialects/mssql/pymssql.py,sha256=6s7nFd0QcbMuoJBJ-s4leOrF2_EXPncJjtdAL3UMNqQ,4097 -sqlalchemy/dialects/mssql/pyodbc.py,sha256=OtgY_eGYVPv2jJ7MASVQbIlaLFe5EPXeqpkbNnFogNI,27173 +sqlalchemy/dialects/mssql/pymssql.py,sha256=w4PUFJ0-en3scehaZWbPd6JnN9R_AM5qAMY9qU9VyF4,4098 +sqlalchemy/dialects/mssql/pyodbc.py,sha256=1gggqGV_ed0pR0qxnuCFMyToBvTvYFCvnsMMxY4Acj4,27172 sqlalchemy/dialects/mysql/__init__.py,sha256=XQz65UCpOpFR_NAxg6sM2zJgPmLEp16D_yEqgJPjirI,2206 -sqlalchemy/dialects/mysql/aiomysql.py,sha256=3yjKHxvIAn91a8X80ypVgoIsuevCIMXBUAvnLVzMVpM,7927 -sqlalchemy/dialects/mysql/asyncmy.py,sha256=VEZF_7rdQvcPcV1nFNqpmALFAUmII5tfD16--TOEyCM,7292 -sqlalchemy/dialects/mysql/base.py,sha256=a794VXC17_P16Ep41eiMr1uEQQOLvlMVGMK3X7XO3XQ,138544 -sqlalchemy/dialects/mysql/cymysql.py,sha256=Zvh6HOWt4icgyT1PuSmSByr5_KR41yVm52yFCqgJzqg,3215 -sqlalchemy/dialects/mysql/dml.py,sha256=uwu1J7BLdYNP8LR6-mCkoTKOA4USo4Av6d3I_n5c3nI,7768 +sqlalchemy/dialects/mysql/aiomysql.py,sha256=hV5D9Mggliorocbirvk5DBV8b8dbWZI9KbCyYb1zemQ,8687 +sqlalchemy/dialects/mysql/asyncmy.py,sha256=PjJwbyMwbAmu_lOfE22chxRuq4QlK2ARgKZRR4VaY_4,8052 +sqlalchemy/dialects/mysql/base.py,sha256=FM7AWVbDLSAyMGTqY4VwpIg4JzNUGEUVuOTjB-8m1es,138601 +sqlalchemy/dialects/mysql/cymysql.py,sha256=GSoPEjug8UXoNxatfhXqC_LhL970arpzrZIItOnvpos,3216 +sqlalchemy/dialects/mysql/dml.py,sha256=94CWMrG5VyzGU4LdZ3xvVv-edvIPrg1XietuUlatgSw,7767 sqlalchemy/dialects/mysql/enumerated.py,sha256=sSG5qiYGu-cobxzoRwF9x8Qfc-iSWh0qeGGe5oQlXWM,10292 sqlalchemy/dialects/mysql/expression.py,sha256=ShV5V6Z-YaXi_ETbPcLz1AOuSj9pZrvgmh2Ne61x66A,4241 sqlalchemy/dialects/mysql/json.py,sha256=WUE0BDCpVv5_RsNoBS1BJAz6TUpNls1XC19WEuHP0AM,2806 sqlalchemy/dialects/mysql/mariadb.py,sha256=InsZjmtWSPURW_gy6hXZhJ6f8FXCkaLCphhqHVMosEw,1844 -sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=qaqM-tKtWNAb8X6GRVtsKkO11hMw4fUhDiD85b2kKk8,10385 -sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=mHq9510MvTNzDYqvIQnNbLKod8ItcQ5-GK18XEtmgs4,10097 -sqlalchemy/dialects/mysql/mysqldb.py,sha256=6ooLaaSue8yQxgonxz4v_opMHV-jfi9x9vWqexc_xgU,9943 +sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=ntpHnLKeGGWWpr99y1ZGlFOG9iwkJKuGXv_CXiV7qEM,10386 +sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=N3HCWrei2ddojVEDgKBHOL5wYJgSCRSleJNCeOs-xLE,10100 +sqlalchemy/dialects/mysql/mysqldb.py,sha256=_25trtahW3dtwcxcK8ksaxMWRUsjHKfQuZy_dmZXYmQ,9944 sqlalchemy/dialects/mysql/provision.py,sha256=pVIeBB4ZF5_jCLSJIphcQFWDxXo4opU7qS1cOkDhSGc,4816 -sqlalchemy/dialects/mysql/pymysql.py,sha256=9x2rQKztutmapJ9-vNzxF_CC3EaBLjJ_Jd05xUKdAKQ,4886 -sqlalchemy/dialects/mysql/pyodbc.py,sha256=21AyRRqK8nDWoIY1OsVjUYAdrrlWVFyrDuajgDs4HoI,5111 -sqlalchemy/dialects/mysql/reflection.py,sha256=WyhlAH4QQpKNm3-SAu7y8DZd5PrzcA6LIkJJeKyxD9g,24690 +sqlalchemy/dialects/mysql/pymysql.py,sha256=B-uQtdZpEQfWpQuoB-Rg8EmvqLDGxef-uFZonTjEw6M,5660 +sqlalchemy/dialects/mysql/pyodbc.py,sha256=092yrxp3BAYzhVJYGcRfqlQxk6obb8YwL2uwGKWS47I,5112 +sqlalchemy/dialects/mysql/reflection.py,sha256=Vxlt1mrbB8vThPp2hzhaC8WqaBV5TLXlrodzzDv_DuE,24716 sqlalchemy/dialects/mysql/reserved_words.py,sha256=2pOnlrM0uJLHABpRMfijWOzHlGxFTmWznlPwNKHXphY,9236 sqlalchemy/dialects/mysql/types.py,sha256=Xs7BCkTA1kbfWQE2Lh9OKQdhRNBVTHqrp4ZaU4Pr6-8,26459 sqlalchemy/dialects/oracle/__init__.py,sha256=_Nfi7_C1Lj_RAW3IITJD7nEagUnfJAGG_tfRtFyCG30,1898 -sqlalchemy/dialects/oracle/base.py,sha256=G0GumOkELYQjcQJ52UVW4cWKw7wLFEAqeGZHOrnaXTo,139157 -sqlalchemy/dialects/oracle/cx_oracle.py,sha256=0PL8KinYbRjI6iBlORK44AMpGWAkVgh1soEamUFa8Ic,56717 +sqlalchemy/dialects/oracle/base.py,sha256=AA5AKMOsH2osgonqMEGnUzGqctqJSBkrd0FVo9W23DM,139340 +sqlalchemy/dialects/oracle/cx_oracle.py,sha256=5H1XnzSeJprf0DyGFiE1Ze_sr2KJ_2MaZhPRlbQga54,56718 sqlalchemy/dialects/oracle/dictionary.py,sha256=hotIZctkY58oepLJ3NYQxWb44oMOurxsXgD2yhe3neg,19519 -sqlalchemy/dialects/oracle/oracledb.py,sha256=QSJsCb1TOWzC2i8_hXxjF0icOCrf1qYGBZJ4X7U1Cus,33570 +sqlalchemy/dialects/oracle/oracledb.py,sha256=5Oj_teS1RxZ5kiHB1EwMoP1ozDcpngbNbVH6mPLVxuc,33571 sqlalchemy/dialects/oracle/provision.py,sha256=htp6jQ17KTcnYnPQpeOnE0SQeyyg5DlsK7EhF_QLqQg,11031 sqlalchemy/dialects/oracle/types.py,sha256=ArWTqkop5usdj9aCp9o0xTUOUiCn1yqwcjYd0fR5zEA,9058 -sqlalchemy/dialects/oracle/vector.py,sha256=pluR7hD4hV6S9Yt3Gc5fomcoYE8jDzXn8EaGMart52A,10853 -sqlalchemy/dialects/postgresql/__init__.py,sha256=DHkUDl8giT4WzSt_s-EXum59en5S8bsP83-deBgKQ94,3892 -sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=rHrq7cVvsdq5yxg9glY-P-ff7WsPcm2rBWVpR-oYYks,5783 +sqlalchemy/dialects/oracle/vector.py,sha256=NbZL8IfduFWBjBgRplNL5Qy6kyKMO0QnQ7M3NP-PtdY,10815 +sqlalchemy/dialects/postgresql/__init__.py,sha256=Nai79gxCtxM05bqCv4HT8PxN9tIedFse2g2k0G8Kj_o,3891 +sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=1_Vb8JXRlNcCYoQn2Vgq6afvDiR6RJ77alGZUZEUbi8,6965 sqlalchemy/dialects/postgresql/array.py,sha256=cz42HV28g_VbH1Nt4TnEeU1GUS8uSCbTqU2CNJ4qDfg,17279 -sqlalchemy/dialects/postgresql/asyncpg.py,sha256=vNnaXdMU1ObkPqFstWZUTV6_WOWp84WTppJFRunkxcU,40977 -sqlalchemy/dialects/postgresql/base.py,sha256=v-SzI65jF0ERLzuId5Gc8Lvj8059DfjVidklgmthuGM,191547 -sqlalchemy/dialects/postgresql/dml.py,sha256=aomgosojlTWlKKQcEwMZlf8PcN7a77fk3Nc5ZqyvxiA,12126 -sqlalchemy/dialects/postgresql/ext.py,sha256=KNk2rGG3tx5O8-5dCLVbH6ib6WIR591sCC21ratI-Wc,17481 -sqlalchemy/dialects/postgresql/hstore.py,sha256=PIhn5hdjlqBAl54CvXz8im3xIZbNEezSmXMWMqT8bT8,11934 +sqlalchemy/dialects/postgresql/asyncpg.py,sha256=xXhN0OJiiSy9C6bK4tr2mzFSA3FSralWGan7EPbxK9o,41050 +sqlalchemy/dialects/postgresql/base.py,sha256=M1g6Waogdim-ObO6xQew4hFbSWecbc9lQGMrJ0Pl0bI,193208 +sqlalchemy/dialects/postgresql/dml.py,sha256=kAnq3ZC-WBLgZTd0FQyKMkmmam7hxdw5Yxs-Um2j98A,12125 +sqlalchemy/dialects/postgresql/ext.py,sha256=bh3FeNNZkPABqX7caCSzP_iNGNRL-gOLJsdeBiQpERs,17695 +sqlalchemy/dialects/postgresql/hstore.py,sha256=jt2t4HDMCbmR_AW3JjkOF_Ip-4EQZ_nvi0w6F5dmMsY,11933 sqlalchemy/dialects/postgresql/json.py,sha256=fWihCjHxwAwCrSElIb__iueVxLvs9_hsqYDs2ox5NLw,14629 sqlalchemy/dialects/postgresql/named_types.py,sha256=_i435mLsrF9EEJyLxBihQsjWktXKkKnNCrkCWpjy03M,17812 -sqlalchemy/dialects/postgresql/operators.py,sha256=wB1DePoWlqwAoxkdVH35R7ReFH4Uwa8G0h6HfZY-5ik,2808 -sqlalchemy/dialects/postgresql/pg8000.py,sha256=x5xhYJBttlftQoKL0p4xMjywl68yMhZeoyXPuij02D4,18743 +sqlalchemy/dialects/postgresql/operators.py,sha256=Pn8yN0CllvTfKUWZrw1PQqP7CXOTPIsV32L6LfGW9pk,2807 +sqlalchemy/dialects/postgresql/pg8000.py,sha256=JqpKBqCo0dskrmFbQu0EDxvtwd5Cqf6I3h6UZ5KAcXU,18705 sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=IHv4trpvq3xz0T8MKGPua-pNgvV1ijEf3mXaLLic7pE,9999 -sqlalchemy/dialects/postgresql/provision.py,sha256=3IYX5zIsqkm9tpnWuFMpZEAYvsCteD2ZRna8qz4Zg7k,5895 -sqlalchemy/dialects/postgresql/psycopg.py,sha256=IbU2pV9r8uUi-ft5DLhMweOiy_v5D9oJHWmNHhYYtG8,26448 -sqlalchemy/dialects/postgresql/psycopg2.py,sha256=cMI3QqnlMHgUodDTz_xo9hY1tcgrJ-bo0ESpdy0DsYk,32032 -sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=MUx2xY7DpNKywLQcOzVbD3WAMl0HViAoU5U2iHFEMqE,1756 +sqlalchemy/dialects/postgresql/provision.py,sha256=Edmu7uPbNMgkbVIbEo2wNor7LJyjCFCmDn27vgXFtiE,5951 +sqlalchemy/dialects/postgresql/psycopg.py,sha256=Eu5k5YIT_Fg2nAYJf0S9Ton9nm_ZvEpRe-2XxkIbOL0,25808 +sqlalchemy/dialects/postgresql/psycopg2.py,sha256=JYEnAZltbqlF-yQZ_bthE1jblu3U5J8bPUU9Ybpm3gM,31161 +sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=65Zmo9TIFOAULRfs24AzJFDxkhBAVBOc2JE_Kk7OzhU,1757 sqlalchemy/dialects/postgresql/ranges.py,sha256=aIvHYRs-tKQkt8eJTmE3_JBaHxQA97WwFf7t6XLEfvQ,32946 sqlalchemy/dialects/postgresql/types.py,sha256=BNh6rvqDn0Uzn6IO8T-kGYYtv3W-axAg8OwIQDVNJF4,7629 sqlalchemy/dialects/sqlite/__init__.py,sha256=knYUrALP96JZpMecAoxmIouoJrRbGQ4KPkjGXlpzl14,1182 -sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=V7wwmVSzRIj6TBBXj02cauSxmDc581WI-9Chm1s8ps4,16033 -sqlalchemy/dialects/sqlite/base.py,sha256=gmcCzknBUYztVP2M-znqFsREvz_O-oitAjNZH7n0_lk,107380 +sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=4Mcm3xoVTf_peVNXTA8Rv6FRxLd9JxL3CbNJUfrvcwE,16034 +sqlalchemy/dialects/sqlite/base.py,sha256=WLwH9OFrQ2f5ySeYN6e-S8DGOar9COSGVPJn3YALcFI,107339 sqlalchemy/dialects/sqlite/dml.py,sha256=b4pV3f_7JMm2ZxuW906YQI7YcZiyHOYxqRKPge0cMaM,9138 sqlalchemy/dialects/sqlite/json.py,sha256=PuBnoOMcUcV0MUiXgC3mXCCv_iiglRhmX3ZUT3NiUx0,2777 sqlalchemy/dialects/sqlite/provision.py,sha256=0DEDRGGOrnie09Ub0TPUZjN_490WfBK_PXvHiMfJ8cw,6710 -sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=zIqkVotyk-NTFfx7TsWqi9apQXwtL2_CEw4yISGTgeY,5371 -sqlalchemy/dialects/sqlite/pysqlite.py,sha256=ipVVka48K3FYeV9_ggSqIlbiEZWxfgm1bPwB4yXbL4o,27791 +sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=eM5MfS7SfjV8xU5a_H9u5BYNTJOsoY62dH5N7mr9y7o,5481 +sqlalchemy/dialects/sqlite/pysqlite.py,sha256=w8il5ElXHYw1AZgakAKYQeYIXppuYv9yTgpby1MU5hs,27792 sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239 sqlalchemy/engine/__init__.py,sha256=Nn05AJVAfZn_sToGjt09vBcZET343-wYwl-oo2Fg6R8,2818 -sqlalchemy/engine/_py_processors.py,sha256=a6exvDGQTyZcvnVBaX11w0RusCuVESxVc3mABH72_i4,3744 +sqlalchemy/engine/_py_processors.py,sha256=9vtsn7jB3QNIt3WQPOP4YjGbOEzGJUwnF_aj3eJxmxI,3743 sqlalchemy/engine/_py_row.py,sha256=IZvYZytnYiKykwAnpGv-rR5XsFBqoaApWdlZcg_H9yc,3787 sqlalchemy/engine/_py_util.py,sha256=SIoKkVUjOF2U7Obrg0aTZNORfsHyzPhX3u7is14JAp4,2465 -sqlalchemy/engine/base.py,sha256=bq9cZPuH0wZw_5oI4aHASvxjLyDPnZW6yJsiN1DNUxA,123795 +sqlalchemy/engine/base.py,sha256=U_OOVEAE0ymsOOJDEEv0u7r30PDOyr0RKxkjt43eVhQ,123864 sqlalchemy/engine/characteristics.py,sha256=wRg6uE8NYSH38wvp0CMY2U5hv9u3VTOkTHyJeXYrkvQ,4765 -sqlalchemy/engine/create.py,sha256=5xQOAH0PGxcNsnorWA2i9c1p9qZDV1ea3eCam2JE9vY,34179 -sqlalchemy/engine/cursor.py,sha256=5-pkJ5xWpXyZ56BuXp8ZUdAjwDGOIRmRbv-IiYS6IJc,80786 +sqlalchemy/engine/create.py,sha256=SdhAoQYPRvfJlOIC8sQ7S3FbNOO9Na_hAGszSswouXI,34177 +sqlalchemy/engine/cursor.py,sha256=3a0INyP8CVW7hgumjsmR9s6_Ss_B46EizGorRrpnRbs,81094 sqlalchemy/engine/default.py,sha256=Q4hs3lW8bsHaIjogMZyBH41GqBKCxgRVaR3yy2MqDC8,85896 sqlalchemy/engine/events.py,sha256=1MAJj-D5dxHBhTXZYnbxY8AViItRtjm-GExTrXN-nrY,37400 -sqlalchemy/engine/interfaces.py,sha256=5mwH2wzZBeiLH4zth0Es_QmHtH7GzpJaA2Vg8te1t1I,115384 -sqlalchemy/engine/mock.py,sha256=MC_S2KihM_iCjr9kZsvT1DU--YaX4Wa-1WbXcgBgbWg,4156 -sqlalchemy/engine/processors.py,sha256=IeUU3p8547y9uUMfn0ti8Ul5p8wnrvIkBMWiAV_clZY,2379 -sqlalchemy/engine/reflection.py,sha256=gZYzVYRsURyJvPSDdjo-h6a1DW8WGe9o4c37iUG5uYU,75565 -sqlalchemy/engine/result.py,sha256=9Wlskt_zv1Yqs_4EdqElqMVlyKuhqQrm3Epl7i3SFpg,78094 +sqlalchemy/engine/interfaces.py,sha256=hH3s2yyQU1O1jH19f9xWYRkwoedE-ezcK7ozMtbUCOs,115384 +sqlalchemy/engine/mock.py,sha256=i9J-z0NsL78VlqC85MRgo5SNNQU256UtEqficO3DCwY,4155 +sqlalchemy/engine/processors.py,sha256=Yw1UZVsyFf2MdFUrQKaTHOewFTZ0d6p_GlhBXDu250k,2380 +sqlalchemy/engine/reflection.py,sha256=1xEJTCw1Pl27c_8DqWCn0_sx0-9VsA1YHo8y9k1AiY4,75566 +sqlalchemy/engine/result.py,sha256=LbcBlmLK_AZD8yiSql5anC2eBqoZv-jKisWq0pU4wv4,80437 sqlalchemy/engine/row.py,sha256=sC91jITBoitEhjeQm36IS7KjGwiZJjiwCKzOFbxp5qo,12031 sqlalchemy/engine/strategies.py,sha256=EUOp1y78tDlpbC2J9XJnyDMH-lVjusFBx_39zCh1khE,439 sqlalchemy/engine/url.py,sha256=JsrXOwip3933VuYh0OObHfLk2okV2rgqEP8S7saccSg,31067 sqlalchemy/engine/util.py,sha256=Zk8LKTyOO_WfjQfmFVdXqlVFDJz1FjOb7bwF485Rago,5682 sqlalchemy/event/__init__.py,sha256=DfSYrhLMf1nrCnEBeKaZT4bJkksge2cY2Tg506E41mU,1066 -sqlalchemy/event/api.py,sha256=yWst2rlagTvUYXzZdDymyzj-cYwcYeJR34bHcp2gKUQ,8109 -sqlalchemy/event/attr.py,sha256=ObYvNSVrnAwBmp3zJq7FeA3uUKnXnoSwpi6NzGLkAZk,21589 -sqlalchemy/event/base.py,sha256=N8Z1ejrvALILC_HRKFqeyZRzQ902PdFksh_1gzqL1K8,15254 -sqlalchemy/event/legacy.py,sha256=a-C-eN4vA5CoPYTo2c8Z1lYipNZMJRqIH-6sMoZ1Llg,8457 -sqlalchemy/event/registry.py,sha256=tRc0xdUf0s21T5_raMyq4IrKaR8uLD6PbSW8Q_tJyOM,11144 +sqlalchemy/event/api.py,sha256=s1qCAQ0yxC3W1erIvTt79QoEvOYo4u44xostJYaBWOs,8109 +sqlalchemy/event/attr.py,sha256=8of8a-l3XuU3P3UM5mmuCsxmJ5HzL-MZujqJLaeP2LY,21590 +sqlalchemy/event/base.py,sha256=3o2c0FFk-nb7qJtReSqHfmWsCqNKWoOyATYRz4wF0q0,15255 +sqlalchemy/event/legacy.py,sha256=GHq4EbU7EG9PiXh1Me_qVlxA84n9vvl_Uuc5UyPDX9M,8458 +sqlalchemy/event/registry.py,sha256=zQUfT67ZV9VyrBppeCqWjWRADVGgiTJ4bZCXlfQAHn8,11145 sqlalchemy/events.py,sha256=vEDW04X_PMhMw5y0_95tbEzht3wua8_gnkMBJstYNRw,525 -sqlalchemy/exc.py,sha256=2CipjflHTdtmi6G-TbbA4j94sJX39R-EgmzrQHyW5bs,23978 -sqlalchemy/ext/__init__.py,sha256=u0bbjHfhcS_znSEJvrfimKY5Sk-M7GJXfFiDKR3FwLw,322 -sqlalchemy/ext/associationproxy.py,sha256=kTTOuCXOIq_ytrWefGLn-dqo-nK9N-TQmk2cYgyon3s,66443 +sqlalchemy/exc.py,sha256=JVVIizdjrbZYKA3poXDMSY5qDxTW8hwJzU_ZWx05LYc,23979 +sqlalchemy/ext/__init__.py,sha256=Bg8EWR7ypWrYGcIBETUFAlUXeFB7MJNEh6VYQCmapD0,321 +sqlalchemy/ext/associationproxy.py,sha256=XUFZRLyERtX-Y9iRXts5P0gO4Gr9DOMGfQNSHQ-yFRo,66444 sqlalchemy/ext/asyncio/__init__.py,sha256=RCYIx4_exZUgBPms8I5h2zXe8bKzpwFsnNBiBnywNUE,1317 sqlalchemy/ext/asyncio/base.py,sha256=eXWAYYd5vBjosF8_ZY1j2gsbrlY_xyegJLtqu9lgZjY,9034 -sqlalchemy/ext/asyncio/engine.py,sha256=CnQ3M_gW7_wYOo9h18go6Lj1_-0A0DOP7LxjztY5PdU,48482 +sqlalchemy/ext/asyncio/engine.py,sha256=19tmzmg-6VwwmCEDx8nywnzP9qU-5jDjHTNMOvTiJSg,48483 sqlalchemy/ext/asyncio/exc.py,sha256=CtGx8sjHzykpUZojihduYtXjZUz-q-OLcuP8ENoHFBY,639 sqlalchemy/ext/asyncio/result.py,sha256=xFfTYL8i9yqsmCD9ErHb2JqhKhAwuleGz1qutgQ4KSA,30659 sqlalchemy/ext/asyncio/scoping.py,sha256=6T7wF1dxOQ6c-PyN-Cd6azHB-GmhjoV9lk6RQDq9sXw,52086 -sqlalchemy/ext/asyncio/session.py,sha256=2nZl5B5mh6MCxyN1uCWGg_-Gd9j4MY5uh2tziBK07HM,63259 -sqlalchemy/ext/automap.py,sha256=RED0MS72sghKG43O8mS59bX_I92evnNQD5FlBQldKmc,61675 -sqlalchemy/ext/baked.py,sha256=ijm4xtdf0NuAObxEy6IH2VfGscMzJrvz_EoyKihCtto,17753 -sqlalchemy/ext/compiler.py,sha256=XQgGCCqnHQKkNzOjsHyRVjNlHDtYk5nwsm650kyhoo8,20889 +sqlalchemy/ext/asyncio/session.py,sha256=h16JsUEQ57m9Q1yGvyZxah44AxY46AF5vARPEpLEPn4,63258 +sqlalchemy/ext/automap.py,sha256=peytEDTjsc8rf5QlWum2E7jTYyfPYk3fBUsyHOB7ams,61676 +sqlalchemy/ext/baked.py,sha256=ZtuKDbXivsoyAgjxdvA-UclF8jzRh6tJ2opccEeDH3o,17752 +sqlalchemy/ext/compiler.py,sha256=bZVH-A-vdu-mX99grqUBo5Ujd682kZ2jRxMLvAkvJdI,20890 sqlalchemy/ext/declarative/__init__.py,sha256=wrIs8z6am8-rSwDYwoxZntw_3T2Dj_28NbyODnDxA8s,1818 -sqlalchemy/ext/declarative/extensions.py,sha256=TgUQY40vkhPjBp9B9wbpr6UF2aUUJrCTEQ0Uac_hFqU,19531 -sqlalchemy/ext/horizontal_shard.py,sha256=lxCjwZMWBwmql2mFQgqV9fOFT8MPJuAEJ-d0_CSEmis,16692 -sqlalchemy/ext/hybrid.py,sha256=_S-jHHutTcXVaorGkyf1Pv5iyoRsql20gWHgyn3roo8,52605 +sqlalchemy/ext/declarative/extensions.py,sha256=-KawB2nr6AIljJjbBM0hREhFE5OQmJfitkUGphoBLGM,19532 +sqlalchemy/ext/horizontal_shard.py,sha256=3gUVSGr7O33fJxQjrxoQdm-qYKPTcrdPaAd_B6iX7ME,16693 +sqlalchemy/ext/hybrid.py,sha256=jgnnykcvGEC8JX2Tmrl9oTx1M0-z71lA7YkfVh-Htkc,52633 sqlalchemy/ext/indexable.py,sha256=zNUO08hNUg--_LW8YBaRvyONnsCoePaOs6KKpPTs9VE,11762 -sqlalchemy/ext/instrumentation.py,sha256=SJUsb_5a2PCmHFIvLsXoUjXnNwtcauuuMN5xzTRN7zw,15707 -sqlalchemy/ext/mutable.py,sha256=auwCKpXlqgP4kZgi1bV5AyU-XWQQ8zxLj7ut3bYufFo,37164 +sqlalchemy/ext/instrumentation.py,sha256=O0e1hOtBMX6IWUMUVDuCxC_56Kawnk9BDoBKkd5QwVo,15707 +sqlalchemy/ext/mutable.py,sha256=kSzbBcrBgrUQl6inMc0fIMxwkPYn_yxqRrcBRcPcZ8E,37231 sqlalchemy/ext/mypy/__init__.py,sha256=ovuYQ9Z8oiOl0KqmrOKotAchksj_2G6_bOaVLb95ugw,241 sqlalchemy/ext/mypy/apply.py,sha256=1lmDsxDulQUiSc8w3LkVqMKZD88cLcSfJ3d9lmMg0M4,10591 sqlalchemy/ext/mypy/decl_class.py,sha256=VHoYiSVmhWx0NTrXMa7fLmP-QuMO19uYv_rXhercQfE,17384 sqlalchemy/ext/mypy/infer.py,sha256=FI6m4fescVdNtcSX5BggrD7i_orH_3NOs3V49upGQ28,19367 sqlalchemy/ext/mypy/names.py,sha256=XkIHA8sT_YPJ6TXkH8tcQMnrY-e3Sg4FgvhY05toSVU,10480 -sqlalchemy/ext/mypy/plugin.py,sha256=W_TG9uNQ10UqBF7JKAWGD6ybdhxZBkPQLgLkuYrrwyc,9750 +sqlalchemy/ext/mypy/plugin.py,sha256=slTpyv-_R2gcp5UljJeP2H-QKX71Jo-qg8FTkRHsins,9751 sqlalchemy/ext/mypy/util.py,sha256=Vgl-FeZlmxEnOgJbwlzxusmOUWL46vVXyeKMMq4MF1g,9960 -sqlalchemy/ext/orderinglist.py,sha256=YKvR7H0gRs8M-uKcj25mgW-Rip0dDlOKAPSYyy3xk00,15163 -sqlalchemy/ext/serializer.py,sha256=oFkEz_tV_gMiPkP9gacEMACEJxu9sjw3Z3R0yp8EiTo,6169 -sqlalchemy/future/__init__.py,sha256=nXe1m2o0nREIBGrR77Ccd5QeuMJebBjVyiz81YZ7BcU,512 +sqlalchemy/ext/orderinglist.py,sha256=6SRxpMyPhduNGQ9KJzUoP7VELe0BjXB9INqy0QGtYa0,15164 +sqlalchemy/ext/serializer.py,sha256=R4VsPDd9dQLT4sXMDwV0Zr2BGODHXeffUY5S3m47hbs,6168 +sqlalchemy/future/__init__.py,sha256=9pngQPAhogCThkP0WMqp43MYiMHQ8usQekk0IBhrUYw,513 sqlalchemy/future/engine.py,sha256=CNIeZz-03GSBylYCTTam_Yk5kHD0EprfTxaNEgQc-0I,495 -sqlalchemy/inspection.py,sha256=zd3f-Zz-_ZrhgRm9JLZbC4UDy8xwO93x68LSRXvgfR8,5063 -sqlalchemy/log.py,sha256=jRscCEdMnnPyTikHKrT-y6EuR76CHoNmCUNfpHieAVQ,8607 +sqlalchemy/inspection.py,sha256=Rqw5kFWLY5MH3pr0j2f3OQGZca3aFR-NOaL2sRGuBBo,5064 +sqlalchemy/log.py,sha256=obSZ7MquT2vauxBwexDEYPEjRunvnNOQ11-HfYHIhYo,8610 sqlalchemy/orm/__init__.py,sha256=ENX3G4_mZ1Zhl6AlaXASCWA_hEfX1t2Qb3DG5h6rvss,8528 sqlalchemy/orm/_orm_constructors.py,sha256=-bHeVy7LoRNlUVbFFudOOlM4c20y581rSc3b19eCcA4,105599 sqlalchemy/orm/_typing.py,sha256=6Cpv7LlnboFzDRQqi3EB8f-6PsLk7NvFWM43KbwP-Ac,4973 @@ -153,94 +153,94 @@ sqlalchemy/orm/attributes.py,sha256=5B0fgSr2JLQbxMe8sgKOSkbpAX9Z7yFFpG0cIIwMFp4, sqlalchemy/orm/base.py,sha256=ZllqtOLEOSzBujwuJge49rsaGNyOcWZIX1eDdf7-jgY,27501 sqlalchemy/orm/bulk_persistence.py,sha256=YpJFIoEh3jsorX18HwZP2awWKMjqDFBtaurD2nREnIo,72983 sqlalchemy/orm/clsregistry.py,sha256=mMpVBCC0YVTYf1uQ9u-YJnbRy4yfL5zeQV60tAjEuSs,17952 -sqlalchemy/orm/collections.py,sha256=C_44SFELIGBX7I32SsTCA7YT0IS1g-UzC1h0KJOO7J4,52281 -sqlalchemy/orm/context.py,sha256=lVtX8hy4qjHTX24QditDcTHnrc8tDMbLdeYCZbvuVa8,115082 -sqlalchemy/orm/decl_api.py,sha256=hpKay7ZeEdxo9ahlHJ1tpp9cgh9PXbFY4yfw5cQONME,67841 +sqlalchemy/orm/collections.py,sha256=T0M6cg-R-n27CEPBVUz6m-CC6OVmmEziPCk56471_wA,52282 +sqlalchemy/orm/context.py,sha256=2hsNE2YiXsQm3D2XRYBM-R3jZi7ocbRIwRnwUjwtr0k,115080 +sqlalchemy/orm/decl_api.py,sha256=YWInXYKNuqeA6LxzABlfuaAbNGH6uaOSGUBvCw4uu0c,67952 sqlalchemy/orm/decl_base.py,sha256=NHISXpnu6jjwaPylBXPiD5zY-YkHb7rSD720Qk0JiX4,83516 -sqlalchemy/orm/dependency.py,sha256=qrCDBldEKNOUgliPFhwvWkQZIrt5NRLeZdqJNjWYTsk,47619 -sqlalchemy/orm/descriptor_props.py,sha256=txKoqcEkZ9rtTmcZm1BesOxzIO6X8payDO44T8Q3VhQ,37795 -sqlalchemy/orm/dynamic.py,sha256=mFwe1h65YHvn0GBgw5Cgs-hMppevY21clu7y3TvQIOs,9816 -sqlalchemy/orm/evaluator.py,sha256=Yfy228GyvvjqvEL88ka1azvq7ugicz6Nw48Pw0iL0dA,12353 -sqlalchemy/orm/events.py,sha256=RXf1dHIQ_OuEHgwONi0B935jrR0OX5uh3pitcEBoCyA,127231 +sqlalchemy/orm/dependency.py,sha256=DkvtKgvIxqYiQDgmv_0VrDFabbQLKIIaW9vQEaFtkaI,47617 +sqlalchemy/orm/descriptor_props.py,sha256=_KkEziuEWU4CrxJBxb-5nblMAR6IP1WyzCRNRqSqyVE,37795 +sqlalchemy/orm/dynamic.py,sha256=t3RyVwVcINQgJqAsvGPvzxVLAxv604FT4trNeLqdmpU,9949 +sqlalchemy/orm/evaluator.py,sha256=DkKS71DYkzZRtxXRh-8pCyup_X7Xfbi9FjWHceTicOA,12352 +sqlalchemy/orm/events.py,sha256=g4bfkcJsRFnjyzfWHxQ84h2GCNPHKRL5X0OeQBN6HSQ,127232 sqlalchemy/orm/exc.py,sha256=W11TA2kDVVDweYvrDKwoqgFVO7wiIoFW_yHqDts7fGU,7636 sqlalchemy/orm/identity.py,sha256=kIgJgbN2MLaL3kc43-i1CA39eww7s3UXsv9xxijlQZo,9249 -sqlalchemy/orm/instrumentation.py,sha256=bEZTirsGqQdThkRXU1-1LFu2VBx2MjkHhLvGQ-XyGGg,24321 +sqlalchemy/orm/instrumentation.py,sha256=mNleDJMFAUgdrQ3CO1O5ioaS9i3zTivsLG-nUqZSoxo,24320 sqlalchemy/orm/interfaces.py,sha256=eXGWQLKfx1dTAIY6CXC_IO9zzWmcc0VZaQQ5NYQc57A,49073 -sqlalchemy/orm/loading.py,sha256=Odq8qMfpIRCm5zlBoBvxe0wd0FekrQmVsBDy3cImfiE,58488 +sqlalchemy/orm/loading.py,sha256=EbAZIvnniRruXQRm_V4r0x0Q534jSamCn0w75hyBMEk,58484 sqlalchemy/orm/mapped_collection.py,sha256=UB3JmCNZQ2k0XB0MH6T7bgRPueE7pSrhcyFjSX_g8Ms,19734 -sqlalchemy/orm/mapper.py,sha256=o44rGirM171OmLtajkQGGXxQjwmsxid2iMc5XmlBhY0,172154 -sqlalchemy/orm/path_registry.py,sha256=cxDDZlVjz1mS6naotiP6jGKzikdm-MQGr70rETzIYlk,25914 -sqlalchemy/orm/persistence.py,sha256=M1i2wm3YmxX3bxQXL1Mcw2Wdtib0bkMTYG0NmZByvU8,61812 -sqlalchemy/orm/properties.py,sha256=fR8UAsk-YyzWCPu7HhCEjVraS_gcN8DN4emspC57_kY,31233 -sqlalchemy/orm/query.py,sha256=u6xNWXhANTumvwp2nfv-rgW-UnA68QQcvJNUIQMWDT0,118856 -sqlalchemy/orm/relationships.py,sha256=4s6GXdL4yJEtQgyTjQ5gqOOpU8FWtuH9N3uhNTDbi5w,128763 -sqlalchemy/orm/scoping.py,sha256=0dibgN30cW1LOgVS6MJmjxZHdNdCC1s00qiaAAzQGqE,78123 -sqlalchemy/orm/session.py,sha256=lCOVA7dVLkSMNnpDdJoEvHhJvEaZeC97KPhKHONjXI8,195400 +sqlalchemy/orm/mapper.py,sha256=MYFrewCv6mISFuNNABURssCjt1_2RywU97u359TmzwA,172155 +sqlalchemy/orm/path_registry.py,sha256=ACAgABRi6jxixs1i9xTrPHvsPmtQ6y7LvBvxx8uXTEE,26713 +sqlalchemy/orm/persistence.py,sha256=3WLjhe9ws8C8D6lJcIV2jAOjNBj2I8wCQzmA1iSfnC8,61813 +sqlalchemy/orm/properties.py,sha256=nNAKPeU7Wf0UhltZet_ieyBgsZbE6A12ddomoKVq_R8,31197 +sqlalchemy/orm/query.py,sha256=-7egf1GSUanKb7WRWpgmU-OMM0-3hG2SL579IuL2Gpc,119040 +sqlalchemy/orm/relationships.py,sha256=tsk4GWWPGFG9cZ9f9mQ_ZrxJHhGJ3WDKx2n-_ijHYaU,129216 +sqlalchemy/orm/scoping.py,sha256=ADOKdjFkryTknwaw00C3cVs6VM_cOrRT30I6qARknDQ,78108 +sqlalchemy/orm/session.py,sha256=t1pOQn9FPC6vGfBNh_ZsLaZEaYoU87EYgPVqc-Kgdg0,196575 sqlalchemy/orm/state.py,sha256=YS0voN_ffj7hg7dqklGdvNMGwEILKo1tXv725OIW9mU,38433 sqlalchemy/orm/state_changes.py,sha256=zMhwxbQNVlcRmK7YwaPsYsxN4uhJkw4Mmg_snftAGDg,6816 -sqlalchemy/orm/strategies.py,sha256=REbXPL97Z5VcYPBOZ6L5wEzCW7gD4Z1BVYsvElVI1Ug,119802 -sqlalchemy/orm/strategy_options.py,sha256=9mSKfC99qOsdRXc4TnvQ4x6NkXvmmzVWX-ZeuGBSEvs,85626 +sqlalchemy/orm/strategies.py,sha256=Fi8xlz8KtDyUWAsTSnfsWCyxGmZGcF-4wTAQYyHCiTw,120414 +sqlalchemy/orm/strategy_options.py,sha256=XW1Q-n4oXyvAheeppMiOyz9hRKb8UzKYKRGI2f1ChuQ,86331 sqlalchemy/orm/sync.py,sha256=3ycmsWAHK4zwUfvfXj2e4v58MzmQ9KULWy4awoGzoMI,5779 -sqlalchemy/orm/unitofwork.py,sha256=LK_aGoLgKdwa17Ia1y7_Vl5dxzIvRB7VMGhOQLm9bcM,27033 -sqlalchemy/orm/util.py,sha256=sGgts2KOPEUq5kD0YGPzLBONh9alauDF5V3PRJAfGHM,80889 +sqlalchemy/orm/unitofwork.py,sha256=wjVo-pQBtx93p4awjkdKpbfWG5ENz-k-VtpkFejcJho,27028 +sqlalchemy/orm/util.py,sha256=G0rEwFreVwwJ4MbtmIbWV8bYiWPsm6k39VtEhRf2BWw,80998 sqlalchemy/orm/writeonly.py,sha256=OqRWliWa_9muWZII_CtDOm58YMYxNQWtaP6F_r9k-ig,22297 sqlalchemy/pool/__init__.py,sha256=R0tdjvXH2TfTv8sS36CDs3g3RVwnno3eXWlfNJhhze0,1804 sqlalchemy/pool/base.py,sha256=Xb6R4y7EJPHkz-5duEiWuWz7I8YBAg4pHQpmRP9QiD8,52588 sqlalchemy/pool/events.py,sha256=Fma2EvUl8XfbnPEYsRYmarRtumYJmpFTExxvO4bmOmk,13455 -sqlalchemy/pool/impl.py,sha256=3vTqmewu59tUGQbMy1PJtFMOUkc9jvL-ChaT7YA5gls,19306 +sqlalchemy/pool/impl.py,sha256=7ROIywKBUO2tKoVULZmAj4GdHn6AM7sZv2DiLsu7Mgo,19307 sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 sqlalchemy/schema.py,sha256=RSA_jCI0MFkeOqiQSsorkayN-aFVRYsOfDM4E0vlk1I,3251 sqlalchemy/sql/__init__.py,sha256=X6VqQucEmXS5_0jW4Ik5768yJ--RwNPLCeV0ptoSG3k,5820 sqlalchemy/sql/_dml_constructors.py,sha256=yECqkq8P88msKKJmvHqiIGeAtQECSRa9vQpC8VPzGyI,3795 sqlalchemy/sql/_elements_constructors.py,sha256=4dzcMS0xNSvxFdXMxx-2p7OK98Srui_cWq06Ck0KlUQ,63096 -sqlalchemy/sql/_orm_types.py,sha256=fQICo_xMjlnNe4WCWaps8Mwf7teKzl17v2_uff5_ikQ,625 -sqlalchemy/sql/_py_util.py,sha256=7q_UH9SzK6nV6nn1zzOStvfdWjiBRYPagdr4UhrFUTk,2173 +sqlalchemy/sql/_orm_types.py,sha256=W8cSD-zcm2w-psFSHKG6vpt60IboinxynPTBbjk4qIY,624 +sqlalchemy/sql/_py_util.py,sha256=O1LX5flZHNQpB20oHCypMJOtmYhYI6ut1jIfvuS5Bi4,2171 sqlalchemy/sql/_selectable_constructors.py,sha256=EBeBQir2cKSBykejV27rzprJpNum3xJBMMJTk4kh4FU,22222 sqlalchemy/sql/_typing.py,sha256=_-4VX-ZtWoIpR-gsxwiOzlwHzMuR7OWLTBkcPz_yh90,13443 sqlalchemy/sql/annotation.py,sha256=y-vGSCbLJsrgLkkQL5M3_ytczOsyx4z32kDnpwReJDs,18355 -sqlalchemy/sql/base.py,sha256=9ZFt_EL_sV95PYaQbrManVzt5xkF61pYxVZKON6mKsI,78977 +sqlalchemy/sql/base.py,sha256=vmHGxJwz_cuYymAjuCsMnXIcZWDiYmV7Ynhy-GB3AsY,78976 sqlalchemy/sql/cache_key.py,sha256=AE0LHB3h-ukCE95svW9HUcM7rWUQWoJKe9TYW9pKxKc,33653 sqlalchemy/sql/coercions.py,sha256=9_GdOP59AflDIstWsDRPnJraJGVxS5B4lPfL-6iHfg0,40782 -sqlalchemy/sql/compiler.py,sha256=eJKifmVjc2h5rpCHwFZ1DyppVmaUoxzlVcQXHlIiNZg,286116 -sqlalchemy/sql/crud.py,sha256=wxLQo10QEkbdgIxeyspP9uGX0T-YzA2aDOztpCEMzs4,59881 -sqlalchemy/sql/ddl.py,sha256=snmxh5NLIi-7i4iz1ARYFZFcJCbFmOrjeTxYSiVq4oI,47993 +sqlalchemy/sql/compiler.py,sha256=vG7RqvrQHxz7p5mihTQAIfLQt2OKHnRe7ptJTNK8Yns,286247 +sqlalchemy/sql/crud.py,sha256=nCFEdYyodH995HawyqFaIltIe2r3KKYAF9RrXgkw8WU,59880 +sqlalchemy/sql/ddl.py,sha256=fJabeoCAj3Yc6RrmaiGBUiSnKG0xZe7eWZs5QgtJ2LI,47994 sqlalchemy/sql/default_comparator.py,sha256=jaxd49IC1flS349qPZ2ZcJhHl1ayA-BuWq52B4RaJew,16706 -sqlalchemy/sql/dml.py,sha256=8TYs2PUdbPYvGnSA9U3ApELergttmOo-XgNc6vfBD3M,66573 -sqlalchemy/sql/elements.py,sha256=4RtLbbNILP-uHi5pWK6MgP9fnS56ZibhAGY3M1kjXJU,179645 +sqlalchemy/sql/dml.py,sha256=TcHQV2JOZm0vRlfTM2qsF5cb4i_CITs_ob8alkGK38Q,66719 +sqlalchemy/sql/elements.py,sha256=KoVIDYefgz5t-G1WMAKOBMGw6Dtio1FJC2s7bDIrq4E,179896 sqlalchemy/sql/events.py,sha256=xpqx3irkG8N3gysXAViLCqXXke9KCSU-6w4zZUdYXzw,18312 -sqlalchemy/sql/expression.py,sha256=_12nQiLpln3ifSFiCAbfjy4BeCr6bROHx-1CebfKSvA,7583 -sqlalchemy/sql/functions.py,sha256=rfp50_ud85CcYBk5yAOxIDYCwEtpv6dM0RreAOn0xvc,66692 -sqlalchemy/sql/lambdas.py,sha256=9e5SVNxYQluid-t1MinspQ1vMpupkNU36eLS6oDcFCo,49400 +sqlalchemy/sql/expression.py,sha256=ExolOUyIJ90eBZGpp0G5w29TXhJ0Vf4qyrKA-rKe9VU,7582 +sqlalchemy/sql/functions.py,sha256=Ws1jaG07G2zETpmJzyaMk24CDWMEXe1JRReTkY9J3p4,68652 +sqlalchemy/sql/lambdas.py,sha256=IiBq804zFJsne6eUsm9IjQNu_Ctl_xQjMHU5c9BjpRE,49391 sqlalchemy/sql/naming.py,sha256=w62T2yj6pafLNRe7SDUsutklHSYgIabXj2UtnaCJRSM,6855 sqlalchemy/sql/operators.py,sha256=7h8oPww8gIzqyhizpiW-iyVNSFCEWUouGyMlhG1kXac,76792 sqlalchemy/sql/roles.py,sha256=9KrnEd1mt5_Lm8G--7uKCKHVdrW3Pks3ZXSacQ79DQE,7662 -sqlalchemy/sql/schema.py,sha256=dmk0Wo6BTHeg_mlxv-8o6miUnfWGr7Uqr4mBtCUWzyA,230556 -sqlalchemy/sql/selectable.py,sha256=3hy9dMMw5kvRXzD4VXQXqkWzt9ORfDyJd2RTOvcDJ50,244366 -sqlalchemy/sql/sqltypes.py,sha256=abjSPdInubjTYK0yC_I2pqAbYwQXB6zAOGaeRKnDhZg,132159 -sqlalchemy/sql/traversals.py,sha256=pTe81TkrJKRVi4b1nF0Am4IkOl2Q_NzxMOl_9alReVI,33664 +sqlalchemy/sql/schema.py,sha256=7kALq-4m2crK05yXzN5r_AMise-UbTBQnrWOx51Ij_Q,230558 +sqlalchemy/sql/selectable.py,sha256=k4kdbZHIYwSBPDMe8XvvcpHzktcJUAv6mYm6CDue5RU,244137 +sqlalchemy/sql/sqltypes.py,sha256=wKIkOFMRKStdFDVojZEu2tM8Ryz10Ioo0GcWFyYCQe0,132159 +sqlalchemy/sql/traversals.py,sha256=nN1MDmqxKQA5WHAolJfLbDylSCkugu0D44iFLKoiFdc,33663 sqlalchemy/sql/type_api.py,sha256=qTKPD0U6M33SMexwFMNHYh5K7urESfUmHuWopCtWqGA,85425 -sqlalchemy/sql/util.py,sha256=P1fGrTag55hEaw5jFW7RhRZgDSsbFvy7ALo8Y0ZejHo,48110 +sqlalchemy/sql/util.py,sha256=mZGH05ZYbXQm07OWWT3G8cmMfL96vNRm7Y1lks6HfaU,48109 sqlalchemy/sql/visitors.py,sha256=q7sBQIDWATwU0lWaQVYSEmAWal-3gZENsvFGqRihSa0,36317 sqlalchemy/testing/__init__.py,sha256=z4r9klZyzWB_AQ_jY7o5eg72IwgGfej87ueEt4QFCC0,3160 sqlalchemy/testing/assertions.py,sha256=QNTONVfg86Q1TcMRxLhqID27bcrQ7jntwl7kgA8Jbvs,31704 sqlalchemy/testing/assertsql.py,sha256=FAiTBvUOJWQB61vEB21bR1fEzamRrpUHF9fe4qtvKwo,16900 sqlalchemy/testing/asyncio.py,sha256=8OKkkkGbJNMbOZwsC_Gw8pLeM87_xJhpooS4Zaf0tuE,3830 sqlalchemy/testing/config.py,sha256=aHi95Jk_hWjzPGaT_uNNtxmbMCAblk9SiKt7cjpme3o,12370 -sqlalchemy/testing/engines.py,sha256=Y9BngZzBObQBrv48dj5IjDacxIMz4q0V_Wb57M2qMQM,13882 +sqlalchemy/testing/engines.py,sha256=P1f99EJ1bPqeMZ0y2Y4ufwBSNtdc9RXPcDUH5tofCLc,13881 sqlalchemy/testing/entities.py,sha256=ZIE4OCzDlKE1OxVArkWF755gvYZDS8oUuX5fuwPAQgo,3354 sqlalchemy/testing/exclusions.py,sha256=PC-47lBShhUkiion_n9Gp7XDwmpXg5-jI2VpJn0YpIk,13704 sqlalchemy/testing/fixtures/__init__.py,sha256=Hd_6mk0OFkPpH1MVMvK0rY4kyG82JfIIwm8HSR_IZrQ,1198 -sqlalchemy/testing/fixtures/base.py,sha256=uVwEK26dwLf4nx9cgYP0VQ2NwyxIxVJmaNsVoAW_PBY,12612 -sqlalchemy/testing/fixtures/mypy.py,sha256=h820NghV4sVzY0XrV4imPkYqb7NA4KoLm0w9F3fBn-Q,12755 +sqlalchemy/testing/fixtures/base.py,sha256=7loNJHers5iVTaL7h143ttDyHzGK8wKI7UXU7erZu8c,12817 +sqlalchemy/testing/fixtures/mypy.py,sha256=H5PlaLXSXFozu0cz01M4URMD4WaTdIrM2SirGw75SAA,12886 sqlalchemy/testing/fixtures/orm.py,sha256=_vMxIRSuCmOnBcqV2xwqUWjFp1YwMcgQQsDaxsDM3xg,6095 sqlalchemy/testing/fixtures/sql.py,sha256=JmgD0leWLEYaXBWEcVoMfcGermZeEmWHRwTYB3XOmRg,15074 sqlalchemy/testing/pickleable.py,sha256=gXgySNI39dUcuNY6e-vM9HFKxQ7Uva7NVZgeYyNY4Zw,2833 sqlalchemy/testing/plugin/__init__.py,sha256=8LB-gN7jwvGVW4bPAM1b8MmAQQwBSKh_BGVS3HQc4EM,247 -sqlalchemy/testing/plugin/bootstrap.py,sha256=kvYJzq7R2xINvmgyQnJ7VsDDxGlOj724iHkhraXNNMo,1685 +sqlalchemy/testing/plugin/bootstrap.py,sha256=5hrNoDrExsOv6fUaa-4JPPEnTJJVJYlBMYGMPu7PjZ8,1684 sqlalchemy/testing/plugin/plugin_base.py,sha256=aS9A5zoKaoGKMy8F2OIKbPOMe5SG49lChANZ_cd3UM0,23102 -sqlalchemy/testing/plugin/pytestplugin.py,sha256=eTlUfe_YU3rBC5lUuW9kWP-n8fd6AX6OEBmxVlBrrQk,28528 -sqlalchemy/testing/profiling.py,sha256=QcDKrd0xZx4IdXQLQZQ0VfFgmjAAek5KfKp6Jp73_K4,10310 -sqlalchemy/testing/provision.py,sha256=lqHX9n9Qv3MWbITEXRkjkW-jCAMBg97vkeSxFbGva20,17519 +sqlalchemy/testing/plugin/pytestplugin.py,sha256=SqNuiraFWDdMaRby3wS5BeHzKJhV8J5jHVqx1XVWob8,28492 +sqlalchemy/testing/profiling.py,sha256=0T1kpLIswS2IrJi2pCuN61jdmK-poOmG6E4JbhAhhJU,10309 +sqlalchemy/testing/provision.py,sha256=bfwcTxBA4QfQG-tsnJEY5WvJX21O1EzdTCpT66YAeKM,17518 sqlalchemy/testing/requirements.py,sha256=_-GLvNw85YhZnm-9Uy3JufpLRlk6kyJlCrn3l9wmoYc,56471 sqlalchemy/testing/schema.py,sha256=49x-TXARL2k8oIiaSzyGxXG5fh7vJOuBoUv5h9YTfKo,5360 sqlalchemy/testing/suite/__init__.py,sha256=lfGIsu9gw1L5dCZNuQ7-waq9f_IuvEtn3ywGsWNyD5A,722 @@ -249,7 +249,7 @@ sqlalchemy/testing/suite/test_ddl.py,sha256=u6GRxHKHYw9OC6yiqMJvlXUYn6sEkEgns2xd sqlalchemy/testing/suite/test_deprecations.py,sha256=WvuZ0mZkUL3dHWU_9-Mr0xywM3XxD1v_4ujalLF4J60,5337 sqlalchemy/testing/suite/test_dialect.py,sha256=b8jmV4F2mnBAxN_a5v430WcBJU9aDlB0OTIqmlliBRA,24215 sqlalchemy/testing/suite/test_insert.py,sha256=tapEYecSPKqL8xrjlcQzDOfbWdKJz9ldT7pRi0biDeM,18824 -sqlalchemy/testing/suite/test_reflection.py,sha256=jXMVC51nZr3MLROAC8sEjebHOOrfso43Kauwtz2gqk0,120062 +sqlalchemy/testing/suite/test_reflection.py,sha256=qLMxxsda1GKGMmBip7HQ5SbGVocJE87gvgQEiwwypQI,120061 sqlalchemy/testing/suite/test_results.py,sha256=eIKE6hbXudwde8oiKlKd6B1E2Bn5pyIDj_KPMmPjiIc,17042 sqlalchemy/testing/suite/test_rowcount.py,sha256=V6RQGwe9LO5tKzFPFoa29dDs8M0eY8JZ9tqgbzgtCYs,7900 sqlalchemy/testing/suite/test_select.py,sha256=uh_AFK_hba_s-i12QZAdWFybvBI4naPAeLeNskUJUas,62254 @@ -257,20 +257,20 @@ sqlalchemy/testing/suite/test_sequence.py,sha256=8a6UO3pjRFnQ23ELG9m5cMMRoD3iD7a sqlalchemy/testing/suite/test_types.py,sha256=wWpsUZnxj2soQrG5m1sar-ZyaYsaMJwdbAuQfBDoVBI,68059 sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=tVLf9RqCJe-CsyhCWL9d-WuWazDx5iWYzoBrSCbgjXs,6141 sqlalchemy/testing/suite/test_update_delete.py,sha256=xUHytwsMyvJcH3UhzmvGpzix7yBbQ-0thmlEpF-eNI8,4008 -sqlalchemy/testing/util.py,sha256=Cdr8oXKmIhcPqKTC3tHhM6TO2WfR1LL7VXCwaBIbmrQ,14526 +sqlalchemy/testing/util.py,sha256=bXP2dAQSH1Cn6J0x5uheODsHk7SQmzuiT5XTGNs7W3c,14525 sqlalchemy/testing/warnings.py,sha256=j8GWiP-yraCW0Ig5iJxAa5ekN82ZWqcQWyR-BUb4MZA,1546 -sqlalchemy/types.py,sha256=mOJzzF5LHN2EXX9PN5AFk1zSqUWtJgeyhZ_awAArr9c,3166 +sqlalchemy/types.py,sha256=RxpfIQMXJ46b-k6yuDZc5ZES3yO2nut0EliFnRyJ8yY,3165 sqlalchemy/util/__init__.py,sha256=Z4tsLRzNFX_VzCagSFfEzl5nvup8Oz3P4-87CSIYYXM,8401 -sqlalchemy/util/_collections.py,sha256=b1yyOQwnsSUlkBSL4RodiDMDKp0agIvfWGNLe_afWLg,20011 +sqlalchemy/util/_collections.py,sha256=EZ1GjqrZieXpltJqGws8psbvW0Hry09IiyGy26FcaxE,20011 sqlalchemy/util/_concurrency_py3k.py,sha256=d9j6lSK_RRImFq_ItBBOlI25bAjdq7Ts9NnNxpoNbSw,9170 sqlalchemy/util/_has_cy.py,sha256=hy1eXhxE25VT5bvWIKX6jtfqjawSQBnssrTbGo8kw9Q,1247 -sqlalchemy/util/_py_collections.py,sha256=1Oj4FVZW7DelXSYbiAUIDzqN1-2PA2h2Y_6_LyRYAX8,16650 -sqlalchemy/util/compat.py,sha256=iEhUMxayGqocUxYH0cixp43YBClhMIaG7vR3NK7S1fo,12862 +sqlalchemy/util/_py_collections.py,sha256=9kc23VK2LWYoIJYIp-xEj26EQA8OLFCYcdTxjAYfj2k,16886 +sqlalchemy/util/compat.py,sha256=Eo9n4IlT-Cwpae91OSD_ozivaDVWSWIMV6oA9qZdDk8,12895 sqlalchemy/util/concurrency.py,sha256=Qr7bVBAehbv8gv6--dqg8By5pTE2oIdinZMc7rcEsew,3350 sqlalchemy/util/deprecations.py,sha256=FaXayotv2JoI2UXwKxuCMMGZ8emX1W1dX4kQN06bRmY,12012 -sqlalchemy/util/langhelpers.py,sha256=f6QB41KlkAS8J71W7E4xnC8EHYRApMEqwsBCFXpJYHw,64872 -sqlalchemy/util/preloaded.py,sha256=3PApZM11IzFL9i7QYPJ8aL3xn4KtHiUDiB-rvUKt9RA,5904 -sqlalchemy/util/queue.py,sha256=IGc9tSEypeKZae2tHf5u9oV8CsOqObH8dGU_gl7Wv0Y,10185 -sqlalchemy/util/tool_support.py,sha256=Bd_L5U4KFExDQvhc7XO3ta4eBC9LoKz6-ppAdEpH7aw,6135 +sqlalchemy/util/langhelpers.py,sha256=c9YEJiZ5zJhqAvX1NHmNl6QbkSc4Km_TUk7AX7wTZp0,64983 +sqlalchemy/util/preloaded.py,sha256=BvEjS_KDVIlsErMGM2hCSLFWLeMkCgd_b-D58b1Quww,5905 +sqlalchemy/util/queue.py,sha256=GFVZYmWf86ejTQwSatw7UOhDYNKGzIFtTDBU0TanPF8,10185 +sqlalchemy/util/tool_support.py,sha256=iZSwvKdF5pA8UtXuwbtln6dEDgerKJWasELSi7q6Epw,6136 sqlalchemy/util/topological.py,sha256=yMpaA2s0JYIekQF4XKwzJzy8lVo-8yeo8o1J2xzxq48,3451 -sqlalchemy/util/typing.py,sha256=do1tX-habDEwaNE8AWfmqIY8F6jdX8MsPFS5Kd1RiXM,22493 +sqlalchemy/util/typing.py,sha256=4kisI5OYrctF2_oVZQ1ZYWICMnrqJw-TBhPvPflxZco,22517 diff --git a/libs/stevedore-5.7.0.dist-info/REQUESTED b/libs/sqlalchemy-2.0.51.dist-info/REQUESTED similarity index 100% rename from libs/stevedore-5.7.0.dist-info/REQUESTED rename to libs/sqlalchemy-2.0.51.dist-info/REQUESTED diff --git a/libs/sqlalchemy-2.0.48.dist-info/WHEEL b/libs/sqlalchemy-2.0.51.dist-info/WHEEL similarity index 100% rename from libs/sqlalchemy-2.0.48.dist-info/WHEEL rename to libs/sqlalchemy-2.0.51.dist-info/WHEEL diff --git a/libs/sqlalchemy-2.0.48.dist-info/licenses/LICENSE b/libs/sqlalchemy-2.0.51.dist-info/licenses/LICENSE similarity index 100% rename from libs/sqlalchemy-2.0.48.dist-info/licenses/LICENSE rename to libs/sqlalchemy-2.0.51.dist-info/licenses/LICENSE diff --git a/libs/sqlalchemy-2.0.48.dist-info/top_level.txt b/libs/sqlalchemy-2.0.51.dist-info/top_level.txt similarity index 100% rename from libs/sqlalchemy-2.0.48.dist-info/top_level.txt rename to libs/sqlalchemy-2.0.51.dist-info/top_level.txt diff --git a/libs/sqlalchemy/__init__.py b/libs/sqlalchemy/__init__.py index 2006f577a..e47b9646e 100644 --- a/libs/sqlalchemy/__init__.py +++ b/libs/sqlalchemy/__init__.py @@ -269,7 +269,7 @@ from .types import Uuid as Uuid from .types import VARBINARY as VARBINARY from .types import VARCHAR as VARCHAR -__version__ = "2.0.48" +__version__ = "2.0.51" def __go(lcls: Any) -> None: diff --git a/libs/sqlalchemy/connectors/aioodbc.py b/libs/sqlalchemy/connectors/aioodbc.py index 5df698fc5..44df0a9c4 100644 --- a/libs/sqlalchemy/connectors/aioodbc.py +++ b/libs/sqlalchemy/connectors/aioodbc.py @@ -20,7 +20,6 @@ from .. import util from ..util.concurrency import await_fallback from ..util.concurrency import await_only - if TYPE_CHECKING: from ..engine.interfaces import ConnectArgsType from ..engine.url import URL @@ -36,6 +35,14 @@ class AsyncAdapt_aioodbc_cursor(AsyncAdapt_dbapi_cursor): # how it's supposed to work # return self.await_(self._cursor.setinputsizes(*inputsizes)) + @property + def fast_executemany(self): + return self._cursor._impl.fast_executemany + + @fast_executemany.setter + def fast_executemany(self, value): + self._cursor._impl.fast_executemany = value + class AsyncAdapt_aioodbc_ss_cursor( AsyncAdapt_aioodbc_cursor, AsyncAdapt_dbapi_ss_cursor @@ -125,6 +132,7 @@ class AsyncAdapt_aioodbc_dbapi: "ProgrammingError", "InternalError", "NotSupportedError", + "SQL_DRIVER_NAME", "NUMBER", "STRING", "DATETIME", @@ -133,6 +141,7 @@ class AsyncAdapt_aioodbc_dbapi: "BinaryNull", "SQL_VARCHAR", "SQL_WVARCHAR", + "SQL_DECIMAL", ): setattr(self, name, getattr(self.pyodbc, name)) diff --git a/libs/sqlalchemy/dialects/_typing.py b/libs/sqlalchemy/dialects/_typing.py index bf9b67a0a..aef755d18 100644 --- a/libs/sqlalchemy/dialects/_typing.py +++ b/libs/sqlalchemy/dialects/_typing.py @@ -18,7 +18,6 @@ from ..sql.schema import Column from ..sql.schema import ColumnCollectionConstraint from ..sql.schema import Index - _OnConflictConstraintT = Union[str, ColumnCollectionConstraint, Index, None] _OnConflictIndexElementsT = Optional[ Iterable[Union[Column[Any], str, roles.DDLConstraintColumnRole]] diff --git a/libs/sqlalchemy/dialects/mssql/__init__.py b/libs/sqlalchemy/dialects/mssql/__init__.py index 68d1d7c74..ababcc138 100644 --- a/libs/sqlalchemy/dialects/mssql/__init__.py +++ b/libs/sqlalchemy/dialects/mssql/__init__.py @@ -45,7 +45,6 @@ from .base import VARCHAR from .base import XML from ...sql import try_cast - base.dialect = dialect = pyodbc.dialect diff --git a/libs/sqlalchemy/dialects/mssql/base.py b/libs/sqlalchemy/dialects/mssql/base.py index a4f23f9a5..9d718b057 100644 --- a/libs/sqlalchemy/dialects/mssql/base.py +++ b/libs/sqlalchemy/dialects/mssql/base.py @@ -3220,8 +3220,7 @@ class MSDialect(default.DefaultDialect): view_name = f"sys.{row[0]}" - cursor.execute( - """ + cursor.execute(""" SELECT CASE transaction_isolation_level WHEN 0 THEN NULL WHEN 1 THEN 'READ UNCOMMITTED' @@ -3232,10 +3231,7 @@ class MSDialect(default.DefaultDialect): AS TRANSACTION_ISOLATION_LEVEL FROM {} where session_id = @@SPID - """.format( - view_name - ) - ) + """.format(view_name)) except self.dbapi.Error as err: raise NotImplementedError( "Can't fetch isolation level; encountered error {} when " @@ -3254,13 +3250,6 @@ class MSDialect(default.DefaultDialect): self._setup_supports_comments(connection) def _setup_version_attributes(self): - if self.server_version_info[0] not in list(range(8, 17)): - util.warn( - "Unrecognized server version info '%s'. Some SQL Server " - "features may not function properly." - % ".".join(str(x) for x in self.server_version_info) - ) - if self.server_version_info >= MS_2008_VERSION: self.supports_multivalues_insert = True else: @@ -3439,8 +3428,7 @@ class MSDialect(default.DefaultDialect): else "NULL as filter_definition" ) rp = connection.execution_options(future_result=True).execute( - sql.text( - f""" + sql.text(f""" select ind.index_id, ind.is_unique, @@ -3460,8 +3448,7 @@ where and ind.type != 0 order by ind.name - """ - ) + """) .bindparams( sql.bindparam("tabname", tablename, ischema.CoerceUnicode()), sql.bindparam("schname", owner, ischema.CoerceUnicode()), @@ -3489,8 +3476,7 @@ order by do["mssql_where"] = row["filter_definition"] rp = connection.execution_options(future_result=True).execute( - sql.text( - """ + sql.text(""" select ind_col.index_id, col.name, @@ -3510,8 +3496,7 @@ where order by ind_col.index_id, ind_col.key_ordinal - """ - ) + """) .bindparams( sql.bindparam("tabname", tablename, ischema.CoerceUnicode()), sql.bindparam("schname", owner, ischema.CoerceUnicode()), @@ -3641,6 +3626,7 @@ order by def get_columns(self, connection, tablename, dbname, owner, schema, **kw): sys_columns = ischema.sys_columns sys_types = ischema.sys_types + sys_base_types = ischema.sys_types.alias("base_types") sys_default_constraints = ischema.sys_default_constraints computed_cols = ischema.computed_columns identity_cols = ischema.identity_columns @@ -3682,6 +3668,7 @@ order by sql.select( sys_columns.c.name, sys_types.c.name, + sys_base_types.c.name.label("base_type"), sys_columns.c.is_nullable, sys_columns.c.max_length, sys_columns.c.precision, @@ -3701,6 +3688,15 @@ order by onclause=sys_columns.c.user_type_id == sys_types.c.user_type_id, ) + .outerjoin( + sys_base_types, + onclause=sql.and_( + sys_types.c.system_type_id + == sys_base_types.c.system_type_id, + sys_base_types.c.user_type_id + == sys_base_types.c.system_type_id, + ), + ) .outerjoin( sys_default_constraints, sql.and_( @@ -3747,6 +3743,7 @@ order by for row in c.mappings(): name = row[sys_columns.c.name] type_ = row[sys_types.c.name] + base_type = row["base_type"] nullable = row[sys_columns.c.is_nullable] == 1 maxlen = row[sys_columns.c.max_length] numericprec = row[sys_columns.c.precision] @@ -3760,7 +3757,17 @@ order by identity_increment = row[identity_cols.c.increment_value] comment = row[extended_properties.c.value] + # Try to resolve the user type first (e.g., "sysname"), + # then fall back to the base type (e.g., "nvarchar"). + # base_type may be None for CLR types (geography, geometry, + # hierarchyid) which have no corresponding base type. coltype = self.ischema_names.get(type_, None) + if ( + coltype is None + and base_type is not None + and base_type != type_ + ): + coltype = self.ischema_names.get(base_type, None) kwargs = {} @@ -3788,10 +3795,16 @@ order by kwargs["collation"] = collation if coltype is None: - util.warn( - "Did not recognize type '%s' of column '%s'" - % (type_, name) - ) + if base_type is not None and base_type != type_: + util.warn( + "Did not recognize type '%s' (user type) or '%s' " + "(base type) of column '%s'" % (type_, base_type, name) + ) + else: + util.warn( + "Did not recognize type '%s' of column '%s'" + % (type_, name) + ) coltype = sqltypes.NULLTYPE else: if issubclass(coltype, sqltypes.Numeric): @@ -3909,8 +3922,7 @@ order by ): # Foreign key constraints s = ( - text( - """\ + text("""\ WITH fk_info AS ( SELECT ischema_ref_con.constraint_schema, @@ -4010,8 +4022,7 @@ index_info AS ( ORDER BY fk_info.constraint_schema, fk_info.constraint_name, fk_info.ordinal_position -""" - ) +""") .bindparams( sql.bindparam("tablename", tablename, ischema.CoerceUnicode()), sql.bindparam("owner", owner, ischema.CoerceUnicode()), diff --git a/libs/sqlalchemy/dialects/mssql/information_schema.py b/libs/sqlalchemy/dialects/mssql/information_schema.py index d739d7182..5249f3e39 100644 --- a/libs/sqlalchemy/dialects/mssql/information_schema.py +++ b/libs/sqlalchemy/dialects/mssql/information_schema.py @@ -20,7 +20,6 @@ from ...types import String from ...types import TypeDecorator from ...types import Unicode - ischema = MetaData() diff --git a/libs/sqlalchemy/dialects/mssql/pymssql.py b/libs/sqlalchemy/dialects/mssql/pymssql.py index 7244bb0d3..6c6c3a2e7 100644 --- a/libs/sqlalchemy/dialects/mssql/pymssql.py +++ b/libs/sqlalchemy/dialects/mssql/pymssql.py @@ -22,6 +22,7 @@ pymssql is a Python module that provides a Python DBAPI interface around """ # noqa + import re from .base import MSDialect diff --git a/libs/sqlalchemy/dialects/mssql/pyodbc.py b/libs/sqlalchemy/dialects/mssql/pyodbc.py index 5d3ae0b13..874cc70a4 100644 --- a/libs/sqlalchemy/dialects/mssql/pyodbc.py +++ b/libs/sqlalchemy/dialects/mssql/pyodbc.py @@ -359,7 +359,6 @@ hook. See that method for usage examples. """ # noqa - import datetime import decimal import re diff --git a/libs/sqlalchemy/dialects/mysql/aiomysql.py b/libs/sqlalchemy/dialects/mysql/aiomysql.py index 7e68c61da..c7cc1922b 100644 --- a/libs/sqlalchemy/dialects/mysql/aiomysql.py +++ b/libs/sqlalchemy/dialects/mysql/aiomysql.py @@ -28,6 +28,7 @@ This dialect should normally be used only with the ) """ # noqa + from __future__ import annotations from types import ModuleType @@ -38,6 +39,7 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union +from .pymysql import _connection_ping_reconnects_true from .pymysql import MySQLDialect_pymysql from ... import pool from ... import util @@ -46,6 +48,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_cursor from ...connectors.asyncio import AsyncAdapt_dbapi_module from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor from ...connectors.asyncio import AsyncAdapt_terminate +from ...util import langhelpers from ...util.concurrency import await_fallback from ...util.concurrency import await_only @@ -91,9 +94,12 @@ class AsyncAdapt_aiomysql_connection( _cursor_cls = AsyncAdapt_aiomysql_cursor _ss_cursor_cls = AsyncAdapt_aiomysql_ss_cursor - def ping(self, reconnect: bool) -> None: + def ping(self, reconnect: bool = False) -> None: assert not reconnect - self.await_(self._connection.ping(reconnect)) + if self.dbapi._send_false_to_ping: + self.await_(self._connection.ping(reconnect=False)) + else: + self.await_(self._connection.ping()) def character_set_name(self) -> Optional[str]: return self._connection.character_set_name() # type: ignore[no-any-return] # noqa: E501 @@ -170,6 +176,24 @@ class AsyncAdapt_aiomysql_dbapi(AsyncAdapt_dbapi_module): await_only(creator_fn(*arg, **kw)), ) + @langhelpers.memoized_property + def _send_false_to_ping(self) -> bool: + """determine if aiomysql has deprecated, changed the default of, + or removed the 'reconnect' argument of connection.ping(). + + See #13306 and #10492 + + """ # noqa: E501 + + try: + Connection = __import__( + "aiomysql.connection" + ).connection.Connection + except (ImportError, AttributeError): + return True + else: + return _connection_ping_reconnects_true(Connection) + def _init_cursors_subclasses( self, ) -> Tuple[AsyncIODBAPICursor, AsyncIODBAPICursor]: diff --git a/libs/sqlalchemy/dialects/mysql/asyncmy.py b/libs/sqlalchemy/dialects/mysql/asyncmy.py index 575747617..0f7d351d4 100644 --- a/libs/sqlalchemy/dialects/mysql/asyncmy.py +++ b/libs/sqlalchemy/dialects/mysql/asyncmy.py @@ -26,6 +26,7 @@ This dialect should normally be used only with the ) """ # noqa + from __future__ import annotations from types import ModuleType @@ -35,6 +36,7 @@ from typing import Optional from typing import TYPE_CHECKING from typing import Union +from .pymysql import _connection_ping_reconnects_true from .pymysql import MySQLDialect_pymysql from ... import pool from ... import util @@ -43,6 +45,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_cursor from ...connectors.asyncio import AsyncAdapt_dbapi_module from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor from ...connectors.asyncio import AsyncAdapt_terminate +from ...util import langhelpers from ...util.concurrency import await_fallback from ...util.concurrency import await_only @@ -86,18 +89,21 @@ class AsyncAdapt_asyncmy_connection( if isinstance(error, AttributeError): raise self.dbapi.InternalError( "network operation failed due to asyncmy attribute error" - ) + ) from error raise error - def ping(self, reconnect: bool) -> None: + def ping(self, reconnect: bool = False) -> None: assert not reconnect return self.await_(self._do_ping()) async def _do_ping(self) -> None: try: async with self._execute_mutex: - await self._connection.ping(False) + if self.dbapi._send_false_to_ping: + await self._connection.ping(reconnect=False) + else: + await self._connection.ping() except Exception as error: self._handle_exception(error) @@ -171,6 +177,22 @@ class AsyncAdapt_asyncmy_dbapi(AsyncAdapt_dbapi_module): await_only(creator_fn(*arg, **kw)), ) + @langhelpers.memoized_property + def _send_false_to_ping(self) -> bool: + """determine if asyncmy has deprecated, changed the default of, + or removed the 'reconnect' argument of connection.ping(). + + See #13306 and #10492 + + """ # noqa: E501 + + try: + Connection = __import__("asyncmy.connection").connection.Connection + except (ImportError, AttributeError): + return True + else: + return _connection_ping_reconnects_true(Connection) + class MySQLDialect_asyncmy(MySQLDialect_pymysql): driver = "asyncmy" diff --git a/libs/sqlalchemy/dialects/mysql/base.py b/libs/sqlalchemy/dialects/mysql/base.py index bc8875efc..506f2f284 100644 --- a/libs/sqlalchemy/dialects/mysql/base.py +++ b/libs/sqlalchemy/dialects/mysql/base.py @@ -1063,6 +1063,7 @@ output: ) """ # noqa + from __future__ import annotations from collections import defaultdict @@ -2764,6 +2765,7 @@ class MySQLDialect(default.DefaultDialect): # i.e. first connect _backslash_escapes = True _server_ansiquotes = False + _casing = 0 server_version_info: Tuple[int, ...] identifier_preparer: MySQLIdentifierPreparer @@ -3186,9 +3188,9 @@ class MySQLDialect(default.DefaultDialect): self._is_mysql and self.server_version_info >= (8, 0, 1) ) - self._needs_correct_for_88718_96365 = ( - not self.is_mariadb and self.server_version_info >= (8,) - ) + self._needs_correct_for_88718_96365 = self.server_version_info >= ( + 8, + ) and (self.server_version_info < (8, 0, 14) or self._casing == 2) self.delete_returning = ( self.is_mariadb and self.server_version_info >= (10, 0, 5) @@ -3253,7 +3255,7 @@ class MySQLDialect(default.DefaultDialect): def _is_mariadb_102(self) -> bool: return ( self.is_mariadb - and self._mariadb_normalized_version_info # type:ignore[operator] + and self._mariadb_normalized_version_info # type: ignore[operator] > ( 10, 2, diff --git a/libs/sqlalchemy/dialects/mysql/cymysql.py b/libs/sqlalchemy/dialects/mysql/cymysql.py index 325d3321e..cc3c2a8bc 100644 --- a/libs/sqlalchemy/dialects/mysql/cymysql.py +++ b/libs/sqlalchemy/dialects/mysql/cymysql.py @@ -20,6 +20,7 @@ r""" dialects are mysqlclient and PyMySQL. """ # noqa + from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/dialects/mysql/dml.py b/libs/sqlalchemy/dialects/mysql/dml.py index d5bbf14bb..b76041226 100644 --- a/libs/sqlalchemy/dialects/mysql/dml.py +++ b/libs/sqlalchemy/dialects/mysql/dml.py @@ -28,7 +28,6 @@ from ...sql.expression import alias from ...sql.selectable import NamedFromClause from ...util.typing import Self - __all__ = ("Insert", "insert") diff --git a/libs/sqlalchemy/dialects/mysql/mariadbconnector.py b/libs/sqlalchemy/dialects/mysql/mariadbconnector.py index bd5d7a258..5e78da405 100644 --- a/libs/sqlalchemy/dialects/mysql/mariadbconnector.py +++ b/libs/sqlalchemy/dialects/mysql/mariadbconnector.py @@ -27,6 +27,7 @@ be ``mysqldb``. ``mariadb+mariadbconnector://`` is required to use this driver. .. mariadb: https://github.com/mariadb-corporation/mariadb-connector-python """ # noqa + from __future__ import annotations import re diff --git a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py index d97dbe219..1cc0c9e08 100644 --- a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py +++ b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py @@ -45,6 +45,7 @@ charset/collation will allow connectivity. """ # noqa + from __future__ import annotations import re @@ -110,8 +111,8 @@ class IdentifierPreparerCommon_mysqlconnector: def _escape_identifier(self, value: str) -> str: value = value.replace( - self.escape_quote, # type:ignore[attr-defined] - self.escape_to_quote, # type:ignore[attr-defined] + self.escape_quote, # type: ignore[attr-defined] + self.escape_to_quote, # type: ignore[attr-defined] ) return value diff --git a/libs/sqlalchemy/dialects/mysql/mysqldb.py b/libs/sqlalchemy/dialects/mysql/mysqldb.py index 188668be7..60fa3821d 100644 --- a/libs/sqlalchemy/dialects/mysql/mysqldb.py +++ b/libs/sqlalchemy/dialects/mysql/mysqldb.py @@ -84,6 +84,7 @@ Server Side Cursors The mysqldb dialect supports server-side cursors. See :ref:`mysql_ss_cursors`. """ + from __future__ import annotations import re diff --git a/libs/sqlalchemy/dialects/mysql/pymysql.py b/libs/sqlalchemy/dialects/mysql/pymysql.py index ae86938e6..b87a69f29 100644 --- a/libs/sqlalchemy/dialects/mysql/pymysql.py +++ b/libs/sqlalchemy/dialects/mysql/pymysql.py @@ -47,11 +47,13 @@ and targets 100% compatibility. Most behavioral notes for MySQL-python apply to the pymysql driver as well. """ # noqa + from __future__ import annotations from typing import Any from typing import Dict from typing import Optional +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -69,6 +71,31 @@ if TYPE_CHECKING: from ...engine.url import URL +def _connection_ping_reconnects_true(connection_cls: Type[Any]) -> bool: + """Given a Connection class like pymysql.Connection, aiomysql.Connection, + asyncmy.Connection, inspect the ping() method and determine if it + has a "reconnect" parameter that either defaults to True, or is positional. + + a return value of True here means that when we call ``connection.ping()``, + we **must** pass `reconnect=False`. a return value of False means that + we should call ``connection.ping()`` with **no arguments**. + + This routine originates from issue #10492 for pymysql, however arg + signature mismatches in aiomysql/asyncmy tracked by issue #13306 + necessitated a more open ended function. + + """ + insp = langhelpers.get_callable_argspec(connection_cls.ping) + try: + reconnect_arg = insp.args[1] + except IndexError: + return False + else: + return reconnect_arg == "reconnect" and ( + not insp.defaults or insp.defaults[0] is not False + ) + + class MySQLDialect_pymysql(MySQLDialect_mysqldb): driver = "pymysql" supports_statement_cache = True @@ -97,6 +124,8 @@ class MySQLDialect_pymysql(MySQLDialect_mysqldb): https://github.com/PyMySQL/mysqlclient/discussions/651#discussioncomment-7308971 for background. + Revised as part of #13306 + """ # noqa: E501 try: @@ -106,15 +135,7 @@ class MySQLDialect_pymysql(MySQLDialect_mysqldb): except (ImportError, AttributeError): return True else: - insp = langhelpers.get_callable_argspec(Connection.ping) - try: - reconnect_arg = insp.args[1] - except IndexError: - return False - else: - return reconnect_arg == "reconnect" and ( - not insp.defaults or insp.defaults[0] is not False - ) + return _connection_ping_reconnects_true(Connection) def do_ping(self, dbapi_connection: DBAPIConnection) -> Literal[True]: if self._send_false_to_ping: diff --git a/libs/sqlalchemy/dialects/mysql/pyodbc.py b/libs/sqlalchemy/dialects/mysql/pyodbc.py index bb37ff0e5..023c17682 100644 --- a/libs/sqlalchemy/dialects/mysql/pyodbc.py +++ b/libs/sqlalchemy/dialects/mysql/pyodbc.py @@ -42,6 +42,7 @@ Pass through exact pyodbc connection string:: connection_uri = "mysql+pyodbc:///?odbc_connect=%s" % params """ # noqa + from __future__ import annotations import datetime diff --git a/libs/sqlalchemy/dialects/mysql/reflection.py b/libs/sqlalchemy/dialects/mysql/reflection.py index c9dfb1308..0ceda89f2 100644 --- a/libs/sqlalchemy/dialects/mysql/reflection.py +++ b/libs/sqlalchemy/dialects/mysql/reflection.py @@ -371,7 +371,7 @@ class MySQLTableDefinitionParser: buffer = [] for row in columns: - (name, col_type, nullable, default, extra) = ( + name, col_type, nullable, default, extra = ( row[i] for i in (0, 1, 2, 4, 5) ) @@ -704,7 +704,7 @@ def cleanup_text(raw_text: str) -> str: if "\\" in raw_text: raw_text = re.sub( _control_char_regexp, - lambda s: _control_char_map[s[0]], # type: ignore[index] + lambda s: _control_char_map[s[0]], # type: ignore[unused-ignore,index] # noqa: E501 raw_text, ) return raw_text.replace("''", "'") diff --git a/libs/sqlalchemy/dialects/oracle/base.py b/libs/sqlalchemy/dialects/oracle/base.py index 785ec92a6..c8e089784 100644 --- a/libs/sqlalchemy/dialects/oracle/base.py +++ b/libs/sqlalchemy/dialects/oracle/base.py @@ -2778,6 +2778,7 @@ class OracleDialect(default.DefaultDialect): all_cols.c.column_name, all_cols.c.data_type, all_cols.c.char_length, + all_cols.c.data_length, all_cols.c.data_precision, all_cols.c.data_scale, all_cols.c.nullable, @@ -2896,6 +2897,9 @@ class OracleDialect(default.DefaultDialect): elif coltype in ("VARCHAR2", "NVARCHAR2", "CHAR", "NCHAR"): char_length = maybe_int(row_dict["char_length"]) coltype = self.ischema_names.get(coltype)(char_length) + elif coltype == "RAW": + data_length = maybe_int(row_dict["data_length"]) + coltype = RAW(data_length) elif "WITH TIME ZONE" in coltype: coltype = TIMESTAMP(timezone=True) elif "WITH LOCAL TIME ZONE" in coltype: diff --git a/libs/sqlalchemy/dialects/oracle/cx_oracle.py b/libs/sqlalchemy/dialects/oracle/cx_oracle.py index 8750354c9..2dc823d47 100644 --- a/libs/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/libs/sqlalchemy/dialects/oracle/cx_oracle.py @@ -489,6 +489,7 @@ SQLAlchemy type (or a subclass of such). as better integration of outputtypehandlers. """ # noqa + from __future__ import annotations import decimal diff --git a/libs/sqlalchemy/dialects/oracle/oracledb.py b/libs/sqlalchemy/dialects/oracle/oracledb.py index a41f6be87..abe8e571f 100644 --- a/libs/sqlalchemy/dialects/oracle/oracledb.py +++ b/libs/sqlalchemy/dialects/oracle/oracledb.py @@ -588,6 +588,7 @@ SQLAlchemy type (or a subclass of such). .. versionadded:: 2.0.0 added support for the python-oracledb driver. """ # noqa + from __future__ import annotations import collections diff --git a/libs/sqlalchemy/dialects/oracle/vector.py b/libs/sqlalchemy/dialects/oracle/vector.py index 53057675b..ac1c9b040 100644 --- a/libs/sqlalchemy/dialects/oracle/vector.py +++ b/libs/sqlalchemy/dialects/oracle/vector.py @@ -313,12 +313,10 @@ class VECTOR(types.TypeEngine): ) else: - raise TypeError( - """ + raise TypeError(""" Invalid input for VECTOR: expected a list, an array.array, or a SparseVector object. - """ - ) + """) return process diff --git a/libs/sqlalchemy/dialects/postgresql/__init__.py b/libs/sqlalchemy/dialects/postgresql/__init__.py index a94cfef2c..e1900ec9b 100644 --- a/libs/sqlalchemy/dialects/postgresql/__init__.py +++ b/libs/sqlalchemy/dialects/postgresql/__init__.py @@ -89,7 +89,6 @@ from .types import TIMESTAMP from .types import TSQUERY from .types import TSVECTOR - # Alias psycopg also as psycopg_async psycopg_async = type( "psycopg_async", (ModuleType,), {"dialect": psycopg.dialect_async} diff --git a/libs/sqlalchemy/dialects/postgresql/_psycopg_common.py b/libs/sqlalchemy/dialects/postgresql/_psycopg_common.py index 03b0f76ec..abecf2637 100644 --- a/libs/sqlalchemy/dialects/postgresql/_psycopg_common.py +++ b/libs/sqlalchemy/dialects/postgresql/_psycopg_common.py @@ -187,3 +187,39 @@ class _PGDialect_common_psycopg(PGDialect): dbapi_connection.autocommit = before_autocommit return True + + def do_begin_twophase(self, connection, xid): + connection.connection.tpc_begin(xid) + + def do_prepare_twophase(self, connection, xid): + connection.connection.tpc_prepare() + + def _do_twophase(self, dbapi_conn, operation, xid, recover=False): + if recover: + if not self._twophase_idle_check(dbapi_conn): + dbapi_conn.rollback() + operation(xid) + else: + operation() + + def _twophase_idle_check(self, dbapi_conn): + raise NotImplementedError + + def do_rollback_twophase( + self, connection, xid, is_prepared=True, recover=False + ): + dbapi_conn = connection.connection.dbapi_connection + self._do_twophase( + dbapi_conn, dbapi_conn.tpc_rollback, xid, recover=recover + ) + + def do_commit_twophase( + self, connection, xid, is_prepared=True, recover=False + ): + dbapi_conn = connection.connection.dbapi_connection + self._do_twophase( + dbapi_conn, dbapi_conn.tpc_commit, xid, recover=recover + ) + + def do_recover_twophase(self, connection): + return [str(row) for row in connection.connection.tpc_recover()] diff --git a/libs/sqlalchemy/dialects/postgresql/asyncpg.py b/libs/sqlalchemy/dialects/postgresql/asyncpg.py index 616bc467f..f36b45aea 100644 --- a/libs/sqlalchemy/dialects/postgresql/asyncpg.py +++ b/libs/sqlalchemy/dialects/postgresql/asyncpg.py @@ -990,6 +990,9 @@ class AsyncAdapt_asyncpg_dbapi: class InternalServerError(InternalError): pass + class InternalClientError(InternalError): + pass + class InvalidCachedStatementError(NotSupportedError): def __init__(self, message): super().__init__( @@ -1014,6 +1017,7 @@ class AsyncAdapt_asyncpg_dbapi: asyncpg.exceptions.InterfaceError: self.InterfaceError, asyncpg.exceptions.InvalidCachedStatementError: self.InvalidCachedStatementError, # noqa: E501 asyncpg.exceptions.InternalServerError: self.InternalServerError, + asyncpg.exceptions.InternalClientError: self.InternalClientError, } def Binary(self, value): @@ -1212,7 +1216,6 @@ class PGDialect_asyncpg(PGDialect): """ asyncpg_connection = conn._connection - deserializer = self._json_deserializer or _py_json.loads def _jsonb_encoder(str_value): # \x01 is the prefix for jsonb used by PostgreSQL. diff --git a/libs/sqlalchemy/dialects/postgresql/base.py b/libs/sqlalchemy/dialects/postgresql/base.py index a5a15bb36..872cc2b9c 100644 --- a/libs/sqlalchemy/dialects/postgresql/base.py +++ b/libs/sqlalchemy/dialects/postgresql/base.py @@ -1198,6 +1198,21 @@ constraints, enabling table inheritance hierarchies in PostgreSQL. Table("some_table", metadata, ..., postgresql_inherits=("t1", "t2", ...)) +For schema-qualified parent table names, use :class:`.quoted_name` with +``quote=False`` to prevent the dotted name from being quoted as a single +identifier:: + + from sqlalchemy.sql import quoted_name + + Table( + "some_table", + metadata, + ..., + postgresql_inherits=quoted_name( + "my_schema.some_supertable", quote=False + ), + ) + ``ON COMMIT`` ^^^^^^^^^^^^^ @@ -1390,6 +1405,29 @@ that are subject to the action:: .. versionadded:: 2.0.40 +``NULLS NOT DISTINCT`` +^^^^^^^^^^^^^^^^^^^^^^ + +By default, two ``null`` values are not considered equal for unique constraints +and indexes. Therefore, seemingly duplicate rows may be stored if one of the +values in the constraint is ``null``. This default behavior is implementation +defined, so other SQL dialects may behave differently than PostgreSQL. + +The ``NULLS NOT DISTINCT`` clause can be used to change this behavior, treating +null values as equal and preventing unintended duplicate rows. The opposite +``NULLS DISTINCT`` clause can also be used to make PostgreSQL's default behavior +explict. + +The ``postgresql_nulls_not_distinct`` parameter can be set to ``True`` to +add the ``NULLS NOT DISTINCT`` clause, or ``False`` to add ``NULLS DISTINCT``. +Not setting it, or passing ``None``, will not add a clause and keep the default +behavior. + +This feature requires PostgreSQL 15 or later. + +.. versionadded:: 2.0.16 + + .. _postgresql_table_valued_overview: Table values, Table and Column valued functions, Row and Tuple objects @@ -3478,7 +3516,11 @@ class PGDialect(default.DefaultDialect): self.do_begin(connection.connection) def do_prepare_twophase(self, connection, xid): - connection.exec_driver_sql("PREPARE TRANSACTION '%s'" % xid) + connection.execute( + sql.text("PREPARE TRANSACTION :xid").bindparams( + sql.bindparam("xid", xid, literal_execute=True) + ) + ) def do_rollback_twophase( self, connection, xid, is_prepared=True, recover=False @@ -3490,7 +3532,11 @@ class PGDialect(default.DefaultDialect): # Must find out a way how to make the dbapi not # open a transaction. connection.exec_driver_sql("ROLLBACK") - connection.exec_driver_sql("ROLLBACK PREPARED '%s'" % xid) + connection.execute( + sql.text("ROLLBACK PREPARED :xid").bindparams( + sql.bindparam("xid", xid, literal_execute=True) + ) + ) connection.exec_driver_sql("BEGIN") self.do_rollback(connection.connection) else: @@ -3502,7 +3548,11 @@ class PGDialect(default.DefaultDialect): if is_prepared: if recover: connection.exec_driver_sql("ROLLBACK") - connection.exec_driver_sql("COMMIT PREPARED '%s'" % xid) + connection.execute( + sql.text("COMMIT PREPARED :xid").bindparams( + sql.bindparam("xid", xid, literal_execute=True) + ) + ) connection.exec_driver_sql("BEGIN") self.do_rollback(connection.connection) else: @@ -4507,7 +4557,7 @@ class PGDialect(default.DefaultDialect): @util.memoized_property def _fk_regex_pattern(self): # optionally quoted token - qtoken = r'(?:"[^"]+"|[\w]+?)' + qtoken = r'(?:"(?:[^"]|"")+"|[\w]+?)' # https://www.postgresql.org/docs/current/static/sql-createtable.html return re.compile( diff --git a/libs/sqlalchemy/dialects/postgresql/dml.py b/libs/sqlalchemy/dialects/postgresql/dml.py index 053770e1c..6eea13179 100644 --- a/libs/sqlalchemy/dialects/postgresql/dml.py +++ b/libs/sqlalchemy/dialects/postgresql/dml.py @@ -35,7 +35,6 @@ from ...sql.elements import TextClause from ...sql.expression import alias from ...util.typing import Self - __all__ = ("Insert", "insert") diff --git a/libs/sqlalchemy/dialects/postgresql/ext.py b/libs/sqlalchemy/dialects/postgresql/ext.py index 651f40f91..55bfebb03 100644 --- a/libs/sqlalchemy/dialects/postgresql/ext.py +++ b/libs/sqlalchemy/dialects/postgresql/ext.py @@ -234,6 +234,11 @@ class ExcludeConstraint(ColumnCollectionConstraint): Optional string. If set, emit INITIALLY when issuing DDL for this constraint. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 2.0.50 + :param using: Optional string. If set, emit USING when issuing DDL for this constraint. Defaults to 'gist'. @@ -288,6 +293,7 @@ class ExcludeConstraint(ColumnCollectionConstraint): name=kw.get("name"), deferrable=kw.get("deferrable"), initially=kw.get("initially"), + info=kw.get("info"), ) self.using = kw.get("using", "gist") where = kw.get("where") diff --git a/libs/sqlalchemy/dialects/postgresql/hstore.py b/libs/sqlalchemy/dialects/postgresql/hstore.py index 27005f063..d0623a9f0 100644 --- a/libs/sqlalchemy/dialects/postgresql/hstore.py +++ b/libs/sqlalchemy/dialects/postgresql/hstore.py @@ -19,7 +19,6 @@ from .operators import HAS_KEY from ... import types as sqltypes from ...sql import functions as sqlfunc - __all__ = ("HSTORE", "hstore") @@ -303,12 +302,12 @@ class _HStoreMatrixFunction(sqlfunc.GenericFunction): HSTORE_PAIR_RE = re.compile( r""" ( - "(?P (\\ . | [^"])* )" # Quoted key + "(?P (\\ . | [^"\\])* )" # Quoted key ) [ ]* => [ ]* # Pair operator, optional adjoining whitespace ( (?P NULL ) # NULL value - | "(?P (\\ . | [^"])* )" # Quoted value + | "(?P (\\ . | [^"\\])* )" # Quoted value ) """, re.VERBOSE, diff --git a/libs/sqlalchemy/dialects/postgresql/operators.py b/libs/sqlalchemy/dialects/postgresql/operators.py index 9e4d26570..d38d0160b 100644 --- a/libs/sqlalchemy/dialects/postgresql/operators.py +++ b/libs/sqlalchemy/dialects/postgresql/operators.py @@ -7,7 +7,6 @@ # mypy: ignore-errors from ...sql import operators - _getitem_precedence = operators._PRECEDENCE[operators.json_getitem_op] _eq_precedence = operators._PRECEDENCE[operators.eq] diff --git a/libs/sqlalchemy/dialects/postgresql/pg8000.py b/libs/sqlalchemy/dialects/postgresql/pg8000.py index cd233de13..faee3414f 100644 --- a/libs/sqlalchemy/dialects/postgresql/pg8000.py +++ b/libs/sqlalchemy/dialects/postgresql/pg8000.py @@ -95,6 +95,7 @@ of the :ref:`psycopg2 ` dialect: """ # noqa + import decimal import re @@ -587,11 +588,8 @@ class PGDialect_pg8000(PGDialect): def _set_client_encoding(self, dbapi_connection, client_encoding): cursor = dbapi_connection.cursor() - cursor.execute( - f"""SET CLIENT_ENCODING TO '{ - client_encoding.replace("'", "''") - }'""" - ) + cursor.execute(f""" + SET CLIENT_ENCODING TO '{client_encoding.replace("'", "''")}'""") cursor.execute("COMMIT") cursor.close() diff --git a/libs/sqlalchemy/dialects/postgresql/provision.py b/libs/sqlalchemy/dialects/postgresql/provision.py index dfe0f627d..dfe67dce9 100644 --- a/libs/sqlalchemy/dialects/postgresql/provision.py +++ b/libs/sqlalchemy/dialects/postgresql/provision.py @@ -95,9 +95,10 @@ def _postgresql_set_default_schema_on_connection( def drop_all_schema_objects_pre_tables(cfg, eng): with eng.connect().execution_options(isolation_level="AUTOCOMMIT") as conn: for xid in conn.exec_driver_sql( - "select gid from pg_prepared_xacts" + "SELECT gid FROM pg_prepared_xacts " + "WHERE database = current_database()" ).scalars(): - conn.exec_driver_sql("ROLLBACK PREPARED '%s'" % xid) + eng.dialect.do_rollback_twophase(conn, xid, recover=True) @drop_all_schema_objects_post_tables.for_db("postgresql") diff --git a/libs/sqlalchemy/dialects/postgresql/psycopg.py b/libs/sqlalchemy/dialects/postgresql/psycopg.py index 67dc5ca86..4b8d4e8ee 100644 --- a/libs/sqlalchemy/dialects/postgresql/psycopg.py +++ b/libs/sqlalchemy/dialects/postgresql/psycopg.py @@ -168,6 +168,7 @@ specified:: `Client-side-binding cursors `_ """ # noqa + from __future__ import annotations from collections import deque @@ -596,44 +597,12 @@ class PGDialect_psycopg(_PGDialect_common_psycopg): return True return False - def _do_prepared_twophase(self, connection, command, recover=False): - dbapi_conn = connection.connection.dbapi_connection - if ( - recover - # don't rely on psycopg providing enum symbols, compare with - # eq/ne - or dbapi_conn.info.transaction_status - != self._psycopg_TransactionStatus.IDLE - ): - dbapi_conn.rollback() - before_autocommit = dbapi_conn.autocommit - try: - if not before_autocommit: - self._do_autocommit(dbapi_conn, True) - dbapi_conn.execute(command) - finally: - if not before_autocommit: - self._do_autocommit(dbapi_conn, before_autocommit) - - def do_rollback_twophase( - self, connection, xid, is_prepared=True, recover=False - ): - if is_prepared: - self._do_prepared_twophase( - connection, f"ROLLBACK PREPARED '{xid}'", recover=recover - ) - else: - self.do_rollback(connection.connection) - - def do_commit_twophase( - self, connection, xid, is_prepared=True, recover=False - ): - if is_prepared: - self._do_prepared_twophase( - connection, f"COMMIT PREPARED '{xid}'", recover=recover - ) - else: - self.do_commit(connection.connection) + def _twophase_idle_check(self, dbapi_conn): + # don't rely on psycopg providing enum symbols, compare with eq/ne + return ( + dbapi_conn.info.transaction_status + == self._psycopg_TransactionStatus.IDLE + ) @util.memoized_property def _dialect_specific_select_one(self): @@ -784,6 +753,21 @@ class AsyncAdapt_psycopg_connection(AdaptedConnection): def set_deferrable(self, value): self.await_(self._connection.set_deferrable(value)) + def tpc_begin(self, xid): + return self.await_(self._connection.tpc_begin(xid)) + + def tpc_prepare(self): + return self.await_(self._connection.tpc_prepare()) + + def tpc_commit(self, xid=None): + return self.await_(self._connection.tpc_commit(xid)) + + def tpc_rollback(self, xid=None): + return self.await_(self._connection.tpc_rollback(xid)) + + def tpc_recover(self): + return self.await_(self._connection.tpc_recover()) + class AsyncAdaptFallback_psycopg_connection(AsyncAdapt_psycopg_connection): __slots__ = () diff --git a/libs/sqlalchemy/dialects/postgresql/psycopg2.py b/libs/sqlalchemy/dialects/postgresql/psycopg2.py index 189e6566c..e56eb6d6d 100644 --- a/libs/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/libs/sqlalchemy/dialects/postgresql/psycopg2.py @@ -488,6 +488,7 @@ place within SQLAlchemy's own marshalling logic, and not that of ``psycopg2`` which may be more performant. """ # noqa + from __future__ import annotations import collections.abc as collections_abc @@ -799,35 +800,8 @@ class PGDialect_psycopg2(_PGDialect_common_psycopg): else: cursor.executemany(statement, parameters) - def do_begin_twophase(self, connection, xid): - connection.connection.tpc_begin(xid) - - def do_prepare_twophase(self, connection, xid): - connection.connection.tpc_prepare() - - def _do_twophase(self, dbapi_conn, operation, xid, recover=False): - if recover: - if dbapi_conn.status != self._psycopg2_extensions.STATUS_READY: - dbapi_conn.rollback() - operation(xid) - else: - operation() - - def do_rollback_twophase( - self, connection, xid, is_prepared=True, recover=False - ): - dbapi_conn = connection.connection.dbapi_connection - self._do_twophase( - dbapi_conn, dbapi_conn.tpc_rollback, xid, recover=recover - ) - - def do_commit_twophase( - self, connection, xid, is_prepared=True, recover=False - ): - dbapi_conn = connection.connection.dbapi_connection - self._do_twophase( - dbapi_conn, dbapi_conn.tpc_commit, xid, recover=recover - ) + def _twophase_idle_check(self, dbapi_conn): + return dbapi_conn.status == self._psycopg2_extensions.STATUS_READY @util.memoized_instancemethod def _hstore_oids(self, dbapi_connection): diff --git a/libs/sqlalchemy/dialects/postgresql/psycopg2cffi.py b/libs/sqlalchemy/dialects/postgresql/psycopg2cffi.py index f4dcfc701..cfad97a69 100644 --- a/libs/sqlalchemy/dialects/postgresql/psycopg2cffi.py +++ b/libs/sqlalchemy/dialects/postgresql/psycopg2cffi.py @@ -22,6 +22,7 @@ is as per ``psycopg2``. :mod:`sqlalchemy.dialects.postgresql.psycopg2` """ # noqa + from .psycopg2 import PGDialect_psycopg2 from ... import util diff --git a/libs/sqlalchemy/dialects/sqlite/aiosqlite.py b/libs/sqlalchemy/dialects/sqlite/aiosqlite.py index afd3688ed..4cb4a2c31 100644 --- a/libs/sqlalchemy/dialects/sqlite/aiosqlite.py +++ b/libs/sqlalchemy/dialects/sqlite/aiosqlite.py @@ -77,6 +77,7 @@ based on the kind of SQLite database that's requested: :paramref:`_sa.create_engine.poolclass` parameter. """ # noqa + from __future__ import annotations import asyncio diff --git a/libs/sqlalchemy/dialects/sqlite/base.py b/libs/sqlalchemy/dialects/sqlite/base.py index 35eb74bfa..11c69ef21 100644 --- a/libs/sqlalchemy/dialects/sqlite/base.py +++ b/libs/sqlalchemy/dialects/sqlite/base.py @@ -250,7 +250,7 @@ BEGIN. * **Using SQLAlchemy to emit BEGIN in lieu of SQLite's transaction control** (all Python versions, sqlite3 and aiosqlite) - For older versions of ``sqlite3`` or for cross-compatiblity with older and + For older versions of ``sqlite3`` or for cross-compatibility with older and newer versions, SQLAlchemy can also take over the job of transaction control. This is achieved by using the :meth:`.ConnectionEvents.begin` hook to emit the "BEGIN" command directly, while also disabling SQLite's control @@ -847,13 +847,11 @@ The bug, entirely outside of SQLAlchemy, can be illustrated thusly:: cursor.execute("select x.a, x.b from x") assert [c[0] for c in cursor.description] == ["a", "b"] - cursor.execute( - """ + cursor.execute(""" select x.a, x.b from x where a=1 union select x.a, x.b from x where a=2 - """ - ) + """) assert [c[0] for c in cursor.description] == ["a", "b"], [ c[0] for c in cursor.description ] @@ -888,13 +886,11 @@ to filter these out:: result = conn.exec_driver_sql("select x.a, x.b from x") assert result.keys() == ["a", "b"] - result = conn.exec_driver_sql( - """ + result = conn.exec_driver_sql(""" select x.a, x.b from x where a=1 union select x.a, x.b from x where a=2 - """ - ) + """) assert result.keys() == ["a", "b"] Note that above, even though SQLAlchemy filters out the dots, *both @@ -987,6 +983,7 @@ passed to methods such as :meth:`_schema.MetaData.reflect` or documentation. ''' # noqa + from __future__ import annotations import datetime @@ -2212,7 +2209,7 @@ class SQLiteDialect(default.DefaultDialect): 14, ) - if self.dbapi.sqlite_version_info < (3, 35) or util.pypy: + if self.dbapi.sqlite_version_info < (3, 35): self.update_returning = self.delete_returning = ( self.insert_returning ) = False @@ -2582,7 +2579,7 @@ class SQLiteDialect(default.DefaultDialect): fks = {} for row in pragma_fks: - (numerical_id, rtbl, lcol, rcol) = (row[0], row[2], row[3], row[4]) + numerical_id, rtbl, lcol, rcol = (row[0], row[2], row[3], row[4]) if not rcol: # no referred column, which means it was not named in the diff --git a/libs/sqlalchemy/dialects/sqlite/pysqlcipher.py b/libs/sqlalchemy/dialects/sqlite/pysqlcipher.py index 1a4c251a4..f294a66ee 100644 --- a/libs/sqlalchemy/dialects/sqlite/pysqlcipher.py +++ b/libs/sqlalchemy/dialects/sqlite/pysqlcipher.py @@ -131,16 +131,19 @@ class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite): # pull the info we need from the URL early. Even though URL # is immutable, we don't want any in-place changes to the URL # to affect things - passphrase = url.password or "" - url_query = dict(url.query) + ip = self.identifier_preparer + passphrase = ip.quote_identifier(url.password or "") + query_pragmas = { + prag: ip.quote_identifier(url.query[prag]) + for prag in self.pragmas + if url.query.get(prag) is not None + } def on_connect(conn): cursor = conn.cursor() - cursor.execute('pragma key="%s"' % passphrase) - for prag in self.pragmas: - value = url_query.get(prag, None) - if value is not None: - cursor.execute('pragma %s="%s"' % (prag, value)) + cursor.execute(f"pragma key={passphrase}") + for prag, value in query_pragmas.items(): + cursor.execute(f"pragma {prag}={value}") cursor.close() if super_on_connect: diff --git a/libs/sqlalchemy/dialects/sqlite/pysqlite.py b/libs/sqlalchemy/dialects/sqlite/pysqlite.py index d2e84466c..54fa940a7 100644 --- a/libs/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/libs/sqlalchemy/dialects/sqlite/pysqlite.py @@ -392,6 +392,7 @@ connection when it is created. That is accomplished with an event listener:: print(conn.scalar(text("SELECT UDF()"))) """ # noqa + from __future__ import annotations import math diff --git a/libs/sqlalchemy/engine/_py_processors.py b/libs/sqlalchemy/engine/_py_processors.py index 7b1d6b0db..11c7ef01c 100644 --- a/libs/sqlalchemy/engine/_py_processors.py +++ b/libs/sqlalchemy/engine/_py_processors.py @@ -28,7 +28,6 @@ from typing import Type from typing import TypeVar from typing import Union - _DT = TypeVar( "_DT", bound=Union[datetime.datetime, datetime.time, datetime.date] ) diff --git a/libs/sqlalchemy/engine/base.py b/libs/sqlalchemy/engine/base.py index 26f162b88..af313b878 100644 --- a/libs/sqlalchemy/engine/base.py +++ b/libs/sqlalchemy/engine/base.py @@ -5,6 +5,7 @@ # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php """Defines :class:`_engine.Connection` and :class:`_engine.Engine`.""" + from __future__ import annotations import contextlib @@ -951,7 +952,8 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): :meth:`~.TwoPhaseTransaction.prepare` method. :param xid: the two phase transaction id. If not supplied, a - random id will be generated. + random id will be generated. The accepted type and value depends on + the driver in use. .. seealso:: diff --git a/libs/sqlalchemy/engine/create.py b/libs/sqlalchemy/engine/create.py index 9ab94763b..dee77dab4 100644 --- a/libs/sqlalchemy/engine/create.py +++ b/libs/sqlalchemy/engine/create.py @@ -630,7 +630,7 @@ def create_engine(url: Union[str, _url.URL], **kwargs: Any) -> Engine: dialect = dialect_cls(**dialect_args) # assemble connection arguments - (cargs_tup, _cparams) = dialect.create_connect_args(u) + cargs_tup, _cparams = dialect.create_connect_args(u) cparams = util.immutabledict(_cparams).union(pop_kwarg("connect_args", {})) if "async_fallback" in cparams and util.asbool(cparams["async_fallback"]): diff --git a/libs/sqlalchemy/engine/cursor.py b/libs/sqlalchemy/engine/cursor.py index 801fa4ff9..11f4c521e 100644 --- a/libs/sqlalchemy/engine/cursor.py +++ b/libs/sqlalchemy/engine/cursor.py @@ -8,7 +8,6 @@ """Define cursor-specific result set constructs including :class:`.CursorResult`.""" - from __future__ import annotations import collections @@ -56,7 +55,6 @@ from ..util.typing import Final from ..util.typing import Literal from ..util.typing import Self - if typing.TYPE_CHECKING: from .base import Connection from .default import DefaultExecutionContext @@ -185,9 +183,15 @@ class CursorResultMetaData(ResultMetaData): return key in self._keymap def _for_freeze(self) -> ResultMetaData: + ambiguous = { + rec[MD_LOOKUP_KEY] + for rec in self._keymap.values() + if rec[MD_INDEX] is None + } return SimpleResultMetaData( self._keys, extra=[self._keymap[key][MD_OBJECTS] for key in self._keys], + _ambiguous_keys=frozenset(ambiguous) if ambiguous else None, ) def _make_new_metadata( @@ -2218,6 +2222,11 @@ class CursorResult(Result[_T]): using the MSSQL / pyodbc dialect a SELECT is emitted inline in order to retrieve an inserted primary key value. + .. seealso:: + + :meth:`.Result.close` + + :attr:`.Result.closed` """ return self._metadata.returns_rows diff --git a/libs/sqlalchemy/engine/interfaces.py b/libs/sqlalchemy/engine/interfaces.py index ac082b0d3..fc030eda2 100644 --- a/libs/sqlalchemy/engine/interfaces.py +++ b/libs/sqlalchemy/engine/interfaces.py @@ -1098,7 +1098,7 @@ class Dialect(EventTarget): If the above construct is established on the PostgreSQL dialect, the :class:`.Index` construct will now accept the keyword arguments - ``postgresql_using``, ``postgresql_where``, nad ``postgresql_ops``. + ``postgresql_using``, ``postgresql_where``, and ``postgresql_ops``. Any other argument specified to the constructor of :class:`.Index` which is prefixed with ``postgresql_`` will raise :class:`.ArgumentError`. diff --git a/libs/sqlalchemy/engine/mock.py b/libs/sqlalchemy/engine/mock.py index a5173b9d7..adcf6eb36 100644 --- a/libs/sqlalchemy/engine/mock.py +++ b/libs/sqlalchemy/engine/mock.py @@ -19,7 +19,6 @@ from typing import Union from . import url as _url from .. import util - if typing.TYPE_CHECKING: from .base import Engine from .interfaces import _CoreAnyExecuteParams diff --git a/libs/sqlalchemy/engine/processors.py b/libs/sqlalchemy/engine/processors.py index 0888257f2..aa854aa69 100644 --- a/libs/sqlalchemy/engine/processors.py +++ b/libs/sqlalchemy/engine/processors.py @@ -12,6 +12,7 @@ processors. They all share one common characteristic: None is passed through unchanged. """ + from __future__ import annotations import typing diff --git a/libs/sqlalchemy/engine/reflection.py b/libs/sqlalchemy/engine/reflection.py index c976ae0f1..b3659acf9 100644 --- a/libs/sqlalchemy/engine/reflection.py +++ b/libs/sqlalchemy/engine/reflection.py @@ -24,6 +24,7 @@ methods such as get_table_names, get_columns, etc. use the key 'name'. So for most return values, each record will have a 'name' attribute.. """ + from __future__ import annotations import contextlib diff --git a/libs/sqlalchemy/engine/result.py b/libs/sqlalchemy/engine/result.py index b0788a607..341554e59 100644 --- a/libs/sqlalchemy/engine/result.py +++ b/libs/sqlalchemy/engine/result.py @@ -267,6 +267,7 @@ class SimpleResultMetaData(ResultMetaData): "_translated_indexes", "_unique_filters", "_key_to_index", + "_ambiguous_keys", ) _keys: Sequence[str] @@ -279,6 +280,7 @@ class SimpleResultMetaData(ResultMetaData): _tuplefilter: Optional[_TupleGetterType] = None, _translated_indexes: Optional[Sequence[int]] = None, _unique_filters: Optional[Sequence[Callable[[Any], Any]]] = None, + _ambiguous_keys: Optional[frozenset[str]] = None, ): self._keys = list(keys) self._tuplefilter = _tuplefilter @@ -300,8 +302,15 @@ class SimpleResultMetaData(ResultMetaData): self._keymap = {key: rec for keys, rec in recs_names for key in keys} + if _ambiguous_keys: + for name in _ambiguous_keys.intersection(self._keymap): + rec = self._keymap[name] + self._keymap[name] = (None,) + rec[1:] + self._processors = _processors + self._ambiguous_keys = _ambiguous_keys + self._key_to_index = self._make_key_to_index(self._keymap, 0) def _has_key(self, key: object) -> bool: @@ -318,12 +327,14 @@ class SimpleResultMetaData(ResultMetaData): self._keys, extra=[self._keymap[key][2] for key in self._keys], _unique_filters=unique_filters, + _ambiguous_keys=self._ambiguous_keys, ) def __getstate__(self) -> Dict[str, Any]: return { "_keys": self._keys, "_translated_indexes": self._translated_indexes, + "_ambiguous_keys": self._ambiguous_keys, } def __setstate__(self, state: Dict[str, Any]) -> None: @@ -336,6 +347,7 @@ class SimpleResultMetaData(ResultMetaData): state["_keys"], _translated_indexes=_translated_indexes, _tuplefilter=_tuplefilter, + _ambiguous_keys=state.get("_ambiguous_keys"), ) def _index_for_key(self, key: Any, raiseerr: bool = True) -> int: @@ -346,9 +358,21 @@ class SimpleResultMetaData(ResultMetaData): except KeyError as ke: rec = self._key_fallback(key, ke, raiseerr) + if rec[0] is None: + self._raise_for_ambiguous_column_name(rec) return rec[0] # type: ignore[no-any-return] + def _raise_for_ambiguous_column_name( + self, rec: _KeyMapRecType + ) -> NoReturn: + raise exc.InvalidRequestError( + "Ambiguous column name '%s' in " + "result set column descriptions" % rec[1] + ) + def _indexes_for_keys(self, keys: Sequence[Any]) -> Sequence[int]: + # only used by the ORM with Column objects; does not need + # ambiguous column name support return [self._keymap[key][0] for key in keys] def _metadata_for_keys( @@ -363,6 +387,9 @@ class SimpleResultMetaData(ResultMetaData): except KeyError as ke: rec = self._key_fallback(key, ke, True) + if rec[0] is None: + self._raise_for_ambiguous_column_name(rec) + yield rec def _reduce(self, keys: Sequence[Any]) -> ResultMetaData: @@ -966,7 +993,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): self.close() def close(self) -> None: - """close this :class:`_engine.Result`. + """Hard close this :class:`_engine.Result`. The behavior of this method is implementation specific, and is not implemented by default. The method should generally end @@ -993,9 +1020,19 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): @property def closed(self) -> bool: - """return ``True`` if this :class:`_engine.Result` reports .closed + """Return ``True`` if this :class:`_engine.Result` was **hard closed** + by explicitly calling the :meth:`close` method. - .. versionadded:: 1.4.43 + The attribute is **not** True if the :class:`_engine.Result` was only + **soft closed**; a "soft close" is the style of close that takes place + for example when the :class:`.CursorResult` is returned for a DML + only statement without RETURNING, or when all result rows are fetched. + + .. seealso:: + + :attr:`.CursorResult.returns_rows` - attribute specific to + :class:`.CursorResult` which indicates if the result is one that + may return zero or more rows """ raise NotImplementedError() @@ -1661,10 +1698,13 @@ class FilterResult(ResultInternal[_R]): @property def closed(self) -> bool: - """Return ``True`` if the underlying :class:`_engine.Result` reports - closed + """Return ``True`` if this :class:`_engine.Result` being + proxied by this :class:`_engine.FilterResult` was + **hard closed** by explicitly calling the :meth:`_engine.Result.close` + method. - .. versionadded:: 1.4.43 + This is a direct proxy for the :attr:`_engine.Result.closed` attribute; + see that attribute for details. """ return self._real_result.closed @@ -2239,10 +2279,20 @@ class IteratorResult(Result[_TP]): @property def closed(self) -> bool: - """Return ``True`` if this :class:`_engine.IteratorResult` has - been closed + """Return ``True`` if this :class:`_engine.IteratorResult` was + **hard closed** by explicitly calling the :meth:`_engine.Result.close` + method. - .. versionadded:: 1.4.43 + The attribute is **not** True if the :class:`_engine.Result` was only + **soft closed**; a "soft close" is the style of close that takes place + for example when the :class:`.CursorResult` is returned for a DML + only statement without RETURNING, or when all result rows are fetched. + + .. seealso:: + + :attr:`.CursorResult.returns_rows` - attribute specific to + :class:`.CursorResult` which indicates if the result is one that + may return zero or more rows """ return self._hard_closed diff --git a/libs/sqlalchemy/event/api.py b/libs/sqlalchemy/event/api.py index 38b2716b8..2f8547df5 100644 --- a/libs/sqlalchemy/event/api.py +++ b/libs/sqlalchemy/event/api.py @@ -6,6 +6,7 @@ # the MIT License: https://www.opensource.org/licenses/mit-license.php """Public API functions for the event system.""" + from __future__ import annotations from typing import Any @@ -18,7 +19,6 @@ from .registry import _ListenerFnType from .. import exc from .. import util - CANCEL = util.symbol("CANCEL") NO_RETVAL = util.symbol("NO_RETVAL") diff --git a/libs/sqlalchemy/event/attr.py b/libs/sqlalchemy/event/attr.py index e09cb4d5a..2727bfaa5 100644 --- a/libs/sqlalchemy/event/attr.py +++ b/libs/sqlalchemy/event/attr.py @@ -28,6 +28,7 @@ as well as support for subclass propagation (e.g. events assigned to ``Pool`` vs. ``QueuePool``) are all implemented here. """ + from __future__ import annotations import collections diff --git a/libs/sqlalchemy/event/base.py b/libs/sqlalchemy/event/base.py index 9f6e410d6..239e4d761 100644 --- a/libs/sqlalchemy/event/base.py +++ b/libs/sqlalchemy/event/base.py @@ -15,6 +15,7 @@ at the class level of a particular ``_Dispatch`` class as well as within instances of ``_Dispatch``. """ + from __future__ import annotations import typing diff --git a/libs/sqlalchemy/event/legacy.py b/libs/sqlalchemy/event/legacy.py index 63fc1ee9a..b2fc4ca70 100644 --- a/libs/sqlalchemy/event/legacy.py +++ b/libs/sqlalchemy/event/legacy.py @@ -9,6 +9,7 @@ generation of deprecation notes and docstrings. """ + from __future__ import annotations import typing diff --git a/libs/sqlalchemy/event/registry.py b/libs/sqlalchemy/event/registry.py index 22be5628e..a6b07c24b 100644 --- a/libs/sqlalchemy/event/registry.py +++ b/libs/sqlalchemy/event/registry.py @@ -14,6 +14,7 @@ membership in all those collections can be revoked at once, based on an equivalent :class:`._EventKey`. """ + from __future__ import annotations import collections diff --git a/libs/sqlalchemy/exc.py b/libs/sqlalchemy/exc.py index c31c16b4d..ed928eaab 100644 --- a/libs/sqlalchemy/exc.py +++ b/libs/sqlalchemy/exc.py @@ -12,6 +12,7 @@ raised as a result of DBAPI exceptions are all subclasses of :exc:`.DBAPIError`. """ + from __future__ import annotations import typing diff --git a/libs/sqlalchemy/ext/__init__.py b/libs/sqlalchemy/ext/__init__.py index 5d61ac765..a72b4aec5 100644 --- a/libs/sqlalchemy/ext/__init__.py +++ b/libs/sqlalchemy/ext/__init__.py @@ -7,5 +7,4 @@ from .. import util as _sa_util - _sa_util.preloaded.import_prefix("sqlalchemy.ext") diff --git a/libs/sqlalchemy/ext/associationproxy.py b/libs/sqlalchemy/ext/associationproxy.py index 5fbabe578..c1f0cc8bb 100644 --- a/libs/sqlalchemy/ext/associationproxy.py +++ b/libs/sqlalchemy/ext/associationproxy.py @@ -13,6 +13,7 @@ transparent proxied access to the endpoint of an association object. See the example ``examples/association/proxied_association.py``. """ + from __future__ import annotations import operator diff --git a/libs/sqlalchemy/ext/asyncio/engine.py b/libs/sqlalchemy/ext/asyncio/engine.py index 263bd75cf..57d6fcc6f 100644 --- a/libs/sqlalchemy/ext/asyncio/engine.py +++ b/libs/sqlalchemy/ext/asyncio/engine.py @@ -187,7 +187,7 @@ class AsyncConnectable: ], ) # "Class has incompatible disjoint bases" - no idea -class AsyncConnection( # type:ignore[misc] +class AsyncConnection( # type: ignore[misc] ProxyComparable[Connection], StartableContext["AsyncConnection"], AsyncConnectable, diff --git a/libs/sqlalchemy/ext/asyncio/session.py b/libs/sqlalchemy/ext/asyncio/session.py index c547b9781..003954b4a 100644 --- a/libs/sqlalchemy/ext/asyncio/session.py +++ b/libs/sqlalchemy/ext/asyncio/session.py @@ -41,7 +41,6 @@ from ...util.concurrency import greenlet_spawn from ...util.typing import Concatenate from ...util.typing import ParamSpec - if TYPE_CHECKING: from .engine import AsyncConnection from .engine import AsyncEngine diff --git a/libs/sqlalchemy/ext/automap.py b/libs/sqlalchemy/ext/automap.py index a810b82e8..289ef1915 100644 --- a/libs/sqlalchemy/ext/automap.py +++ b/libs/sqlalchemy/ext/automap.py @@ -710,6 +710,7 @@ be applied as:: """ # noqa + from __future__ import annotations import dataclasses diff --git a/libs/sqlalchemy/ext/baked.py b/libs/sqlalchemy/ext/baked.py index 2377c7d12..da9acd143 100644 --- a/libs/sqlalchemy/ext/baked.py +++ b/libs/sqlalchemy/ext/baked.py @@ -28,7 +28,6 @@ from ..sql import func from ..sql import literal_column from ..sql import util as sql_util - log = logging.getLogger(__name__) diff --git a/libs/sqlalchemy/ext/compiler.py b/libs/sqlalchemy/ext/compiler.py index 2de5001ee..1c10ba423 100644 --- a/libs/sqlalchemy/ext/compiler.py +++ b/libs/sqlalchemy/ext/compiler.py @@ -481,6 +481,7 @@ Example usage:: ) """ + from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/ext/declarative/extensions.py b/libs/sqlalchemy/ext/declarative/extensions.py index d119aaebe..d2a8d8b09 100644 --- a/libs/sqlalchemy/ext/declarative/extensions.py +++ b/libs/sqlalchemy/ext/declarative/extensions.py @@ -8,6 +8,7 @@ """Public API functions and helpers for declarative.""" + from __future__ import annotations import collections diff --git a/libs/sqlalchemy/ext/horizontal_shard.py b/libs/sqlalchemy/ext/horizontal_shard.py index 4722353f3..e0bb658f8 100644 --- a/libs/sqlalchemy/ext/horizontal_shard.py +++ b/libs/sqlalchemy/ext/horizontal_shard.py @@ -23,6 +23,7 @@ the source distribution. """ + from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/ext/hybrid.py b/libs/sqlalchemy/ext/hybrid.py index f9a9459bf..cfcbb35a1 100644 --- a/libs/sqlalchemy/ext/hybrid.py +++ b/libs/sqlalchemy/ext/hybrid.py @@ -1436,7 +1436,7 @@ class hybrid_property(interfaces.InspectionAttrInfo, ORMDescriptor[_T]): name = self.__name__ break else: - name = attributes._UNKNOWN_ATTR_KEY # type: ignore[assignment] + name = attributes._UNKNOWN_ATTR_KEY # type: ignore[assignment,unused-ignore] # noqa: E501 return cast( "_HybridClassLevelAccessor[_T]", diff --git a/libs/sqlalchemy/ext/instrumentation.py b/libs/sqlalchemy/ext/instrumentation.py index 08d55dbdb..79c83f8f4 100644 --- a/libs/sqlalchemy/ext/instrumentation.py +++ b/libs/sqlalchemy/ext/instrumentation.py @@ -23,6 +23,7 @@ For examples of how the instrumentation extension is used, see the example :ref:`examples_instrumentation`. """ + import weakref from .. import util @@ -39,7 +40,6 @@ from ..orm.instrumentation import _default_state_getter from ..orm.instrumentation import ClassManager from ..orm.instrumentation import InstrumentationFactory - INSTRUMENTATION_MANAGER = "__sa_instrumentation_manager__" """Attribute, elects custom instrumentation when present on a mapped class. diff --git a/libs/sqlalchemy/ext/mutable.py b/libs/sqlalchemy/ext/mutable.py index 43967b784..32525d918 100644 --- a/libs/sqlalchemy/ext/mutable.py +++ b/libs/sqlalchemy/ext/mutable.py @@ -1038,7 +1038,7 @@ class MutableSet(Mutable, Set[_T]): self.symmetric_difference_update(other) return self - def __isub__(self, other: AbstractSet[object]) -> MutableSet[_T]: # type: ignore[misc] # noqa: E501 + def __isub__(self, other: AbstractSet[object]) -> MutableSet[_T]: # type: ignore[misc,unused-ignore] # noqa: E501 self.difference_update(other) return self @@ -1050,7 +1050,7 @@ class MutableSet(Mutable, Set[_T]): set.remove(self, elem) self.changed() - def discard(self, elem: _T) -> None: + def discard(self, elem: _T) -> None: # type: ignore[override,unused-ignore] # noqa: E501 set.discard(self, elem) self.changed() diff --git a/libs/sqlalchemy/ext/mypy/plugin.py b/libs/sqlalchemy/ext/mypy/plugin.py index 73b0d6d26..fe2ab07a6 100644 --- a/libs/sqlalchemy/ext/mypy/plugin.py +++ b/libs/sqlalchemy/ext/mypy/plugin.py @@ -9,6 +9,7 @@ Mypy plugin for SQLAlchemy ORM. """ + from __future__ import annotations from typing import Callable diff --git a/libs/sqlalchemy/ext/orderinglist.py b/libs/sqlalchemy/ext/orderinglist.py index fcd483cf5..489a3eff2 100644 --- a/libs/sqlalchemy/ext/orderinglist.py +++ b/libs/sqlalchemy/ext/orderinglist.py @@ -126,6 +126,7 @@ start numbering at 1 or some other integer, provide ``count_from=1``. """ + from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/ext/serializer.py b/libs/sqlalchemy/ext/serializer.py index 2fb022c34..92aae7aa7 100644 --- a/libs/sqlalchemy/ext/serializer.py +++ b/libs/sqlalchemy/ext/serializer.py @@ -82,7 +82,6 @@ from ..orm.session import Session from ..util import b64decode from ..util import b64encode - __all__ = ["Serializer", "Deserializer", "dumps", "loads"] diff --git a/libs/sqlalchemy/future/__init__.py b/libs/sqlalchemy/future/__init__.py index 3a0641864..dfe828f66 100644 --- a/libs/sqlalchemy/future/__init__.py +++ b/libs/sqlalchemy/future/__init__.py @@ -10,6 +10,7 @@ this module is legacy as 2.0 APIs are now standard. """ + from .engine import Connection as Connection from .engine import create_engine as create_engine from .engine import Engine as Engine diff --git a/libs/sqlalchemy/inspection.py b/libs/sqlalchemy/inspection.py index c6380c7ca..21898c1f2 100644 --- a/libs/sqlalchemy/inspection.py +++ b/libs/sqlalchemy/inspection.py @@ -28,6 +28,7 @@ tools which build on top of SQLAlchemy configurations to be constructed in a forwards-compatible way. """ + from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/log.py b/libs/sqlalchemy/log.py index 732708942..875a99c58 100644 --- a/libs/sqlalchemy/log.py +++ b/libs/sqlalchemy/log.py @@ -17,6 +17,7 @@ and :class:`_pool.Pool` objects, corresponds to a logger specific to that instance only. """ + from __future__ import annotations import logging @@ -33,7 +34,6 @@ from .util import py311 from .util import py38 from .util.typing import Literal - if py38: STACKLEVEL = True # needed as of py3.11.0b1 @@ -52,7 +52,7 @@ _EchoFlagType = Union[None, bool, Literal["debug"]] # logging being enabled for 'sqlalchemy'. rootlogger = logging.getLogger("sqlalchemy") if rootlogger.level == logging.NOTSET: - rootlogger.setLevel(logging.WARN) + rootlogger.setLevel(logging.WARNING) def _add_default_handler(logger: logging.Logger) -> None: diff --git a/libs/sqlalchemy/orm/collections.py b/libs/sqlalchemy/orm/collections.py index 742ebfb05..01e6e6370 100644 --- a/libs/sqlalchemy/orm/collections.py +++ b/libs/sqlalchemy/orm/collections.py @@ -105,6 +105,7 @@ The owning object and :class:`.CollectionAttributeImpl` are also reachable through the adapter, allowing for some very sophisticated behavior. """ + from __future__ import annotations import operator diff --git a/libs/sqlalchemy/orm/context.py b/libs/sqlalchemy/orm/context.py index 9acc59314..43e3bb465 100644 --- a/libs/sqlalchemy/orm/context.py +++ b/libs/sqlalchemy/orm/context.py @@ -2565,7 +2565,7 @@ def _determine_last_joined_entity( if not setup_joins: return None - (target, onclause, from_, flags) = setup_joins[-1] + target, onclause, from_, flags = setup_joins[-1] if isinstance( target, diff --git a/libs/sqlalchemy/orm/decl_api.py b/libs/sqlalchemy/orm/decl_api.py index 9f455d602..84019884b 100644 --- a/libs/sqlalchemy/orm/decl_api.py +++ b/libs/sqlalchemy/orm/decl_api.py @@ -95,6 +95,7 @@ if TYPE_CHECKING: from ..sql.type_api import _MatchedOnType _T = TypeVar("_T", bound=Any) +_T_co = TypeVar("_T_co", bound=Any, covariant=True) _TT = TypeVar("_TT", bound=Any) @@ -104,7 +105,7 @@ _TypeAnnotationMapType = Mapping[Any, "_TypeEngineArgument[Any]"] _MutableTypeAnnotationMapType = Dict[Any, "_TypeEngineArgument[Any]"] _DeclaredAttrDecorated = Callable[ - ..., Union[Mapped[_T], ORMDescriptor[_T], SQLCoreOperations[_T]] + ..., Union[Mapped[_T_co], ORMDescriptor[_T_co], SQLCoreOperations[_T_co]] ] @@ -329,7 +330,7 @@ class _declared_directive(_declared_attr_common, Generic[_T]): ... -class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common): +class declared_attr(interfaces._MappedAttribute[_T_co], _declared_attr_common): """Mark a class-level method as representing the definition of a mapped property or Declarative directive. @@ -426,7 +427,7 @@ class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common): def __init__( self, - fn: _DeclaredAttrDecorated[_T], + fn: _DeclaredAttrDecorated[_T_co], cascading: bool = False, ): ... @@ -441,17 +442,17 @@ class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common): @overload def __get__( self, instance: None, owner: Any - ) -> InstrumentedAttribute[_T]: ... + ) -> InstrumentedAttribute[_T_co]: ... @overload - def __get__(self, instance: object, owner: Any) -> _T: ... + def __get__(self, instance: object, owner: Any) -> _T_co: ... def __get__( self, instance: Optional[object], owner: Any - ) -> Union[InstrumentedAttribute[_T], _T]: ... + ) -> Union[InstrumentedAttribute[_T_co], _T_co]: ... @hybridmethod - def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T]: + def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T_co]: return _stateful_declared_attr(**kw) @hybridproperty @@ -460,24 +461,26 @@ class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common): return _declared_directive # type: ignore @hybridproperty - def cascading(cls) -> _stateful_declared_attr[_T]: + def cascading(cls) -> _stateful_declared_attr[_T_co]: # see mapping_api.rst for docstring return cls._stateful(cascading=True) -class _stateful_declared_attr(declared_attr[_T]): +class _stateful_declared_attr(declared_attr[_T_co]): kw: Dict[str, Any] def __init__(self, **kw: Any): self.kw = kw @hybridmethod - def _stateful(self, **kw: Any) -> _stateful_declared_attr[_T]: + def _stateful(self, **kw: Any) -> _stateful_declared_attr[_T_co]: new_kw = self.kw.copy() new_kw.update(kw) return _stateful_declared_attr(**new_kw) - def __call__(self, fn: _DeclaredAttrDecorated[_T]) -> declared_attr[_T]: + def __call__( + self, fn: _DeclaredAttrDecorated[_T_co] + ) -> declared_attr[_T_co]: return declared_attr(fn, **self.kw) diff --git a/libs/sqlalchemy/orm/dependency.py b/libs/sqlalchemy/orm/dependency.py index 0019bcd68..f86136443 100644 --- a/libs/sqlalchemy/orm/dependency.py +++ b/libs/sqlalchemy/orm/dependency.py @@ -190,7 +190,7 @@ class DependencyProcessor: if child_state not in uow.states: child_action = (None, None) else: - (deleted, listonly) = uow.states[child_state] + deleted, listonly = uow.states[child_state] if deleted: child_action = ( unitofwork.DeleteState(uow, child_state), diff --git a/libs/sqlalchemy/orm/descriptor_props.py b/libs/sqlalchemy/orm/descriptor_props.py index a493373e6..460c4d934 100644 --- a/libs/sqlalchemy/orm/descriptor_props.py +++ b/libs/sqlalchemy/orm/descriptor_props.py @@ -10,6 +10,7 @@ that exist as configurational elements, but don't participate as actively in the load/persist ORM loop. """ + from __future__ import annotations from dataclasses import is_dataclass @@ -57,7 +58,6 @@ from ..util.typing import get_args from ..util.typing import is_fwd_ref from ..util.typing import is_pep593 - if typing.TYPE_CHECKING: from ._typing import _InstanceDict from ._typing import _RegistryType diff --git a/libs/sqlalchemy/orm/dynamic.py b/libs/sqlalchemy/orm/dynamic.py index e8a97d6cc..2dc6b82be 100644 --- a/libs/sqlalchemy/orm/dynamic.py +++ b/libs/sqlalchemy/orm/dynamic.py @@ -24,6 +24,7 @@ from typing import Iterable from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Tuple from typing import Type from typing import TYPE_CHECKING @@ -44,7 +45,6 @@ from .writeonly import WriteOnlyLoader from .. import util from ..engine import result - if TYPE_CHECKING: from . import QueryableAttribute from .mapper import Mapper @@ -176,7 +176,13 @@ class AppenderMixin(AbstractCollectionWriter[_T]): def __iter__(self) -> Iterator[_T]: ... - def __getitem__(self, index: Any) -> Union[_T, List[_T]]: + @overload + def __getitem__(self, index: int) -> _T: ... + + @overload + def __getitem__(self, index: slice) -> List[_T]: ... + + def __getitem__(self, index: Union[int, slice]) -> Union[_T, List[_T]]: sess = self.session if sess is None: return self.attr._get_collection_history( @@ -184,7 +190,7 @@ class AppenderMixin(AbstractCollectionWriter[_T]): PassiveFlag.PASSIVE_NO_INITIALIZE, ).indexed(index) else: - return self._generate(sess).__getitem__(index) # type: ignore[no-any-return] # noqa: E501 + return self._generate(sess).__getitem__(index) def count(self) -> int: sess = self.session diff --git a/libs/sqlalchemy/orm/evaluator.py b/libs/sqlalchemy/orm/evaluator.py index 392d726e9..9d4652f11 100644 --- a/libs/sqlalchemy/orm/evaluator.py +++ b/libs/sqlalchemy/orm/evaluator.py @@ -16,7 +16,6 @@ This module is **private, for internal use by SQLAlchemy**. """ - from __future__ import annotations from typing import Type diff --git a/libs/sqlalchemy/orm/events.py b/libs/sqlalchemy/orm/events.py index 2affb0002..ea4f16422 100644 --- a/libs/sqlalchemy/orm/events.py +++ b/libs/sqlalchemy/orm/events.py @@ -6,6 +6,7 @@ # the MIT License: https://www.opensource.org/licenses/mit-license.php """ORM event interfaces.""" + from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/orm/instrumentation.py b/libs/sqlalchemy/orm/instrumentation.py index 7091d369b..59c145392 100644 --- a/libs/sqlalchemy/orm/instrumentation.py +++ b/libs/sqlalchemy/orm/instrumentation.py @@ -30,7 +30,6 @@ alternate instrumentation forms. """ - from __future__ import annotations from typing import Any diff --git a/libs/sqlalchemy/orm/loading.py b/libs/sqlalchemy/orm/loading.py index fb7efa76e..cfd184978 100644 --- a/libs/sqlalchemy/orm/loading.py +++ b/libs/sqlalchemy/orm/loading.py @@ -109,7 +109,7 @@ def instances(cursor: CursorResult[Any], context: QueryContext) -> Result[Any]: ) try: - (process, labels, extra) = list( + process, labels, extra = list( zip( *[ query_entity.row_processor(context, cursor) @@ -561,7 +561,7 @@ def load_on_pk_identity( if primary_key_identity is not None: mapper = query._propagate_attrs["plugin_subject"] - (_get_clause, _get_params) = mapper._get_clause + _get_clause, _get_params = mapper._get_clause # None present in ident - turn those comparisons # into "IS NULL" diff --git a/libs/sqlalchemy/orm/mapper.py b/libs/sqlalchemy/orm/mapper.py index 8ee0ac4ff..a7c361a7e 100644 --- a/libs/sqlalchemy/orm/mapper.py +++ b/libs/sqlalchemy/orm/mapper.py @@ -15,6 +15,7 @@ This is a semi-private module; the main configurational API of the ORM is available in :class:`~sqlalchemy.orm.`. """ + from __future__ import annotations from collections import deque diff --git a/libs/sqlalchemy/orm/path_registry.py b/libs/sqlalchemy/orm/path_registry.py index 97065d585..8588477cc 100644 --- a/libs/sqlalchemy/orm/path_registry.py +++ b/libs/sqlalchemy/orm/path_registry.py @@ -699,13 +699,28 @@ class AbstractEntityRegistry(CreatesToken): # This is basically the only place that the "is_unnatural" flag # actually changes behavior. if parent.path and (self.is_aliased_class or parent.is_unnatural): - # this is an infrequent code path used only for loader strategies - # that also make use of of_type(). - if entity.mapper.isa(parent.natural_path[-1].mapper): # type: ignore # noqa: E501 + # this is an infrequent code path used for loader strategies that + # also make use of of_type() or other intricate polymorphic + # base/subclass combinations + parent_natural_entity = parent.natural_path[-1] + + if entity.mapper.isa( + parent_natural_entity.mapper # type: ignore + ) or parent_natural_entity.mapper.isa( # type: ignore + entity.mapper + ): + # when the entity mapper and parent mapper are in an + # inheritance relationship, use entity.mapper in natural_path. + # First case: entity.mapper inherits from parent mapper (e.g., + # accessing a subclass mapper through parent path). Second case + # (issue #13193): parent mapper inherits from entity.mapper + # (e.g., parent path has Sub(Base) but we're accessing with + # Base where Base.related is declared, so use Base in + # natural_path). self.natural_path = parent.natural_path + (entity.mapper,) else: self.natural_path = parent.natural_path + ( - parent.natural_path[-1].entity, # type: ignore + parent_natural_entity.entity, # type: ignore ) # it seems to make sense that since these paths get mixed up # with statements that are cached or not, we should make diff --git a/libs/sqlalchemy/orm/persistence.py b/libs/sqlalchemy/orm/persistence.py index 0b7a218ca..268065083 100644 --- a/libs/sqlalchemy/orm/persistence.py +++ b/libs/sqlalchemy/orm/persistence.py @@ -15,6 +15,7 @@ The functions here are called only by the unit of work functions in unitofwork.py. """ + from __future__ import annotations from itertools import chain diff --git a/libs/sqlalchemy/orm/properties.py b/libs/sqlalchemy/orm/properties.py index 65d3fcc3f..118b5158f 100644 --- a/libs/sqlalchemy/orm/properties.py +++ b/libs/sqlalchemy/orm/properties.py @@ -912,10 +912,9 @@ class MappedColumn( ) else: detail = ( - f"""none of { - ", ".join(f"'{t}'" for t in checks) - } """ - "are resolvable by the registry" + "none of " + + ", ".join(f"'{t}'" for t in checks) + + " are resolvable by the registry" ) raise orm_exc.MappedAnnotationError( "Could not locate SQLAlchemy Core type when resolving " diff --git a/libs/sqlalchemy/orm/query.py b/libs/sqlalchemy/orm/query.py index 5552b1168..696b04276 100644 --- a/libs/sqlalchemy/orm/query.py +++ b/libs/sqlalchemy/orm/query.py @@ -18,6 +18,7 @@ ORM session, whereas the ``Select`` construct interacts directly with the database to return iterable result sets. """ + from __future__ import annotations import collections.abc as collections_abc @@ -93,7 +94,7 @@ from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..sql.selectable import SelectLabelStyle from ..util.typing import Literal from ..util.typing import Self - +from ..util.typing import SupportsIndex if TYPE_CHECKING: from ._typing import _EntityType @@ -2558,6 +2559,12 @@ class Query( self._set_select_from(from_obj, False) return self + @overload + def __getitem__(self, item: slice) -> List[_T]: ... + + @overload + def __getitem__(self, item: SupportsIndex) -> _T: ... + def __getitem__(self, item: Any) -> Any: return orm_util._getitem( self, diff --git a/libs/sqlalchemy/orm/relationships.py b/libs/sqlalchemy/orm/relationships.py index b185ef604..e2a02bd01 100644 --- a/libs/sqlalchemy/orm/relationships.py +++ b/libs/sqlalchemy/orm/relationships.py @@ -13,6 +13,7 @@ SQL annotation and aliasing behavior focused on the `primaryjoin` and `secondaryjoin` aspects of :func:`_orm.relationship`. """ + from __future__ import annotations import collections @@ -728,7 +729,7 @@ class RelationshipProperty( many-to-one comparisons: * Comparisons against collections are not supported. - Use :meth:`~.Relationship.Comparator.contains`. + Use :meth:`~.RelationshipProperty.Comparator.contains`. * Compared to a scalar one-to-many, will produce a clause that compares the target columns in the parent to the given target. @@ -739,7 +740,7 @@ class RelationshipProperty( queries that go beyond simple AND conjunctions of comparisons, such as those which use OR. Use explicit joins, outerjoins, or - :meth:`~.Relationship.Comparator.has` for + :meth:`~.RelationshipProperty.Comparator.has` for more comprehensive non-many-to-one scalar membership tests. * Comparisons against ``None`` given in a one-to-many @@ -891,12 +892,12 @@ class RelationshipProperty( EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id AND related.x=2) - Because :meth:`~.Relationship.Comparator.any` uses + Because :meth:`~.RelationshipProperty.Comparator.any` uses a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. - :meth:`~.Relationship.Comparator.any` is particularly + :meth:`~.RelationshipProperty.Comparator.any` is particularly useful for testing for empty collections:: session.query(MyClass).filter(~MyClass.somereference.any()) @@ -909,10 +910,10 @@ class RelationshipProperty( NOT (EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id)) - :meth:`~.Relationship.Comparator.any` is only + :meth:`~.RelationshipProperty.Comparator.any` is only valid for collections, i.e. a :func:`_orm.relationship` that has ``uselist=True``. For scalar references, - use :meth:`~.Relationship.Comparator.has`. + use :meth:`~.RelationshipProperty.Comparator.has`. """ if not self.property.uselist: @@ -945,15 +946,15 @@ class RelationshipProperty( EXISTS (SELECT 1 FROM related WHERE related.id==my_table.related_id AND related.x=2) - Because :meth:`~.Relationship.Comparator.has` uses + Because :meth:`~.RelationshipProperty.Comparator.has` uses a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. - :meth:`~.Relationship.Comparator.has` is only + :meth:`~.RelationshipProperty.Comparator.has` is only valid for scalar references, i.e. a :func:`_orm.relationship` that has ``uselist=False``. For collection references, - use :meth:`~.Relationship.Comparator.any`. + use :meth:`~.RelationshipProperty.Comparator.any`. """ if self.property.uselist: @@ -968,7 +969,7 @@ class RelationshipProperty( """Return a simple expression that tests a collection for containment of a particular item. - :meth:`~.Relationship.Comparator.contains` is + :meth:`~.RelationshipProperty.Comparator.contains` is only valid for a collection, i.e. a :func:`_orm.relationship` that implements one-to-many or many-to-many with ``uselist=True``. @@ -987,12 +988,12 @@ class RelationshipProperty( Where ```` is the value of the foreign key attribute on ``other`` which refers to the primary key of its parent object. From this it follows that - :meth:`~.Relationship.Comparator.contains` is + :meth:`~.RelationshipProperty.Comparator.contains` is very useful when used with simple one-to-many operations. For many-to-many operations, the behavior of - :meth:`~.Relationship.Comparator.contains` + :meth:`~.RelationshipProperty.Comparator.contains` has more caveats. The association table will be rendered in the statement, producing an "implicit" join, that is, includes multiple tables in the FROM @@ -1011,14 +1012,14 @@ class RelationshipProperty( Where ```` would be the primary key of ``other``. From the above, it is clear that - :meth:`~.Relationship.Comparator.contains` + :meth:`~.RelationshipProperty.Comparator.contains` will **not** work with many-to-many collections when used in queries that move beyond simple AND conjunctions, such as multiple - :meth:`~.Relationship.Comparator.contains` + :meth:`~.RelationshipProperty.Comparator.contains` expressions joined by OR. In such cases subqueries or explicit "outer joins" will need to be used instead. - See :meth:`~.Relationship.Comparator.any` for + See :meth:`~.RelationshipProperty.Comparator.any` for a less-performant alternative using EXISTS, or refer to :meth:`_query.Query.outerjoin` as well as :ref:`orm_queryguide_joins` @@ -1118,7 +1119,7 @@ class RelationshipProperty( * Comparisons against collections are not supported. Use - :meth:`~.Relationship.Comparator.contains` + :meth:`~.RelationshipProperty.Comparator.contains` in conjunction with :func:`_expression.not_`. * Compared to a scalar one-to-many, will produce a clause that compares the target columns in the parent to @@ -1130,7 +1131,7 @@ class RelationshipProperty( queries that go beyond simple AND conjunctions of comparisons, such as those which use OR. Use explicit joins, outerjoins, or - :meth:`~.Relationship.Comparator.has` in + :meth:`~.RelationshipProperty.Comparator.has` in conjunction with :func:`_expression.not_` for more comprehensive non-many-to-one scalar membership tests. @@ -3318,8 +3319,14 @@ class JoinCondition: ) if ( + # NOTE: it's not clear yet if this needs to test for + # parentmapper_for_element.isa(self.prop.parent). so far + # we have not come up with a test. parentmapper_for_element is not self.prop.parent - and parentmapper_for_element is not self.prop.mapper + and ( + parentmapper_for_element is None + or not parentmapper_for_element.isa(self.prop.mapper) + ) and elem not in self._secondary_lineage_set ): return _safe_annotate(elem, annotations) diff --git a/libs/sqlalchemy/orm/scoping.py b/libs/sqlalchemy/orm/scoping.py index b8ae381a4..e6d3f84f1 100644 --- a/libs/sqlalchemy/orm/scoping.py +++ b/libs/sqlalchemy/orm/scoping.py @@ -39,7 +39,6 @@ if TYPE_CHECKING: from ._typing import OrmExecuteOptionsParameter from .identity import IdentityMap from .interfaces import ORMOption - from .mapper import Mapper from .query import Query from .query import RowReturningQuery from .session import _BindArguments @@ -1350,7 +1349,7 @@ class scoped_session(Generic[_S]): def bulk_insert_mappings( self, - mapper: Mapper[Any], + mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]], return_defaults: bool = False, render_nulls: bool = False, @@ -1436,7 +1435,7 @@ class scoped_session(Generic[_S]): ) def bulk_update_mappings( - self, mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]] + self, mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]] ) -> None: r"""Perform a bulk update of the given list of mapping dictionaries. diff --git a/libs/sqlalchemy/orm/session.py b/libs/sqlalchemy/orm/session.py index 61bf180ec..c2354d821 100644 --- a/libs/sqlalchemy/orm/session.py +++ b/libs/sqlalchemy/orm/session.py @@ -345,7 +345,7 @@ class ORMExecuteState(util.MemoizedSlots): _compile_state_cls: Optional[Type[ORMCompileState]] _starting_event_idx: int _events_todo: List[Any] - _update_execution_options: Optional[_ExecuteOptions] + _update_execution_options: _ExecuteOptions def __init__( self, @@ -372,6 +372,7 @@ class ORMExecuteState(util.MemoizedSlots): self.bind_arguments = bind_arguments self._compile_state_cls = compile_state_cls self._events_todo = list(events_todo) + self._update_execution_options = util.EMPTY_DICT def _remaining_events(self) -> List[_InstanceLevelDispatch[Session]]: return self._events_todo[self._starting_event_idx + 1 :] @@ -643,6 +644,9 @@ class ORMExecuteState(util.MemoizedSlots): def update_execution_options(self, **opts: Any) -> None: """Update the local execution options with new values.""" self.local_execution_options = self.local_execution_options.union(opts) + self._update_execution_options = self._update_execution_options.union( + opts + ) def _orm_compile_options( self, @@ -1297,7 +1301,8 @@ class SessionTransaction(_StateChange, TransactionalContext): cast("TwoPhaseTransaction", t[1]).prepare() except: with util.safe_reraise(): - self.rollback() + with self._expect_state(SessionTransactionState.CLOSED): + self.rollback() self._state = SessionTransactionState.PREPARED @@ -2176,6 +2181,17 @@ class Session(_SessionClassMethods, EventTarget): events_todo = list(events_todo) + [_add_event] if events_todo: + # save the original execution options before + # orm_pre_session_exec processes them, so that we can pass + # the unprocessed options (plus any explicit updates from event + # hooks) to the second orm_pre_session_exec call. This + # prevents internal state like _sa_orm_load_options and + # yield_per from the first call leaking into the second call, + # which would otherwise cause issues like yield_per incorrectly + # propagating into post-load (selectinload etc.) queries. + # part of #13301. + original_execution_options = execution_options + if compile_state_cls is not None: # for event handlers, do the orm_pre_session_exec # pass ahead of the event handlers, so that things like @@ -2214,7 +2230,13 @@ class Session(_SessionClassMethods, EventTarget): return fn_result statement = orm_exec_state.statement - execution_options = orm_exec_state.local_execution_options + + # use the original execution options plus only the explicit + # updates from event hooks, not the processed options from + # the first orm_pre_session_exec call + execution_options = original_execution_options.union( + orm_exec_state._update_execution_options + ) if compile_state_cls is not None: # now run orm_pre_session_exec() "for real". if there were @@ -3815,10 +3837,12 @@ class Session(_SessionClassMethods, EventTarget): ) ) from err + for_update_arg = ForUpdateArg._from_argument(with_for_update) + if ( not populate_existing and not mapper.always_refresh - and with_for_update is None + and for_update_arg is None ): instance = self._identity_lookup( mapper, @@ -3849,10 +3873,8 @@ class Session(_SessionClassMethods, EventTarget): statement = sql.select(mapper).set_label_style( LABEL_STYLE_TABLENAME_PLUS_COL ) - if with_for_update is not None: - statement._for_update_arg = ForUpdateArg._from_argument( - with_for_update - ) + if for_update_arg is not None: + statement._for_update_arg = for_update_arg if options: statement = statement.options(*options) @@ -4573,7 +4595,7 @@ class Session(_SessionClassMethods, EventTarget): def bulk_insert_mappings( self, - mapper: Mapper[Any], + mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]], return_defaults: bool = False, render_nulls: bool = False, @@ -4655,7 +4677,7 @@ class Session(_SessionClassMethods, EventTarget): ) def bulk_update_mappings( - self, mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]] + self, mapper: _EntityBindKey[Any], mappings: Iterable[Dict[str, Any]] ) -> None: """Perform a bulk update of the given list of mapping dictionaries. @@ -4704,7 +4726,7 @@ class Session(_SessionClassMethods, EventTarget): def _bulk_save_mappings( self, - mapper: Mapper[_O], + mapper: _EntityBindKey[_O], mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]], *, isupdate: bool, @@ -4810,7 +4832,7 @@ class Session(_SessionClassMethods, EventTarget): ) or not hasattr(attr.impl, "get_history"): continue - (added, unchanged, deleted) = attr.impl.get_history( + added, unchanged, deleted = attr.impl.get_history( state, dict_, passive=PassiveFlag.NO_CHANGE ) diff --git a/libs/sqlalchemy/orm/strategies.py b/libs/sqlalchemy/orm/strategies.py index 966abfd6a..5381d7002 100644 --- a/libs/sqlalchemy/orm/strategies.py +++ b/libs/sqlalchemy/orm/strategies.py @@ -1746,7 +1746,7 @@ class SubqueryLoader(PostLoader): if loadopt and loadopt._extra_criteria: new_options += ( orm_util.LoaderCriteriaOption( - self.entity, + effective_entity, loadopt._generate_extra_criteria(context), ), ) @@ -2020,7 +2020,12 @@ class SubqueryLoader(PostLoader): if len(path) == 1: if not orm_util._entity_isa(query_entity.entity_zero, self.parent): return - elif not orm_util._entity_isa(path[-1], self.parent): + elif not orm_util._entity_isa( + path[-1], self.parent + ) and not self.parent.isa(path[-1].mapper): + # second check accommodates a polymorphic entity where + # the path has been normalized to the base mapper but + # self.parent is a subclass mapper. Fixes #13209. return subq = self._setup_query_from_rowproc( @@ -3099,7 +3104,14 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): if len(path) == 1: if not orm_util._entity_isa(query_entity.entity_zero, self.parent): return - elif not orm_util._entity_isa(path[-1], self.parent): + elif not orm_util._entity_isa( + path[-1], self.parent + ) and not self.parent.isa(path[-1].mapper): + # second check accommodates a polymorphic entity where + # the path has been normalized to the base mapper but + # self.parent is a subclass mapper, e.g. + # joinedload(A.b.of_type(poly)).selectinload(poly.Sub.rel) + # Fixes #13209. return selectin_path = effective_path diff --git a/libs/sqlalchemy/orm/strategy_options.py b/libs/sqlalchemy/orm/strategy_options.py index bd94e3251..a5edc5482 100644 --- a/libs/sqlalchemy/orm/strategy_options.py +++ b/libs/sqlalchemy/orm/strategy_options.py @@ -1586,7 +1586,7 @@ class _LoadElement( def is_opts_only(self) -> bool: return bool(self.local_opts and self.strategy is None) - def _clone(self, **kw: Any) -> _LoadElement: + def _clone(self, **kw: Any) -> Self: cls = self.__class__ s = cls.__new__(cls) @@ -1807,7 +1807,7 @@ class _LoadElement( return self._prepend_path(parent.path) - def _prepend_path(self, path: PathRegistry) -> _LoadElement: + def _prepend_path(self, path: PathRegistry) -> Self: cloned = self._clone() assert cloned.strategy == self.strategy @@ -1976,6 +1976,24 @@ class _AttributeStrategyLoad(_LoadElement): return path + def _prepend_path(self, path: PathRegistry) -> Self: + """Override to also prepend the path for _path_with_polymorphic_path. + + When using .options() to chain loader options with of_type(), this + ensures that the polymorphic path information is correctly updated + to include the parent path. Fixes issue #13202. + """ + cloned = super()._prepend_path(path) + + # Also prepend the parent path to _path_with_polymorphic_path if + # present + if self._path_with_polymorphic_path is not None: + cloned._path_with_polymorphic_path = PathRegistry.coerce( + path[0:-1] + self._path_with_polymorphic_path[:] + ) + + return cloned + def _generate_extra_criteria(self, context): """Apply the current bound parameters in a QueryContext to the immediate "extra_criteria" stored with this Load object. diff --git a/libs/sqlalchemy/orm/unitofwork.py b/libs/sqlalchemy/orm/unitofwork.py index c65b37e54..7de3b8831 100644 --- a/libs/sqlalchemy/orm/unitofwork.py +++ b/libs/sqlalchemy/orm/unitofwork.py @@ -30,7 +30,6 @@ from .. import event from .. import util from ..util import topological - if TYPE_CHECKING: from .dependency import DependencyProcessor from .interfaces import MapperProperty @@ -521,7 +520,7 @@ class Preprocess(IterateMappersMixin): for mapper in self._mappers(uow): for state in uow.mappers[mapper].difference(self.processed): - (isdelete, listonly) = uow.states[state] + isdelete, listonly = uow.states[state] if not listonly: if isdelete: delete_states.add(state) @@ -607,7 +606,7 @@ class ProcessAll(IterateMappersMixin, PostSortRec): def _elements(self, uow): for mapper in self._mappers(uow): for state in uow.mappers[mapper]: - (isdelete, listonly) = uow.states[state] + isdelete, listonly = uow.states[state] if isdelete == self.isdelete and not listonly: yield state diff --git a/libs/sqlalchemy/orm/util.py b/libs/sqlalchemy/orm/util.py index 090a27412..d665b1848 100644 --- a/libs/sqlalchemy/orm/util.py +++ b/libs/sqlalchemy/orm/util.py @@ -1007,6 +1007,10 @@ class AliasedInsp( self._adapt_on_names = adapt_on_names self._target = mapped_class_or_ac + @property + def _post_inspect(self): # type: ignore[override] + self.mapper._check_configure() + @classmethod def _alias_factory( cls, diff --git a/libs/sqlalchemy/pool/impl.py b/libs/sqlalchemy/pool/impl.py index f3a5c2dd8..758e7fdf5 100644 --- a/libs/sqlalchemy/pool/impl.py +++ b/libs/sqlalchemy/pool/impl.py @@ -7,6 +7,7 @@ """Pool implementation classes.""" + from __future__ import annotations import threading diff --git a/libs/sqlalchemy/sql/_orm_types.py b/libs/sqlalchemy/sql/_orm_types.py index 39754a3c2..ead3a770c 100644 --- a/libs/sqlalchemy/sql/_orm_types.py +++ b/libs/sqlalchemy/sql/_orm_types.py @@ -11,7 +11,6 @@ are meaningful to the ORM. """ - from __future__ import annotations from ..util.typing import Literal diff --git a/libs/sqlalchemy/sql/_py_util.py b/libs/sqlalchemy/sql/_py_util.py index c65e96c0d..b5608efc1 100644 --- a/libs/sqlalchemy/sql/_py_util.py +++ b/libs/sqlalchemy/sql/_py_util.py @@ -32,7 +32,7 @@ class prefix_anon_map(Dict[str, str]): """ def __missing__(self, key: str) -> str: - (ident, derived) = key.split(" ", 1) + ident, derived = key.split(" ", 1) anonymous_counter = self.get(derived, 1) self[derived] = anonymous_counter + 1 # type: ignore value = f"{derived}_{anonymous_counter}" diff --git a/libs/sqlalchemy/sql/base.py b/libs/sqlalchemy/sql/base.py index 39772c9e7..b5e6cbc14 100644 --- a/libs/sqlalchemy/sql/base.py +++ b/libs/sqlalchemy/sql/base.py @@ -8,7 +8,6 @@ """Foundational utilities common to many sql modules.""" - from __future__ import annotations import collections diff --git a/libs/sqlalchemy/sql/compiler.py b/libs/sqlalchemy/sql/compiler.py index 7819d51ba..7f7243bbe 100644 --- a/libs/sqlalchemy/sql/compiler.py +++ b/libs/sqlalchemy/sql/compiler.py @@ -23,6 +23,7 @@ To generate user-defined SQL strings, see :doc:`/ext/compiler`. """ + from __future__ import annotations import collections @@ -2129,7 +2130,7 @@ class SQLCompiler(Compiled): leep_res = self._literal_execute_expanding_parameter( escaped_name, parameter, values ) - (to_update, replacement_expr) = leep_res + to_update, replacement_expr = leep_res to_update_sets[escaped_name] = to_update replacement_expressions[escaped_name] = replacement_expr @@ -3169,6 +3170,7 @@ class SQLCompiler(Compiled): if ( self.dialect.div_is_floordiv and binary.right.type._type_affinity is sqltypes.Integer + and binary.left.type._type_affinity is sqltypes.Integer ): return ( self.process(binary.left, **kw) @@ -4569,6 +4571,8 @@ class SQLCompiler(Compiled): "_label_select_column is only relevant within " "the columns clause of a SELECT or RETURNING" ) + result_expr: Union[elements.Label[Any], _CompileLabel] + if isinstance(column, elements.Label): if col_expr is not column: result_expr = _CompileLabel( diff --git a/libs/sqlalchemy/sql/crud.py b/libs/sqlalchemy/sql/crud.py index d37274c8e..80e90eb83 100644 --- a/libs/sqlalchemy/sql/crud.py +++ b/libs/sqlalchemy/sql/crud.py @@ -10,6 +10,7 @@ within INSERT and UPDATE statements. """ + from __future__ import annotations import functools @@ -848,7 +849,7 @@ def _setup_delete_return_defaults( toplevel, kw, ): - (_, _, implicit_return_defaults, *_) = _get_returning_modifiers( + _, _, implicit_return_defaults, *_ = _get_returning_modifiers( compiler, stmt, compile_state, toplevel ) diff --git a/libs/sqlalchemy/sql/ddl.py b/libs/sqlalchemy/sql/ddl.py index 636500ee8..eda87c221 100644 --- a/libs/sqlalchemy/sql/ddl.py +++ b/libs/sqlalchemy/sql/ddl.py @@ -11,6 +11,7 @@ Provides the hierarchy of DDL-defining schema items as well as routines to invoke them for a create/drop call. """ + from __future__ import annotations import contextlib diff --git a/libs/sqlalchemy/sql/dml.py b/libs/sqlalchemy/sql/dml.py index 9b00bc459..ad4f0f57d 100644 --- a/libs/sqlalchemy/sql/dml.py +++ b/libs/sqlalchemy/sql/dml.py @@ -9,6 +9,7 @@ Provide :class:`_expression.Insert`, :class:`_expression.Update` and :class:`_expression.Delete`. """ + from __future__ import annotations import collections.abc as collections_abc @@ -1125,6 +1126,9 @@ class ValuesBase(UpdateBase): the traditional Core method of multiple parameter set invocation for INSERTs and other statements. + :ref:`tutorial_core_insert_values_clause` - Insert tutorial + detailing alternatives to the multiple values syntax. + The UPDATE construct also supports rendering the SET parameters in a specific order. For this feature refer to the :meth:`_expression.Update.ordered_values` method. diff --git a/libs/sqlalchemy/sql/elements.py b/libs/sqlalchemy/sql/elements.py index dcecb20c4..b13839e0b 100644 --- a/libs/sqlalchemy/sql/elements.py +++ b/libs/sqlalchemy/sql/elements.py @@ -80,7 +80,6 @@ from ..util.typing import Literal from ..util.typing import ParamSpec from ..util.typing import Self - if typing.TYPE_CHECKING: from ._typing import _ByArgument from ._typing import _ColumnExpressionArgument @@ -5268,9 +5267,8 @@ class quoted_name(util.MemoizedSlots, str): :class:`_schema.Table`, :class:`_schema.Column`, and others. The class can also be passed explicitly as the name to any function that receives a name which - can be quoted. Such as to use the :meth:`_engine.Engine.has_table` - method with - an unconditionally quoted name:: + can be quoted, such as :meth:`.Inspector.has_table` with an + unconditionally quoted name:: from sqlalchemy import create_engine from sqlalchemy import inspect @@ -5283,6 +5281,11 @@ class quoted_name(util.MemoizedSlots, str): backend, passing the name exactly as ``"some_table"`` without converting to upper case. + A :class:`.quoted_name` object with ``quote=False`` may be passed to APIs + that apply automatic quoting in order to keep the given name unquoted, + such as when a PostgreSQL ``INHERITS`` option refers to a schema-qualified + table name like ``my_schema.some_table``. + .. versionchanged:: 1.2 The :class:`.quoted_name` construct is now importable from ``sqlalchemy.sql``, in addition to the previous location of ``sqlalchemy.sql.elements``. diff --git a/libs/sqlalchemy/sql/expression.py b/libs/sqlalchemy/sql/expression.py index b4a185a09..b91376c44 100644 --- a/libs/sqlalchemy/sql/expression.py +++ b/libs/sqlalchemy/sql/expression.py @@ -7,7 +7,6 @@ """Defines the public namespace for SQL expression constructs.""" - from __future__ import annotations from ._dml_constructors import delete as delete diff --git a/libs/sqlalchemy/sql/functions.py b/libs/sqlalchemy/sql/functions.py index 663ced0e4..498caf134 100644 --- a/libs/sqlalchemy/sql/functions.py +++ b/libs/sqlalchemy/sql/functions.py @@ -59,7 +59,7 @@ from .sqltypes import TableValueType from .type_api import TypeEngine from .visitors import InternalTraversal from .. import util - +from ..util.typing import TypeAlias if TYPE_CHECKING: from ._typing import _ByArgument @@ -1003,10 +1003,10 @@ class _FunctionGenerator: # statically generated** by tools/generate_sql_functions.py @property - def aggregate_strings(self) -> Type[aggregate_strings]: ... + def aggregate_strings(self) -> Type[_aggregate_strings_func]: ... @property - def ansifunction(self) -> Type[AnsiFunction[Any]]: ... + def ansifunction(self) -> Type[_AnsiFunction_func[Any]]: ... # set ColumnElement[_T] as a separate overload, to appease # mypy which seems to not want to accept _T from @@ -1019,7 +1019,7 @@ class _FunctionGenerator: col: ColumnElement[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> array_agg[_T]: ... + ) -> _array_agg_func[_T]: ... @overload def array_agg( @@ -1027,7 +1027,7 @@ class _FunctionGenerator: col: _ColumnExpressionArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> array_agg[_T]: ... + ) -> _array_agg_func[_T]: ... @overload def array_agg( @@ -1035,20 +1035,20 @@ class _FunctionGenerator: col: _T, *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> array_agg[_T]: ... + ) -> _array_agg_func[_T]: ... def array_agg( self, col: _ColumnExpressionOrLiteralArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> array_agg[_T]: ... + ) -> _array_agg_func[_T]: ... @property - def cast(self) -> Type[Cast[Any]]: ... + def cast(self) -> Type[_Cast_func[Any]]: ... @property - def char_length(self) -> Type[char_length]: ... + def char_length(self) -> Type[_char_length_func]: ... # set ColumnElement[_T] as a separate overload, to appease # mypy which seems to not want to accept _T from @@ -1061,7 +1061,7 @@ class _FunctionGenerator: col: ColumnElement[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> coalesce[_T]: ... + ) -> _coalesce_func[_T]: ... @overload def coalesce( @@ -1069,7 +1069,7 @@ class _FunctionGenerator: col: _ColumnExpressionArgument[Optional[_T]], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> coalesce[_T]: ... + ) -> _coalesce_func[_T]: ... @overload def coalesce( @@ -1077,53 +1077,53 @@ class _FunctionGenerator: col: Optional[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> coalesce[_T]: ... + ) -> _coalesce_func[_T]: ... def coalesce( self, col: _ColumnExpressionOrLiteralArgument[Optional[_T]], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> coalesce[_T]: ... + ) -> _coalesce_func[_T]: ... @property - def concat(self) -> Type[concat]: ... + def concat(self) -> Type[_concat_func]: ... @property - def count(self) -> Type[count]: ... + def count(self) -> Type[_count_func]: ... @property - def cube(self) -> Type[cube[Any]]: ... + def cube(self) -> Type[_cube_func[Any]]: ... @property - def cume_dist(self) -> Type[cume_dist]: ... + def cume_dist(self) -> Type[_cume_dist_func]: ... @property - def current_date(self) -> Type[current_date]: ... + def current_date(self) -> Type[_current_date_func]: ... @property - def current_time(self) -> Type[current_time]: ... + def current_time(self) -> Type[_current_time_func]: ... @property - def current_timestamp(self) -> Type[current_timestamp]: ... + def current_timestamp(self) -> Type[_current_timestamp_func]: ... @property - def current_user(self) -> Type[current_user]: ... + def current_user(self) -> Type[_current_user_func]: ... @property - def dense_rank(self) -> Type[dense_rank]: ... + def dense_rank(self) -> Type[_dense_rank_func]: ... @property - def extract(self) -> Type[Extract]: ... + def extract(self) -> Type[_Extract_func]: ... @property - def grouping_sets(self) -> Type[grouping_sets[Any]]: ... + def grouping_sets(self) -> Type[_grouping_sets_func[Any]]: ... @property - def localtime(self) -> Type[localtime]: ... + def localtime(self) -> Type[_localtime_func]: ... @property - def localtimestamp(self) -> Type[localtimestamp]: ... + def localtimestamp(self) -> Type[_localtimestamp_func]: ... # set ColumnElement[_T] as a separate overload, to appease # mypy which seems to not want to accept _T from @@ -1136,7 +1136,7 @@ class _FunctionGenerator: col: ColumnElement[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> max[_T]: ... + ) -> _max_func[_T]: ... @overload def max( # noqa: A001 @@ -1144,7 +1144,7 @@ class _FunctionGenerator: col: _ColumnExpressionArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> max[_T]: ... + ) -> _max_func[_T]: ... @overload def max( # noqa: A001 @@ -1152,14 +1152,14 @@ class _FunctionGenerator: col: _T, *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> max[_T]: ... + ) -> _max_func[_T]: ... def max( # noqa: A001 self, col: _ColumnExpressionOrLiteralArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> max[_T]: ... + ) -> _max_func[_T]: ... # set ColumnElement[_T] as a separate overload, to appease # mypy which seems to not want to accept _T from @@ -1172,7 +1172,7 @@ class _FunctionGenerator: col: ColumnElement[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> min[_T]: ... + ) -> _min_func[_T]: ... @overload def min( # noqa: A001 @@ -1180,7 +1180,7 @@ class _FunctionGenerator: col: _ColumnExpressionArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> min[_T]: ... + ) -> _min_func[_T]: ... @overload def min( # noqa: A001 @@ -1188,47 +1188,47 @@ class _FunctionGenerator: col: _T, *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> min[_T]: ... + ) -> _min_func[_T]: ... def min( # noqa: A001 self, col: _ColumnExpressionOrLiteralArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> min[_T]: ... + ) -> _min_func[_T]: ... @property - def mode(self) -> Type[mode[Any]]: ... + def mode(self) -> Type[_mode_func[Any]]: ... @property - def next_value(self) -> Type[next_value]: ... + def next_value(self) -> Type[_next_value_func]: ... @property - def now(self) -> Type[now]: ... + def now(self) -> Type[_now_func]: ... @property - def orderedsetagg(self) -> Type[OrderedSetAgg[Any]]: ... + def orderedsetagg(self) -> Type[_OrderedSetAgg_func[Any]]: ... @property - def percent_rank(self) -> Type[percent_rank]: ... + def percent_rank(self) -> Type[_percent_rank_func]: ... @property - def percentile_cont(self) -> Type[percentile_cont[Any]]: ... + def percentile_cont(self) -> Type[_percentile_cont_func[Any]]: ... @property - def percentile_disc(self) -> Type[percentile_disc[Any]]: ... + def percentile_disc(self) -> Type[_percentile_disc_func[Any]]: ... @property - def random(self) -> Type[random]: ... + def random(self) -> Type[_random_func]: ... @property - def rank(self) -> Type[rank]: ... + def rank(self) -> Type[_rank_func]: ... @property - def rollup(self) -> Type[rollup[Any]]: ... + def rollup(self) -> Type[_rollup_func[Any]]: ... @property - def session_user(self) -> Type[session_user]: ... + def session_user(self) -> Type[_session_user_func]: ... # set ColumnElement[_T] as a separate overload, to appease # mypy which seems to not want to accept _T from @@ -1241,7 +1241,7 @@ class _FunctionGenerator: col: ColumnElement[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> sum[_T]: ... + ) -> _sum_func[_T]: ... @overload def sum( # noqa: A001 @@ -1249,7 +1249,7 @@ class _FunctionGenerator: col: _ColumnExpressionArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> sum[_T]: ... + ) -> _sum_func[_T]: ... @overload def sum( # noqa: A001 @@ -1257,20 +1257,20 @@ class _FunctionGenerator: col: _T, *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> sum[_T]: ... + ) -> _sum_func[_T]: ... def sum( # noqa: A001 self, col: _ColumnExpressionOrLiteralArgument[_T], *args: _ColumnExpressionOrLiteralArgument[Any], **kwargs: Any, - ) -> sum[_T]: ... + ) -> _sum_func[_T]: ... @property - def sysdate(self) -> Type[sysdate]: ... + def sysdate(self) -> Type[_sysdate_func]: ... @property - def user(self) -> Type[user]: ... + def user(self) -> Type[_user_func]: ... # END GENERATED FUNCTION ACCESSORS @@ -2156,3 +2156,44 @@ class aggregate_strings(GenericFunction[str]): self, clause: _ColumnExpressionArgument[Any], separator: str ) -> None: super().__init__(clause, separator) + + +# These aliases are required to avoid shadowing the class with the function +# name. See https://github.com/sqlalchemy/sqlalchemy/issues/13167 +# START GENERATED FUNCTION ALIASES +_aggregate_strings_func: TypeAlias = aggregate_strings +_AnsiFunction_func: TypeAlias = AnsiFunction[_T] +_array_agg_func: TypeAlias = array_agg[_T] +_Cast_func: TypeAlias = Cast[_T] +_char_length_func: TypeAlias = char_length +_coalesce_func: TypeAlias = coalesce[_T] +_concat_func: TypeAlias = concat +_count_func: TypeAlias = count +_cube_func: TypeAlias = cube[_T] +_cume_dist_func: TypeAlias = cume_dist +_current_date_func: TypeAlias = current_date +_current_time_func: TypeAlias = current_time +_current_timestamp_func: TypeAlias = current_timestamp +_current_user_func: TypeAlias = current_user +_dense_rank_func: TypeAlias = dense_rank +_Extract_func: TypeAlias = Extract +_grouping_sets_func: TypeAlias = grouping_sets[_T] +_localtime_func: TypeAlias = localtime +_localtimestamp_func: TypeAlias = localtimestamp +_max_func: TypeAlias = max[_T] +_min_func: TypeAlias = min[_T] +_mode_func: TypeAlias = mode[_T] +_next_value_func: TypeAlias = next_value +_now_func: TypeAlias = now +_OrderedSetAgg_func: TypeAlias = OrderedSetAgg[_T] +_percent_rank_func: TypeAlias = percent_rank +_percentile_cont_func: TypeAlias = percentile_cont[_T] +_percentile_disc_func: TypeAlias = percentile_disc[_T] +_random_func: TypeAlias = random +_rank_func: TypeAlias = rank +_rollup_func: TypeAlias = rollup[_T] +_session_user_func: TypeAlias = session_user +_sum_func: TypeAlias = sum[_T] +_sysdate_func: TypeAlias = sysdate +_user_func: TypeAlias = user +# END GENERATED FUNCTION ALIASES diff --git a/libs/sqlalchemy/sql/lambdas.py b/libs/sqlalchemy/sql/lambdas.py index a9e94aec1..1b0d47837 100644 --- a/libs/sqlalchemy/sql/lambdas.py +++ b/libs/sqlalchemy/sql/lambdas.py @@ -44,7 +44,6 @@ from .. import inspection from .. import util from ..util.typing import Literal - if TYPE_CHECKING: from .elements import BindParameter from .elements import ClauseElement @@ -301,7 +300,7 @@ class LambdaElement(elements.ClauseElement): rec = lambda_element._rec if rec.bindparam_trackers: tracker_instrumented_fn = ( - rec.tracker_instrumented_fn # type:ignore [union-attr] # noqa: E501 + rec.tracker_instrumented_fn # type: ignore [union-attr] # noqa: E501 ) for tracker in rec.bindparam_trackers: tracker( @@ -314,7 +313,7 @@ class LambdaElement(elements.ClauseElement): return rec def __getattr__(self, key): - return getattr(self._rec.expected_expr, key) + return getattr(self._resolved, key) @property def _is_sequence(self): diff --git a/libs/sqlalchemy/sql/schema.py b/libs/sqlalchemy/sql/schema.py index db397195d..b893bf724 100644 --- a/libs/sqlalchemy/sql/schema.py +++ b/libs/sqlalchemy/sql/schema.py @@ -28,6 +28,7 @@ Since these objects are part of the SQL expression language, they are usable as components in SQL expressions. """ + from __future__ import annotations from abc import ABC @@ -2357,11 +2358,9 @@ class Column(DialectKWArgs, SchemaItem, ColumnClause[_T]): if existing_col is not self: if not allow_replacements: raise exc.DuplicateColumnError( - f"A column with {conflicts_on} " - f"""'{ + f"A column with {conflicts_on} " f"""'{ self.key if conflicts_on == 'key' else self.name - }' """ - f"is already present in table '{table.name}'." + }' """ f"is already present in table '{table.name}'." ) for fk in existing_col.foreign_keys: table.foreign_keys.remove(fk) @@ -5357,7 +5356,10 @@ class Index( ) -_NamingSchemaCallable = Callable[[Constraint, Table], str] +_NamingSchemaCallable = Union[ + Callable[[Constraint, Table], str], + Callable[[Index, Table], str], +] _NamingSchemaDirective = Union[str, _NamingSchemaCallable] diff --git a/libs/sqlalchemy/sql/selectable.py b/libs/sqlalchemy/sql/selectable.py index d7fd2cc9d..53fc3e29f 100644 --- a/libs/sqlalchemy/sql/selectable.py +++ b/libs/sqlalchemy/sql/selectable.py @@ -103,7 +103,6 @@ from ..util.typing import Literal from ..util.typing import Protocol from ..util.typing import Self - and_ = BooleanClauseList.and_ @@ -4957,42 +4956,28 @@ class SelectState(util.MemoizedSlots, CompileState): if self.statement._correlate: to_correlate = self.statement._correlate if to_correlate: - froms = [ - f - for f in froms - if f - not in _cloned_intersection( - _cloned_intersection( - froms, explicit_correlate_froms or () - ), - to_correlate, - ) - ] - - if self.statement._correlate_except is not None: - froms = [ - f - for f in froms - if f - not in _cloned_difference( + to_remove = _cloned_intersection( _cloned_intersection( froms, explicit_correlate_froms or () ), - self.statement._correlate_except, + to_correlate, ) - ] + froms = [f for f in froms if f not in to_remove] + + if self.statement._correlate_except is not None: + to_remove = _cloned_difference( + _cloned_intersection(froms, explicit_correlate_froms or ()), + self.statement._correlate_except, + ) + froms = [f for f in froms if f not in to_remove] if ( self.statement._auto_correlate and implicit_correlate_froms and len(froms) > 1 ): - froms = [ - f - for f in froms - if f - not in _cloned_intersection(froms, implicit_correlate_froms) - ] + to_remove = _cloned_intersection(froms, implicit_correlate_froms) + froms = [f for f in froms if f not in to_remove] if not len(froms): raise exc.InvalidRequestError( @@ -5063,7 +5048,7 @@ class SelectState(util.MemoizedSlots, CompileState): raw_columns, left, right, onclause ) else: - (replace_from_obj_index) = self._join_place_explicit_left_side( + replace_from_obj_index = self._join_place_explicit_left_side( left ) diff --git a/libs/sqlalchemy/sql/sqltypes.py b/libs/sqlalchemy/sql/sqltypes.py index dccc64bba..98fa8890f 100644 --- a/libs/sqlalchemy/sql/sqltypes.py +++ b/libs/sqlalchemy/sql/sqltypes.py @@ -7,6 +7,7 @@ # mypy: allow-untyped-defs, allow-untyped-calls """SQL specific types.""" + from __future__ import annotations import collections.abc as collections_abc @@ -3637,7 +3638,6 @@ class Uuid(Emulated, TypeEngine[_UUID_RETURN]): from sqlalchemy import Uuid from sqlalchemy import Table, Column, MetaData, String - metadata_obj = MetaData() t = Table( diff --git a/libs/sqlalchemy/sql/traversals.py b/libs/sqlalchemy/sql/traversals.py index c9b6290f4..56a7c5f68 100644 --- a/libs/sqlalchemy/sql/traversals.py +++ b/libs/sqlalchemy/sql/traversals.py @@ -35,7 +35,6 @@ from .. import util from ..util import langhelpers from ..util.typing import Self - SKIP_TRAVERSE = util.symbol("skip_traverse") COMPARE_FAILED = False COMPARE_SUCCEEDED = True diff --git a/libs/sqlalchemy/sql/util.py b/libs/sqlalchemy/sql/util.py index a0a0934f4..9f1127b01 100644 --- a/libs/sqlalchemy/sql/util.py +++ b/libs/sqlalchemy/sql/util.py @@ -7,6 +7,7 @@ # mypy: allow-untyped-defs, allow-untyped-calls """High level utilities which build upon other modules here.""" + from __future__ import annotations from collections import deque @@ -852,7 +853,7 @@ def splice_joins( adapter = ClauseAdapter(left) ret = None while stack: - (right, prevright) = stack.pop() + right, prevright = stack.pop() if isinstance(right, Join) and right is not stop_on: right = right._clone() right.onclause = adapter.traverse(right.onclause) diff --git a/libs/sqlalchemy/testing/engines.py b/libs/sqlalchemy/testing/engines.py index 5c20ee728..e509c9413 100644 --- a/libs/sqlalchemy/testing/engines.py +++ b/libs/sqlalchemy/testing/engines.py @@ -27,7 +27,6 @@ from .. import pool from ..util import await_only from ..util.typing import Literal - if typing.TYPE_CHECKING: from ..engine import Engine from ..engine.url import URL diff --git a/libs/sqlalchemy/testing/fixtures/base.py b/libs/sqlalchemy/testing/fixtures/base.py index 4a31e5a9c..d45ff4615 100644 --- a/libs/sqlalchemy/testing/fixtures/base.py +++ b/libs/sqlalchemy/testing/fixtures/base.py @@ -9,6 +9,9 @@ from __future__ import annotations +from collections.abc import Callable +from collections.abc import Sequence + import sqlalchemy as sa from .. import assertions from .. import config @@ -28,23 +31,25 @@ from ...orm import registry @config.mark_base_test_class() class TestBase: # A sequence of requirement names matching testing.requires decorators - __requires__ = () + __requires__: tuple[str, ...] = () # A sequence of dialect names to exclude from the test class. - __unsupported_on__ = () + __unsupported_on__: tuple[str, ...] = () # If present, test class is only runnable for the *single* specified # dialect. If you need multiple, use __unsupported_on__ and invert. - __only_on__ = None + __only_on__: tuple[str, ...] | str | None = None # A sequence of no-arg callables. If any are True, the entire testcase is # skipped. - __skip_if__ = None + __skip_if__: Sequence[Callable[[], bool]] | None = None # if True, the testing reaper will not attempt to touch connection # state after a test is completed and before the outer teardown # starts - __leave_connections_for_teardown__ = False + __leave_connections_for_teardown__: bool = False + + __backend__: bool def assert_(self, val, msg=None): assert val, msg diff --git a/libs/sqlalchemy/testing/fixtures/mypy.py b/libs/sqlalchemy/testing/fixtures/mypy.py index 3cd5cbc2e..baac539af 100644 --- a/libs/sqlalchemy/testing/fixtures/mypy.py +++ b/libs/sqlalchemy/testing/fixtures/mypy.py @@ -42,8 +42,7 @@ class MypyTest(TestBase): with open( Path(cachedir) / "sqla_mypy_config.cfg", "w" ) as config_file: - config_file.write( - f""" + config_file.write(f""" [mypy]\n plugins = sqlalchemy.ext.mypy.plugin\n show_error_codes = True\n @@ -53,13 +52,11 @@ class MypyTest(TestBase): [mypy-sqlalchemy.*] ignore_errors = True - """ - ) + """) with open( Path(cachedir) / "plain_mypy_config.cfg", "w" ) as config_file: - config_file.write( - f""" + config_file.write(f""" [mypy]\n show_error_codes = True\n {mypy_path} @@ -67,8 +64,7 @@ class MypyTest(TestBase): [mypy-sqlalchemy.*] ignore_errors = True - """ - ) + """) yield cachedir @config.fixture() @@ -202,15 +198,18 @@ class MypyTest(TestBase): expected_msg, ) - expected_msg = re.sub( - "List", "builtins.list", expected_msg - ) + # as of mypy 1.20.0 it's not spitting out + # "builtins" anymore (using assert_type() would be + # better overall) + # expected_msg = re.sub( + # "List", "builtins.list", expected_msg + # ) - expected_msg = re.sub( - r"\b(int|str|float|bool)\b", - lambda m: rf"builtins.{m.group(0)}\*?", - expected_msg, - ) + # expected_msg = re.sub( + # r"\b(int|str|float|bool)\b", + # lambda m: rf"builtins.{m.group(0)}\*?", + # expected_msg, + # ) # expected_msg = re.sub( # r"(Sequence|Tuple|List|Union)", # lambda m: fr"typing.{m.group(0)}\*?", diff --git a/libs/sqlalchemy/testing/plugin/bootstrap.py b/libs/sqlalchemy/testing/plugin/bootstrap.py index 3d0d06241..5498e87c6 100644 --- a/libs/sqlalchemy/testing/plugin/bootstrap.py +++ b/libs/sqlalchemy/testing/plugin/bootstrap.py @@ -27,7 +27,6 @@ import importlib.util import os import sys - bootstrap_file = locals()["bootstrap_file"] to_bootstrap = locals()["to_bootstrap"] diff --git a/libs/sqlalchemy/testing/plugin/pytestplugin.py b/libs/sqlalchemy/testing/plugin/pytestplugin.py index bee8189ed..c430e76c1 100644 --- a/libs/sqlalchemy/testing/plugin/pytestplugin.py +++ b/libs/sqlalchemy/testing/plugin/pytestplugin.py @@ -654,13 +654,10 @@ def _pytest_fn_decorator(target): __target_fn="__target_fn", __orig_fn="__orig_fn", name=fn.__name__ ) metadata.update(format_argspec_plus(spec, grouped=False)) - code = ( - """\ + code = """\ def %(name)s%(grouped_args)s: return %(__target_fn)s(%(__orig_fn)s, %(apply_kw)s) -""" - % metadata - ) +""" % metadata decorated = _exec_code_in_env( code, {"__target_fn": target, "__orig_fn": fn}, fn.__name__ ) diff --git a/libs/sqlalchemy/testing/profiling.py b/libs/sqlalchemy/testing/profiling.py index 37ddc0233..79a45871d 100644 --- a/libs/sqlalchemy/testing/profiling.py +++ b/libs/sqlalchemy/testing/profiling.py @@ -29,7 +29,6 @@ from .util import gc_collect from ..util import freethreading from ..util import has_compiled_ext - try: import cProfile except ImportError: diff --git a/libs/sqlalchemy/testing/provision.py b/libs/sqlalchemy/testing/provision.py index b61cb5035..a3f72363e 100644 --- a/libs/sqlalchemy/testing/provision.py +++ b/libs/sqlalchemy/testing/provision.py @@ -25,7 +25,6 @@ from ..sql import ddl from ..sql import schema from ..util import decorator - log = logging.getLogger(__name__) FOLLOWER_IDENT = None diff --git a/libs/sqlalchemy/testing/suite/test_reflection.py b/libs/sqlalchemy/testing/suite/test_reflection.py index 8f6fe5684..d808783eb 100644 --- a/libs/sqlalchemy/testing/suite/test_reflection.py +++ b/libs/sqlalchemy/testing/suite/test_reflection.py @@ -52,7 +52,6 @@ from ...testing import is_none from ...testing import is_true from ...testing import mock - metadata, users = None, None diff --git a/libs/sqlalchemy/testing/util.py b/libs/sqlalchemy/testing/util.py index 05aff5f36..8e2c6996d 100644 --- a/libs/sqlalchemy/testing/util.py +++ b/libs/sqlalchemy/testing/util.py @@ -40,7 +40,6 @@ from ..util import defaultdict from ..util import has_refcount_gc from ..util import inspect_getfullargspec - if not has_refcount_gc: def non_refcount_gc_collect(*args): diff --git a/libs/sqlalchemy/types.py b/libs/sqlalchemy/types.py index 23b9eac40..27e366993 100644 --- a/libs/sqlalchemy/types.py +++ b/libs/sqlalchemy/types.py @@ -7,7 +7,6 @@ """Compatibility namespace for sqlalchemy.sql.types.""" - from __future__ import annotations from .sql.sqltypes import _Binary as _Binary diff --git a/libs/sqlalchemy/util/_collections.py b/libs/sqlalchemy/util/_collections.py index f67fbf7c1..8ac193f15 100644 --- a/libs/sqlalchemy/util/_collections.py +++ b/libs/sqlalchemy/util/_collections.py @@ -7,6 +7,7 @@ # mypy: allow-untyped-defs, allow-untyped-calls """Collection classes and helpers.""" + from __future__ import annotations import operator @@ -63,7 +64,6 @@ else: unique_list as unique_list, ) - _T = TypeVar("_T", bound=Any) _KT = TypeVar("_KT", bound=Any) _VT = TypeVar("_VT", bound=Any) diff --git a/libs/sqlalchemy/util/_py_collections.py b/libs/sqlalchemy/util/_py_collections.py index 5c5d2b13f..ff94d3e04 100644 --- a/libs/sqlalchemy/util/_py_collections.py +++ b/libs/sqlalchemy/util/_py_collections.py @@ -147,14 +147,14 @@ class immutabledict(ImmutableDictBase[_KT, _VT]): self, __value: Mapping[_KT, _VT] ) -> immutabledict[_KT, _VT]: return immutabledict( - super().__or__(__value), # type: ignore[call-overload] + super().__or__(__value), # type: ignore[call-overload,operator,unused-ignore] # noqa: E501 ) def __ror__( # type: ignore[override] self, __value: Mapping[_KT, _VT] ) -> immutabledict[_KT, _VT]: return immutabledict( - super().__ror__(__value), # type: ignore[call-overload] + super().__ror__(__value), # type: ignore[call-overload,operator,unused-ignore] # noqa: E501 ) @@ -198,7 +198,7 @@ class OrderedSet(Set[_T]): self._list.insert(pos, element) super().add(element) - def discard(self, element: _T) -> None: + def discard(self, element: _T) -> None: # type: ignore[override,unused-ignore] # noqa: E501 if element in self: self._list.remove(element) super().remove(element) @@ -248,7 +248,7 @@ class OrderedSet(Set[_T]): def __and__(self, other: AbstractSet[object]) -> OrderedSet[_T]: return self.intersection(other) - def symmetric_difference(self, other: Iterable[_T]) -> OrderedSet[_T]: + def symmetric_difference(self, other: Iterable[_T]) -> OrderedSet[_T]: # type: ignore[override,unused-ignore] # noqa: E501 collection: Collection[_T] if isinstance(other, set): collection = other_set = other @@ -271,7 +271,7 @@ class OrderedSet(Set[_T]): other_set = super().difference(*other) return self.__class__(a for a in self._list if a in other_set) - def __sub__(self, other: AbstractSet[Optional[_T]]) -> OrderedSet[_T]: + def __sub__(self, other: AbstractSet[Optional[_T]]) -> OrderedSet[_T]: # type: ignore[override,unused-ignore] # noqa: E501 return self.difference(other) def intersection_update(self, *other: Iterable[Any]) -> None: diff --git a/libs/sqlalchemy/util/compat.py b/libs/sqlalchemy/util/compat.py index 947624225..bbc3d44d5 100644 --- a/libs/sqlalchemy/util/compat.py +++ b/libs/sqlalchemy/util/compat.py @@ -33,7 +33,6 @@ from typing import Tuple from typing import Type from typing import TypeVar - py314b1 = sys.version_info >= (3, 14, 0, "beta", 1) py314 = sys.version_info >= (3, 14) py313 = sys.version_info >= (3, 13) @@ -177,7 +176,7 @@ def inspect_getfullargspec(func: Callable[..., Any]) -> FullArgSpec: if inspect.ismethod(func): func = func.__func__ - if not inspect.isfunction(func): + if not inspect.isfunction(func) and not hasattr(func, "__code__"): raise TypeError(f"{func!r} is not a Python function") co = func.__code__ diff --git a/libs/sqlalchemy/util/langhelpers.py b/libs/sqlalchemy/util/langhelpers.py index cb72771e3..4b7d428d2 100644 --- a/libs/sqlalchemy/util/langhelpers.py +++ b/libs/sqlalchemy/util/langhelpers.py @@ -10,6 +10,7 @@ modules, classes, hierarchies, attributes, functions, and methods. """ + from __future__ import annotations import collections @@ -277,21 +278,15 @@ def decorator(target: Callable[..., Any]) -> Callable[[_Fn], _Fn]: # more kinds of methods that use @decorator, things may have to # be further improved in this area if "__" in repr(spec[0]): - code = ( - """\ + code = """\ %(prefix)sdef %(name)s%(grouped_args)s: return %(target_prefix)s%(target)s(%(fn)s, %(apply_pos)s) -""" - % metadata - ) +""" % metadata else: - code = ( - """\ + code = """\ %(prefix)sdef %(name)s%(grouped_args)s: return %(target_prefix)s%(target)s(%(fn)s, %(apply_kw)s) -""" - % metadata - ) +""" % metadata env: Dict[str, Any] = { targ_name: target, @@ -476,7 +471,11 @@ def get_callable_argspec( """ if inspect.isbuiltin(fn): raise TypeError("Can't inspect builtin: %s" % fn) - elif inspect.isfunction(fn): + elif inspect.isfunction(fn) or ( + hasattr(fn, "__code__") + and not inspect.isclass(fn) + and not inspect.ismethod(fn) + ): if _is_init and no_self: spec = compat.inspect_getfullargspec(fn) return compat.FullArgSpec( @@ -1564,9 +1563,13 @@ class hybridmethod(Generic[_T]): def __get__(self, instance: Any, owner: Any) -> Callable[..., _T]: if instance is None: - return self.clslevel.__get__(owner, owner.__class__) # type:ignore + return self.clslevel.__get__( # type: ignore[no-any-return] + owner, owner.__class__ + ) else: - return self.func.__get__(instance, owner) # type:ignore + return self.func.__get__( # type: ignore[no-any-return] + instance, owner + ) def classlevel(self, func: Callable[..., Any]) -> hybridmethod[_T]: self.clslevel = func @@ -1862,7 +1865,7 @@ def _warnings_warn( if frame.f_code in _warning_tags: warning_tag_found = True - (_suffix, _category) = _warning_tags[frame.f_code] + _suffix, _category = _warning_tags[frame.f_code] category = category or _category message = f"{message} ({_suffix})" diff --git a/libs/sqlalchemy/util/preloaded.py b/libs/sqlalchemy/util/preloaded.py index b57abdc4d..6ef671956 100644 --- a/libs/sqlalchemy/util/preloaded.py +++ b/libs/sqlalchemy/util/preloaded.py @@ -10,6 +10,7 @@ runtime. """ + from __future__ import annotations import sys diff --git a/libs/sqlalchemy/util/queue.py b/libs/sqlalchemy/util/queue.py index c89f9d23c..ab4f3e3a4 100644 --- a/libs/sqlalchemy/util/queue.py +++ b/libs/sqlalchemy/util/queue.py @@ -18,6 +18,7 @@ producing a ``put()`` inside the ``get()`` and therefore a reentrant condition. """ + from __future__ import annotations import asyncio @@ -36,7 +37,6 @@ from .concurrency import await_fallback from .concurrency import await_only from .langhelpers import memoized_property - _T = TypeVar("_T", bound=Any) __all__ = ["Empty", "Full", "Queue"] diff --git a/libs/sqlalchemy/util/tool_support.py b/libs/sqlalchemy/util/tool_support.py index 9e1246f8f..7ad4fbf6a 100644 --- a/libs/sqlalchemy/util/tool_support.py +++ b/libs/sqlalchemy/util/tool_support.py @@ -11,6 +11,7 @@ These aren't imported by the enclosing util package as the are not needed for normal library use. """ + from __future__ import annotations from argparse import ArgumentParser diff --git a/libs/sqlalchemy/util/typing.py b/libs/sqlalchemy/util/typing.py index 361cfac14..b4875b68f 100644 --- a/libs/sqlalchemy/util/typing.py +++ b/libs/sqlalchemy/util/typing.py @@ -469,7 +469,7 @@ def de_optionalize_union_types( else: typ.add(elem) - typ.discard(None) # type: ignore + typ.discard(None) # type: ignore[arg-type,unused-ignore] typ = {t for t in typ if t is not NoneType and not is_fwd_none(t)} diff --git a/libs/stevedore-5.7.0.dist-info/licenses/LICENSE b/libs/stevedore-5.7.0.dist-info/licenses/LICENSE deleted file mode 100644 index d64569567..000000000 --- a/libs/stevedore-5.7.0.dist-info/licenses/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/libs/stevedore-5.7.0.dist-info/INSTALLER b/libs/stevedore-5.8.0.dist-info/INSTALLER similarity index 100% rename from libs/stevedore-5.7.0.dist-info/INSTALLER rename to libs/stevedore-5.8.0.dist-info/INSTALLER diff --git a/libs/stevedore-5.7.0.dist-info/METADATA b/libs/stevedore-5.8.0.dist-info/METADATA similarity index 95% rename from libs/stevedore-5.7.0.dist-info/METADATA rename to libs/stevedore-5.8.0.dist-info/METADATA index 63205df7d..e285b2d8d 100644 --- a/libs/stevedore-5.7.0.dist-info/METADATA +++ b/libs/stevedore-5.8.0.dist-info/METADATA @@ -1,15 +1,14 @@ Metadata-Version: 2.4 Name: stevedore -Version: 5.7.0 +Version: 5.8.0 Summary: Manage dynamic plugins for Python applications Author-email: OpenStack -License: Apache-2.0 +License-Expression: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/stevedore Project-URL: Repository, https://opendev.org/openstack/stevedore Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 diff --git a/libs/stevedore-5.7.0.dist-info/RECORD b/libs/stevedore-5.8.0.dist-info/RECORD similarity index 76% rename from libs/stevedore-5.7.0.dist-info/RECORD rename to libs/stevedore-5.8.0.dist-info/RECORD index 149724963..9858fac4f 100644 --- a/libs/stevedore-5.7.0.dist-info/RECORD +++ b/libs/stevedore-5.8.0.dist-info/RECORD @@ -1,12 +1,12 @@ -stevedore-5.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -stevedore-5.7.0.dist-info/METADATA,sha256=wkFPopEj82Sf2g5VPVsPkMsjyMi3P5-M2D5rzrgGPDQ,2373 -stevedore-5.7.0.dist-info/RECORD,, -stevedore-5.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -stevedore-5.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -stevedore-5.7.0.dist-info/entry_points.txt,sha256=pESxBgqYcVV5wevQpKBLQoutvWWhIyRZbEzhsD9z_LY,387 -stevedore-5.7.0.dist-info/licenses/AUTHORS,sha256=QQb_ThCRK-AvzWkx5sPEG9axRZOOE1nCuBRK0l0VzUg,2815 -stevedore-5.7.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 -stevedore-5.7.0.dist-info/top_level.txt,sha256=rtOULIhauZOXFiAgHRCDBdnqb0wKxA-NqLlvo_b_SOM,10 +stevedore-5.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +stevedore-5.8.0.dist-info/METADATA,sha256=QWJqMEedyLI5Vkr16AoO_qCdIdHS45RTaJzR3SuTWYc,2321 +stevedore-5.8.0.dist-info/RECORD,, +stevedore-5.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +stevedore-5.8.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +stevedore-5.8.0.dist-info/entry_points.txt,sha256=pESxBgqYcVV5wevQpKBLQoutvWWhIyRZbEzhsD9z_LY,387 +stevedore-5.8.0.dist-info/licenses/AUTHORS,sha256=QQb_ThCRK-AvzWkx5sPEG9axRZOOE1nCuBRK0l0VzUg,2815 +stevedore-5.8.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 +stevedore-5.8.0.dist-info/top_level.txt,sha256=rtOULIhauZOXFiAgHRCDBdnqb0wKxA-NqLlvo_b_SOM,10 stevedore/__init__.py,sha256=lwsEP3iDFwk2lPJjgW3IbeQkhN6TeLM76tCl9V5BWYM,544 stevedore/_cache.py,sha256=BiEGT6KtQuJTCMGu9ymvfQzJot-fTtudcnhQy5MP9y4,7065 stevedore/dispatch.py,sha256=VYu5VBzdu4ekPsPMowrnkl3XJ8_rJbsRxxRRjfLOLOY,10409 @@ -24,7 +24,7 @@ stevedore/example2/fields.py,sha256=kFTnV0Vwc5QSb2pNpCEtYXxWX7BekzQ04RKz-n6KYwI, stevedore/example2/pyproject.toml,sha256=dkS5rdwOYzRqvGB4COK3anlzlnym8jMRxWRx8gQcQmg,1155 stevedore/example2/setup.py,sha256=B6iVsuHfOHmOjXNqze01phgW8OXn9fyCcyn2P1yYlLw,621 stevedore/exception.py,sha256=D0oRCv7A_tLG3AKIOGGoKuj1dAqEkCwNa99qLcCxzBs,864 -stevedore/extension.py,sha256=wbRPqN8Tivg2GA1LUmSfPFnEhF7jWqfAm-QT8iQ13nY,17101 +stevedore/extension.py,sha256=UNtyyoPfYKuXDzTs4r92TrTt2sjUw-_BbFXIfGX4hOc,17092 stevedore/hook.py,sha256=pemEFoJ23bQt3RHcr6okieB316kpvBDs_HBVTfEwMXk,4580 stevedore/named.py,sha256=HkYVg_1h34OmqV88IPR8NCkunjLAbQvdX6_wMNSj3hY,8953 stevedore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 @@ -35,13 +35,13 @@ stevedore/tests/manager.py,sha256=9Dwtw-GR1klonFaB3g4JA8lSKvX0_SypA1yTYVMvp8g,21 stevedore/tests/test_cache.py,sha256=RlnbgbxToTlaHEzFzQZZm1zS6UhKTaGWt-bMIVaAPTc,2363 stevedore/tests/test_callback.py,sha256=kNoZU8zQQHd9R0RdCqvrMTKMhRrjUCFnrje1nkKWVIM,1990 stevedore/tests/test_dispatch.py,sha256=MPrpPAPIkhBm8ATbGQOB8pVkLuSsV9V35KlVAhIZcUs,3726 -stevedore/tests/test_driver.py,sha256=AYFLUF7Sl4KfZ0sIJgLYxhylnAtVHHF8mtv4klANzzo,3529 +stevedore/tests/test_driver.py,sha256=IJsmXMMFRvne80LQDukPaSSEdgAqjtgK-IBhsTDRaog,3571 stevedore/tests/test_enabled.py,sha256=TaRcjqUOiFdf8FeZiCejoRYZsU_LQRVZlX1Ilpg3-sI,1506 stevedore/tests/test_example_fields.py,sha256=53SaSGtfMOIGrwC1ZmoerYVtvfDS0qgkIkzcN8l6lLk,1344 stevedore/tests/test_example_simple.py,sha256=ceGeqtC2qD9CU0ZXnQk6H6vEKvr4_65eaG6z5K3QDqA,924 -stevedore/tests/test_extension.py,sha256=zCxVtdxUlmeeuf3JbQqDBV757wu6x99J7yw43B5eG5I,12893 +stevedore/tests/test_extension.py,sha256=Zh6zCvzSTACW7F14lXpR41fTVX0DEu5nIeKZKn7NWOU,12982 stevedore/tests/test_hook.py,sha256=aZ5PgCTbICWj3cs49CujjLH5qlvjmgHF4VAc0nTxclA,1839 stevedore/tests/test_named.py,sha256=8si_q_uc1Yw-k0A3H95gorWxD3FsWQ_M0gLTIQVK--U,3511 -stevedore/tests/test_sphinxext.py,sha256=RfiUXUx3pVj1pu8-Y9SmE0w6kO_dUwyzqjn919IQoMw,3956 +stevedore/tests/test_sphinxext.py,sha256=sMaUdOniaUvV1sTYikCErdmZcp7HXD7aD6EaPQAI-zg,3941 stevedore/tests/test_test_manager.py,sha256=TunCGdckQ_aZ0dD1d33E8YYUSFszCP21Oa7atkWFTCM,10104 stevedore/tests/utils.py,sha256=ClMCj0b9u1ZYVf2cc6Y4Gq_Sm0m7PRU0ynjZ4EAYifs,617 diff --git a/libs/tomli-2.4.0.dist-info/REQUESTED b/libs/stevedore-5.8.0.dist-info/REQUESTED similarity index 100% rename from libs/tomli-2.4.0.dist-info/REQUESTED rename to libs/stevedore-5.8.0.dist-info/REQUESTED diff --git a/libs/python_socketio-5.16.1.dist-info/WHEEL b/libs/stevedore-5.8.0.dist-info/WHEEL similarity index 100% rename from libs/python_socketio-5.16.1.dist-info/WHEEL rename to libs/stevedore-5.8.0.dist-info/WHEEL diff --git a/libs/stevedore-5.7.0.dist-info/entry_points.txt b/libs/stevedore-5.8.0.dist-info/entry_points.txt similarity index 100% rename from libs/stevedore-5.7.0.dist-info/entry_points.txt rename to libs/stevedore-5.8.0.dist-info/entry_points.txt diff --git a/libs/stevedore-5.7.0.dist-info/licenses/AUTHORS b/libs/stevedore-5.8.0.dist-info/licenses/AUTHORS similarity index 100% rename from libs/stevedore-5.7.0.dist-info/licenses/AUTHORS rename to libs/stevedore-5.8.0.dist-info/licenses/AUTHORS diff --git a/libs/importlib_resources-6.5.2.dist-info/licenses/LICENSE b/libs/stevedore-5.8.0.dist-info/licenses/LICENSE similarity index 100% rename from libs/importlib_resources-6.5.2.dist-info/licenses/LICENSE rename to libs/stevedore-5.8.0.dist-info/licenses/LICENSE diff --git a/libs/stevedore-5.7.0.dist-info/top_level.txt b/libs/stevedore-5.8.0.dist-info/top_level.txt similarity index 100% rename from libs/stevedore-5.7.0.dist-info/top_level.txt rename to libs/stevedore-5.8.0.dist-info/top_level.txt diff --git a/libs/stevedore/extension.py b/libs/stevedore/extension.py index c8864ff27..b73870f26 100644 --- a/libs/stevedore/extension.py +++ b/libs/stevedore/extension.py @@ -62,7 +62,7 @@ class Extension(Generic[T]): self, name: str, entry_point: importlib.metadata.EntryPoint, - plugin: Callable[..., T], + plugin: type[T], obj: T | None, ) -> None: self.name = name diff --git a/libs/stevedore/tests/test_driver.py b/libs/stevedore/tests/test_driver.py index 0af7d1941..db5c3eddb 100644 --- a/libs/stevedore/tests/test_driver.py +++ b/libs/stevedore/tests/test_driver.py @@ -22,6 +22,15 @@ from stevedore.tests import test_extension from stevedore.tests import utils +class Base: ... + + +class Foo(Base): ... + + +class Bar(Base): ... + + class TestCallback(utils.TestCase): def test_detect_plugins(self): em: driver.DriverManager[Any] @@ -79,7 +88,7 @@ class TestCallback(utils.TestCase): importlib.metadata.EntryPoint( 'backend', 'pkg1:driver', 'backend' ), - lambda x: None, + Foo, None, ), extension.Extension( @@ -87,7 +96,7 @@ class TestCallback(utils.TestCase): importlib.metadata.EntryPoint( 'backend', 'pkg2:driver', 'backend' ), - lambda x: None, + Bar, None, ), ] diff --git a/libs/stevedore/tests/test_extension.py b/libs/stevedore/tests/test_extension.py index dc2e9e9e8..6f68763c2 100644 --- a/libs/stevedore/tests/test_extension.py +++ b/libs/stevedore/tests/test_extension.py @@ -230,16 +230,31 @@ class TestCallback(utils.TestCase): self.assertEqual(expected_output, set(em.items())) +class Base: + def greet(self, user): + raise NotImplementedError() + + +class Foo(Base): + def greet(self, user): + print("Hello {user}. I'm foo.") + + +class Bar(Base): + def greet(self, user): + print("Hello {user}. I'm bar.") + + class TestConflictResolution(utils.TestCase): def test_ignore_conflicts(self): """Test that ignore_conflicts logs a warning when conflicts exist.""" - extensions = [ + extensions: list[extension.Extension[Base]] = [ extension.Extension( 'conflict', importlib.metadata.EntryPoint( 'conflict', 'module1:Class1', 'test.group' ), - type('TestClass1', (), {}), + Foo, None, ), extension.Extension( @@ -247,7 +262,7 @@ class TestConflictResolution(utils.TestCase): importlib.metadata.EntryPoint( 'conflict', 'module2:Class2', 'test.group' ), - type('TestClass2', (), {}), + Bar, None, ), ] @@ -266,21 +281,21 @@ class TestConflictResolution(utils.TestCase): def test_error_on_conflict(self): """Test error_on_conflict raises MultipleMatches exception.""" - extensions = [ + extensions: list[extension.Extension[Base]] = [ extension.Extension( 'conflict', importlib.metadata.EntryPoint( - 'conflict', 'module1:Class1', 'test.group' + 'conflict', 'module1:Foo', 'test.group' ), - type('TestClass1', (), {}), + Foo, None, ), extension.Extension( 'conflict', importlib.metadata.EntryPoint( - 'conflict', 'module2:Class2', 'test.group' + 'conflict', 'module2:Bar', 'test.group' ), - type('TestClass2', (), {}), + Bar, None, ), ] @@ -301,18 +316,14 @@ class TestConflictResolution(utils.TestCase): ext1 = extension.Extension( 'test', - importlib.metadata.EntryPoint( - 'test', 'module1:Class1', 'test.group' - ), - type('TestClass1', (), {}), + importlib.metadata.EntryPoint('test', 'module1:Foo', 'test.group'), + Foo, None, ) ext2 = extension.Extension( 'test', - importlib.metadata.EntryPoint( - 'test', 'module2:Class2', 'test.group' - ), - type('TestClass2', (), {}), + importlib.metadata.EntryPoint('test', 'module2:Bar', 'test.group'), + Bar, None, ) @@ -348,7 +359,7 @@ class TestExtensionProperties(utils.TestCase): importlib.metadata.EntryPoint( 'name', 'module.name:attribute.name [extra]', 'group_name' ), - mock.Mock(), + Foo, None, ) self.ext2 = extension.Extension( @@ -356,7 +367,7 @@ class TestExtensionProperties(utils.TestCase): importlib.metadata.EntryPoint( 'name', 'module:attribute', 'group_name' ), - mock.Mock(), + Bar, None, ) diff --git a/libs/stevedore/tests/test_sphinxext.py b/libs/stevedore/tests/test_sphinxext.py index 8fe49e374..d301f37fa 100644 --- a/libs/stevedore/tests/test_sphinxext.py +++ b/libs/stevedore/tests/test_sphinxext.py @@ -20,12 +20,11 @@ from stevedore.tests import utils def _make_ext(name, docstring): - def inner(): - pass + class Foo: ... - inner.__doc__ = docstring + Foo.__doc__ = docstring m1 = importlib.metadata.EntryPoint(name, f'{name}_module:{name}', 'group') - return extension.Extension(name, m1, inner, None) + return extension.Extension(name, m1, Foo, None) class TestSphinxExt(utils.TestCase): diff --git a/libs/tomli-2.4.0.dist-info/RECORD b/libs/tomli-2.4.0.dist-info/RECORD deleted file mode 100644 index dd6cf3672..000000000 --- a/libs/tomli-2.4.0.dist-info/RECORD +++ /dev/null @@ -1,11 +0,0 @@ -tomli-2.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -tomli-2.4.0.dist-info/METADATA,sha256=9awKH4-6kItGRs1lUwnpGq2Wm2eHYWrFccpGKjgy_84,10567 -tomli-2.4.0.dist-info/RECORD,, -tomli-2.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -tomli-2.4.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 -tomli-2.4.0.dist-info/licenses/LICENSE,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072 -tomli/__init__.py,sha256=ahtDjGJA2M_wWVvGpzx4YJtWxrWBx6qE-GH5-UYoECA,314 -tomli/_parser.py,sha256=txeATLE3zHyZ-ushXtYfrZ3LoIs7JzQF2W2KL1gwJPg,25958 -tomli/_re.py,sha256=oSNZ_ilFI6chEuQ01YRSoUydBQr_okF_mSdHTkFmv90,3396 -tomli/_types.py,sha256=-GTG2VUqkpxwMqzmVO4F7ybKddIbAnuAHXfmWQcTi3Q,254 -tomli/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26 diff --git a/libs/tomli-2.4.0.dist-info/INSTALLER b/libs/tomli-2.4.1.dist-info/INSTALLER similarity index 100% rename from libs/tomli-2.4.0.dist-info/INSTALLER rename to libs/tomli-2.4.1.dist-info/INSTALLER diff --git a/libs/tomli-2.4.0.dist-info/METADATA b/libs/tomli-2.4.1.dist-info/METADATA similarity index 93% rename from libs/tomli-2.4.0.dist-info/METADATA rename to libs/tomli-2.4.1.dist-info/METADATA index ffe8e40d7..f8ba90619 100644 --- a/libs/tomli-2.4.0.dist-info/METADATA +++ b/libs/tomli-2.4.1.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: tomli -Version: 2.4.0 +Version: 2.4.1 Summary: A lil' TOML parser Keywords: toml Author-email: Taneli Hukkinen @@ -45,8 +45,8 @@ Project-URL: Homepage, https://github.com/hukkin/tomli - [Is there a `dumps`, `write` or `encode` function?](#is-there-a-dumps-write-or-encode-function) - [How do TOML types map into Python types?](#how-do-toml-types-map-into-python-types) - [Performance](#performance) - - [Pure Python](#pure-python) - [Mypyc generated wheel](#mypyc-generated-wheel) + - [Pure Python](#pure-python) @@ -177,7 +177,7 @@ tomllib.loads("['This parses fine with Python 3.6+']") - it's lil' - pure Python with zero dependencies - the fastest pure Python parser [\*](#pure-python): - 18x as fast as [tomlkit](https://pypi.org/project/tomlkit/), + 14x as fast as [tomlkit](https://pypi.org/project/tomlkit/), 2.1x as fast as [toml](https://pypi.org/project/toml/) - outputs [basic data types](#how-do-toml-types-map-into-python-types) only - 100% spec compliant: passes all tests in @@ -223,48 +223,43 @@ The core library does not include write capability, as most TOML use cases are r The `benchmark/` folder in this repository contains a performance benchmark for comparing the various Python TOML parsers. -Below are the results for commit [0724e2a](https://github.com/hukkin/tomli/tree/0724e2ab1858da7f5e05a9bffdb24c33589d951c). - -### Pure Python - -```console -foo@bar:~/dev/tomli$ python --version -Python 3.12.7 -foo@bar:~/dev/tomli$ pip freeze -attrs==21.4.0 -click==8.1.7 -pytomlpp==1.0.13 -qtoml==0.3.1 -rtoml==0.11.0 -toml==0.10.2 -tomli @ file:///home/foo/dev/tomli -tomlkit==0.13.2 -foo@bar:~/dev/tomli$ python benchmark/run.py -Parsing data.toml 5000 times: ------------------------------------------------------- - parser | exec time | performance (more is better) ------------+------------+----------------------------- - rtoml | 0.647 s | baseline (100%) - pytomlpp | 0.891 s | 72.62% - tomli | 3.14 s | 20.56% - toml | 6.69 s | 9.67% - qtoml | 8.27 s | 7.82% - tomlkit | 56.1 s | 1.15% -``` +Below are the results for commit [064e492](https://github.com/hukkin/tomli/tree/064e492919b2338def788753b8c981c9131334c0). ### Mypyc generated wheel ```console +foo@bar:~/dev/tomli$ python --version +Python 3.14.2 +foo@bar:~/dev/tomli$ pip freeze +pytomlpp==1.1.0 +rtoml==0.13.0 +toml==0.10.2 +tomli @ file:///home/foo/dev/tomli +tomlkit==0.13.3 foo@bar:~/dev/tomli$ python benchmark/run.py Parsing data.toml 5000 times: ------------------------------------------------------ parser | exec time | performance (more is better) -----------+------------+----------------------------- - rtoml | 0.668 s | baseline (100%) - pytomlpp | 0.893 s | 74.81% - tomli | 1.96 s | 34.18% - toml | 6.64 s | 10.07% - qtoml | 8.26 s | 8.09% - tomlkit | 52.9 s | 1.26% + rtoml | 0.328 s | baseline (100%) + pytomlpp | 0.365 s | 89.75% + tomli | 0.838 s | 39.12% + toml | 3.01 s | 10.90% + tomlkit | 20.7 s | 1.59% +``` + +### Pure Python + +```console +foo@bar:~/dev/tomli$ python benchmark/run.py +Parsing data.toml 5000 times: +------------------------------------------------------ + parser | exec time | performance (more is better) +-----------+------------+----------------------------- + rtoml | 0.323 s | baseline (100%) + pytomlpp | 0.365 s | 88.40% + tomli | 1.44 s | 22.36% + toml | 3.03 s | 10.65% + tomlkit | 20.6 s | 1.57% ``` diff --git a/libs/tomli-2.4.1.dist-info/RECORD b/libs/tomli-2.4.1.dist-info/RECORD new file mode 100644 index 000000000..55f3285f1 --- /dev/null +++ b/libs/tomli-2.4.1.dist-info/RECORD @@ -0,0 +1,11 @@ +tomli-2.4.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +tomli-2.4.1.dist-info/METADATA,sha256=srP98lZrujExBqv0qAb5whf4-BRYfxAXxJTUkHmgpoM,10463 +tomli-2.4.1.dist-info/RECORD,, +tomli-2.4.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tomli-2.4.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +tomli-2.4.1.dist-info/licenses/LICENSE,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072 +tomli/__init__.py,sha256=nrBC18DbXRTCFo7ElG5BDeWpHJzOhokvXk215GM8Z2I,314 +tomli/_parser.py,sha256=pBIjTIa_cQs2HglDJ2lh8OJfptfDa6eg5-7Iej4BjHs,26440 +tomli/_re.py,sha256=oSNZ_ilFI6chEuQ01YRSoUydBQr_okF_mSdHTkFmv90,3396 +tomli/_types.py,sha256=-GTG2VUqkpxwMqzmVO4F7ybKddIbAnuAHXfmWQcTi3Q,254 +tomli/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26 diff --git a/libs/tqdm-4.67.3.dist-info/REQUESTED b/libs/tomli-2.4.1.dist-info/REQUESTED similarity index 100% rename from libs/tqdm-4.67.3.dist-info/REQUESTED rename to libs/tomli-2.4.1.dist-info/REQUESTED diff --git a/libs/tomli-2.4.0.dist-info/WHEEL b/libs/tomli-2.4.1.dist-info/WHEEL similarity index 100% rename from libs/tomli-2.4.0.dist-info/WHEEL rename to libs/tomli-2.4.1.dist-info/WHEEL diff --git a/libs/tomli-2.4.0.dist-info/licenses/LICENSE b/libs/tomli-2.4.1.dist-info/licenses/LICENSE similarity index 100% rename from libs/tomli-2.4.0.dist-info/licenses/LICENSE rename to libs/tomli-2.4.1.dist-info/licenses/LICENSE diff --git a/libs/tomli/__init__.py b/libs/tomli/__init__.py index 55699b1ea..ebe900130 100644 --- a/libs/tomli/__init__.py +++ b/libs/tomli/__init__.py @@ -3,6 +3,6 @@ # Licensed to PSF under a Contributor Agreement. __all__ = ("loads", "load", "TOMLDecodeError") -__version__ = "2.4.0" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT +__version__ = "2.4.1" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT from ._parser import TOMLDecodeError, load, loads diff --git a/libs/tomli/_parser.py b/libs/tomli/_parser.py index 3038891af..41b064188 100644 --- a/libs/tomli/_parser.py +++ b/libs/tomli/_parser.py @@ -34,6 +34,13 @@ if TYPE_CHECKING: # lower number than where mypyc binaries crash. MAX_INLINE_NESTING: Final = sys.getrecursionlimit() +# Pathologically excessive number of parts in a key runs into quadratic +# behavior (e.g. in Flags.is_). +# Even if keys aren't currently parsed using recursion, they name a +# recursive structure, so it makes sense to limit it using getrecursionlimit() +# and RecursionError. +MAX_KEY_PARTS: Final = sys.getrecursionlimit() + ASCII_CTRL: Final = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) # Neither of these sets include quotation mark or backslash. They are @@ -475,6 +482,10 @@ def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]: pos = skip_chars(src, pos, TOML_WS) pos, key_part = parse_key_part(src, pos) key += (key_part,) + if len(key) > MAX_KEY_PARTS: + raise RecursionError( + f"TOML key has more than the allowed {MAX_KEY_PARTS} parts" + ) pos = skip_chars(src, pos, TOML_WS) diff --git a/libs/tqdm-4.67.3.dist-info/RECORD b/libs/tqdm-4.67.3.dist-info/RECORD deleted file mode 100644 index 9e5606003..000000000 --- a/libs/tqdm-4.67.3.dist-info/RECORD +++ /dev/null @@ -1,42 +0,0 @@ -../../bin/tqdm,sha256=jVIvqkAmuV_K0wwiZJmo0UsNisjPMiaAwPMJkH4NsDY,183 -tqdm-4.67.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -tqdm-4.67.3.dist-info/METADATA,sha256=fA6aZiwZCV8zmkt2EgHrBs_5xh-5WBU1svlvROcY7Sk,57679 -tqdm-4.67.3.dist-info/RECORD,, -tqdm-4.67.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -tqdm-4.67.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -tqdm-4.67.3.dist-info/entry_points.txt,sha256=ReJCH7Ui3Zyh6M16E4OhsZ1oU7WtMXCfbtoyBhGO29Y,39 -tqdm-4.67.3.dist-info/licenses/LICENCE,sha256=_P-Hw6R86AKKhRKqGC1PzwrRyQVE7nXPmzQ2hMrBlN4,1985 -tqdm-4.67.3.dist-info/top_level.txt,sha256=NLiUJNfmc9At15s7JURiwvqMEjUi9G5PMGRrmMYzNSM,5 -tqdm/__init__.py,sha256=9mQNYSSqP99JasubEC1POJLMmhkkBH6cJZxPIR5G2pQ,1572 -tqdm/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30 -tqdm/_main.py,sha256=9ySvgmi_2Sw4CAo5UDW0Q2dxfTryboEWGHohfCJz0sA,283 -tqdm/_monitor.py,sha256=Uku-DPWgzJ7dO5CK08xKJK-E_F6qQ-JB3ksuXczSYR0,3699 -tqdm/_tqdm.py,sha256=LfLCuJ6bpsVo9xilmtBXyEm1vGnUCFrliW85j3J-nD4,283 -tqdm/_tqdm_gui.py,sha256=03Hc8KayxJveieI5-0-2NGiDpLvw9jZekofJUV7CCwk,287 -tqdm/_tqdm_notebook.py,sha256=BuHiLuxu6uEfZFaPJW3RPpPaxaVctEQA3kdSJSDL1hw,307 -tqdm/_tqdm_pandas.py,sha256=c9jptUgigN6axRDhRd4Rif98Tmxeopc1nFNFhIpbFUE,888 -tqdm/_utils.py,sha256=_4E73bfDj4f1s3sM42NLHNrZDOkijZoWq-n6xWLkdZ8,553 -tqdm/asyncio.py,sha256=Kp2rSkNRf9KRqa3d9YpgeZQ7L7EZf2Ki4bSc7UPIyoo,2757 -tqdm/auto.py,sha256=nDZflj6p2zKkjBCNBourrhS81zYfZy1_dQvbckrdW8o,871 -tqdm/autonotebook.py,sha256=Yb9F5uaiBPhfbDDFpbtoG8I2YUw3uQJ89rUDLbfR6ws,956 -tqdm/cli.py,sha256=orEaahqDEBGZR1uCcTW8BkhSPEhBlSO0BKYbJ1Qgc6k,10994 -tqdm/completion.sh,sha256=j79KbSmpIj_E11jfTfBXrGnUTzKXVpQ1vGVQvsyDRl4,946 -tqdm/contrib/__init__.py,sha256=1GMYYE866WCvlHg5MdmzOvPsQj4ZuMlXzz8xcRcJD60,2479 -tqdm/contrib/bells.py,sha256=Yx1HqGCmHrESCAO700j5wE__JCleNODJxedh1ijPLD0,837 -tqdm/contrib/concurrent.py,sha256=K1yjloKS5WRNFyjLRth0DmU5PAnDbF0A-GD27N-J4a8,3986 -tqdm/contrib/discord.py,sha256=MtVIL1s_dxH21G4sL8FBgQ4Wei23ho9Ek5T-AommvNc,5243 -tqdm/contrib/itertools.py,sha256=WdKKQU5eSzsqHu29SN_oH12huYZo0Jihqoi9-nVhwz4,774 -tqdm/contrib/logging.py,sha256=NsYtnKttj2mMrGm58mEdo5a9DP_2vv8pZyrimSuWulA,3760 -tqdm/contrib/slack.py,sha256=eP_Mr5sQonYniHxxQNGue3jk2JkIPmPWFZqIYxnOui0,4007 -tqdm/contrib/telegram.py,sha256=vn_9SATMbbwn2PAbzSDyOX6av3eBB01QBug11P4H-Og,5008 -tqdm/contrib/utils_worker.py,sha256=vaxyJyagYwOCQXLUMd86bCbI4sPqZysuMGbDSGmvEWo,1199 -tqdm/dask.py,sha256=9Ei58eVqTossRLhAfWyUFCduXYKjmLmwkaXIy-CHYfs,1319 -tqdm/gui.py,sha256=STIB3K8iDzDgkNUqWIpvcI_u0OGtbGNy5NwpALXhfWs,5479 -tqdm/keras.py,sha256=op9sBkb6q6c6dw2wJ0SD2ZwpPK7yM1Vbg4l1Qiy3MIo,4373 -tqdm/notebook.py,sha256=scgzRQmDYzIFag_1gRh98LhtZdu3ig2yrmZdqvlgops,10802 -tqdm/rich.py,sha256=YyMPkEHVyYUVUR3adJKbVX26iTmNKpNMf3DEqmm-m60,5021 -tqdm/std.py,sha256=Sk24SwOd59hpNbb0UL8Y3_8ueRmL3_5O72Vy2XKasjE,57510 -tqdm/tk.py,sha256=Gu0uwXwLCGPRGHORdi3WvBLGiseUp_xxX_h_gp9VpK0,6701 -tqdm/tqdm.1,sha256=cDyo6hSykXedJ3Pcx5tkogR1ai3QXRnrJ0MbR2Gd9-s,7581 -tqdm/utils.py,sha256=dpGZUeqoLPinM-kEsULTSe2E3jzp8wUunL6eKzkYOgk,11810 -tqdm/version.py,sha256=oBM1eCOTDZDGZHffNqlehXTeQdKdracckNAEaMRmPjM,337 diff --git a/libs/tqdm-4.67.3.dist-info/INSTALLER b/libs/tqdm-4.68.3.dist-info/INSTALLER similarity index 100% rename from libs/tqdm-4.67.3.dist-info/INSTALLER rename to libs/tqdm-4.68.3.dist-info/INSTALLER diff --git a/libs/tqdm-4.67.3.dist-info/METADATA b/libs/tqdm-4.68.3.dist-info/METADATA similarity index 91% rename from libs/tqdm-4.67.3.dist-info/METADATA rename to libs/tqdm-4.68.3.dist-info/METADATA index fb6453e5b..4990bcd71 100644 --- a/libs/tqdm-4.67.3.dist-info/METADATA +++ b/libs/tqdm-4.68.3.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: tqdm -Version: 4.67.3 +Version: 4.68.3 Summary: Fast, Extensible Progress Meter Maintainer-email: tqdm developers License: MPL-2.0 AND MIT @@ -35,13 +35,13 @@ Classifier: Operating System :: POSIX :: SunOS/Solaris Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation :: IronPython @@ -63,23 +63,19 @@ Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Shells Classifier: Topic :: Terminals Classifier: Topic :: Utilities -Requires-Python: >=3.7 +Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENCE Requires-Dist: colorama; platform_system == "Windows" -Requires-Dist: importlib_metadata; python_version < "3.8" -Provides-Extra: dev -Requires-Dist: pytest>=6; extra == "dev" -Requires-Dist: pytest-cov; extra == "dev" -Requires-Dist: pytest-timeout; extra == "dev" -Requires-Dist: pytest-asyncio>=0.24; extra == "dev" -Requires-Dist: nbval; extra == "dev" Provides-Extra: discord Requires-Dist: requests; extra == "discord" +Requires-Dist: envwrap; extra == "discord" Provides-Extra: slack Requires-Dist: slack-sdk; extra == "slack" +Requires-Dist: envwrap; extra == "slack" Provides-Extra: telegram Requires-Dist: requests; extra == "telegram" +Requires-Dist: envwrap; extra == "telegram" Provides-Extra: notebook Requires-Dist: ipywidgets>=6; extra == "notebook" Dynamic: license-file @@ -93,7 +89,7 @@ tqdm |Build-Status| |Coverage-Status| |Branch-Coverage-Status| |Codacy-Grade| |Libraries-Rank| |PyPI-Downloads| -|LICENCE| |OpenHub-Status| |binder-demo| |awesome-python| +|LICENCE| |OpenHub-Status| |colab-demo| |binder-demo| |awesome-python| ``tqdm`` derives from the Arabic word *taqaddum* (تقدّم) which can mean "progress," and is an abbreviation for "I love you so much" in Spanish (*te quiero demasiado*). @@ -145,6 +141,17 @@ in any console or in a GUI, and is also friendly with IPython/Jupyter notebooks. Python and an environment supporting ``carriage return \r`` and ``line feed \n`` control characters. +**Featured Sponsors** + +======== ================================================ +|OpenAI| Your Logo Here +======== ================================================ +OpenAI `Sponsors `__ +======== ================================================ + +.. |OpenAI| image:: https://avatars.githubusercontent.com/u/14957082?s=96 + :target: https://openai.com + ------------------------------------------ .. contents:: Table of contents @@ -400,7 +407,7 @@ of a neat one-line progress bar. - Unicode: * Environments which report that they support unicode will have solid smooth - progressbars. The fallback is an ``ascii``-only bar. + progress bars. The fallback is an ``ascii``-only bar. * Windows consoles often only partially support unicode and thus `often require explicit ascii=True `__ (also `here `__). This is due to @@ -440,10 +447,10 @@ Documentation """ Decorate an iterable object, returning an iterator which acts exactly like the original iterable, but prints a dynamically updating - progressbar every time a value is requested. + progress bar every time a value is requested. """ - @envwrap("TQDM_") # override defaults via env vars + @envwrap("tqdm") # override defaults via env vars def __init__(self, iterable=None, desc=None, total=None, leave=True, file=None, ncols=None, mininterval=0.1, maxinterval=10.0, miniters=None, ascii=None, disable=False, @@ -456,20 +463,20 @@ Parameters ~~~~~~~~~~ * iterable : iterable, optional - Iterable to decorate with a progressbar. + Iterable to decorate with a progress bar. Leave blank to manually manage the updates. * desc : str, optional - Prefix for the progressbar. + Prefix for the progress bar. * total : int or float, optional The number of expected iterations. If unspecified, len(iterable) is used if possible. If float("inf") or as a last resort, only basic progress statistics are displayed - (no ETA, no progressbar). + (no ETA, no progress bar). If ``gui`` is True and this parameter needs subsequent updating, specify an initial arbitrary large positive number, e.g. 9e9. * leave : bool, optional - If [default: True], keeps all traces of the progressbar + If [default: True], keeps all traces of the progress bar upon termination of iteration. If ``None``, will leave only if ``position`` is ``0``. * file : ``io.TextIOWrapper`` or ``io.StringIO``, optional @@ -478,7 +485,7 @@ Parameters methods. For encoding, see ``write_bytes``. * ncols : int, optional The width of the entire output message. If specified, - dynamically resizes the progressbar to stay within this bound. + dynamically resizes the progress bar to stay within this bound. If unspecified, attempts to use environment width. The fallback is a meter width of 10 and no limit for the counter and statistics. If 0, will not print any meter (only stats). @@ -501,7 +508,7 @@ Parameters If unspecified or False, use unicode (smooth blocks) to fill the meter. The fallback is to use ASCII characters " 123456789#". * disable : bool, optional - Whether to disable the entire progressbar wrapper + Whether to disable the entire progress bar wrapper [default: False]. If set to None, disable on non-TTY. * unit : str, optional String that will be used to define the unit of each iteration @@ -609,24 +616,24 @@ Returns ... t.update(len(current_buffer)) >>> t.close() The last line is highly recommended, but possibly not necessary if - ``t.update()`` will be called in such a way that ``filesize`` will be + `t.update()` will be called in such a way that `filesize` will be exactly reached and printed. Parameters ---------- n : int or float, optional Increment to add to the internal counter of iterations - [default: 1]. If using float, consider specifying ``{n:.3f}`` - or similar in ``bar_format``, or specifying ``unit_scale``. + [default: 1]. If using float, consider specifying `{n:.3f}` + or similar in `bar_format`, or specifying `unit_scale`. Returns ------- out : bool or None - True if a ``display()`` was triggered. + True if a `display()` was triggered. """ def close(self): - """Cleanup and (if leave=False) close the progressbar.""" + """Cleanup and (if leave=False) close the progress bar.""" def clear(self, nomove=False): """Clear current bar display.""" @@ -638,11 +645,11 @@ Returns Parameters ---------- nolock : bool, optional - If ``True``, does not lock. - If [default: ``False``]: calls ``acquire()`` on internal lock. + If `True`, does not lock. + If [default: `False`]: calls `acquire()` on internal lock. lock_args : tuple, optional - Passed to internal lock's ``acquire()``. - If specified, will only ``display()`` if ``acquire()`` returns ``True``. + Passed to internal lock's `acquire()`. + If specified, will only `display()` if `acquire()` returns `True`. """ def unpause(self): @@ -652,7 +659,7 @@ Returns """ Resets to 0 iterations for repeated use. - Consider combining with ``leave=True``. + Consider combining with `leave=True`. Parameters ---------- @@ -693,16 +700,16 @@ Returns def display(self, msg=None, pos=None): """ - Use ``self.sp`` to display ``msg`` in the specified ``pos``. + Use `self.sp` to display `msg` in the specified `pos`. Consider overloading this function when inheriting to use e.g.: - ``self.some_frontend(**self.format_dict)`` instead of ``self.sp``. + `self.some_frontend(**self.format_dict)` instead of `self.sp`. Parameters ---------- - msg : str, optional. What to display (default: ``repr(self)``). - pos : int, optional. Position to ``moveto`` - (default: ``abs(self.pos)``). + msg : str, optional. What to display (default: `repr(self)`). + pos : int, optional. Position to `moveto` + (default: `abs(self.pos)`). """ @classmethod @@ -710,8 +717,8 @@ Returns def wrapattr(cls, stream, method, total=None, bytes=True, **tqdm_kwargs): """ stream : file-like object. - method : str, "read" or "write". The result of ``read()`` and - the first argument of ``write()`` should have a ``len()``. + method : str, "read" or "write". The result of `read()` and + the first argument of `write()` should have a `len()`. >>> with tqdm.wrapattr(file_obj, "read", total=file_obj.size) as fobj: ... while True: @@ -751,7 +758,7 @@ Submodules """IPython/Jupyter Notebook widget.""" class tqdm.auto.tqdm(tqdm.tqdm): - """Automatically chooses beween `tqdm.notebook` and `tqdm.tqdm`.""" + """Automatically chooses between `tqdm.notebook` and `tqdm.tqdm`.""" class tqdm.asyncio.tqdm(tqdm.tqdm): """Asynchronous version.""" @@ -781,15 +788,18 @@ Submodules The ``tqdm.contrib`` package also contains experimental modules: -- ``tqdm.contrib.itertools``: Thin wrappers around ``itertools`` -- ``tqdm.contrib.concurrent``: Thin wrappers around ``concurrent.futures`` -- ``tqdm.contrib.slack``: Posts to `Slack `__ bots -- ``tqdm.contrib.discord``: Posts to `Discord `__ bots -- ``tqdm.contrib.telegram``: Posts to `Telegram `__ bots -- ``tqdm.contrib.bells``: Automagically enables all optional features +- `tqdm.contrib.itertools `_: Thin wrappers around ``itertools`` +- `tqdm.contrib.concurrent `_: Thin wrappers around ``concurrent.futures`` +- `tqdm.contrib.slack `_: Posts to `Slack `__ bots +- `tqdm.contrib.discord `_: Posts to `Discord `__ bots +- `tqdm.contrib.telegram `_: Posts to `Telegram `__ bots +- `tqdm.contrib.bells `_: Automagically enables all optional features * ``auto``, ``pandas``, ``slack``, ``discord``, ``telegram`` +.. image:: https://tqdm.github.io/img/screenshot-discord.png + :alt: Screenshot of `tqdm.contrib.discord` in action + Examples and Advanced Usage --------------------------- @@ -800,7 +810,7 @@ Examples and Advanced Usage * this has an `excellent article `__ - on how to make a **great** progressbar; + on how to make a **great** progress bar; - check out the `slides from PyData London `__, or - run the |binder-demo|. @@ -986,7 +996,7 @@ Here's an example with ``urllib``: self.total = tsize return self.update(b * bsize - self.n) # also sets self.n = b * bsize - eg_link = "https://caspersci.uk.to/matryoshka.zip" + eg_link = "https://cgi.cdcl.ml/matryoshka.zip" with TqdmUpTo(unit='B', unit_scale=True, unit_divisor=1024, miniters=1, desc=eg_link.split('/')[-1]) as t: # all optional kwargs urllib.urlretrieve(eg_link, filename=os.devnull, @@ -1030,7 +1040,7 @@ down to: import urllib, os from tqdm import tqdm - eg_link = "https://caspersci.uk.to/matryoshka.zip" + eg_link = "https://cgi.cdcl.ml/matryoshka.zip" response = getattr(urllib, 'request', urllib).urlopen(eg_link) with tqdm.wrapattr(open(os.devnull, "wb"), "write", miniters=1, desc=eg_link.split('/')[-1], @@ -1045,7 +1055,7 @@ The ``requests`` equivalent is nearly identical: import requests, os from tqdm import tqdm - eg_link = "https://caspersci.uk.to/matryoshka.zip" + eg_link = "https://cgi.cdcl.ml/matryoshka.zip" response = requests.get(eg_link, stream=True) with tqdm.wrapattr(open(os.devnull, "wb"), "write", miniters=1, desc=eg_link.split('/')[-1], @@ -1340,7 +1350,7 @@ This is a work in progress (see Since ``tqdm`` uses a simple printing mechanism to display progress bars, you should not write any message in the terminal using ``print()`` while -a progressbar is open. +a progress bar is open. To write messages in the terminal without any collision with ``tqdm`` bar display, a ``.write()`` method is provided: @@ -1488,27 +1498,17 @@ See the `CONTRIBUTING `__ file for more information. -Developers who have made significant contributions, ranked by *SLoC* -(surviving lines of code, -`git fame `__ ``-wMC --excl '\.(png|gif|jpg)$'``), -are: +Developers who have contributed more that 5 *loc* (surviving lines of code): -==================== ======================================================== ==== ================================ -Name ID SLoC Notes -==================== ======================================================== ==== ================================ -Casper da Costa-Luis `casperdcl `__ ~80% primary maintainer |Gift-Casper| -Stephen Larroque `lrq3000 `__ ~9% team member -Martin Zugnoni `martinzugnoni `__ ~3% -Daniel Ecer `de-code `__ ~2% -Richard Sheridan `richardsheridan `__ ~1% -Guangshuo Chen `chengs `__ ~1% -Helio Machado `0x2b3bfa0 `__ ~1% -Kyle Altendorf `altendky `__ <1% -Noam Yorav-Raphael `noamraph `__ <1% original author -Matthew Stevens `mjstevens777 `__ <1% -Hadrien Mary `hadim `__ <1% team member -Mikhail Korobov `kmike `__ <1% team member -==================== ======================================================== ==== ================================ +|git-fame| + +Honourable mentions: + +- Casper da Costa-Luis `casperdcl `__ primary maintainer |Gift-Casper| +- Stephen Larroque `lrq3000 `__ maintainer +- Hadrien Mary `hadim `__ maintainer +- Mikhail Korobov `kmike `__ maintainer +- Noam Yorav-Raphael `noamraph `__ original author Ports to Other Languages ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1541,7 +1541,7 @@ Citation information: |DOI| .. |Branch-Coverage-Status| image:: https://codecov.io/gh/tqdm/tqdm/branch/master/graph/badge.svg :target: https://codecov.io/gh/tqdm/tqdm .. |Codacy-Grade| image:: https://app.codacy.com/project/badge/Grade/3f965571598f44549c7818f29cdcf177 - :target: https://www.codacy.com/gh/tqdm/tqdm/dashboard + :target: https://app.codacy.com/gh/tqdm/tqdm/dashboard .. |CII Best Practices| image:: https://bestpractices.coreinfrastructure.org/projects/3264/badge :target: https://bestpractices.coreinfrastructure.org/projects/3264 .. |GitHub-Status| image:: https://img.shields.io/github/tag/tqdm/tqdm.svg?maxAge=86400&logo=github&logoColor=white @@ -1560,7 +1560,10 @@ Citation information: |DOI| :target: https://github.com/tqdm/tqdm/graphs/contributors .. |GitHub-Updated| image:: https://img.shields.io/github/last-commit/tqdm/tqdm/master.svg?logo=github&logoColor=white&label=pushed :target: https://github.com/tqdm/tqdm/pulse -.. |Gift-Casper| image:: https://img.shields.io/badge/dynamic/json.svg?color=ff69b4&label=gifts%20received&prefix=%C2%A3&query=%24..sum&url=https%3A%2F%2Fcaspersci.uk.to%2Fgifts.json +.. |git-fame| image:: https://git-fame.cdcl.ml/gh/tqdm/tqdm?min=5&w=1&M=1&C=1&excl=(png|gif|jpg)$&enum=1 + :alt: git-fame.cdcl.ml/gh/tqdm/tqdm + :target: https://git-fame.cdcl.ml/gh/tqdm/tqdm?w=1&M=1&C=1&excl=(png|gif|jpg)$&enum=1 +.. |Gift-Casper| image:: https://img.shields.io/badge/sponsor-casper-ff69b4.svg :target: https://cdcl.ml/sponsor .. |Versions| image:: https://img.shields.io/pypi/v/tqdm.svg :target: https://tqdm.github.io/releases @@ -1586,6 +1589,8 @@ Citation information: |DOI| :target: https://raw.githubusercontent.com/tqdm/tqdm/master/LICENCE .. |DOI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.595120-blue.svg :target: https://doi.org/10.5281/zenodo.595120 +.. |colab-demo| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/tqdm/tqdm/blob/master/DEMO.ipynb .. |binder-demo| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/tqdm/tqdm/master?filepath=DEMO.ipynb .. |Screenshot-Jupyter1| image:: https://tqdm.github.io/img/jupyter-1.gif diff --git a/libs/tqdm-4.68.3.dist-info/RECORD b/libs/tqdm-4.68.3.dist-info/RECORD new file mode 100644 index 000000000..d299128e3 --- /dev/null +++ b/libs/tqdm-4.68.3.dist-info/RECORD @@ -0,0 +1,42 @@ +../../bin/tqdm,sha256=jVIvqkAmuV_K0wwiZJmo0UsNisjPMiaAwPMJkH4NsDY,183 +tqdm-4.68.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +tqdm-4.68.3.dist-info/METADATA,sha256=mGITVt-CGcWXIXbW8y8jKLfaBKnscmGzUMsQFyzEt9M,57356 +tqdm-4.68.3.dist-info/RECORD,, +tqdm-4.68.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tqdm-4.68.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +tqdm-4.68.3.dist-info/entry_points.txt,sha256=ReJCH7Ui3Zyh6M16E4OhsZ1oU7WtMXCfbtoyBhGO29Y,39 +tqdm-4.68.3.dist-info/licenses/LICENCE,sha256=_P-Hw6R86AKKhRKqGC1PzwrRyQVE7nXPmzQ2hMrBlN4,1985 +tqdm-4.68.3.dist-info/top_level.txt,sha256=NLiUJNfmc9At15s7JURiwvqMEjUi9G5PMGRrmMYzNSM,5 +tqdm/__init__.py,sha256=9mQNYSSqP99JasubEC1POJLMmhkkBH6cJZxPIR5G2pQ,1572 +tqdm/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30 +tqdm/_main.py,sha256=9ySvgmi_2Sw4CAo5UDW0Q2dxfTryboEWGHohfCJz0sA,283 +tqdm/_monitor.py,sha256=1VcavuotYSN6K-1jbpNuy6HYtr3mwo5Nh_jZMQ-jg3A,4013 +tqdm/_tqdm.py,sha256=LfLCuJ6bpsVo9xilmtBXyEm1vGnUCFrliW85j3J-nD4,283 +tqdm/_tqdm_gui.py,sha256=03Hc8KayxJveieI5-0-2NGiDpLvw9jZekofJUV7CCwk,287 +tqdm/_tqdm_notebook.py,sha256=BuHiLuxu6uEfZFaPJW3RPpPaxaVctEQA3kdSJSDL1hw,307 +tqdm/_tqdm_pandas.py,sha256=c9jptUgigN6axRDhRd4Rif98Tmxeopc1nFNFhIpbFUE,888 +tqdm/_utils.py,sha256=CfLztxFY-QRiNfxQCqAb2N8E13uauIM95RSQiFlaEuE,559 +tqdm/asyncio.py,sha256=d6vZ88ZFq6zq3tAWpkInB2xITVKCNEBakudm_-8Xtf8,2758 +tqdm/auto.py,sha256=nDZflj6p2zKkjBCNBourrhS81zYfZy1_dQvbckrdW8o,871 +tqdm/autonotebook.py,sha256=2pNgGCJDsesNNyYnmSve8BNQLiGSgo97SsPoMXCrNRQ,1440 +tqdm/cli.py,sha256=t1j_PqI0SuMyhJGoBxMPCXY-UTbfjciaAQYxAU_f_V0,11011 +tqdm/completion.sh,sha256=j79KbSmpIj_E11jfTfBXrGnUTzKXVpQ1vGVQvsyDRl4,946 +tqdm/contrib/__init__.py,sha256=1GMYYE866WCvlHg5MdmzOvPsQj4ZuMlXzz8xcRcJD60,2479 +tqdm/contrib/bells.py,sha256=IaxC2YW57xHDinxdUDQHvSego0HSTdDTExQQENvSB_k,921 +tqdm/contrib/concurrent.py,sha256=K1yjloKS5WRNFyjLRth0DmU5PAnDbF0A-GD27N-J4a8,3986 +tqdm/contrib/discord.py,sha256=LPjxDbmfiYtw3OAm_WhtwbX9BVYDlPc8lJ96aY3YZDE,5337 +tqdm/contrib/itertools.py,sha256=MA9-Jvb3ToseppInvev2iF4bokN7GQy4Q4LgjUyYdA4,2949 +tqdm/contrib/logging.py,sha256=clW-AxaNwaynw9QGWVByjKgB3WJULaocoG_9C3cJ-qc,3791 +tqdm/contrib/slack.py,sha256=sC182v7xBkfGftnDeUmQALMOfwQzU3ZiNf1Q_XA1DjI,4049 +tqdm/contrib/telegram.py,sha256=ybTraGjUQx5m443TsX6pJ7Uxrpsh1m4a43BTWVl_4e4,5081 +tqdm/contrib/utils_worker.py,sha256=vaxyJyagYwOCQXLUMd86bCbI4sPqZysuMGbDSGmvEWo,1199 +tqdm/dask.py,sha256=9Ei58eVqTossRLhAfWyUFCduXYKjmLmwkaXIy-CHYfs,1319 +tqdm/gui.py,sha256=maQ7jgB5leWHwxMlbBv08hpe5lytvmH0hEARu22GRdU,5481 +tqdm/keras.py,sha256=ugd80-xCxMqnrbx_DEOpmj6c2Sh1xoVtQ8V9-bw29eo,4396 +tqdm/notebook.py,sha256=6xkUTcYRsFxN4_BSUYYZ51kAgI7ycnT0HLpP9HukEqg,10888 +tqdm/rich.py,sha256=YyMPkEHVyYUVUR3adJKbVX26iTmNKpNMf3DEqmm-m60,5021 +tqdm/std.py,sha256=_T-OKI1PBEKy_JWAOWCq9MOOYMjxqVq8lpr3z_YIVUA,57728 +tqdm/tk.py,sha256=3VsB1MDAkCItaWu7CmMb2CoTLMiqmDiEgNi3yiqQ5GY,6702 +tqdm/tqdm.1,sha256=MLg9qjeHThZpPEQbWVrf5P5gOXMKHE_X606FSOBI9eU,7538 +tqdm/utils.py,sha256=o_f0V32H_wrn0XmRno3j9pJ9xlp5xL0fHT3gtqL-k58,9746 +tqdm/version.py,sha256=0uH4TLe8I8H71x29dlmtEZBObLT3b5IW-nytF3V0IVw,233 diff --git a/libs/tzdata-2025.3.dist-info/REQUESTED b/libs/tqdm-4.68.3.dist-info/REQUESTED similarity index 100% rename from libs/tzdata-2025.3.dist-info/REQUESTED rename to libs/tqdm-4.68.3.dist-info/REQUESTED diff --git a/libs/requests-2.32.5.dist-info/WHEEL b/libs/tqdm-4.68.3.dist-info/WHEEL similarity index 100% rename from libs/requests-2.32.5.dist-info/WHEEL rename to libs/tqdm-4.68.3.dist-info/WHEEL diff --git a/libs/tqdm-4.67.3.dist-info/entry_points.txt b/libs/tqdm-4.68.3.dist-info/entry_points.txt similarity index 100% rename from libs/tqdm-4.67.3.dist-info/entry_points.txt rename to libs/tqdm-4.68.3.dist-info/entry_points.txt diff --git a/libs/tqdm-4.67.3.dist-info/licenses/LICENCE b/libs/tqdm-4.68.3.dist-info/licenses/LICENCE similarity index 100% rename from libs/tqdm-4.67.3.dist-info/licenses/LICENCE rename to libs/tqdm-4.68.3.dist-info/licenses/LICENCE diff --git a/libs/tqdm-4.67.3.dist-info/top_level.txt b/libs/tqdm-4.68.3.dist-info/top_level.txt similarity index 100% rename from libs/tqdm-4.67.3.dist-info/top_level.txt rename to libs/tqdm-4.68.3.dist-info/top_level.txt diff --git a/libs/tqdm/_monitor.py b/libs/tqdm/_monitor.py index f71aa5681..9d97b8313 100644 --- a/libs/tqdm/_monitor.py +++ b/libs/tqdm/_monitor.py @@ -7,9 +7,10 @@ __all__ = ["TMonitor", "TqdmSynchronisationWarning"] class TqdmSynchronisationWarning(RuntimeWarning): - """tqdm multi-thread/-process errors which may cause incorrect nesting - but otherwise no adverse effects""" - pass + """ + tqdm multi-thread/-process errors which may cause incorrect nesting + but otherwise no adverse effects + """ class TMonitor(Thread): @@ -28,16 +29,24 @@ class TMonitor(Thread): _test = {} # internal vars for unit testing def __init__(self, tqdm_cls, sleep_interval): - Thread.__init__(self) + Thread.__init__(self, name="tqdm_monitor") self.daemon = True # kill thread when main killed (KeyboardInterrupt) self.woken = 0 # last time woken up, to sync with monitor self.tqdm_cls = tqdm_cls self.sleep_interval = sleep_interval self._time = self._test.get("time", time) self.was_killed = self._test.get("Event", Event)() - atexit.register(self.exit) + atexit.register(self._atexit_signal) self.start() + def _atexit_signal(self): + """ + Non-joining shutdown signal. + Avoids deadlocks at interpreter exit from other threads, dead forks, etc. + This daemon thread is auto-reaped on shutdown without needing a join. + """ + self.was_killed.set() + def exit(self): self.was_killed.set() if self is not current_thread(): diff --git a/libs/tqdm/_utils.py b/libs/tqdm/_utils.py index 385e849e1..28665dfe7 100644 --- a/libs/tqdm/_utils.py +++ b/libs/tqdm/_utils.py @@ -1,7 +1,7 @@ from warnings import warn from .std import TqdmDeprecationWarning -from .utils import ( # NOQA, pylint: disable=unused-import +from .utils import ( # noqa: F401, pylint: disable=unused-import CUR_OS, IS_NIX, IS_WIN, RE_ANSI, Comparable, FormatReplace, SimpleTextIOWrapper, _environ_cols_wrapper, _is_ascii, _is_utf, _screen_shape_linux, _screen_shape_tput, _screen_shape_windows, _screen_shape_wrapper, _supports_unicode, _term_move_up, colorama) diff --git a/libs/tqdm/asyncio.py b/libs/tqdm/asyncio.py index 2d00a0a2e..b32e7673b 100644 --- a/libs/tqdm/asyncio.py +++ b/libs/tqdm/asyncio.py @@ -1,5 +1,5 @@ """ -Asynchronous progressbar decorator for iterators. +Asynchronous progress bar decorator for iterators. Includes a default `range` iterator printing to `stderr`. Usage: diff --git a/libs/tqdm/autonotebook.py b/libs/tqdm/autonotebook.py index a09f2ec4b..df1d21b2b 100644 --- a/libs/tqdm/autonotebook.py +++ b/libs/tqdm/autonotebook.py @@ -6,13 +6,22 @@ Usage: >>> for i in trange(10): ... ... """ +import os import sys from warnings import warn try: - get_ipython = sys.modules['IPython'].get_ipython - if 'IPKernelApp' not in get_ipython().config: # pragma: no cover - raise ImportError("console") + if 'ipykernel.zmqshell' in sys.modules: + if any(i == 'QT_API' or i.startswith('SPYDER') for i in os.environ): + raise ImportError("console") # jupyter-qtconsole/spyder + ipy = sys.modules['IPython'].get_ipython().__class__.__name__.lower() + if 'qt' in ipy or 'spyder' in ipy: + raise ImportError("console") # older jupyter-qtconsole/spyder + # jupyter-notebook/jupyterlab/vscode/binder/colab + elif 'IPython.utils._process_emscripten' in sys.modules: + pass # jupyterlite (pyodide)/jupyterlite-xeus + else: + raise ImportError("console") # ipython/jupyter-console from .notebook import WARN_NOIPYW, IProgress if IProgress is None: from .std import TqdmWarning diff --git a/libs/tqdm/cli.py b/libs/tqdm/cli.py index 1dd08f605..e5599efdc 100644 --- a/libs/tqdm/cli.py +++ b/libs/tqdm/cli.py @@ -4,7 +4,7 @@ Module version for monitoring CLI pipes (`... | python -m tqdm | ...`). import logging import re import sys -from ast import literal_eval as numeric +from ast import literal_eval from textwrap import indent from .std import TqdmKeyError, TqdmTypeError, tqdm @@ -35,7 +35,7 @@ def cast(val, typ): if len(val) == 1: return val.encode() if re.match(r"^\\\w+$", val): - return eval(f'"{val}"').encode() + return literal_eval(f'"{val}"').encode() raise TqdmTypeError(f"{val} : {typ}") if typ == 'str': return val @@ -298,10 +298,10 @@ Options: with tqdm(**tqdm_args) as t: if update: def callback(i): - t.update(numeric(i.decode())) + t.update(literal_eval(i.decode())) else: # update_to def callback(i): - t.update(numeric(i.decode()) - t.n) + t.update(literal_eval(i.decode()) - t.n) for i in stdin: write(i) callback(i) @@ -314,10 +314,10 @@ Options: callback_len = False if update: def callback(i): - t.update(numeric(i.decode())) + t.update(literal_eval(i.decode())) elif update_to: def callback(i): - t.update(numeric(i.decode()) - t.n) + t.update(literal_eval(i.decode()) - t.n) else: callback = t.update callback_len = True diff --git a/libs/tqdm/contrib/bells.py b/libs/tqdm/contrib/bells.py index 5b8f4b9ec..e4968e775 100644 --- a/libs/tqdm/contrib/bells.py +++ b/libs/tqdm/contrib/bells.py @@ -3,6 +3,8 @@ Even more features than `tqdm.auto` (all the bells & whistles): - `tqdm.auto` - `tqdm.tqdm.pandas` +- `tqdm.contrib.slack` + + uses `${TQDM_SLACK_TOKEN}` and `${TQDM_SLACK_CHANNEL}` - `tqdm.contrib.telegram` + uses `${TQDM_TELEGRAM_TOKEN}` and `${TQDM_TELEGRAM_CHAT_ID}` - `tqdm.contrib.discord` diff --git a/libs/tqdm/contrib/discord.py b/libs/tqdm/contrib/discord.py index 574baa84b..f46a4992b 100644 --- a/libs/tqdm/contrib/discord.py +++ b/libs/tqdm/contrib/discord.py @@ -8,7 +8,6 @@ Usage: ![screenshot](https://tqdm.github.io/img/screenshot-discord.png) """ -from os import getenv from warnings import warn from requests import Session @@ -16,6 +15,7 @@ from requests.utils import default_user_agent from ..auto import tqdm as tqdm_auto from ..std import TqdmWarning +from ..utils import envwrap from ..version import __version__ from .utils_worker import MonoWorker @@ -25,7 +25,7 @@ __all__ = ['DiscordIO', 'tqdm_discord', 'tdrange', 'tqdm', 'trange'] class DiscordIO(MonoWorker): """Non-blocking file-like IO using a Discord Bot.""" - API = "https://discord.com/api/v10" + API = 'https://discord.com/api/v10' UA = f"tqdm (https://tqdm.github.io, {__version__}) {default_user_agent()}" def __init__(self, token, channel_id): @@ -35,26 +35,28 @@ class DiscordIO(MonoWorker): self.channel_id = channel_id self.session = Session() self.text = self.__class__.__name__ - self.message_id + self.message_id # pylint: disable=pointless-statement @property def message_id(self): if hasattr(self, '_message_id'): - return self._message_id + return self._message_id # pylint: disable=access-member-before-definition try: - res = self.session.post( + req = self.session.post( f'{self.API}/channels/{self.channel_id}/messages', headers={'Authorization': f'Bot {self.token}', 'User-Agent': self.UA}, - json={'content': f"`{self.text}`"}).json() + json={'content': f"`{self.text}`"}) + res = req.json() + req.raise_for_status() except Exception as e: - tqdm_auto.write(str(e)) - else: - if res.get('error_code') == 429: + if req.status_code == 429: warn("Creation rate limit: try increasing `mininterval`.", TqdmWarning, stacklevel=2) else: - self._message_id = res['id'] - return self._message_id + tqdm_auto.write(str(e)) + else: + self._message_id = res['id'] + return self._message_id def write(self, s): """Replaces internal `message_id`'s text with `s`.""" @@ -91,7 +93,7 @@ class DiscordIO(MonoWorker): return future -class tqdm_discord(tqdm_auto): +class tqdm_discord(tqdm_auto): # pylint: disable=inconsistent-mro """ Standard `tqdm.auto.tqdm` but also sends updates to a Discord Bot. May take a few seconds to create (`__init__`). @@ -105,7 +107,8 @@ class tqdm_discord(tqdm_auto): >>> for i in tqdm(iterable, token='{token}', channel_id='{channel_id}'): ... ... """ - def __init__(self, *args, **kwargs): + @envwrap("tqdm", "discord", is_method=True) + def __init__(self, *args, token=None, channel_id=None, **kwargs): """ Parameters ---------- @@ -118,19 +121,15 @@ class tqdm_discord(tqdm_auto): """ if not kwargs.get('disable'): kwargs = kwargs.copy() - self.dio = DiscordIO( - kwargs.pop('token', getenv('TQDM_DISCORD_TOKEN')), - kwargs.pop('channel_id', getenv('TQDM_DISCORD_CHANNEL_ID'))) + self.dio = DiscordIO(token, channel_id) super().__init__(*args, **kwargs) - def display(self, **kwargs): + def display(self, **kwargs): # pylint: disable=arguments-differ super().display(**kwargs) fmt = self.format_dict if fmt.get('bar_format', None): fmt['bar_format'] = fmt['bar_format'].replace( '', '{bar:10u}').replace('{bar}', '{bar:10u}') - else: - fmt['bar_format'] = '{l_bar}{bar:10u}{r_bar}' self.dio.write(self.format_meter(**fmt)) def clear(self, *args, **kwargs): diff --git a/libs/tqdm/contrib/itertools.py b/libs/tqdm/contrib/itertools.py index e67651a41..469961867 100644 --- a/libs/tqdm/contrib/itertools.py +++ b/libs/tqdm/contrib/itertools.py @@ -2,34 +2,91 @@ Thin wrappers around `itertools`. """ import itertools +import math from ..auto import tqdm as tqdm_auto __author__ = {"github.com/": ["casperdcl"]} -__all__ = ['product'] +__all__ = [ + 'chain', 'product', 'permutations', 'combinations', 'combinations_with_replacement', 'batched'] -def product(*iterables, **tqdm_kwargs): - """ - Equivalent of `itertools.product`. +def chain(*iterables, total=None, tqdm_class=tqdm_auto, **kwargs): + """Equivalent of `itertools.chain`.""" + if total is None: + try: + total = sum(map(len, iterables)) + except (TypeError, AttributeError): + pass + return tqdm_class(itertools.chain(*iterables), total=total, **kwargs) - Parameters - ---------- - tqdm_class : [default: tqdm.auto.tqdm]. - """ - kwargs = tqdm_kwargs.copy() - tqdm_class = kwargs.pop("tqdm_class", tqdm_auto) - try: - lens = list(map(len, iterables)) - except TypeError: - total = None - else: - total = 1 - for i in lens: - total *= i - kwargs.setdefault("total", total) - with tqdm_class(**kwargs) as t: - it = itertools.product(*iterables) - for i in it: - yield i - t.update() + +def product(*iterables, repeat=1, total=None, tqdm_class=tqdm_auto, **kwargs): + """Equivalent of `itertools.product`.""" + if total is None: + try: + lens = list(map(len, iterables)) + except (TypeError, AttributeError): + pass + else: + total = math.prod(lens) ** repeat + yield from tqdm_class(itertools.product(*iterables, repeat=repeat), total=total, **kwargs) + + +def permutations(iterable, r=None, total=None, tqdm_class=tqdm_auto, **kwargs): + """Equivalent of `itertools.permutations`.""" + if total is None: + try: + n = len(iterable) + except (TypeError, AttributeError): + pass + else: + r = n if r is None else r + if r > n: + total = 0 + else: + total = math.perm(n, r) + return tqdm_class(itertools.permutations(iterable, r), total=total, **kwargs) + + +def combinations(iterable, r, total=None, tqdm_class=tqdm_auto, **kwargs): + """Equivalent of `itertools.combinations`.""" + if total is None: + try: + n = len(iterable) + except (TypeError, AttributeError): + pass + else: + if r > n: + total = 0 + else: + total = math.comb(n, r) + return tqdm_class(itertools.combinations(iterable, r), total=total, **kwargs) + + +def combinations_with_replacement(iterable, r, total=None, tqdm_class=tqdm_auto, **kwargs): + """Equivalent of `itertools.combinations_with_replacement`.""" + if total is None: + try: + n = len(iterable) + except (TypeError, AttributeError): + pass + else: + total = 1 + for i in range(n+r-1, n-1, -1): + total *= i + for i in range(1, r+1): + total //= i + return tqdm_class(itertools.combinations_with_replacement(iterable, r), total=total, **kwargs) + + +def batched(iterable, n, total=None, tqdm_class=tqdm_auto, **kwargs): + """Equivalent of `itertools.batched`.""" + if total is None: + try: + total = len(iterable) + except (TypeError, AttributeError): + pass + return tqdm_class(itertools.batched(iterable, n), unit_scale=n, + total=(total+n-1) // n if total is not None else None, + **kwargs) diff --git a/libs/tqdm/contrib/logging.py b/libs/tqdm/contrib/logging.py index e06febe37..16edd45ab 100644 --- a/libs/tqdm/contrib/logging.py +++ b/libs/tqdm/contrib/logging.py @@ -6,7 +6,7 @@ import sys from contextlib import contextmanager try: - from typing import Iterator, List, Optional, Type # noqa: F401 + from typing import Iterator, List, Optional, Type # noqa: F401, pylint: disable=unused-import except ImportError: pass diff --git a/libs/tqdm/contrib/slack.py b/libs/tqdm/contrib/slack.py index 9bca8ee98..e9808f55a 100644 --- a/libs/tqdm/contrib/slack.py +++ b/libs/tqdm/contrib/slack.py @@ -9,7 +9,6 @@ Usage: ![screenshot](https://tqdm.github.io/img/screenshot-slack.png) """ import logging -from os import getenv try: from slack_sdk import WebClient @@ -17,6 +16,7 @@ except ImportError: raise ImportError("Please `pip install slack-sdk`") from ..auto import tqdm as tqdm_auto +from ..utils import envwrap from .utils_worker import MonoWorker __author__ = {"github.com/": ["0x2b3bfa0", "casperdcl"]} @@ -56,7 +56,7 @@ class SlackIO(MonoWorker): return future -class tqdm_slack(tqdm_auto): +class tqdm_slack(tqdm_auto): # pylint: disable=inconsistent-mro """ Standard `tqdm.auto.tqdm` but also sends updates to a Slack app. May take a few seconds to create (`__init__`). @@ -68,7 +68,8 @@ class tqdm_slack(tqdm_auto): >>> for i in tqdm(iterable, token='{token}', channel='{channel}'): ... ... """ - def __init__(self, *args, **kwargs): + @envwrap("tqdm", "slack", is_method=True) + def __init__(self, *args, token=None, channel=None, **kwargs): """ Parameters ---------- @@ -84,19 +85,17 @@ class tqdm_slack(tqdm_auto): if not kwargs.get('disable'): kwargs = kwargs.copy() logging.getLogger("HTTPClient").setLevel(logging.WARNING) - self.sio = SlackIO( - kwargs.pop('token', getenv("TQDM_SLACK_TOKEN")), - kwargs.pop('channel', getenv("TQDM_SLACK_CHANNEL"))) + self.sio = SlackIO(token, channel) kwargs['mininterval'] = max(1.5, kwargs.get('mininterval', 1.5)) super().__init__(*args, **kwargs) - def display(self, **kwargs): + def display(self, **kwargs): # pylint: disable=arguments-differ super().display(**kwargs) fmt = self.format_dict if fmt.get('bar_format', None): fmt['bar_format'] = fmt['bar_format'].replace( '', '`{bar:10}`').replace('{bar}', '`{bar:10u}`') - else: + elif self.total: fmt['bar_format'] = '{l_bar}`{bar:10}`{r_bar}' if fmt['ascii'] is False: fmt['ascii'] = [":black_square:", ":small_blue_diamond:", ":large_blue_diamond:", diff --git a/libs/tqdm/contrib/telegram.py b/libs/tqdm/contrib/telegram.py index 019151800..c019248e3 100644 --- a/libs/tqdm/contrib/telegram.py +++ b/libs/tqdm/contrib/telegram.py @@ -8,13 +8,13 @@ Usage: ![screenshot](https://tqdm.github.io/img/screenshot-telegram.gif) """ -from os import getenv from warnings import warn from requests import Session from ..auto import tqdm as tqdm_auto from ..std import TqdmWarning +from ..utils import envwrap from .utils_worker import MonoWorker __author__ = {"github.com/": ["casperdcl"]} @@ -32,26 +32,28 @@ class TelegramIO(MonoWorker): self.chat_id = chat_id self.session = Session() self.text = self.__class__.__name__ - self.message_id + self.message_id # pylint: disable=pointless-statement @property def message_id(self): if hasattr(self, '_message_id'): - return self._message_id + return self._message_id # pylint: disable=access-member-before-definition try: - res = self.session.post( - self.API + '%s/sendMessage' % self.token, - data={'text': '`' + self.text + '`', 'chat_id': self.chat_id, - 'parse_mode': 'MarkdownV2'}).json() + req = self.session.post( + f'{self.API}{self.token}/sendMessage', + data={'text': f"`{self.text}`", 'chat_id': self.chat_id, + 'parse_mode': 'MarkdownV2'}) + res = req.json() + req.raise_for_status() except Exception as e: - tqdm_auto.write(str(e)) - else: - if res.get('error_code') == 429: + if req.status_code == 429: warn("Creation rate limit: try increasing `mininterval`.", TqdmWarning, stacklevel=2) else: - self._message_id = res['result']['message_id'] - return self._message_id + tqdm_auto.write(str(e)) + else: + self._message_id = res['result']['message_id'] + return self._message_id def write(self, s): """Replaces internal `message_id`'s text with `s`.""" @@ -66,8 +68,8 @@ class TelegramIO(MonoWorker): self.text = s try: future = self.submit( - self.session.post, self.API + '%s/editMessageText' % self.token, - data={'text': '`' + s + '`', 'chat_id': self.chat_id, + self.session.post, f'{self.API}{self.token}/editMessageText', + data={'text': f"`{s}`", 'chat_id': self.chat_id, 'message_id': message_id, 'parse_mode': 'MarkdownV2'}) except Exception as e: tqdm_auto.write(str(e)) @@ -78,7 +80,7 @@ class TelegramIO(MonoWorker): """Deletes internal `message_id`.""" try: future = self.submit( - self.session.post, self.API + '%s/deleteMessage' % self.token, + self.session.post, '{self.API}{self.token}/deleteMessage', data={'chat_id': self.chat_id, 'message_id': self.message_id}) except Exception as e: tqdm_auto.write(str(e)) @@ -86,7 +88,7 @@ class TelegramIO(MonoWorker): return future -class tqdm_telegram(tqdm_auto): +class tqdm_telegram(tqdm_auto): # pylint: disable=inconsistent-mro """ Standard `tqdm.auto.tqdm` but also sends updates to a Telegram Bot. May take a few seconds to create (`__init__`). @@ -102,7 +104,8 @@ class tqdm_telegram(tqdm_auto): >>> for i in tqdm(iterable, token='{token}', chat_id='{chat_id}'): ... ... """ - def __init__(self, *args, **kwargs): + @envwrap("tqdm", "telegram", is_method=True) + def __init__(self, *args, token=None, chat_id=None, **kwargs): """ Parameters ---------- @@ -115,19 +118,15 @@ class tqdm_telegram(tqdm_auto): """ if not kwargs.get('disable'): kwargs = kwargs.copy() - self.tgio = TelegramIO( - kwargs.pop('token', getenv('TQDM_TELEGRAM_TOKEN')), - kwargs.pop('chat_id', getenv('TQDM_TELEGRAM_CHAT_ID'))) + self.tgio = TelegramIO(token, chat_id) super().__init__(*args, **kwargs) - def display(self, **kwargs): + def display(self, **kwargs): # pylint: disable=arguments-differ super().display(**kwargs) fmt = self.format_dict if fmt.get('bar_format', None): fmt['bar_format'] = fmt['bar_format'].replace( '', '{bar:10u}').replace('{bar}', '{bar:10u}') - else: - fmt['bar_format'] = '{l_bar}{bar:10u}{r_bar}' self.tgio.write(self.format_meter(**fmt)) def clear(self, *args, **kwargs): diff --git a/libs/tqdm/gui.py b/libs/tqdm/gui.py index cb52fb91a..e7995089f 100644 --- a/libs/tqdm/gui.py +++ b/libs/tqdm/gui.py @@ -1,5 +1,5 @@ """ -Matplotlib GUI progressbar decorator for iterators. +Matplotlib GUI progress bar decorator for iterators. Usage: >>> from tqdm.gui import trange, tqdm @@ -65,7 +65,7 @@ class tqdm_gui(std_tqdm): # pragma: no cover ax.set_xlabel("percent") self.fig.legend((self.line1, self.line2), ("cur", "est"), loc='center right') - # progressbar + # progress bar self.hspan = plt.axhspan(0, 0.001, xmin=0, xmax=0, color=colour) else: # ax.set_xlim(-60, 0) diff --git a/libs/tqdm/keras.py b/libs/tqdm/keras.py index cce9467c5..317b76a69 100644 --- a/libs/tqdm/keras.py +++ b/libs/tqdm/keras.py @@ -23,7 +23,8 @@ class TqdmCallback(keras.callbacks.Callback): if logs: if pop: logs = copy(logs) - [logs.pop(i, 0) for i in pop] + for i in pop: + logs.pop(i, 0) bar.set_postfix(logs, refresh=False) bar.update(n) diff --git a/libs/tqdm/notebook.py b/libs/tqdm/notebook.py index 83178e981..f6e1b0a6d 100644 --- a/libs/tqdm/notebook.py +++ b/libs/tqdm/notebook.py @@ -1,5 +1,5 @@ """ -IPython/Jupyter Notebook progressbar decorator for iterators. +IPython/Jupyter Notebook progress bar decorator for iterators. Includes a default `range` iterator printing to `stderr`. Usage: @@ -20,7 +20,7 @@ if True: # pragma: no cover # import IPython/Jupyter base widget and display utilities IPY = 0 try: # IPython 4.x - import ipywidgets + import ipywidgets # noqa: F401, pylint: disable=unused-import IPY = 4 except ImportError: # IPython 3.x / 2.x IPY = 32 @@ -29,9 +29,11 @@ if True: # pragma: no cover warnings.filterwarnings( 'ignore', message=".*The `IPython.html` package has been deprecated.*") try: - import IPython.html.widgets as ipywidgets # NOQA: F401 + import IPython.html.widgets except ImportError: pass + else: + ipywidgets = IPython.html.widgets try: # IPython 4.x / 3.x if IPY == 32: diff --git a/libs/tqdm/std.py b/libs/tqdm/std.py index 14b0c1963..79a868f53 100644 --- a/libs/tqdm/std.py +++ b/libs/tqdm/std.py @@ -1,5 +1,5 @@ """ -Customisable progressbar decorator for iterators. +Customisable progress bar decorator for iterators. Includes a default `range` iterator printing to `stderr`. Usage: @@ -51,17 +51,15 @@ class TqdmWarning(Warning): class TqdmExperimentalWarning(TqdmWarning, FutureWarning): """beta feature, unstable API and behaviour""" - pass class TqdmDeprecationWarning(TqdmWarning, DeprecationWarning): + """may be removed in a future release""" # not suppressed if raised - pass class TqdmMonitorWarning(TqdmWarning, RuntimeWarning): """tqdm monitor errors which do not affect external functionality""" - pass def TRLock(*args, **kwargs): @@ -245,25 +243,25 @@ class tqdm(Comparable): """ Decorate an iterable object, returning an iterator which acts exactly like the original iterable, but prints a dynamically updating - progressbar every time a value is requested. + progress bar every time a value is requested. Parameters ---------- iterable : iterable, optional - Iterable to decorate with a progressbar. + Iterable to decorate with a progress bar. Leave blank to manually manage the updates. desc : str, optional - Prefix for the progressbar. + Prefix for the progress bar. total : int or float, optional The number of expected iterations. If unspecified, len(iterable) is used if possible. If float("inf") or as a last resort, only basic progress statistics are displayed - (no ETA, no progressbar). + (no ETA, no progress bar). If `gui` is True and this parameter needs subsequent updating, specify an initial arbitrary large positive number, e.g. 9e9. leave : bool, optional - If [default: True], keeps all traces of the progressbar + If [default: True], keeps all traces of the progress bar upon termination of iteration. If `None`, will leave only if `position` is `0`. file : `io.TextIOWrapper` or `io.StringIO`, optional @@ -272,7 +270,7 @@ class tqdm(Comparable): methods. For encoding, see `write_bytes`. ncols : int, optional The width of the entire output message. If specified, - dynamically resizes the progressbar to stay within this bound. + dynamically resizes the progress bar to stay within this bound. If unspecified, attempts to use environment width. The fallback is a meter width of 10 and no limit for the counter and statistics. If 0, will not print any meter (only stats). @@ -295,7 +293,7 @@ class tqdm(Comparable): If unspecified or False, use unicode (smooth blocks) to fill the meter. The fallback is to use ASCII characters " 123456789#". disable : bool, optional - Whether to disable the entire progressbar wrapper + Whether to disable the entire progress bar wrapper [default: False]. If set to None, disable on non-TTY. unit : str, optional String that will be used to define the unit of each iteration @@ -462,8 +460,9 @@ class tqdm(Comparable): return print_status @staticmethod - def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False, unit='it', - unit_scale=False, rate=None, bar_format=None, postfix=None, + def format_meter(n, total, elapsed, ncols=None, prefix='', + ascii=False, # pylint: disable=redefined-builtin + unit='it', unit_scale=False, rate=None, bar_format=None, postfix=None, unit_divisor=1000, initial=0, colour=None, **extra_kwargs): """ Return a string-based progress bar given some parameters @@ -656,7 +655,7 @@ class tqdm(Comparable): res = bar_format.format(bar=full_bar, **format_dict) return disp_trim(res, ncols) if ncols else res else: - # no total: no progressbar, ETA, just progress stats + # no total: no bar & ETA, just progress stats return (f'{(prefix + ": ") if prefix else ""}' f'{n_fmt}{unit} [{elapsed_str}, {rate_fmt}{postfix}]') @@ -949,14 +948,14 @@ class tqdm(Comparable): _Rolling_and_Expanding.progress_apply = inner_generator() # override defaults via env vars - @envwrap("TQDM_", is_method=True, types={'total': float, 'ncols': int, 'miniters': float, - 'position': int, 'nrows': int}) + @envwrap("tqdm", is_method=True, types={'total': float, 'ncols': int, 'miniters': float, + 'position': int, 'nrows': int}) def __init__(self, iterable=None, desc=None, total=None, leave=True, file=None, ncols=None, mininterval=0.1, maxinterval=10.0, miniters=None, - ascii=None, disable=False, unit='it', unit_scale=False, - dynamic_ncols=False, smoothing=0.3, bar_format=None, initial=0, - position=None, postfix=None, unit_divisor=1000, write_bytes=False, - lock_args=None, nrows=None, colour=None, delay=0.0, gui=False, + ascii=None, # pylint: disable=redefined-builtin + disable=False, unit='it', unit_scale=False, dynamic_ncols=False, smoothing=0.3, + bar_format=None, initial=0, position=None, postfix=None, unit_divisor=1000, + write_bytes=False, lock_args=None, nrows=None, colour=None, delay=0.0, gui=False, **kwargs): """see tqdm.tqdm for arguments""" if file is None: @@ -1130,7 +1129,8 @@ class tqdm(Comparable): def __contains__(self, item): contains = getattr(self.iterable, '__contains__', None) - return contains(item) if contains is not None else item in self.__iter__() + return (contains(item) if contains is not None # pylint: disable=not-callable + else item in self.__iter__()) def __enter__(self): return self @@ -1180,7 +1180,7 @@ class tqdm(Comparable): try: for obj in iterable: yield obj - # Update and possibly print the progressbar. + # Update and possibly print the progress bar. # Note: does not call self.update(1) for speed optimisation. n += 1 @@ -1263,7 +1263,7 @@ class tqdm(Comparable): return True def close(self): - """Cleanup and (if leave=False) close the progressbar.""" + """Cleanup and (if leave=False) close the progress bar.""" if self.disable: return @@ -1499,7 +1499,8 @@ class tqdm(Comparable): @classmethod @contextmanager - def wrapattr(cls, stream, method, total=None, bytes=True, **tqdm_kwargs): + def wrapattr(cls, stream, method, total=None, bytes=True, # pylint: disable=redefined-builtin + **tqdm_kwargs): """ stream : file-like object. method : str, "read" or "write". The result of `read()` and diff --git a/libs/tqdm/tk.py b/libs/tqdm/tk.py index 788303c86..6607cc7f5 100644 --- a/libs/tqdm/tk.py +++ b/libs/tqdm/tk.py @@ -1,5 +1,5 @@ """ -Tkinter GUI progressbar decorator for iterators. +Tkinter GUI progress bar decorator for iterators. Usage: >>> from tqdm.tk import trange, tqdm diff --git a/libs/tqdm/tqdm.1 b/libs/tqdm/tqdm.1 index fe29b8a7d..b134ddd74 100644 --- a/libs/tqdm/tqdm.1 +++ b/libs/tqdm/tqdm.1 @@ -1,5 +1,3 @@ -.\" Automatically generated by Pandoc 3.8.3 -.\" .TH "TQDM" "1" "2015\-2026" "tqdm User Manuals" .SH NAME tqdm \- fast, extensible progress bar for Python and CLI @@ -40,28 +38,28 @@ Print version and exit. .TP \-\-desc=\f[I]desc\f[R] str, optional. -Prefix for the progressbar. +Prefix for the progress bar. .TP \-\-total=\f[I]total\f[R] int or float, optional. The number of expected iterations. If unspecified, len(iterable) is used if possible. If float(\(lqinf\(rq) or as a last resort, only basic progress -statistics are displayed (no ETA, no progressbar). +statistics are displayed (no ETA, no progress bar). If \f[CR]gui\f[R] is True and this parameter needs subsequent updating, specify an initial arbitrary large positive number, e.g.\ 9e9. .TP \-\-leave bool, optional. -If [default: True], keeps all traces of the progressbar upon termination -of iteration. +If [default: True], keeps all traces of the progress bar upon +termination of iteration. If \f[CR]None\f[R], will leave only if \f[CR]position\f[R] is \f[CR]0\f[R]. .TP \-\-ncols=\f[I]ncols\f[R] int, optional. The width of the entire output message. -If specified, dynamically resizes the progressbar to stay within this +If specified, dynamically resizes the progress bar to stay within this bound. If unspecified, attempts to use environment width. The fallback is a meter width of 10 and no limit for the counter and @@ -96,7 +94,7 @@ The fallback is to use ASCII characters \(rq 123456789#\(lq. .TP \-\-disable bool, optional. -Whether to disable the entire progressbar wrapper [default: False]. +Whether to disable the entire progress bar wrapper [default: False]. If set to None, disable on non\-TTY. .TP \-\-unit=\f[I]unit\f[R] diff --git a/libs/tqdm/utils.py b/libs/tqdm/utils.py index ab454de7c..329c06447 100644 --- a/libs/tqdm/utils.py +++ b/libs/tqdm/utils.py @@ -31,46 +31,23 @@ else: colorama.init() -def envwrap(prefix, types=None, is_method=False): +def envwrap(name, app="", types=None, is_method=False): """ - Override parameter defaults via `os.environ[prefix + param_name]`. - Maps UPPER_CASE env vars map to lower_case param names. - camelCase isn't supported (because Windows ignores case). - - Precedence (highest first): - - - call (`foo(a=3)`) - - environ (`FOO_A=2`) - - signature (`def foo(a=1)`) - - Parameters - ---------- - prefix : str - Env var prefix, e.g. "FOO_" - types : dict, optional - Fallback mappings `{'param_name': type, ...}` if types cannot be - inferred from function signature. - Consider using `types=collections.defaultdict(lambda: ast.literal_eval)`. - is_method : bool, optional - Whether to use `functools.partialmethod`. If (default: False) use `functools.partial`. - - Examples - -------- - ``` - $ cat foo.py - from tqdm.utils import envwrap - @envwrap("FOO_") - def test(a=1, b=2, c=3): - print(f"received: a={a}, b={b}, c={c}") - - $ FOO_A=42 FOO_C=1337 python -c 'import foo; foo.test(c=99)' - received: a=42, b=2, c=99 - ``` + Basic (env-only) version of [envwrap](https://github.com/tqdm/envwrap). + Install `envwrap` for config file support. """ if types is None: types = {} - i = len(prefix) - env_overrides = {k[i:].lower(): v for k, v in os.environ.items() if k.startswith(prefix)} + if name[-1] == "_": + name = name[:-1] + warn("Trailing underscore in `name` is automatic", DeprecationWarning, stacklevel=2) + prefixes = (name, f"{name}_{app}") if app else (name,) + env_overrides = {} + for prefix in prefixes: + prefix = prefix.upper() + "_" + i = len(prefix) + env_overrides.update( + (k[i:].lower(), v) for k, v in os.environ.items() if k.startswith(prefix)) part = partialmethod if is_method else partial def wrap(func): @@ -84,7 +61,7 @@ def envwrap(prefix, types=None, is_method=False): for typ in getattr(param.annotation, '__args__', (param.annotation,)): try: overrides[k] = typ(overrides[k]) - except Exception: + except Exception: # nosec B110 pass else: break @@ -99,6 +76,12 @@ def envwrap(prefix, types=None, is_method=False): return wrap +try: + from envwrap import envwrap # noqa: F401, F811, pylint: disable=unused-import +except ModuleNotFoundError: + pass + + class FormatReplace: """ >>> a = FormatReplace('something') @@ -284,69 +267,15 @@ def _screen_shape_wrapper(): # pragma: no cover Return a function which returns console dimensions (width, height). Supported: linux, osx, windows, cygwin. """ - _screen_shape = None - if IS_WIN: - _screen_shape = _screen_shape_windows - if _screen_shape is None: - _screen_shape = _screen_shape_tput - if IS_NIX: - _screen_shape = _screen_shape_linux - return _screen_shape - - -def _screen_shape_windows(fp): # pragma: no cover - try: - import struct - from ctypes import create_string_buffer, windll - from sys import stdin, stdout - - io_handle = -12 # assume stderr - if fp == stdin: - io_handle = -10 - elif fp == stdout: - io_handle = -11 - - h = windll.kernel32.GetStdHandle(io_handle) - csbi = create_string_buffer(22) - res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) - if res: - (_bufx, _bufy, _curx, _cury, _wattr, left, top, right, bottom, - _maxx, _maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) - return right - left, bottom - top # +1 - except Exception: # nosec - pass - return None, None - - -def _screen_shape_tput(*_): # pragma: no cover - """cygwin xterm (windows)""" - try: - import shlex - from subprocess import check_call # nosec - return [int(check_call(shlex.split('tput ' + i))) - 1 - for i in ('cols', 'lines')] - except Exception: # nosec - pass - return None, None - - -def _screen_shape_linux(fp): # pragma: no cover - - try: - from array import array - from fcntl import ioctl - from termios import TIOCGWINSZ - except ImportError: - return None, None - else: + def inner(fp): try: - rows, cols = array('h', ioctl(fp, TIOCGWINSZ, '\0' * 8))[:2] - return cols, rows + from os import get_terminal_size + cols, lines = get_terminal_size(getattr(fp, 'fileno', lambda: None)()) + return cols - 1, lines - 1 except Exception: - try: - return [int(os.environ[i]) - 1 for i in ("COLUMNS", "LINES")] - except (KeyError, ValueError): - return None, None + return None, None + + return inner def _environ_cols_wrapper(): # pragma: no cover diff --git a/libs/tqdm/version.py b/libs/tqdm/version.py index 9338fb197..84739ef24 100644 --- a/libs/tqdm/version.py +++ b/libs/tqdm/version.py @@ -1,8 +1,5 @@ """`tqdm` version detector. Precedence: installed dist, git, 'UNKNOWN'.""" -try: - from importlib.metadata import PackageNotFoundError, version -except ImportError: # py<3.8 - from importlib_metadata import PackageNotFoundError, version +from importlib.metadata import PackageNotFoundError, version try: __version__ = version('tqdm') diff --git a/libs/tzdata-2025.3.dist-info/INSTALLER b/libs/tzdata-2026.2.dist-info/INSTALLER similarity index 100% rename from libs/tzdata-2025.3.dist-info/INSTALLER rename to libs/tzdata-2026.2.dist-info/INSTALLER diff --git a/libs/tzdata-2025.3.dist-info/METADATA b/libs/tzdata-2026.2.dist-info/METADATA similarity index 85% rename from libs/tzdata-2025.3.dist-info/METADATA rename to libs/tzdata-2026.2.dist-info/METADATA index 330dd728b..3aee6bd35 100644 --- a/libs/tzdata-2025.3.dist-info/METADATA +++ b/libs/tzdata-2026.2.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: tzdata -Version: 2025.3 +Version: 2026.2 Summary: Provider of IANA time zone data Home-page: https://github.com/python/tzdata Author: Python Software Foundation @@ -8,8 +8,8 @@ Author-email: datetime-sig@python.org License: Apache-2.0 Project-URL: Bug Reports, https://github.com/python/tzdata/issues Project-URL: Source, https://github.com/python/tzdata -Project-URL: Documentation, https://tzdata.readthedocs.io -Classifier: Development Status :: 4 - Beta +Project-URL: Documentation, https://tzdata.python.org +Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 @@ -30,4 +30,4 @@ location), as a part of `PEP 615 `_ This repository generates a ``pip``-installable package, published on PyPI as `tzdata `_. -For more information, see `the documentation `_. +For more information, see `the documentation `_. diff --git a/libs/tzdata-2025.3.dist-info/RECORD b/libs/tzdata-2026.2.dist-info/RECORD similarity index 97% rename from libs/tzdata-2025.3.dist-info/RECORD rename to libs/tzdata-2026.2.dist-info/RECORD index d5755f17e..b91b09c7e 100644 --- a/libs/tzdata-2025.3.dist-info/RECORD +++ b/libs/tzdata-2026.2.dist-info/RECORD @@ -1,12 +1,12 @@ -tzdata-2025.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -tzdata-2025.3.dist-info/METADATA,sha256=QiXXu1WSsgB_Zls4Yk6T263TZBP7_PPB-nzYCuewh_g,1352 -tzdata-2025.3.dist-info/RECORD,, -tzdata-2025.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -tzdata-2025.3.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 -tzdata-2025.3.dist-info/licenses/LICENSE,sha256=M-jlAC01EtP8wigrmV5rrZ0zR4G5xawxhD9ASQDh87Q,592 -tzdata-2025.3.dist-info/licenses/licenses/LICENSE_APACHE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357 -tzdata-2025.3.dist-info/top_level.txt,sha256=MO6QqC0xRrN67Gh9xU_nMmadwBVlYzPNkq_h4gYuzaQ,7 -tzdata/__init__.py,sha256=Ee9PzA6folcczG2ywJlO9wJ0f-UVfPeaxXtXdoP-zrE,252 +tzdata-2026.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +tzdata-2026.2.dist-info/METADATA,sha256=Hxl9mW20K_QKedNXgISWP2IEUG3-6YCa-BWG27lgcGE,1357 +tzdata-2026.2.dist-info/RECORD,, +tzdata-2026.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tzdata-2026.2.dist-info/WHEEL,sha256=Mk1ST5gDzEO5il5kYREiBnzzM469m5sI8ESPl7TRhJY,110 +tzdata-2026.2.dist-info/licenses/LICENSE,sha256=M-jlAC01EtP8wigrmV5rrZ0zR4G5xawxhD9ASQDh87Q,592 +tzdata-2026.2.dist-info/licenses/licenses/LICENSE_APACHE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357 +tzdata-2026.2.dist-info/top_level.txt,sha256=MO6QqC0xRrN67Gh9xU_nMmadwBVlYzPNkq_h4gYuzaQ,7 +tzdata/__init__.py,sha256=5pfrlnr9q6d3DRfsOXi55kfbWqHQ0bvQUU_9nKvxYGE,252 tzdata/zoneinfo/Africa/Abidjan,sha256=8-f8qg6YQP9BadNWfY-1kmZEhI9JY9es-SMghDxdSG4,130 tzdata/zoneinfo/Africa/Accra,sha256=8-f8qg6YQP9BadNWfY-1kmZEhI9JY9es-SMghDxdSG4,130 tzdata/zoneinfo/Africa/Addis_Ababa,sha256=B4OFT1LDOtprbSpdhnZi8K6OFSONL857mtpPTTGetGY,191 @@ -229,7 +229,7 @@ tzdata/zoneinfo/America/Thunder_Bay,sha256=gVq023obEpKGfS-SS3GOG7oyRVzp-SIF2y_rZ tzdata/zoneinfo/America/Tijuana,sha256=r5rqSu5QUUP6XoR2Q8D_4WQZDDhBEBCsfwsXD0CYpsg,1367 tzdata/zoneinfo/America/Toronto,sha256=gVq023obEpKGfS-SS3GOG7oyRVzp-SIF2y_rZQKcZ2E,1717 tzdata/zoneinfo/America/Tortola,sha256=q76GKN1Uh8iJ24Fs46UHe7tH9rr6_rlBHZLW7y9wzo0,177 -tzdata/zoneinfo/America/Vancouver,sha256=Epou71sUffvHB1rd7wT0krvo3okXAV45_TWcOFpy26Q,1330 +tzdata/zoneinfo/America/Vancouver,sha256=fSNoGzaEmsf1BTe5wCvY06oAC5i9PRglzoXn6j5VNMk,1673 tzdata/zoneinfo/America/Virgin,sha256=q76GKN1Uh8iJ24Fs46UHe7tH9rr6_rlBHZLW7y9wzo0,177 tzdata/zoneinfo/America/Whitehorse,sha256=CyY4jNd0fzNSdf1HlYGfaktApmH71tRNRlpOEO32DGs,1029 tzdata/zoneinfo/America/Winnipeg,sha256=ANzwYGBU1PknQW4LR-H92i5c4Db95LU-UQhPhWZCjDo,1294 @@ -284,7 +284,7 @@ tzdata/zoneinfo/Asia/Famagusta,sha256=385fbaRnx-mdEaXqSyBKVBDDKPzCGKbynWYt75wwCu tzdata/zoneinfo/Asia/Gaza,sha256=-PC__gGODaDGgv5LLzH7ptNLbNdStPkxGY4LmebvcNU,2950 tzdata/zoneinfo/Asia/Harbin,sha256=v4t-2C_m5j5tmPjOqTTurJAc0Wq6hetXVc4_i0KJ6oo,393 tzdata/zoneinfo/Asia/Hebron,sha256=4FujfuE-ECIXgKW4pv0lxq2ZkAj7jDwt0rezuA0fFzg,2968 -tzdata/zoneinfo/Asia/Ho_Chi_Minh,sha256=R-ReVMreMcETG0Sifjfe5z-PgQpUsKjT6dVbEKzT3sE,236 +tzdata/zoneinfo/Asia/Ho_Chi_Minh,sha256=DW52-5QdBUAhTugfFh9EnGp9w9UV5WVGGKeuQtCs0eE,232 tzdata/zoneinfo/Asia/Hong_Kong,sha256=9AaPcyRtuXQX9zRnRTVkxX1mRs5JCbn6JTaSPvzX608,775 tzdata/zoneinfo/Asia/Hovd,sha256=eqAvD2RfuIfSDhtqk58MECIjz5X14OHZ7aO4z14kndk,594 tzdata/zoneinfo/Asia/Irkutsk,sha256=sWxp8g_aSfFan4ZyF9s6-pEX5Vgwxi_jNv7vwN06XIo,760 @@ -323,7 +323,7 @@ tzdata/zoneinfo/Asia/Qostanay,sha256=5tZkj1o0p4vaREsPO0YgIiw6eDf1cqO52x-0EMg_2L4 tzdata/zoneinfo/Asia/Qyzylorda,sha256=JltKDEnuHmIQGYdFTAJMDDpdDA_HxjJOAHHaV7kFrlQ,624 tzdata/zoneinfo/Asia/Rangoon,sha256=6J2DXIEdTaRKqLOGeCzogo3whaoO6PJWYamIHS8A6Qw,187 tzdata/zoneinfo/Asia/Riyadh,sha256=RoU-lCdq8u6o6GwvFSqHHAkt8ZXcUSc7j8cJH6pLRhw,133 -tzdata/zoneinfo/Asia/Saigon,sha256=R-ReVMreMcETG0Sifjfe5z-PgQpUsKjT6dVbEKzT3sE,236 +tzdata/zoneinfo/Asia/Saigon,sha256=DW52-5QdBUAhTugfFh9EnGp9w9UV5WVGGKeuQtCs0eE,232 tzdata/zoneinfo/Asia/Sakhalin,sha256=M_TBd-03j-3Yc9KwhGEoBTwSJxWO1lPBG7ndst16PGo,755 tzdata/zoneinfo/Asia/Samarkand,sha256=KZ_q-6GMDVgJb8RFqcrbVcPC0WLczolClC4nZA1HVNU,366 tzdata/zoneinfo/Asia/Seoul,sha256=ZKcLb7zJtl52Lb0l64m29AwTcUbtyNvU0IHq-s2reN4,415 @@ -332,7 +332,7 @@ tzdata/zoneinfo/Asia/Singapore,sha256=CVSy2aMB2U9DSAJGBqcbvLL6JNPNNwn1vIvKYFA5eF tzdata/zoneinfo/Asia/Srednekolymsk,sha256=06mojetFbDd4ag1p8NK0Fg6rF2OOnZMFRRC90N2ATZc,742 tzdata/zoneinfo/Asia/Taipei,sha256=oEwscvT3aoMXjQNt2X0VfuHzLkeORN2npcEJI2h-5s8,511 tzdata/zoneinfo/Asia/Tashkent,sha256=0vpN2gI9GY50z1nea6zCPFf2B6VCu6XQQHx4l6rhnTI,366 -tzdata/zoneinfo/Asia/Tbilisi,sha256=ON_Uzv2VTSk6mRefNU-aI-qkqtCoUX6oECVqpeS42eI,629 +tzdata/zoneinfo/Asia/Tbilisi,sha256=BzF_LoKBB_s90wgO6DEZfIxtNfh7Z8t8XLW7ZCzsE4k,620 tzdata/zoneinfo/Asia/Tehran,sha256=ZaxewB83IdYIGVpJvcp8esyz3iffAgxEzsj7cMraQ3c,812 tzdata/zoneinfo/Asia/Tel_Aviv,sha256=n83o1YTeoFhfXIcnqvNfSKFJ4NvTqDv2zvi8qcFAIeM,1074 tzdata/zoneinfo/Asia/Thimbu,sha256=N6d_vfFvYORfMnr1fHJjYSt4DBORSbLi_2T-r2dJBnI,154 @@ -400,7 +400,7 @@ tzdata/zoneinfo/Canada/Central,sha256=ANzwYGBU1PknQW4LR-H92i5c4Db95LU-UQhPhWZCjD tzdata/zoneinfo/Canada/Eastern,sha256=gVq023obEpKGfS-SS3GOG7oyRVzp-SIF2y_rZQKcZ2E,1717 tzdata/zoneinfo/Canada/Mountain,sha256=Dq2mxcSNWZhMWRqxwwtMcaqwAIGMwkOzz-mW8fJscV8,970 tzdata/zoneinfo/Canada/Newfoundland,sha256=v99q_AFMPll5MMxMp98aqY40cmis2wciTfTqs2_kb0k,1878 -tzdata/zoneinfo/Canada/Pacific,sha256=Epou71sUffvHB1rd7wT0krvo3okXAV45_TWcOFpy26Q,1330 +tzdata/zoneinfo/Canada/Pacific,sha256=fSNoGzaEmsf1BTe5wCvY06oAC5i9PRglzoXn6j5VNMk,1673 tzdata/zoneinfo/Canada/Saskatchewan,sha256=_JHuns225iE-THc9NFp-RBq4PWULAuGw2OLbpOB_UMw,638 tzdata/zoneinfo/Canada/Yukon,sha256=CyY4jNd0fzNSdf1HlYGfaktApmH71tRNRlpOEO32DGs,1029 tzdata/zoneinfo/Canada/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 @@ -461,7 +461,7 @@ tzdata/zoneinfo/Europe/Brussels,sha256=sQ-VQqhQnwpj68p449gEMt2GuOopZAAoD-vZz6dug tzdata/zoneinfo/Europe/Bucharest,sha256=iY74H96aaTMJvmqAhzUoSI8SjZUtPvv4PGF4ClwFm6U,661 tzdata/zoneinfo/Europe/Budapest,sha256=qNr-valoDI1mevuQXqOMkOhIcT194EczOKIijxrDMV8,766 tzdata/zoneinfo/Europe/Busingen,sha256=GZBiscMM_rI3XshMVt9SvlGJGYamKTt6Ek06YlCfRek,497 -tzdata/zoneinfo/Europe/Chisinau,sha256=5TPhkCtxxa0ByLCv7YxOrc5Vtdui2v2VX8vrSopPkPs,755 +tzdata/zoneinfo/Europe/Chisinau,sha256=Q54cpE4kU4cUjBFB19fF6EvMpzGI_4U1GhJsje6g4S4,1207 tzdata/zoneinfo/Europe/Copenhagen,sha256=p_2ZMteF1NaQkAuDTDVjwYEMHPLgFxG8wJJq9sB2fLc,705 tzdata/zoneinfo/Europe/Dublin,sha256=EcADNuAvExj-dkqylGfF8q_vv_-mRPqN0k9bCDtJW3E,1496 tzdata/zoneinfo/Europe/Gibraltar,sha256=t1hglDTLUIFqs91nY5lulN7oxkoAXHnh0zjyaKG2bG8,1220 @@ -501,7 +501,7 @@ tzdata/zoneinfo/Europe/Sofia,sha256=LQjC-OJkL4TzZcqD-JUofDAg1-qJui_2Ri6Eoii2MuQ, tzdata/zoneinfo/Europe/Stockholm,sha256=p_2ZMteF1NaQkAuDTDVjwYEMHPLgFxG8wJJq9sB2fLc,705 tzdata/zoneinfo/Europe/Tallinn,sha256=R6yRfPqESOYQWftlncDWo_fQak61eeiEQKwg_C-C7W8,675 tzdata/zoneinfo/Europe/Tirane,sha256=I-alATWRd8mfSgvnr3dN_F9vbTB66alvz2GQo0LUbPc,604 -tzdata/zoneinfo/Europe/Tiraspol,sha256=5TPhkCtxxa0ByLCv7YxOrc5Vtdui2v2VX8vrSopPkPs,755 +tzdata/zoneinfo/Europe/Tiraspol,sha256=Q54cpE4kU4cUjBFB19fF6EvMpzGI_4U1GhJsje6g4S4,1207 tzdata/zoneinfo/Europe/Ulyanovsk,sha256=2vK0XahtB_dKjDDXccjMjbQ2bAOfKDe66uMDqtjzHm4,760 tzdata/zoneinfo/Europe/Uzhgorod,sha256=BYnoDd7Ov50wd4mMEpddK-c5PfKFbumSbFNHY-Hia_I,558 tzdata/zoneinfo/Europe/Vaduz,sha256=GZBiscMM_rI3XshMVt9SvlGJGYamKTt6Ek06YlCfRek,497 @@ -627,9 +627,9 @@ tzdata/zoneinfo/WET,sha256=RNL2z4RzfmoeDa-RQQnpQla-ykC0DJoRt6BOi92u5Ow,1463 tzdata/zoneinfo/Zulu,sha256=_dzh5kihcyrCmv2aFhUbKXPN8ILn7AxpD35CvmtZi5M,111 tzdata/zoneinfo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 tzdata/zoneinfo/iso3166.tab,sha256=g3yAeFCAyEM_2dTqh-ePFhrHpAOJMBxRU9T5AZi66yo,4841 -tzdata/zoneinfo/leapseconds,sha256=VRQ0gZCj758PZf7of8pZtNbZKScCrg6ElgARFZp8J2c,3694 -tzdata/zoneinfo/tzdata.zi,sha256=5JWrRFruKp1mB84rtCLX9PUJ1sqoIrJvXPtJq97TqpA,107441 -tzdata/zoneinfo/zone.tab,sha256=WGtCB-bHZyLegq3Npr9J12H2aFF_RaZz9k2oOzM-7MQ,18822 -tzdata/zoneinfo/zone1970.tab,sha256=6dn-MJQqiA91a3P2SWZ9hkeh7PITEUlEXZzCTGXk7o8,17605 -tzdata/zoneinfo/zonenow.tab,sha256=oVuFMStmwzwt9UceOp1cVf2YFT-NbrR4eNijSIsGVqU,8002 +tzdata/zoneinfo/leapseconds,sha256=1sjAMwUn2ZXeXlCvnAe5Kfqdadkucjz7YjtK5vrm07U,3698 +tzdata/zoneinfo/tzdata.zi,sha256=AHhlf9C3aGUL45Q8uitmg5A5W2CgmJg2ZSdnr2M0w3E,107524 +tzdata/zoneinfo/zone.tab,sha256=TY44nl9LDsBGbVsU9C5d-wMIxDdhZfz0eDOa_Z3csAw,18818 +tzdata/zoneinfo/zone1970.tab,sha256=QGVVVG5oWzTrRsJNgmtkndNenSAvTBOjxiH_Ie3cFYM,17601 +tzdata/zoneinfo/zonenow.tab,sha256=bShki0W6r9LAH8O_pLqb-_o5MfcVwCN-qOq_yJiNSbs,8056 tzdata/zones,sha256=UCfmEKENGYPShuIfoftxjw00cERGyzf3B-gXB7s8EkQ,9102 diff --git a/libs/tzlocal-5.3.1.dist-info/REQUESTED b/libs/tzdata-2026.2.dist-info/REQUESTED similarity index 100% rename from libs/tzlocal-5.3.1.dist-info/REQUESTED rename to libs/tzdata-2026.2.dist-info/REQUESTED diff --git a/libs/tzdata-2025.3.dist-info/WHEEL b/libs/tzdata-2026.2.dist-info/WHEEL similarity index 100% rename from libs/tzdata-2025.3.dist-info/WHEEL rename to libs/tzdata-2026.2.dist-info/WHEEL diff --git a/libs/tzdata-2025.3.dist-info/licenses/LICENSE b/libs/tzdata-2026.2.dist-info/licenses/LICENSE similarity index 100% rename from libs/tzdata-2025.3.dist-info/licenses/LICENSE rename to libs/tzdata-2026.2.dist-info/licenses/LICENSE diff --git a/libs/tzdata-2025.3.dist-info/licenses/licenses/LICENSE_APACHE b/libs/tzdata-2026.2.dist-info/licenses/licenses/LICENSE_APACHE similarity index 100% rename from libs/tzdata-2025.3.dist-info/licenses/licenses/LICENSE_APACHE rename to libs/tzdata-2026.2.dist-info/licenses/licenses/LICENSE_APACHE diff --git a/libs/tzdata-2025.3.dist-info/top_level.txt b/libs/tzdata-2026.2.dist-info/top_level.txt similarity index 100% rename from libs/tzdata-2025.3.dist-info/top_level.txt rename to libs/tzdata-2026.2.dist-info/top_level.txt diff --git a/libs/tzdata/__init__.py b/libs/tzdata/__init__.py index b1534dfb6..95e9153fa 100644 --- a/libs/tzdata/__init__.py +++ b/libs/tzdata/__init__.py @@ -1,6 +1,6 @@ # IANA versions like 2020a are not valid PEP 440 identifiers; the recommended # way to translate the version is to use YYYY.n where `n` is a 0-based index. -__version__ = "2025.3" +__version__ = "2026.2" # This exposes the original IANA version number. -IANA_VERSION = "2025c" +IANA_VERSION = "2026b" diff --git a/libs/tzdata/zoneinfo/America/Vancouver b/libs/tzdata/zoneinfo/America/Vancouver index c99849111..1ab2eaf95 100644 Binary files a/libs/tzdata/zoneinfo/America/Vancouver and b/libs/tzdata/zoneinfo/America/Vancouver differ diff --git a/libs/tzdata/zoneinfo/Asia/Ho_Chi_Minh b/libs/tzdata/zoneinfo/Asia/Ho_Chi_Minh index 86e21b0f5..294796a7a 100644 Binary files a/libs/tzdata/zoneinfo/Asia/Ho_Chi_Minh and b/libs/tzdata/zoneinfo/Asia/Ho_Chi_Minh differ diff --git a/libs/tzdata/zoneinfo/Asia/Saigon b/libs/tzdata/zoneinfo/Asia/Saigon index 86e21b0f5..294796a7a 100644 Binary files a/libs/tzdata/zoneinfo/Asia/Saigon and b/libs/tzdata/zoneinfo/Asia/Saigon differ diff --git a/libs/tzdata/zoneinfo/Asia/Tbilisi b/libs/tzdata/zoneinfo/Asia/Tbilisi index 166e4341d..247caa631 100644 Binary files a/libs/tzdata/zoneinfo/Asia/Tbilisi and b/libs/tzdata/zoneinfo/Asia/Tbilisi differ diff --git a/libs/tzdata/zoneinfo/Canada/Pacific b/libs/tzdata/zoneinfo/Canada/Pacific index c99849111..1ab2eaf95 100644 Binary files a/libs/tzdata/zoneinfo/Canada/Pacific and b/libs/tzdata/zoneinfo/Canada/Pacific differ diff --git a/libs/tzdata/zoneinfo/Europe/Chisinau b/libs/tzdata/zoneinfo/Europe/Chisinau index 9152e6859..a60bddd7b 100644 Binary files a/libs/tzdata/zoneinfo/Europe/Chisinau and b/libs/tzdata/zoneinfo/Europe/Chisinau differ diff --git a/libs/tzdata/zoneinfo/Europe/Tiraspol b/libs/tzdata/zoneinfo/Europe/Tiraspol index 9152e6859..a60bddd7b 100644 Binary files a/libs/tzdata/zoneinfo/Europe/Tiraspol and b/libs/tzdata/zoneinfo/Europe/Tiraspol differ diff --git a/libs/tzdata/zoneinfo/leapseconds b/libs/tzdata/zoneinfo/leapseconds index 2a42c83ef..a796e4b4e 100644 --- a/libs/tzdata/zoneinfo/leapseconds +++ b/libs/tzdata/zoneinfo/leapseconds @@ -70,7 +70,7 @@ Leap 2016 Dec 31 23:59:60 + S # Any additional leap seconds will come after this. # This Expires line is commented out for now, # so that pre-2020a zic implementations do not reject this file. -#Expires 2026 Jun 28 00:00:00 +#Expires 2026 Dec 28 00:00:00 # Here are POSIX timestamps for the data in this file. # "#updated" gives the last time the leap seconds data changed @@ -79,8 +79,8 @@ Leap 2016 Dec 31 23:59:60 + S # "#expires" gives the first time this file might be wrong; # if this file was derived from the IERS leap-seconds.list, # this is typically a bit less than one year after "updated". -#updated 1751846400 (2025-07-07 00:00:00 UTC) -#expires 1782604800 (2026-06-28 00:00:00 UTC) +#updated 1767698058 (2026-01-06 11:14:18 UTC) +#expires 1798416000 (2026-12-28 00:00:00 UTC) # Updated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat) -# File expires on 28 June 2026 +# File expires on 28 December 2026 diff --git a/libs/tzdata/zoneinfo/tzdata.zi b/libs/tzdata/zoneinfo/tzdata.zi index c56f67c02..40a46eaf9 100644 --- a/libs/tzdata/zoneinfo/tzdata.zi +++ b/libs/tzdata/zoneinfo/tzdata.zi @@ -1,4 +1,5 @@ -# version 2025c +# version 2026b-dirty +# redo posix_only # This zic input file is in the public domain. R d 1916 o - Jun 14 23s 1 S R d 1916 1919 - O Su>=1 23s 0 - @@ -1304,8 +1305,8 @@ R MT 1974 o - S 16 0s 0 - R MT 1975 1979 - Ap Su>=15 2 1 S R MT 1975 1980 - S Su>=15 2 0 - R MT 1980 o - Mar 31 2 1 S -R MD 1997 ma - Mar lastSu 2 1 S -R MD 1997 ma - O lastSu 3 0 - +R MD 1997 2021 - Mar lastSu 2 1 S +R MD 1997 2021 - O lastSu 3 0 - R O 1918 1919 - S 16 2s 0 - R O 1919 o - Ap 15 2s 1 S R O 1944 o - Ap 3 2s 1 S @@ -2965,7 +2966,9 @@ Z America/Toronto -5:17:32 - LMT 1895 -5 C E%sT Z America/Vancouver -8:12:28 - LMT 1884 -8 Va P%sT 1987 --8 C P%sT +-8 C P%sT 2026 Mar 9 +-8 1 PDT 2026 N 1 2 +-7 - MST Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 -9 Y Y%sT 1965 -9 Yu Y%sT 1966 F 27 @@ -3666,7 +3669,8 @@ Z Europe/Chisinau 1:55:20 - LMT 1880 3 R MSK/MSD 1990 May 6 2 2 R EE%sT 1992 2 e EE%sT 1997 -2 MD EE%sT +2 MD EE%sT 2022 +2 E EE%sT Z Europe/Dublin -0:25:21 - LMT 1880 Au 2 -0:25:21 - DMT 1916 May 21 2s -0:25:21 1 IST 1916 O 1 2s diff --git a/libs/tzdata/zoneinfo/zone.tab b/libs/tzdata/zoneinfo/zone.tab index 2626b0550..2ae8a8c43 100644 --- a/libs/tzdata/zoneinfo/zone.tab +++ b/libs/tzdata/zoneinfo/zone.tab @@ -124,12 +124,12 @@ CA +5017-10750 America/Swift_Current CST - SK (midwest) CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4916-12307 America/Vancouver MST - BC (most areas) CA +4906-11631 America/Creston MST - BC (Creston) CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) CA +6043-13503 America/Whitehorse MST - Yukon (east) CA +6404-13925 America/Dawson MST - Yukon (west) -CA +4916-12307 America/Vancouver Pacific - BC (most areas) CC -1210+09655 Indian/Cocos CD -0418+01518 Africa/Kinshasa Dem. Rep. of Congo (west) CD -1140+02728 Africa/Lubumbashi Dem. Rep. of Congo (east) diff --git a/libs/tzdata/zoneinfo/zone1970.tab b/libs/tzdata/zoneinfo/zone1970.tab index cd43e3d2e..a9b47bcb3 100644 --- a/libs/tzdata/zoneinfo/zone1970.tab +++ b/libs/tzdata/zoneinfo/zone1970.tab @@ -115,11 +115,11 @@ CA +5017-10750 America/Swift_Current CST - SK (midwest) CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4916-12307 America/Vancouver MST - BC (most areas) CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) CA +6043-13503 America/Whitehorse MST - Yukon (east) CA +6404-13925 America/Dawson MST - Yukon (west) -CA +4916-12307 America/Vancouver Pacific - BC (most areas) CH,DE,LI +4723+00832 Europe/Zurich Büsingen CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan CK -2114-15946 Pacific/Rarotonga diff --git a/libs/tzdata/zoneinfo/zonenow.tab b/libs/tzdata/zoneinfo/zonenow.tab index 1d64b3912..54e4485d7 100644 --- a/libs/tzdata/zoneinfo/zonenow.tab +++ b/libs/tzdata/zoneinfo/zonenow.tab @@ -58,6 +58,9 @@ XX -2504-13005 Pacific/Pitcairn Pitcairn # -08/-07 - PST/PDT (North America DST) XX +340308-1181434 America/Los_Angeles Pacific (PST/PDT) - US & Canada; Mexico near US border # +# -08/-07 - PST/PDT (North America DST) until 2026-11-01 02:00; then MST +XX +4916-12307 America/Vancouver MST - BC (most areas) +# # -07 - MST XX +332654-1120424 America/Phoenix Mountain Standard (MST) - Arizona; western Mexico; Yukon # @@ -166,9 +169,6 @@ XX +3003+03115 Africa/Cairo Egypt # +02/+03 - EET/EEST (Lebanon DST) XX +3353+03530 Asia/Beirut Lebanon # -# +02/+03 - EET/EEST (Moldova DST) -XX +4700+02850 Europe/Chisinau Moldova -# # +02/+03 - EET/EEST (Palestine DST) XX +3130+03428 Asia/Gaza Palestine # diff --git a/libs/tzlocal-5.3.1.dist-info/RECORD b/libs/tzlocal-5.3.1.dist-info/RECORD deleted file mode 100644 index 42bd666d0..000000000 --- a/libs/tzlocal-5.3.1.dist-info/RECORD +++ /dev/null @@ -1,13 +0,0 @@ -tzlocal-5.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -tzlocal-5.3.1.dist-info/METADATA,sha256=jX8pVhQA7UvFYfAUhOJQSAwZKld_upYVZgdzi5Gm53s,7599 -tzlocal-5.3.1.dist-info/RECORD,, -tzlocal-5.3.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -tzlocal-5.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -tzlocal-5.3.1.dist-info/licenses/LICENSE.txt,sha256=2ZqyCa6xaq0sJckP_YPBqYHikP__dqQgoqsD4D8EG4w,1060 -tzlocal-5.3.1.dist-info/top_level.txt,sha256=QR6vZWP520waETnkotApPQPyVh9VnjoYPoAVHLK1DrE,8 -tzlocal/__init__.py,sha256=2OQDx61TZv_HmBl7JKKP4ATldlx5OdEXOjKXAt8rQwY,396 -tzlocal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -tzlocal/unix.py,sha256=SyD32zfJn3r7xDJwo4sTYebhQ25XZIssvz24Nd-9tR8,9097 -tzlocal/utils.py,sha256=ov8fPwPjyvXRqW9Wzx_skATsjiB9gAzK1CEHJ2UI91Y,3224 -tzlocal/win32.py,sha256=8lFxjUGxOmcHcVgKWjVEPnLbTswOgxqdpnaBW9wcPQE,4668 -tzlocal/windows_tz.py,sha256=fG2X3ly4i3cRoaQEd6ncJhqDgPp-qvJunnRW-fg0Jkk,35165 diff --git a/libs/tzlocal-5.3.1.dist-info/WHEEL b/libs/tzlocal-5.3.1.dist-info/WHEEL deleted file mode 100644 index 0885d0555..000000000 --- a/libs/tzlocal-5.3.1.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (80.10.2) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/libs/tzlocal-5.3.1.dist-info/INSTALLER b/libs/tzlocal-5.4.3.dist-info/INSTALLER similarity index 100% rename from libs/tzlocal-5.3.1.dist-info/INSTALLER rename to libs/tzlocal-5.4.3.dist-info/INSTALLER diff --git a/libs/tzlocal-5.3.1.dist-info/METADATA b/libs/tzlocal-5.4.3.dist-info/METADATA similarity index 93% rename from libs/tzlocal-5.3.1.dist-info/METADATA rename to libs/tzlocal-5.4.3.dist-info/METADATA index f6ba07d48..60f6dfb95 100644 --- a/libs/tzlocal-5.3.1.dist-info/METADATA +++ b/libs/tzlocal-5.4.3.dist-info/METADATA @@ -1,33 +1,35 @@ Metadata-Version: 2.4 Name: tzlocal -Version: 5.3.1 +Version: 5.4.3 Summary: tzinfo object for the local timezone Author-email: Lennart Regebro -License: MIT +License-Expression: MIT Project-URL: Source code, https://github.com/regebro/tzlocal Project-URL: Changelog, https://github.com/regebro/tzlocal/blob/master/CHANGES.txt Project-URL: Issue tracker, https://github.com/regebro/tzlocal/issues Keywords: timezone -Classifier: Development Status :: 5 - Production/Stable -Classifier: License :: OSI Approved :: MIT License +Classifier: Development Status :: 6 - Mature Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: Unix Classifier: Operating System :: MacOS :: MacOS X Classifier: Typing :: Typed -Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 -Requires-Python: >=3.9 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE.txt Requires-Dist: tzdata; platform_system == "Windows" +Provides-Extra: testing +Requires-Dist: pytest>=4.3; extra == "testing" +Requires-Dist: pytest-mock>=3.3; extra == "testing" +Requires-Dist: pytest-cov; extra == "testing" +Requires-Dist: check_manifest; extra == "testing" +Requires-Dist: pyroma; extra == "testing" +Requires-Dist: ruff; extra == "testing" Provides-Extra: devenv -Requires-Dist: pytest>=4.3; extra == "devenv" -Requires-Dist: pytest-mock>=3.3; extra == "devenv" -Requires-Dist: pytest-cov; extra == "devenv" -Requires-Dist: check_manifest; extra == "devenv" Requires-Dist: zest.releaser; extra == "devenv" Dynamic: license-file diff --git a/libs/tzlocal-5.4.3.dist-info/RECORD b/libs/tzlocal-5.4.3.dist-info/RECORD new file mode 100644 index 000000000..3e8950ff4 --- /dev/null +++ b/libs/tzlocal-5.4.3.dist-info/RECORD @@ -0,0 +1,13 @@ +tzlocal-5.4.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +tzlocal-5.4.3.dist-info/METADATA,sha256=Zg6GACQD1hg1j6o6x_JNwebsk0Pd8_M5WOeCe7pIbcU,7660 +tzlocal-5.4.3.dist-info/RECORD,, +tzlocal-5.4.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tzlocal-5.4.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +tzlocal-5.4.3.dist-info/licenses/LICENSE.txt,sha256=2ZqyCa6xaq0sJckP_YPBqYHikP__dqQgoqsD4D8EG4w,1060 +tzlocal-5.4.3.dist-info/top_level.txt,sha256=QR6vZWP520waETnkotApPQPyVh9VnjoYPoAVHLK1DrE,8 +tzlocal/__init__.py,sha256=ptfATXBcbZF9Yurt_XOx1yvGBE2v68NYtwG4S9Vn0YE,363 +tzlocal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tzlocal/unix.py,sha256=oquFJBgWbcq6VaEHcGXvDSPDGCcs-jHmemRiG-oNPVY,9056 +tzlocal/utils.py,sha256=ov8fPwPjyvXRqW9Wzx_skATsjiB9gAzK1CEHJ2UI91Y,3224 +tzlocal/win32.py,sha256=BPZRlSomHFCeqOLDlYHeqhOt8wNNTL3lkum0mzz-njE,4610 +tzlocal/windows_tz.py,sha256=fG2X3ly4i3cRoaQEd6ncJhqDgPp-qvJunnRW-fg0Jkk,35165 diff --git a/libs/urllib3-2.6.3.dist-info/REQUESTED b/libs/tzlocal-5.4.3.dist-info/REQUESTED similarity index 100% rename from libs/urllib3-2.6.3.dist-info/REQUESTED rename to libs/tzlocal-5.4.3.dist-info/REQUESTED diff --git a/libs/stevedore-5.7.0.dist-info/WHEEL b/libs/tzlocal-5.4.3.dist-info/WHEEL similarity index 100% rename from libs/stevedore-5.7.0.dist-info/WHEEL rename to libs/tzlocal-5.4.3.dist-info/WHEEL diff --git a/libs/tzlocal-5.3.1.dist-info/licenses/LICENSE.txt b/libs/tzlocal-5.4.3.dist-info/licenses/LICENSE.txt similarity index 100% rename from libs/tzlocal-5.3.1.dist-info/licenses/LICENSE.txt rename to libs/tzlocal-5.4.3.dist-info/licenses/LICENSE.txt diff --git a/libs/tzlocal-5.3.1.dist-info/top_level.txt b/libs/tzlocal-5.4.3.dist-info/top_level.txt similarity index 100% rename from libs/tzlocal-5.3.1.dist-info/top_level.txt rename to libs/tzlocal-5.4.3.dist-info/top_level.txt diff --git a/libs/tzlocal/__init__.py b/libs/tzlocal/__init__.py index 8296a15dc..d67fb12ac 100644 --- a/libs/tzlocal/__init__.py +++ b/libs/tzlocal/__init__.py @@ -1,11 +1,7 @@ import sys if sys.platform == "win32": - from tzlocal.win32 import ( - get_localzone, - get_localzone_name, - reload_localzone, - ) + from tzlocal.win32 import get_localzone, get_localzone_name, reload_localzone else: from tzlocal.unix import get_localzone, get_localzone_name, reload_localzone diff --git a/libs/tzlocal/unix.py b/libs/tzlocal/unix.py index a1a8d8972..d204a999a 100644 --- a/libs/tzlocal/unix.py +++ b/libs/tzlocal/unix.py @@ -1,14 +1,12 @@ import logging import os import re -import sys import warnings +import zoneinfo from datetime import timezone from tzlocal import utils -import zoneinfo - _cache_tz = None _cache_tz_name = None @@ -36,11 +34,7 @@ def _get_localzone_name(_root="/"): import subprocess try: - androidtz = ( - subprocess.check_output(["getprop", "persist.sys.timezone"]) - .strip() - .decode() - ) + androidtz = subprocess.check_output(["getprop", "persist.sys.timezone"]).strip().decode() return androidtz except (OSError, subprocess.CalledProcessError): # proot environment or failed to getprop @@ -56,7 +50,7 @@ def _get_localzone_name(_root="/"): for configfile in ("etc/timezone", "var/db/zoneinfo"): tzpath = os.path.join(_root, configfile) try: - with open(tzpath) as tzfile: + with open(tzpath, encoding="ascii") as tzfile: data = tzfile.read() log.debug(f"{tzpath} found, contents:\n {data}") @@ -67,9 +61,9 @@ def _get_localzone_name(_root="/"): for etctz in etctz.splitlines(): # Get rid of host definitions and comments: if " " in etctz: - etctz, dummy = etctz.split(" ", 1) + etctz, _ = etctz.split(" ", 1) if "#" in etctz: - etctz, dummy = etctz.split("#", 1) + etctz, _ = etctz.split("#", 1) if not etctz: continue @@ -141,12 +135,14 @@ def _get_localzone_name(_root="/"): unique_tzs = _get_unique_tzs(found_configs, _root) if len(unique_tzs) != 1 and "etc/timezone" in str(found_configs.keys()): - # For some reason some distros are removing support for /etc/timezone, - # which is bad, because that's the only place where the timezone is stated - # in plain text, and what's worse, they don't delete it. So we can't trust + # For some reason some distros are removing support for /etc/timezone, + # which is bad, because that's the only place where the timezone is stated + # in plain text, and what's worse, they don't delete it. So we can't trust # it now, so when we have conflicting configs, we just ignore it, with a warning. - log.warning("/etc/timezone is deprecated in some distros, and no longer reliable. " - "tzlocal is ignoring it, and you can likely delete it.") + log.warning( + "/etc/timezone is deprecated in some distros, and no longer reliable. " + "tzlocal is ignoring it, and you can likely delete it." + ) found_configs = {k: v for k, v in found_configs.items() if "etc/timezone" not in k} unique_tzs = _get_unique_tzs(found_configs, _root) diff --git a/libs/tzlocal/win32.py b/libs/tzlocal/win32.py index cfcdba535..6bba433bd 100644 --- a/libs/tzlocal/win32.py +++ b/libs/tzlocal/win32.py @@ -93,18 +93,14 @@ def _get_localzone_name(): return timezone if std_offset is None: - raise zoneinfo.ZoneInfoNotFoundError( - f"{tzkeyname} claims to not have a non-DST time!?" - ) + raise zoneinfo.ZoneInfoNotFoundError(f"{tzkeyname} claims to not have a non-DST time!?") if std_offset % 3600: # I can't convert this to an hourly offset - raise zoneinfo.ZoneInfoNotFoundError( - f"tzlocal can't support disabling DST in the {timezone} zone." - ) + raise zoneinfo.ZoneInfoNotFoundError(f"tzlocal can't support disabling DST in the {timezone} zone.") # This has whole hours as offset, return it as Etc/GMT - return f"Etc/GMT{-std_offset//3600:+.0f}" + return f"Etc/GMT{-std_offset // 3600:+.0f}" return timezone diff --git a/libs/urllib3-2.6.3.dist-info/WHEEL b/libs/urllib3-2.6.3.dist-info/WHEEL deleted file mode 100644 index ae8ec1bda..000000000 --- a/libs/urllib3-2.6.3.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.28.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/libs/urllib3-2.6.3.dist-info/INSTALLER b/libs/urllib3-2.7.0.dist-info/INSTALLER similarity index 100% rename from libs/urllib3-2.6.3.dist-info/INSTALLER rename to libs/urllib3-2.7.0.dist-info/INSTALLER diff --git a/libs/urllib3-2.6.3.dist-info/METADATA b/libs/urllib3-2.7.0.dist-info/METADATA similarity index 98% rename from libs/urllib3-2.6.3.dist-info/METADATA rename to libs/urllib3-2.7.0.dist-info/METADATA index b75670fb6..9c7a4703f 100644 --- a/libs/urllib3-2.6.3.dist-info/METADATA +++ b/libs/urllib3-2.7.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: urllib3 -Version: 2.6.3 +Version: 2.7.0 Summary: HTTP library with thread-safe connection pooling, file post, and more. Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst Project-URL: Documentation, https://urllib3.readthedocs.io @@ -17,7 +17,6 @@ Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 @@ -28,7 +27,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Software Development :: Libraries -Requires-Python: >=3.9 +Requires-Python: >=3.10 Provides-Extra: brotli Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli' Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli' diff --git a/libs/urllib3-2.6.3.dist-info/RECORD b/libs/urllib3-2.7.0.dist-info/RECORD similarity index 55% rename from libs/urllib3-2.6.3.dist-info/RECORD rename to libs/urllib3-2.7.0.dist-info/RECORD index 16088de55..845e774c0 100644 --- a/libs/urllib3-2.6.3.dist-info/RECORD +++ b/libs/urllib3-2.7.0.dist-info/RECORD @@ -1,42 +1,42 @@ -urllib3-2.6.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -urllib3-2.6.3.dist-info/METADATA,sha256=6ROQzJr0mwGXOPHXObXNklEwwy6dPmrMCGPCHF2Ygu8,6901 -urllib3-2.6.3.dist-info/RECORD,, -urllib3-2.6.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3-2.6.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87 -urllib3-2.6.3.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 +urllib3-2.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 +urllib3-2.7.0.dist-info/RECORD,, +urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3-2.7.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 -urllib3/_base_connection.py,sha256=T1cwH3RhzsrBh6Bz3AOGVDboRsE7veijqZPXXQTR2Rg,5568 -urllib3/_collections.py,sha256=UvV7UqtGTSKdvw8N_LxWuEikZLm5gB1zFfTZYH9KhAk,17595 +urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 +urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 -urllib3/_version.py,sha256=vKE8or0mmqgsFpVb7FYms-nNOVCPPAEifgxVrTaPByw,704 -urllib3/connection.py,sha256=1ZR2gqfFdIzTYIUwF0K5nftg26hLqU5nr1yHTdKb7WA,42800 -urllib3/connectionpool.py,sha256=ZEhudsa8BIubD2M0XoxBBsjxbsXwMgUScH7oQ9i-j1Y,43371 +urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 +urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 +urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 -urllib3/contrib/emscripten/response.py,sha256=7oVPENYZHuzEGRtG40HonpH5tAIYHsGcHPbJt2Z0U-Y,9507 -urllib3/contrib/pyopenssl.py,sha256=4awTja4o3beTGTGmmWo_3rBoEgzje95Q4bgWz4iiSx8,19724 -urllib3/contrib/socks.py,sha256=eB2eWfu8Wz1fn-qvr_qE_dZAceck2Ncv7XQ15DlvVbU,7547 -urllib3/exceptions.py,sha256=eeQ77nJjF97bP6SvCK4gmx6BpQZKU8yjvM-AIDwZdX8,9952 -urllib3/fields.py,sha256=FCf7UULSkf10cuTRUWTQESzxgl1WT8e2aCy3kfyZins,10829 +urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 +urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 +urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 +urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 +urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 -urllib3/poolmanager.py,sha256=NYP5vkKfadGddaBacUk6z6u8rTP9wgCFGGjVtf1mkcc,23811 +urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 -urllib3/response.py,sha256=2VDtH9KrYNQLUbDYHZ3GgwzH3JZphkn_JoqtB7ozkt0,52931 +urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 -urllib3/util/retry.py,sha256=WOcIHVaxKf-dVb89lUbpvcpeM7rNYF_vsKsCOKw10Z8,19235 -urllib3/util/ssl_.py,sha256=Y9RNkWCIehDxIRvyFnHUjiMlPolm368GYMya2YdDOag,19929 -urllib3/util/ssl_match_hostname.py,sha256=Di7DU7zokoltapT_F0Sj21ffYxwaS_cE5apOtwueeyA,5845 +urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 +urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 +urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 diff --git a/libs/wcwidth-0.6.0.dist-info/REQUESTED b/libs/urllib3-2.7.0.dist-info/REQUESTED similarity index 100% rename from libs/wcwidth-0.6.0.dist-info/REQUESTED rename to libs/urllib3-2.7.0.dist-info/REQUESTED diff --git a/libs/urllib3-2.7.0.dist-info/WHEEL b/libs/urllib3-2.7.0.dist-info/WHEEL new file mode 100644 index 000000000..bd8422674 --- /dev/null +++ b/libs/urllib3-2.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/urllib3-2.6.3.dist-info/licenses/LICENSE.txt b/libs/urllib3-2.7.0.dist-info/licenses/LICENSE.txt similarity index 100% rename from libs/urllib3-2.6.3.dist-info/licenses/LICENSE.txt rename to libs/urllib3-2.7.0.dist-info/licenses/LICENSE.txt diff --git a/libs/urllib3/_base_connection.py b/libs/urllib3/_base_connection.py index dc0f318c0..992ec1657 100644 --- a/libs/urllib3/_base_connection.py +++ b/libs/urllib3/_base_connection.py @@ -6,7 +6,9 @@ from .util.connection import _TYPE_SOCKET_OPTIONS from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT from .util.url import Url -_TYPE_BODY = typing.Union[bytes, typing.IO[typing.Any], typing.Iterable[bytes], str] +_TYPE_BODY = typing.Union[ + bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str +] class ProxyConfig(typing.NamedTuple): diff --git a/libs/urllib3/_collections.py b/libs/urllib3/_collections.py index 0378aab1b..ee9ca662b 100644 --- a/libs/urllib3/_collections.py +++ b/libs/urllib3/_collections.py @@ -356,7 +356,6 @@ class HTTPHeaderDict(typing.MutableMapping[str, str]): for key, val in other.items(): self.add(key, val) elif isinstance(other, typing.Iterable): - other = typing.cast(typing.Iterable[tuple[str, str]], other) for key, value in other: self.add(key, value) elif hasattr(other, "keys") and hasattr(other, "__getitem__"): diff --git a/libs/urllib3/_version.py b/libs/urllib3/_version.py index 268d3b984..bfed03985 100644 --- a/libs/urllib3/_version.py +++ b/libs/urllib3/_version.py @@ -1,5 +1,6 @@ -# file generated by setuptools-scm +# file generated by vcs-versioning # don't change, don't track in version control +from __future__ import annotations __all__ = [ "__version__", @@ -10,25 +11,14 @@ __all__ = [ "commit_id", ] -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] - COMMIT_ID = Union[str, None] -else: - VERSION_TUPLE = object - COMMIT_ID = object - version: str __version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE -commit_id: COMMIT_ID -__commit_id__: COMMIT_ID +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None -__version__ = version = '2.6.3' -__version_tuple__ = version_tuple = (2, 6, 3) +__version__ = version = '2.7.0' +__version_tuple__ = version_tuple = (2, 7, 0) __commit_id__ = commit_id = None diff --git a/libs/urllib3/connection.py b/libs/urllib3/connection.py index 2ceeb0a54..84e1dab94 100644 --- a/libs/urllib3/connection.py +++ b/libs/urllib3/connection.py @@ -531,8 +531,8 @@ class HTTPConnection(_HTTPConnection): """ warnings.warn( "HTTPConnection.request_chunked() is deprecated and will be removed " - "in urllib3 v2.1.0. Instead use HTTPConnection.request(..., chunked=True).", - category=DeprecationWarning, + "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", + category=FutureWarning, stacklevel=2, ) self.request(method, url, body=body, headers=headers, chunked=True) @@ -697,9 +697,9 @@ class HTTPSConnection(HTTPConnection): """ warnings.warn( "HTTPSConnection.set_cert() is deprecated and will be removed " - "in urllib3 v2.1.0. Instead provide the parameters to the " + "in urllib3 v3.0. Instead provide the parameters to the " "HTTPSConnection constructor.", - category=DeprecationWarning, + category=FutureWarning, stacklevel=2, ) diff --git a/libs/urllib3/connectionpool.py b/libs/urllib3/connectionpool.py index 3a0685b4c..70fbc5e72 100644 --- a/libs/urllib3/connectionpool.py +++ b/libs/urllib3/connectionpool.py @@ -216,8 +216,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if self.proxy: # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. - # We cannot know if the user has added default socket options, so we cannot replace the - # list. + # Defaulting `socket_options` to an empty list avoids it defaulting to + # ``HTTPConnection.default_socket_options``. self.conn_kw.setdefault("socket_options", []) self.conn_kw["proxy"] = self.proxy @@ -702,8 +702,15 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): redirect. Typically this won't need to be set because urllib3 will auto-populate the value when needed. """ - parsed_url = parse_url(url) - destination_scheme = parsed_url.scheme + # Ensure that the URL we're connecting to is properly encoded + if url.startswith("/"): + # URLs starting with / are inherently schemeless. + url = to_str(_encode_target(url)) + destination_scheme = None + else: + parsed_url = parse_url(url) + destination_scheme = parsed_url.scheme + url = to_str(parsed_url.url) if headers is None: headers = self.headers @@ -718,12 +725,6 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): if assert_same_host and not self.is_same_host(url): raise HostChangedError(self, url, retries) - # Ensure that the URL we're connecting to is properly encoded - if url.startswith("/"): - url = to_str(_encode_target(url)) - else: - url = to_str(parsed_url.url) - conn = None # Track whether `conn` needs to be released before @@ -896,6 +897,18 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): body = None headers = HTTPHeaderDict(headers)._prepare_for_method_change() + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # self.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not self.is_same_host( + redirect_location + ): + new_headers = headers.copy() # type: ignore[union-attr] + for header in headers: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + headers = new_headers + try: retries = retries.increment(method, url, response=response, _pool=self) except MaxRetryError: diff --git a/libs/urllib3/contrib/emscripten/response.py b/libs/urllib3/contrib/emscripten/response.py index cb1088a18..ec1e1dbe8 100644 --- a/libs/urllib3/contrib/emscripten/response.py +++ b/libs/urllib3/contrib/emscripten/response.py @@ -36,6 +36,7 @@ class EmscriptenHttpResponseWrapper(BaseHTTPResponse): ): self._pool = None # set by pool class self._body = None + self._uncached_read_occurred = False self._response = internal_response self._url = url self._connection = connection @@ -160,10 +161,13 @@ class EmscriptenHttpResponseWrapper(BaseHTTPResponse): # don't cache partial content cache_content = False data = self._response.body.read(amt) + self._uncached_read_occurred = True else: # read all we can (and cache it) data = self._response.body.read() - if cache_content: + if cache_content and not self._uncached_read_occurred: self._body = data + else: + self._uncached_read_occurred = True if self.length_remaining is not None: self.length_remaining = max(self.length_remaining - len(data), 0) if len(data) == 0 or ( diff --git a/libs/urllib3/contrib/pyopenssl.py b/libs/urllib3/contrib/pyopenssl.py index 8e05d3d78..f06b85999 100644 --- a/libs/urllib3/contrib/pyopenssl.py +++ b/libs/urllib3/contrib/pyopenssl.py @@ -6,9 +6,9 @@ support for Python 2.7 all relevant Python versions support SNI so This needs the following packages installed: -* `pyOpenSSL`_ (tested with 16.0.0) -* `cryptography`_ (minimum 1.3.4, from pyopenssl) -* `idna`_ (minimum 2.0) +* `pyOpenSSL`_ (tested with 19.0.0) +* `cryptography`_ (minimum 2.3, from pyopenssl) +* `idna`_ (minimum 2.1, from cryptography) However, pyOpenSSL depends on cryptography, so while we use all three directly here we end up having relatively few packages required. @@ -56,7 +56,6 @@ import ssl import typing from io import BytesIO from socket import socket as socket_cls -from socket import timeout from .. import util @@ -311,7 +310,7 @@ class WrappedSocket: raise except OpenSSL.SSL.WantReadError as e: if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise timeout("The read operation timed out") from e + raise TimeoutError("The read operation timed out") from e else: return self.recv(*args, **kwargs) @@ -336,7 +335,7 @@ class WrappedSocket: raise except OpenSSL.SSL.WantReadError as e: if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise timeout("The read operation timed out") from e + raise TimeoutError("The read operation timed out") from e else: return self.recv_into(*args, **kwargs) @@ -353,7 +352,7 @@ class WrappedSocket: return self.connection.send(data) # type: ignore[no-any-return] except OpenSSL.SSL.WantWriteError as e: if not util.wait_for_write(self.socket, self.socket.gettimeout()): - raise timeout() from e + raise TimeoutError() from e continue except OpenSSL.SSL.SysCallError as e: raise OSError(e.args[0], str(e)) from e @@ -520,7 +519,7 @@ class PyOpenSSLContext: cnx.do_handshake() except OpenSSL.SSL.WantReadError as e: if not util.wait_for_read(sock, sock.gettimeout()): - raise timeout("select timed out") from e + raise TimeoutError("select timed out") from e continue except OpenSSL.SSL.Error as e: raise ssl.SSLError(f"bad handshake: {e!r}") from e diff --git a/libs/urllib3/contrib/socks.py b/libs/urllib3/contrib/socks.py index e3239b569..d37da8fc2 100644 --- a/libs/urllib3/contrib/socks.py +++ b/libs/urllib3/contrib/socks.py @@ -141,7 +141,7 @@ class SOCKSConnection(HTTPConnection): raise NewConnectionError( self, f"Failed to establish a new connection: {error}" ) - else: + else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 raise NewConnectionError( self, f"Failed to establish a new connection: {e}" ) from e diff --git a/libs/urllib3/exceptions.py b/libs/urllib3/exceptions.py index 58723faeb..3d7e9b93d 100644 --- a/libs/urllib3/exceptions.py +++ b/libs/urllib3/exceptions.py @@ -155,8 +155,8 @@ class NewConnectionError(ConnectTimeoutError, HTTPError): def pool(self) -> HTTPConnection: warnings.warn( "The 'pool' property is deprecated and will be removed " - "in urllib3 v2.1.0. Use 'conn' instead.", - DeprecationWarning, + "in urllib3 v3.0. Use 'conn' instead.", + FutureWarning, stacklevel=2, ) diff --git a/libs/urllib3/fields.py b/libs/urllib3/fields.py index 97c4730cf..fe68e1773 100644 --- a/libs/urllib3/fields.py +++ b/libs/urllib3/fields.py @@ -45,16 +45,16 @@ def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: An RFC-2231-formatted unicode string. .. deprecated:: 2.0.0 - Will be removed in urllib3 v2.1.0. This is not valid for + Will be removed in urllib3 v3.0. This is not valid for ``multipart/form-data`` header parameters. """ import warnings warnings.warn( - "'format_header_param_rfc2231' is deprecated and will be " - "removed in urllib3 v2.1.0. This is not valid for " + "'format_header_param_rfc2231' is insecure, deprecated and will be " + "removed in urllib3 v3.0. This is not valid for " "multipart/form-data header parameters.", - DeprecationWarning, + FutureWarning, stacklevel=2, ) @@ -104,7 +104,7 @@ def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: .. versionchanged:: 2.0.0 Renamed from ``format_header_param_html5`` and ``format_header_param``. The old names will be removed in - urllib3 v2.1.0. + urllib3 v3.0. """ if isinstance(value, bytes): value = value.decode("utf-8") @@ -118,15 +118,15 @@ def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: """ .. deprecated:: 2.0.0 Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v2.1.0. + removed in urllib3 v3.0. """ import warnings warnings.warn( "'format_header_param_html5' has been renamed to " "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v2.1.0.", - DeprecationWarning, + "removed in urllib3 v3.0.", + FutureWarning, stacklevel=2, ) return format_multipart_header_param(name, value) @@ -136,15 +136,15 @@ def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: """ .. deprecated:: 2.0.0 Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v2.1.0. + removed in urllib3 v3.0. """ import warnings warnings.warn( "'format_header_param' has been renamed to " "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v2.1.0.", - DeprecationWarning, + "removed in urllib3 v3.0.", + FutureWarning, stacklevel=2, ) return format_multipart_header_param(name, value) @@ -165,7 +165,7 @@ class RequestField: .. versionchanged:: 2.0.0 The ``header_formatter`` parameter is deprecated and will - be removed in urllib3 v2.1.0. + be removed in urllib3 v3.0. """ def __init__( @@ -188,8 +188,8 @@ class RequestField: warnings.warn( "The 'header_formatter' parameter is deprecated and " - "will be removed in urllib3 v2.1.0.", - DeprecationWarning, + "will be removed in urllib3 v3.0.", + FutureWarning, stacklevel=2, ) self.header_formatter = header_formatter diff --git a/libs/urllib3/poolmanager.py b/libs/urllib3/poolmanager.py index 28ec82f01..8f2c56745 100644 --- a/libs/urllib3/poolmanager.py +++ b/libs/urllib3/poolmanager.py @@ -328,8 +328,8 @@ class PoolManager(RequestMethods): if "strict" in request_context: warnings.warn( "The 'strict' parameter is no longer needed on Python 3+. " - "This will raise an error in urllib3 v2.1.0.", - DeprecationWarning, + "This will raise an error in urllib3 v3.0.", + FutureWarning, ) request_context.pop("strict") @@ -436,10 +436,10 @@ class PoolManager(RequestMethods): if u.scheme is None: warnings.warn( "URLs without a scheme (ie 'https://') are deprecated and will raise an error " - "in a future version of urllib3. To avoid this DeprecationWarning ensure all URLs " + "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " "start with 'https://' or 'http://'. Read more in this issue: " "https://github.com/urllib3/urllib3/issues/2920", - category=DeprecationWarning, + category=FutureWarning, stacklevel=2, ) @@ -544,15 +544,17 @@ class ProxyManager(PoolManager): proxy = urllib3.ProxyManager("https://localhost:3128/") - resp1 = proxy.request("GET", "https://google.com/") - resp2 = proxy.request("GET", "https://httpbin.org/") + resp1 = proxy.request("GET", "http://google.com/") + resp2 = proxy.request("GET", "http://httpbin.org/") + # One pool was shared by both plain HTTP requests. print(len(proxy.pools)) # 1 resp3 = proxy.request("GET", "https://httpbin.org/") resp4 = proxy.request("GET", "https://twitter.com/") + # A separate pool was added for each HTTPS target. print(len(proxy.pools)) # 3 diff --git a/libs/urllib3/response.py b/libs/urllib3/response.py index ff6d1f491..e9246b75e 100644 --- a/libs/urllib3/response.py +++ b/libs/urllib3/response.py @@ -663,7 +663,7 @@ class BaseHTTPResponse(io.IOBase): return b"" # Compatibility methods for `io` module - def readinto(self, b: bytearray) -> int: + def readinto(self, b: bytearray | memoryview[int]) -> int: temp = self.read(len(b)) if len(temp) == 0: return 0 @@ -755,6 +755,7 @@ class HTTPResponse(BaseHTTPResponse): self.auto_close = auto_close self._body = None + self._uncached_read_occurred = False self._fp: _HttplibHTTPResponse | None = None self._original_response = original_response self._fp_bytes_read = 0 @@ -797,13 +798,14 @@ class HTTPResponse(BaseHTTPResponse): Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. """ try: - self.read( - # Do not spend resources decoding the content unless - # decoding has already been initiated. - decode_content=self._has_decoded_content, - ) + self._raw_read() except (HTTPError, OSError, BaseSSLError, HTTPException): pass + if self._has_decoded_content: + # `_raw_read` skips decompression, so we should clean up the + # decoder to avoid keeping unnecessary data in memory. + self._decoded_buffer = BytesQueueBuffer() + self._decoder = None @property def data(self) -> bytes: @@ -826,7 +828,7 @@ class HTTPResponse(BaseHTTPResponse): def tell(self) -> int: """ Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:``urllib3.response.HTTPResponse.read`` + the amount of content returned by :meth:`HTTPResponse.read` if bytes are encoded on the wire (e.g, compressed). """ return self._fp_bytes_read @@ -908,12 +910,8 @@ class HTTPResponse(BaseHTTPResponse): raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] except BaseSSLError as e: - # FIXME: Is there a better way to differentiate between SSLErrors? - if "read operation timed out" not in str(e): - # SSL errors related to framing/MAC get wrapped and reraised here - raise SSLError(e) from e - - raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + # SSL errors related to framing/MAC get wrapped and reraised here + raise SSLError(e) from e except IncompleteRead as e: if ( @@ -966,11 +964,7 @@ class HTTPResponse(BaseHTTPResponse): if `amt` or `self.length_remaining` indicate that a problem may happen. - The known cases: - * CPython < 3.9.7 because of a bug - https://github.com/urllib3/urllib3/issues/2513#issuecomment-1152559900. - * urllib3 injected with pyOpenSSL-backed SSL-support. - * CPython < 3.10 only when `amt` does not fit 32-bit int. + This happens to urllib3 injected with pyOpenSSL-backed SSL-support. """ assert self._fp c_int_max = 2**31 - 1 @@ -981,7 +975,7 @@ class HTTPResponse(BaseHTTPResponse): and self.length_remaining and self.length_remaining > c_int_max ) - ) and (util.IS_PYOPENSSL or sys.version_info < (3, 10)): + ) and util.IS_PYOPENSSL: if read1: return self._fp.read1(c_int_max) buffer = io.BytesIO() @@ -1098,7 +1092,11 @@ class HTTPResponse(BaseHTTPResponse): elif amt is not None: cache_content = False - if self._decoder and self._decoder.has_unconsumed_tail: + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and len(self._decoded_buffer) < amt + ): decoded_data = self._decode( b"", decode_content, @@ -1110,6 +1108,8 @@ class HTTPResponse(BaseHTTPResponse): return self._decoded_buffer.get(amt) data = self._raw_read(amt) + if not cache_content: + self._uncached_read_occurred = True flush_decoder = amt is None or (amt != 0 and not data) @@ -1122,7 +1122,13 @@ class HTTPResponse(BaseHTTPResponse): if amt is None: data = self._decode(data, decode_content, flush_decoder) - if cache_content: + # It's possible that there is buffered decoded data after a + # partial read. + if decode_content and len(self._decoded_buffer) > 0: + self._decoded_buffer.put(data) + data = self._decoded_buffer.get_all() + + if cache_content and not self._uncached_read_occurred: self._body = data else: # do not waste memory on buffer when not decoding @@ -1210,6 +1216,7 @@ class HTTPResponse(BaseHTTPResponse): # FIXME, this method's type doesn't say returning None is possible data = self._raw_read(amt, read1=True) + self._uncached_read_occurred = True if not decode_content or data is None: return data @@ -1246,6 +1253,9 @@ class HTTPResponse(BaseHTTPResponse): If True, will attempt to decode the body based on the 'content-encoding' header. """ + if amt == 0: + return + if self.chunked and self.supports_chunked_reads(): yield from self.read_chunked(amt, decode_content=decode_content) else: @@ -1405,7 +1415,9 @@ class HTTPResponse(BaseHTTPResponse): if self._fp.fp is None: # type: ignore[union-attr] return None - if amt and amt < 0: + if amt == 0: + return + elif amt and amt < 0: # Negative numbers and `None` should be treated the same, # but httplib handles only `None` correctly. amt = None @@ -1416,6 +1428,7 @@ class HTTPResponse(BaseHTTPResponse): chunk = b"" else: self._update_chunk_length() + self._uncached_read_occurred = True if self.chunk_left == 0: break chunk = self._handle_chunk(amt) diff --git a/libs/urllib3/util/retry.py b/libs/urllib3/util/retry.py index b21b4b64e..7649898e1 100644 --- a/libs/urllib3/util/retry.py +++ b/libs/urllib3/util/retry.py @@ -157,6 +157,14 @@ class Retry: By default, backoff is disabled (factor set to 0). + :param float backoff_max: + The maximum backoff time (in seconds) between retry attempts. + This value caps the computed backoff from `backoff_factor`. + + :param float backoff_jitter: + Random jitter amount (in seconds) added to the computed backoff. + Jitter is sampled uniformly from `0` to `backoff_jitter`. + :param bool raise_on_redirect: Whether, if the number of redirects is exhausted, to raise a MaxRetryError, or to return a response with a response code in the 3xx range. diff --git a/libs/urllib3/util/ssl_.py b/libs/urllib3/util/ssl_.py index 56fe9093a..e66549a76 100644 --- a/libs/urllib3/util/ssl_.py +++ b/libs/urllib3/util/ssl_.py @@ -27,52 +27,14 @@ HASHFUNC_MAP = { } -def _is_bpo_43522_fixed( - implementation_name: str, - version_info: _TYPE_VERSION_INFO, - pypy_version_info: _TYPE_VERSION_INFO | None, -) -> bool: - """Return True for CPython 3.9.3+ or 3.10+ and PyPy 7.3.8+ where - setting SSLContext.hostname_checks_common_name to False works. - - Outside of CPython and PyPy we don't know which implementations work - or not so we conservatively use our hostname matching as we know that works - on all implementations. - - https://github.com/urllib3/urllib3/issues/2192#issuecomment-821832963 - https://foss.heptapod.net/pypy/pypy/-/issues/3539 - """ - if implementation_name == "pypy": - # https://foss.heptapod.net/pypy/pypy/-/issues/3129 - return pypy_version_info >= (7, 3, 8) # type: ignore[operator] - elif implementation_name == "cpython": - major_minor = version_info[:2] - micro = version_info[2] - return (major_minor == (3, 9) and micro >= 3) or major_minor >= (3, 10) - else: # Defensive: - return False - - def _is_has_never_check_common_name_reliable( openssl_version: str, - openssl_version_number: int, - implementation_name: str, - version_info: _TYPE_VERSION_INFO, - pypy_version_info: _TYPE_VERSION_INFO | None, ) -> bool: # As of May 2023, all released versions of LibreSSL fail to reject certificates with # only common names, see https://github.com/urllib3/urllib3/pull/3024 is_openssl = openssl_version.startswith("OpenSSL ") - # Before fixing OpenSSL issue #14579, the SSL_new() API was not copying hostflags - # like X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, which tripped up CPython. - # https://github.com/openssl/openssl/issues/14579 - # This was released in OpenSSL 1.1.1l+ (>=0x101010cf) - is_openssl_issue_14579_fixed = openssl_version_number >= 0x101010CF - return is_openssl and ( - is_openssl_issue_14579_fixed - or _is_bpo_43522_fixed(implementation_name, version_info, pypy_version_info) - ) + return is_openssl if typing.TYPE_CHECKING: @@ -98,9 +60,9 @@ try: # Do we have ssl at all? OP_NO_COMPRESSION, OP_NO_TICKET, OPENSSL_VERSION, - OPENSSL_VERSION_NUMBER, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT, + VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT, OP_NO_SSLv2, OP_NO_SSLv3, @@ -110,18 +72,11 @@ try: # Do we have ssl at all? PROTOCOL_SSLv23 = PROTOCOL_TLS - # Needed for Python 3.9 which does not define this - VERIFY_X509_PARTIAL_CHAIN = getattr(ssl, "VERIFY_X509_PARTIAL_CHAIN", 0x80000) - - # Setting SSLContext.hostname_checks_common_name = False didn't work before CPython - # 3.9.3, and 3.10 (but OK on PyPy) or OpenSSL 1.1.1l+ + # Setting SSLContext.hostname_checks_common_name = False didn't work with + # LibreSSL, check details in the used function. if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( OPENSSL_VERSION, - OPENSSL_VERSION_NUMBER, - sys.implementation.name, - sys.version_info, - sys.pypy_version_info if sys.implementation.name == "pypy" else None, # type: ignore[attr-defined] - ): # Defensive: for Python < 3.9.3 + ): # Defensive: HAS_NEVER_CHECK_COMMON_NAME = False # Need to be careful here in case old TLS versions get @@ -142,7 +97,7 @@ except ImportError: OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] - VERIFY_X509_PARTIAL_CHAIN = 0x80000 + VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] @@ -289,17 +244,15 @@ def create_urllib3_context( # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' warnings.warn( "'ssl_version' option is deprecated and will be " - "removed in urllib3 v2.6.0. Instead use 'ssl_minimum_version'", - category=DeprecationWarning, + "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", + category=FutureWarning, stacklevel=2, ) - # PROTOCOL_TLS is deprecated in Python 3.10 so we always use PROTOCOL_TLS_CLIENT context = SSLContext(PROTOCOL_TLS_CLIENT) - if ssl_minimum_version is not None: context.minimum_version = ssl_minimum_version - else: # Python <3.10 defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here + else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here context.minimum_version = TLSVersion.TLSv1_2 if ssl_maximum_version is not None: @@ -359,10 +312,7 @@ def create_urllib3_context( context.check_hostname = False context.verify_mode = cert_reqs - try: - context.hostname_checks_common_name = False - except AttributeError: # Defensive: for CPython < 3.9.3; for PyPy < 7.3.8 - pass + context.hostname_checks_common_name = False if "SSLKEYLOGFILE" in os.environ: sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) diff --git a/libs/urllib3/util/ssl_match_hostname.py b/libs/urllib3/util/ssl_match_hostname.py index 25d910004..94994f25a 100644 --- a/libs/urllib3/util/ssl_match_hostname.py +++ b/libs/urllib3/util/ssl_match_hostname.py @@ -110,17 +110,9 @@ def match_hostname( "SSL socket or SSL context with either " "CERT_OPTIONAL or CERT_REQUIRED" ) - try: - # Divergence from upstream: ipaddress can't handle byte str - # - # The ipaddress module shipped with Python < 3.9 does not support - # scoped IPv6 addresses so we unconditionally strip the Zone IDs for - # now. Once we drop support for Python 3.9 we can remove this branch. - if "%" in hostname: - host_ip = ipaddress.ip_address(hostname[: hostname.rfind("%")]) - else: - host_ip = ipaddress.ip_address(hostname) + try: + host_ip = ipaddress.ip_address(hostname) except ValueError: # Not an IP address (common case) host_ip = None @@ -146,7 +138,9 @@ def match_hostname( if key == "commonName": if _dnsname_match(value, hostname): return - dnsnames.append(value) # Defensive: for Python < 3.9.3 + dnsnames.append( + value + ) # Defensive: for older PyPy and OpenSSL versions if len(dnsnames) > 1: raise CertificateError( diff --git a/libs/version.txt b/libs/version.txt index d727f2f73..aa871c62a 100644 --- a/libs/version.txt +++ b/libs/version.txt @@ -1,44 +1,44 @@ # Bazarr dependencies alembic==1.18.4 aniso8601==10.0.1 -apprise==1.9.8 -apscheduler==3.11.2 -attrs==25.4.0 +apprise==1.11.0 +apscheduler==4.0.0a6 +attrs==26.1.0 blinker==1.9.0 -charset-normalizer==3.4.6 +charset-normalizer==3.4.7 deep-translator==1.11.4 dogpile.cache==1.5.0 -dynaconf==3.2.12 +dynaconf==3.2.13 fese==0.3.0 -ffsubsync==0.4.31 -flask-cors==6.0.2 +ffsubsync==0.5.0 +flask-cors==6.0.5 flask-migrate==4.1.0 flask-restx<=1.3.0 # newer version requires jsonschema upgraded to >=4.18.0 Flask-SocketIO==5.6.1 flask_sqlalchemy==3.1.1 Flask==3.1.3 ga4mp==2.0.4 -guess_language-spirit==0.5.3 +guess_language-spirit==0.5.4 guessit==3.8.0 h11==0.16.0 inflect==7.5.0 jsonschema<=4.17.3 # newer version requires other compiled dependency knowit==0.5.11 -Mako==1.3.10 +Mako==1.3.12 plexapi>=4.16.1 pycountry==26.2.16 pyrsistent==0.20.0 -pysubs2==1.8.0 -python-engineio==4.13.1 -python-socketio==5.16.1 +pysubs2==1.8.1 +python-engineio==4.13.2 +python-socketio==5.16.3 pytz==2026.2 rarfile==4.2 referencing<=0.23.0 # newer version requires other compiled dependency -requests==2.32.5 +requests==2.34.2 semver==3.0.4 # signalrcore — moved to custom_libs (patched for WebSocket FD leak in pinned 0.9.5) simple-websocket==1.1.0 -sqlalchemy==2.0.48 +sqlalchemy==2.1.0b2 textdistance==4.6.3 unidecode==1.4.0 waitress==3.0.2 @@ -48,7 +48,7 @@ wsproto==1.3.2 ## indirect dependencies # Required-by: alembic -tomli==2.4.0 +tomli==2.4.1 # Required-by: apprise importlib_metadata<=7.2.1 # Newer version raise a KeyError with knowit @@ -56,11 +56,11 @@ markdown==3.10.2 requests_oauthlib==2.0.0 # Required-by: apscheduler -tzdata==2025.3 -tzlocal==5.3.1 +tzdata==2026.2 +tzlocal==5.4.3 # Required-by: beautifulsoup4 -soupsieve==2.3.2.post1 +soupsieve==2.8.4 # Required-by: deathbycaptcha filetype==1.2.0 @@ -69,22 +69,22 @@ filetype==1.2.0 auditok<=0.1.5 # do not upgrade unless ffsubsync requirements.txt change ffmpeg-python==0.2.0 future==1.0.0 -rich==14.3.3 +rich==15.0.0 srt==3.5.3 -tqdm==4.67.3 +tqdm==4.68.3 typing_extensions==4.15.0 # Required-by: flask -click==8.3.1 +click==8.4.1 itsdangerous==2.2.0 jinja2==3.1.6 -werkzeug==3.1.6 +werkzeug==3.1.8 # Required-by: guessit -python-dateutil==2.9.0 -importlib_resources==6.5.2 +python-dateutil==2.9.0.post0 +importlib_resources==7.1.0 rebulk==3.2.0 -zipp==3.23.0 +zipp==4.1.0 # Required-by: jinja2 markupsafe==3.0.3 @@ -101,19 +101,19 @@ bidict==0.23.1 websocket-client==1.9.0 # Required-by: signalrcore (in custom_libs) -msgpack==1.1.2 +msgpack==1.2.1 # Required-by: subliminal appdirs==1.4.4 babelfish==0.6.1 -beautifulsoup4==4.14.3 -chardet==5.2.0 +beautifulsoup4==4.15.0 +chardet==7.4.3 pysrt==1.1.2 -stevedore==5.7.0 +stevedore==5.8.0 # Required-by: subliminal_patch cloudscraper<=1.2.58 # newer version dropped captcha v1 support -decorator==5.2.1 +decorator==5.3.1 dnspython==2.8.0 enzyme==0.5.2 ftfy==6.3.1 @@ -126,7 +126,7 @@ pyparsing==3.3.2 git+https://github.com/morpheus65535/python-anticaptcha.git retry==0.9.2 six==1.17.0 -wcwidth==0.6.0 +wcwidth==0.8.1 webencodings==0.5.1 # Required-by: subzero @@ -135,19 +135,19 @@ emoji==2.15.0 fcache==0.6.0 json_tricks==3.17.3 tld==0.13.2 -platformdirs==4.9.4 +platformdirs==4.10.0 # Required-by: requests -certifi==2026.2.25 -idna==3.11 +certifi==2026.6.17 +idna==3.18 oauthlib==3.3.1 requests_toolbelt==1.0.0 -urllib3==2.6.3 +urllib3==2.7.0 # Required-by: rich colorama==0.4.6 -commonmark==0.9.1 -Pygments==2.19.2 +commonmark==0.9.2 +Pygments==2.20.0 # Required-by: websocket pysocks==1.7.1 diff --git a/libs/wcwidth-0.6.0.dist-info/RECORD b/libs/wcwidth-0.6.0.dist-info/RECORD deleted file mode 100644 index 113d6d9c1..000000000 --- a/libs/wcwidth-0.6.0.dist-info/RECORD +++ /dev/null @@ -1,22 +0,0 @@ -wcwidth-0.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -wcwidth-0.6.0.dist-info/METADATA,sha256=3_N4a4-6lVqDxdMDTOApuKrCDjOJ2ttiNNJUhQDVHnI,30525 -wcwidth-0.6.0.dist-info/RECORD,, -wcwidth-0.6.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -wcwidth-0.6.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 -wcwidth-0.6.0.dist-info/licenses/LICENSE,sha256=cLmKlaIUTrcK-AF_qMbZXOJH5AhnQ26LxknhN_4T0ho,1322 -wcwidth/__init__.py,sha256=UXJ2nUqbNHUpZvcW_pMfl7qQJr52Kww52u8LKFbJqF4,1793 -wcwidth/bisearch.py,sha256=oF4Wn7JPkkUQlFXMGMcBPDL8VCkAirjXUsGj95BTh7o,812 -wcwidth/control_codes.py,sha256=NGkPsXNA_MSNH_G1xQjvDgX4mSj-kp1TLfLP-GIUz08,1579 -wcwidth/escape_sequences.py,sha256=OmHUFj3f7UcywR0iQMmaD6x9nQXhQhY4bBDkjXdrjJQ,3097 -wcwidth/grapheme.py,sha256=O-0Fn1vZcQ77vrKBusRTCdWWnybmL2RH1QHXqfsb5O4,13382 -wcwidth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -wcwidth/sgr_state.py,sha256=4hUWKl2znPJ13uA1OipcmmQjOO7LBR-W-6y4hPBROAs,12429 -wcwidth/table_ambiguous.py,sha256=6zyqWUQqAswfpF3JesLjrHNeQhI30pfP66GfHm-jmD8,11900 -wcwidth/table_grapheme.py,sha256=yTwC8Yr1_H0GNIhTQMaO4jl-4cc4qHPIyIkoNxh3aGk,142899 -wcwidth/table_mc.py,sha256=KCR35VpvTQ_lv6XtuhukBhdnH18-TurFUrbABDwLzYA,13993 -wcwidth/table_vs16.py,sha256=TWc_Hgen926MO-EW_nFdyAsOrXVmP09N0msHdTXibX8,6890 -wcwidth/table_wide.py,sha256=9JNk7kRfG5awpY7wLIztZddHHA9JGc6TAuxHdBZ-Zrk,8974 -wcwidth/table_zero.py,sha256=096xl4g7GJUVFHfUO0gzJxNzTtBRMgin6-Jf--2fCzM,25642 -wcwidth/textwrap.py,sha256=n1_Bgs3f7t1SMnM1GE2w40jlN01fiDJZlLQvpMZK21A,28997 -wcwidth/unicode_versions.py,sha256=U5NM8ficYOVH_I1nL6Ap0reWhozB5rLIMZ7y8XU8uFU,541 -wcwidth/wcwidth.py,sha256=o5cH_RmQQwLBexguI62HPHrT6yYtxOk5P3--jXDlelI,41127 diff --git a/libs/wcwidth-0.6.0.dist-info/WHEEL b/libs/wcwidth-0.6.0.dist-info/WHEEL deleted file mode 100644 index b1b94fd58..000000000 --- a/libs/wcwidth-0.6.0.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.29.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/libs/wcwidth-0.6.0.dist-info/INSTALLER b/libs/wcwidth-0.8.1.dist-info/INSTALLER similarity index 100% rename from libs/wcwidth-0.6.0.dist-info/INSTALLER rename to libs/wcwidth-0.8.1.dist-info/INSTALLER diff --git a/libs/wcwidth-0.6.0.dist-info/METADATA b/libs/wcwidth-0.8.1.dist-info/METADATA similarity index 66% rename from libs/wcwidth-0.6.0.dist-info/METADATA rename to libs/wcwidth-0.8.1.dist-info/METADATA index f6f0235df..ca4ef7206 100644 --- a/libs/wcwidth-0.6.0.dist-info/METADATA +++ b/libs/wcwidth-0.8.1.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: wcwidth -Version: 0.6.0 +Version: 0.8.1 Summary: Measures the displayed width of unicode strings in a terminal Project-URL: Homepage, https://github.com/jquast/wcwidth Author-email: Jeff Quast @@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 Classifier: Topic :: Software Development :: Internationalization Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Localization @@ -28,6 +29,7 @@ Classifier: Typing :: Typed Requires-Python: >=3.8 Description-Content-Type: text/x-rst + |pypi_downloads| |codecov| |license| ============ @@ -65,33 +67,49 @@ Some examples of **incorrect results**: Solution -------- -The lowest-level functions in this library are the POSIX.1-2001 and POSIX.1-2008 `wcwidth(3)`_ and -`wcswidth(3)`_, which this library precisely copies by interface as `wcwidth()`_ and `wcswidth()`_. -These functions return -1 when C0 and C1 control codes are present. +The lowest-level functions in this library are derived from POSIX.1-2001 and POSIX.1-2008 +`wcwidth(3)`_ and `wcswidth(3)`_, which this library precisely copies by interface as `wcwidth()`_ +and `wcswidth()`_. These functions return -1 when C0 and C1 control codes are present. An easy-to-use `width()`_ function is provided as a wrapper of `wcswidth()`_ that is also capable of measuring most terminal control codes and sequences, like colors, bold, tabstops, and horizontal -cursor movement. +cursor movement. `width()`_ argument ``term_program`` may provide more accurate terminal measurement +Corrections_ as a wrapper of `wcstwidth()`_. -Text-justification is solved by the grapheme and sequence-aware functions `ljust()`_, -`rjust()`_, `center()`_, and `wrap()`_, serving as drop-in replacements to python standard functions -of the same names. +Text-justification is solved by the sequence-aware functions `ljust()`_, `rjust()`_, `center()`_, +and the grapheme-aware function `wrap()`_, serving as drop-in replacements to python standard +functions. + +The `clip()`_ function extracts substrings by their displayed column positions, and +`strip_sequences()`_ removes terminal escape sequences from text altogether. The iterator functions `iter_graphemes()`_ and `iter_sequences()`_ allow for careful navigation of -grapheme and terminal control sequence boundaries. `iter_graphemes_reverse()`_, and -`grapheme_boundary_before()`_ are useful for editing and searching of complex unicode. The -`clip()`_ function extracts substrings by display column positions, and `strip_sequences()`_ removes -terminal escape sequences from text altogether. +grapheme and terminal control sequence boundaries as required by editors or REPLs with cursor +control. `iter_graphemes_reverse()`_ and `grapheme_boundary_before()`_ are necessary for backward +cursor control over complex unicode. Discrepancies ------------- You may find that support *varies* for complex unicode sequences or codepoints. -A companion utility, `jquast/ucs-detect`_ was authored to gather and publish the results of Wide -character, language/grapheme clustering and complex script support, emojis and zero-width joiner, +This library may be considered to presume the terminal is enabled for DEC Private Mode 2027 +("Grapheme Clustering") by default, which may require to be enabled by a TUI application but +is often the default mode for those terminals that support it: Windows Terminal, WezTerm, ghostty, +contour, and foot. This library does support any specific "legacy width" measurement, but does +provide Corrections_ for those terminals without grapheme support. + +See also: + +- `Grapheme Clusters and Terminal Emulators`_ +- `terminal-unicode-core.tex`_ +- `State of Terminal Emulators in 2025`_ +- `Perfecting Terminal Character Width Using Correction Tables (2026)`_ + +The `jquast/ucs-detect`_ project publishes automatic results of compliance to our standard for Wide +character, Languages, grapheme clustering, complex or combining scripts, emojis, zero-width joiner, variations, and regional indicator (flags) as a `General Tabulated Summary`_ by terminal emulator -software and version. +software and version. The results of the ucs-detect project create our correction tables. ======== Overview @@ -138,40 +156,95 @@ Use function `wcswidth()`_ to determine the length of many, a *string of unicode See specification_ of character measurements. Note that ``-1`` is returned if control codes occurs anywhere in the string. +wcstwidth() +----------- + +Same behavior as `wcswidth()`_ with automatic terminal-specific Corrections_, reading +``TERM_PROGRAM`` or ``TERM`` when ``True`` (default), or caller can provide terminal query +XTVERSION_ or ENQ_ response: + +.. code-block:: python + + >>> # '♀️' emoji w/vs-16, uncorrected: + >>> wcwidth.wcswidth('\u2640\ufe0f') + 2 + >>> # corrected, + >>> wcwidth.wcstwidth('\u2640\ufe0f', term_program='vte') + 1 + width() ------- -Use function `width()`_ to measure a string with improved handling of ``control_codes``. +Use function `width()`_ to measure a string with improved handling of ``control_codes`` and +measurement Corrections_ through ``term_program``:. .. code-block:: python >>> # same support as wcswidth(), eg. regional indicator flag: >>> wcwidth.width('\U0001F1FF\U0001F1FC') 2 - >>> # but also supports SGR colored text, 'WARN', followed by SGR reset + >>> # set term_program=True to use wcstwidth() + >>> wcwidth.width('\U0001F1FF\U0001F1FC', term_program=True) + 1 + >>> # or set term_program for measurement of a specific terminal + >>> wcwidth.width('\U0001F1FF\U0001F1FC', term_program='contour') + 2 + >>> # but also supports sequences, like SGR colored text, "WARN", followed by reset >>> wcwidth.width('\x1b[38;2;255;150;100mWARN\x1b[0m') 4 - >>> # tabs, + >>> # tabs are measured as though the string begins at a tabstop, >>> wcwidth.width('\t', tabsize=4) 4 - >>> # or, tab and all other control characters can be ignored - >>> wcwidth.width('\t', control_codes='ignore') + >>> # or, all control characters can be ignored (including tab) + >>> wcwidth.width('\t\n\a\r', control_codes='ignore') 0 - >>> # "vertical" control characters are ignored - >>> wcwidth.width('\n') - 0 - >>> # as well as sequences with "indeterminate" effects like Home + Clear + >>> # sequences with "indeterminate" effects like Home + Clear are zero-width >>> wcwidth.width('\x1b[H\x1b[2J') 0 + >>> # horizontal cursor movements are parsed, + >>> wcwidth.width('hello\b\b\b\b\bworld') + 5 + >>> wcwidth.width('hello\x1b[5Dworld') + 5 + >>> # or ignored, + >>> wcwidth.width('hello\x1b[5Dworld', control_codes='ignore') + 10 + >>> # Measure width of text using kitty text sizing protocol (OSC 66), + >>> width('\x1b]66;w=2;XY\x07') + 2 + >>> # Scaled text sizing: each grapheme occupies 'scale' cells + >>> width('\x1b]66;s=2;ABC\x07') + 6 + +Use ``control_codes='ignore'`` when the input is known not to contain any control characters or +terminal sequences for slightly improved performance. Note that TAB (``'\t'``) is a control +character and is also ignored, you may want to use `str.expandtabs()`_, first. + +Use ``control_codes='strict'`` when input is known to contain some control sequences, such as +SGR color, bold, hyperlinks and cursor movement. Any sequence that cannot be accurately parsed +for horizontal measurement, such as clearing the screen, vertical, or absolute cursor movement will +raise ``ValueError``: + +.. code-block:: python + >>> # or, raise ValueError for "indeterminate" effects using control_codes='strict' >>> wcwidth.width('\n', control_codes='strict') Traceback (most recent call last): ... ValueError: Vertical movement character 0xa at position 0 -Use ``control_codes='ignore'`` when the input is known not to contain any control characters or -terminal sequences for slightly improved performance. Note that TAB (``'\t'``) is a control -character and is also ignored, you may want to use `str.expandtabs()`_, first. + + >>> wcwidth.width('\x1b[H\x1b[2J', control_codes='strict') + Traceback (most recent call last): + ... + ValueError: Indeterminate cursor sequence at position 0, '\x1b[H' + + + >>> # cursor left movement beyond string start raises in strict mode, + >>> wcwidth.width('a\x1b[5Da', control_codes='strict') + Traceback (most recent call last): + ... + ValueError: Cursor left movement at position 1 would move 5 cells left from column 1, exceeding string start iter_sequences() ---------------- @@ -290,9 +363,29 @@ Use `clip()`_ to extract a substring by column positions, preserving terminal se >>> clip('\x1b[1;31mHello world\x1b[0m', 6, 11) '\x1b[1;31mworld\x1b[0m' - >>> # Disable SGR propagation to preserve original sequences as-is - >>> clip('\x1b[31m中文\x1b[0m', 0, 3, propagate_sgr=False) - '\x1b[31m中 \x1b[0m' + >>> # Disable SGR propagation to preserve sequence order outside of clip boundary + >>> clip('\x1b[31m中文\x1b[32m', 0, 3, propagate_sgr=False) + '\x1b[31m中 \x1b[32m' + + >>> # Cursor-left overwrites previous text (painter's algorithm) + >>> clip('hello\x1b[2DXY', 0, 5) + 'helXY' + >>> # Carriage return resets to column 0, overwriting earlier cells + >>> clip('abc\rXY', 0, 5) + 'XYc' + + >>> # even OSC 8 hyperlink text may be clipped, 'Click This link' -> 'is link' ! + >>> clip('\x1b]8;;http://example.com\x07Click This link\x1b]8;;\x07', 8, 15) + '\x1b]8;;http://example.com\x07is link\x1b]8;;\x07' + + >>> # and OSC 66 kitty text sizing, supporting width and scale, 'Look' -> '...ook' + >>> clip('\x1b]66;w=4:s=4;Look\x07', 1, 16, fillchar='.') + '...\x1b]66;s=4:w=3;ook\x07' + +Use ``overtyping=False`` when the input is known not to contain any cursor movement characters +(``\b``, ``\r``, ``CSI C``, ``CSI D``, ``CSI G``) for improved performance. When +``overtyping=None`` (default), a slower "Painter's algorithm" may be used after testing for the +presence of these characters. ``overtyping`` has no effect when ``control_codes='ignore'``. strip_sequences() ----------------- @@ -307,7 +400,7 @@ Use `strip_sequences()`_ to remove all terminal escape sequences from text. .. _ambiguous_width: -ambiguous_width +Ambiguous Width --------------- Some Unicode characters have "East Asian Ambiguous" (A) width. These characters display as 1 cell by @@ -315,6 +408,9 @@ default, matching Western terminal contexts, but many CJK (Chinese, Japanese, Ko may have a preference for 2 cells. This is often found as boolean option, "Ambiguous width as wide" in Terminal Emulator software preferences. +The ``ambiguous_width`` parameter is available on all width-measuring functions: `wcwidth()`_, +`wcswidth()`_, `width()`_, `ljust()`_, `rjust()`_, `center()`_, `wrap()`_, and `clip()`_. + By default, wcwidth treats ambiguous characters as narrow (width 1). For CJK environments where your terminal is configured to display ambiguous characters as double-width, pass ``ambiguous_width=2``: @@ -326,9 +422,6 @@ terminal is configured to display ambiguous characters as double-width, pass ``a >>> wcwidth.width('\u2460', ambiguous_width=2) 2 -The ``ambiguous_width`` parameter is available on all width-measuring functions: `wcwidth()`_, -`wcswidth()`_, `width()`_, `ljust()`_, `rjust()`_, `center()`_, `wrap()`_, and `clip()`_. - **Terminal Detection** The most reliable method to detect whether a terminal profile is set for "Ambiguous width as wide" @@ -336,7 +429,7 @@ mode is to display an ambiguous character surrounded by a pair of Cursor Positio queries with a terminal in cooked or raw mode, and to parse the responses for their ``(y, x)`` locations and measure the difference ``x``. -This code should also be careful check whether it is attached to a terminal and be careful of +This code should also be careful to check whether it is attached to a terminal and be careful of possible timeout, slow network, or non-response when working with "dumb terminals" like a CI build. `jquast/blessed`_ library provides such a helping `Terminal.detect_ambiguous_width()`_ method: @@ -352,6 +445,91 @@ possible timeout, slow network, or non-response when working with "dumb terminal >>> awidth('\u2460') 1 +Corrections +----------- + +Corrections may be automatically applied depending on the detected or given terminal software name +beginning with wcwidth release 0.8.0. This allows to correct widths for terminal software that +differs from the python wcwidth specification_. These corrections are sourced from the +`jquast/ucs-detect`_ project. + +The ``term_program`` parameter is available on all width-measuring functions: `wcstwidth()`_, +`width()`_, `ljust()`_, `rjust()`_, `center()`_, `wrap()`_, and `clip()`_. + +`wcstwidth()`_ defaults to ``term_program=True``, auto-detecting the terminal from the +``TERM_PROGRAM`` or ``TERM`` environment variable. All other functions default to +``term_program=False``, disabling corrections. Use ``term_program=True`` for automatic +detection by environment values of ``TERM`` and ``TERM_PROGRAM``. + +.. code-block:: python + + # VTE terminals (Gnome Terminal Et al.) still render trigrams as narrow (1 cell), but their + # definition was changed to wide in Unicode 16 (September 2024). + >>> wcwidth.wcswidth('\u2630') + 2 + >>> wcwidth.wcstwidth('\u2630', term_program='vte') + 1 + + # account for Alacritty non-support of emoji ZWJ: + # man + ZWJ + woman + ZWJ + girl + ZWJ + boy + >>> family = '\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466' + >>> wcwidth.wcswidth(family) + 2 + >>> wcwidth.wcstwidth(family, term_program='alacritty') + 8 + +Only detectable_ terminals are included: those that identify themselves by XTVERSION_, ENQ_, any +``TERM_PROGRAM`` or a unique ``TERM`` environment value. For the most accurate correction tables, +query the terminal's software version via XTVERSION_ (``CSI > q``) using a higher-level interactive +terminal library like `jquast/blessed`_: + +.. code-block:: python + + >>> import blessed, wcwidth + >>> term = blessed.Terminal() + >>> sw_ver = term.get_software_version() + >>> print(sw_ver) + SoftwareVersion(name='VTE', version='7600') + >>> wcwidth.width('\u2630', term_program=sw_ver.name) + 1 + +This is important because ``TERM_PROGRAM`` is not forwarded for remote hosts, like SSH, and many +terminals may only be identified using XTVERSION_ or ENQ_. Use `list_term_programs()`_ to see all +recognized names: + +.. BEGIN_LIST_TERM_PROGRAMS +.. code-block:: python + + >>> wcwidth.list_term_programs() + ('alacritty', 'apple_terminal', 'bobcat', 'contour', 'extraterm', 'foot', + 'ghostty', 'hyper', 'iterm.app', 'iterm2', 'kitty', 'konsole', 'mintty', + 'mlterm', 'pterm', 'putty', 'rio', 'rxvt', 'rxvt-unicode-256color', 'st', + 'st-256color', 'tabby', 'terminology', 'urxvt', 'vscode', 'vte', 'warp', + 'warpterminal', 'wezterm', 'xterm', 'xterm-ghostty', 'xterm-kitty', + 'xterm.js') + +.. END_LIST_TERM_PROGRAMS + +``term_program=False`` (the default for `width()`_, `ljust()`_, `rjust()`_, `center()`_, +`wrap()`_, and `clip()`_) disables terminal corrections. `wcstwidth()`_ defaults to +``term_program=True`` for auto-detection. + +For automatic tests and other purposes that require cross-environment consistency, set static values +or unset ``TERM`` and ``TERM_PROGRAM`` environment values, such as in ``conftest.py`` with pytest: + +.. code-block:: python + + @pytest.fixture(autouse=True) + def _clear_term_program(): + """unset TERM/TERM_PROGRAM before each test.""" + saved_term = os.environ.pop('TERM', None) + saved_tprog = os.environ.pop('TERM_PROGRAM', None) + yield + if saved_term is not None: + os.environ['TERM'] = saved_term + if saved_tprog is not None: + os.environ['TERM_PROGRAM'] = saved_tprog + ========== Developing ========== @@ -429,9 +607,18 @@ This library is used in: - `jquast/blessed`_: a thin, practical wrapper around terminal capabilities in Python. +- `jquast/telix`_: A Modern telnet client especially designed for BBSs and MUDs. + - `prompt-toolkit/python-prompt-toolkit`_: a Library for building powerful interactive command lines in Python. +- `urwid/urwid`_: Console user interface library for Python + +- `prettytable/prettytable`_: Display tabular data in a visually appealing ASCII table format + +- `leviathan0992/Pylsy`_: Pylsy is a simple python library draw tables in the Terminal. Just two + lines of code. + - `dbcli/pgcli`_: Postgres CLI with autocompletion and syntax highlighting. - `thomasballinger/curtsies`_: a Curses-like terminal wrapper with a display @@ -448,8 +635,8 @@ This library is used in: - `nbedos/termtosvg`_: Terminal recorder that renders sessions as SVG animations. -- `peterbrittain/asciimatics`_: Package to help people create full-screen text - UIs. +- `peterbrittain/asciimatics`_: A cross platform package to do curses-like operations, plus higher + level APIs and widgets to create text UIs and ASCII art animations - `python-cmd2/cmd2`_: A tool for building interactive command line apps @@ -466,9 +653,18 @@ This library is used in: Other Languages =============== -There are similar implementations of the `wcwidth()`_ and `wcswidth()`_ functions in other -languages. +The following libraries provide grapheme and emoji support and closely align with our +specification_: +- `jacobsandlund/uucode`_ Zig +- `contour-terminal/libunicode`_ C++20 + +There are similar implementations of at least the `wcwidth()`_ and `wcswidth()`_ functions in other +languages: + +- `ridiculousfish/widecharwidth`_: Python +- `termux/wcwidth`_: C +- `powerman/wcwidth-icons`_: C - `timoxley/wcwidth`_: JavaScript - `janlelis/unicode-display_width`_: Ruby - `alecrabbit/php-wcwidth`_: PHP @@ -478,6 +674,9 @@ languages. - `grepsuzette/wcwidth`_: Haxe - `aperezdc/lua-wcwidth`_: Lua - `joachimschmidt557/zig-wcwidth`_: Zig +- `mycoboco/wcwidth.js`_: JavaScript +- `ainame/swift-displaywidth`_: Swift +- `pmonks/clj-wcwidth`_: Clojure - `fumiyas/wcwidth-cjk`_: `LD_PRELOAD` override - `joshuarubin/wcwidth9`_: Unicode version 9 in C - `spectreconsole/wcwidth`_: C# @@ -486,6 +685,33 @@ languages. History ======= +0.8.1 *2026-06-08* + * **Improved** `wcstwidth()`_ with new ``zeroer``, ``narrow_wider``, and ``narrow_zeroer`` + Corrections_. `PR #226`_ + +0.8.0 *2026-06-05* + * **New** support for Variation Selector 15 Emojis as narrow, `Issue #211`_. + * **New** argument, ``term_program`` for `wcstwidth()`_, `width()`_, `clip()`_, `wrap()`_, + `ljust()`_, `rjust()`_, and `center()`_. ``False`` disables Corrections_; ``True`` + auto-detects by ``TERM_PROGRAM`` or ``TERM``; string values accept canonical names matching + `list_term_programs()`_. `wcstwidth()`_ defaults to ``True``; all other functions + default to ``False``. + * **Improved** performance on Python 3.15 using standard library iter_graphemes() `PR #206`_. + * **Improved** memory usage and import time for Python 3.15 using lazy imports `PR #221`_. + * **Bugfix** Invisible_Stacker viramas now form conjuncts (Burmese, Khmer, etc.) and + change some Virama width calculations to match `jacobsandlund/uucode`_ (ghostty) `PR #223`_. + * **Updated** graphemes width maximum now 2, matching Ghostty, foot, and Windows Terminal `PR + #224`_. + +0.7.0 *2026-05-02* + * **New** support for `kitty text sizing protocol`_ (OSC 66) in `width()`_ and `clip()`_. + * **New** `clip()`_ parameter ``control_codes='parse'``, ``'ignore'``, and ``'strict'``. `clip()`_ + is now able to clip OSC 8 hyperlinks and OSC 66 text sizing sequences. + * **Improved** `clip()`_ and `width()`_ to support horizontal cursor sequences (``cub``, ``cuf``, + ``hpa``). Cursor-left (``cub``) or backspace (``\b``) now overwrites text. ``column_address`` + (``hpa``) and carriage return (``\r``) are now parsed, and more values conditionally raise + ``ValueError`` when ``control_codes='strict'``. + 0.6.0 *2026-02-06* * **New** Parameters ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``, ``drop_whitespace``, ``max_lines``, and ``placeholder`` for `wrap()`_, completing stdlib @@ -707,10 +933,17 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c:: .. _`PR #200`: https://github.com/jquast/wcwidth/pull/200 .. _`PR #202`: https://github.com/jquast/wcwidth/pull/202 .. _`PR #204`: https://github.com/jquast/wcwidth/pull/204 +.. _`PR #206`: https://github.com/jquast/wcwidth/pull/206 +.. _`PR #221`: https://github.com/jquast/wcwidth/pull/221 +.. _`PR #223`: https://github.com/jquast/wcwidth/pull/223 +.. _`PR #224`: https://github.com/jquast/wcwidth/pull/224 +.. _`PR #226`: https://github.com/jquast/wcwidth/pull/226 .. _`Issue #101`: https://github.com/jquast/wcwidth/issues/101 .. _`Issue #155`: https://github.com/jquast/wcwidth/issues/155 .. _`Issue #190`: https://github.com/jquast/wcwidth/issues/190 +.. _`Issue #211`: https://github.com/jquast/wcwidth/issues/211 .. _`jquast/blessed`: https://github.com/jquast/blessed +.. _`jquast/telix`: https://github.com/jquast/telix .. _`selectel/pyte`: https://github.com/selectel/pyte .. _`thomasballinger/curtsies`: https://github.com/thomasballinger/curtsies .. _`dbcli/pgcli`: https://github.com/dbcli/pgcli @@ -735,10 +968,21 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c:: .. _`fumiyas/wcwidth-cjk`: https://github.com/fumiyas/wcwidth-cjk .. _`joshuarubin/wcwidth9`: https://github.com/joshuarubin/wcwidth9 .. _`spectreconsole/wcwidth`: https://github.com/spectreconsole/wcwidth +.. _`contour-terminal/libunicode`: https://github.com/contour-terminal/libunicode +.. _`jacobsandlund/uucode`: https://github.com/jacobsandlund/uucode +.. _`ridiculousfish/widecharwidth`: https://github.com/ridiculousfish/widecharwidth +.. _`termux/wcwidth`: https://github.com/termux/wcwidth +.. _`powerman/wcwidth-icons`: https://github.com/powerman/wcwidth-icons +.. _`mycoboco/wcwidth.js`: https://github.com/mycoboco/wcwidth.js +.. _`ainame/swift-displaywidth`: https://github.com/ainame/swift-displaywidth +.. _`pmonks/clj-wcwidth`: https://github.com/pmonks/clj-wcwidth .. _`python-cmd2/cmd2`: https://github.com/python-cmd2/cmd2 .. _`stratis-storage/stratis-cli`: https://github.com/stratis-storage/stratis-cli .. _`ihabunek/toot`: https://github.com/ihabunek/toot .. _`saulpw/visidata`: https://github.com/saulpw/visidata +.. _`urwid/urwid`: https://github.com/urwid/urwid +.. _`prettytable/prettytable`: https://github.com/prettytable/prettytable +.. _`leviathan0992/Pylsy`: https://github.com/leviathan0992/Pylsy .. _`pip-tools`: https://pip-tools.readthedocs.io/ .. _`sphinx`: https://www.sphinx-doc.org/ .. _`textwrap.wrap()`: https://docs.python.org/3/library/textwrap.html#textwrap.wrap @@ -749,6 +993,7 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c:: .. _`General Tabulated Summary`: https://ucs-detect.readthedocs.io/results.html#tabulated-results .. _`wcwidth()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.wcwidth .. _`wcswidth()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.wcswidth +.. _`wcstwidth()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.wcstwidth .. _`width()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.width .. _`iter_graphemes()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.iter_graphemes .. _`iter_graphemes_reverse()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.iter_graphemes_reverse @@ -760,11 +1005,22 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c:: .. _`clip()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.clip .. _`strip_sequences()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.strip_sequences .. _`propagate_sgr()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.propagate_sgr +.. _`TextSizing`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.TextSizing +.. _`TextSizingParams`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.TextSizingParams .. _`iter_sequences()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.iter_sequences .. _`list_versions()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.list_versions +.. _`list_term_programs()`: https://wcwidth.readthedocs.io/en/latest/api.html#wcwidth.list_term_programs .. _`Unicode Standard Annex #29`: https://www.unicode.org/reports/tr29/ .. _`Terminal.detect_ambiguous_width()`: https://blessed.readthedocs.io/en/latest/api/terminal.html#blessed.terminal.Terminal.detect_ambiguous_width .. _`parity padding`: https://jazcap53.github.io/pythons-eccentric-strcenter.html +.. _`kitty text sizing protocol`: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ +.. _`Grapheme Clusters and Terminal Emulators`: https://mitchellh.com/writing/grapheme-clusters-in-terminals +.. _`terminal-unicode-core.tex`: https://github.com/contour-terminal/terminal-unicode-core/blob/master/spec/terminal-unicode-core.tex +.. _`State of Terminal Emulators in 2025`: https://www.jeffquast.com/post/state-of-terminal-emulation-2025/ +.. _`Perfecting Terminal Character Width Using Correction Tables (2026)`: https://www.jeffquast.com/post/perfecting-terminal-character-width-using-correction-tables/ +.. _XTVERSION: https://vtdn.dev/docs/dcs/xtversion/ +.. _ENQ: https://documentation.help/PuTTY/config-answerback.html +.. _detectable: https://ucs-detect.readthedocs.io/results.html#terminal-identification .. |pypi_downloads| image:: https://img.shields.io/pypi/dm/wcwidth.svg?logo=pypi :alt: Downloads :target: https://pypi.org/project/wcwidth/ diff --git a/libs/wcwidth-0.8.1.dist-info/RECORD b/libs/wcwidth-0.8.1.dist-info/RECORD new file mode 100644 index 000000000..b0ea2b92a --- /dev/null +++ b/libs/wcwidth-0.8.1.dist-info/RECORD @@ -0,0 +1,54 @@ +wcwidth-0.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +wcwidth-0.8.1.dist-info/METADATA,sha256=Lqul4F0Uq6F46YkUSBncM1NKrK-lJxslZKTfpD4J3rY,43099 +wcwidth-0.8.1.dist-info/RECORD,, +wcwidth-0.8.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +wcwidth-0.8.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +wcwidth-0.8.1.dist-info/licenses/LICENSE,sha256=cLmKlaIUTrcK-AF_qMbZXOJH5AhnQ26LxknhN_4T0ho,1322 +wcwidth/__init__.py,sha256=GBRhIDbX_-uqbXPN8zGmyXDu3OnKGxPniqsU-CXfgGU,3730 +wcwidth/_clip.py,sha256=BCC9WWfSQkvQ-g3aRSD4z7LR_sXmjnU72NNFgxAe4gs,31772 +wcwidth/_constants.py,sha256=0Hjd2rSAa8SNjBE5clDvY7tfFWlJUOTUTXL_wjdf_30,6038 +wcwidth/_wcswidth.py,sha256=c_eP9WXfDEilHrHiIwSLwLjysEcaZBDbTR-mpW0DCYE,17432 +wcwidth/_wcwidth.py,sha256=QMWDw3jodxApmATWmesFVbmN-LP6ejhje2zsBZU6N4Q,6568 +wcwidth/_width.py,sha256=JMbgL_w2WAnIx7fu8vsqOvjGtARC2DQFS1ZfAhbmZwI,22770 +wcwidth/align.py,sha256=AZDGINoCX6lr_DBFhQb5dNvj2tJVciMqHz2v_GueWRc,6786 +wcwidth/bisearch.py,sha256=Y_FdvYTzFXD2pa5cppUegD5SXfy-B8HC4A9rrJOdb40,813 +wcwidth/control_codes.py,sha256=NGkPsXNA_MSNH_G1xQjvDgX4mSj-kp1TLfLP-GIUz08,1579 +wcwidth/escape_sequences.py,sha256=ltkljQ33u-9vZ-O1gxhEOFOD0UhuwT5rOl_qEg72p0o,7639 +wcwidth/grapheme.py,sha256=f2NUZoJf67FVvPnUWELaZAZ3_Gxzm2qznPl9RbAyrAA,15291 +wcwidth/hyperlink.py,sha256=mpQGSshG7K5DM2VEx91dI6kZ2PtdOJ72YhvOo6Ek1H8,4478 +wcwidth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +wcwidth/sgr_state.py,sha256=cM_6NY2jyKoE2uGJZ0ij2QNU_iHRNbykL-7SkdK5tnw,12429 +wcwidth/table_ambiguous.py,sha256=YQ_7nXOAc__g7ksxYaew8mhl3-c1btGIuBvRcVb87zs,11867 +wcwidth/table_grapheme.py,sha256=SAmduTBoy8Qq7DwETy0WaUEzw20ovKyAv9ENT4JeiZs,145001 +wcwidth/table_grapheme_overrides/__init__.py,sha256=rf0ktoiCb2JIwYlU2FAMXVQhZvyhHWZm4YI3LLMCGEk,1270 +wcwidth/table_grapheme_overrides/_known_27e0693f.py,sha256=iuis-Ceqp02FtfQJWtJZjTMV6lexF5Cmba3aqO47nCs,70285 +wcwidth/table_grapheme_overrides/_known_3d4826b8.py,sha256=wBePKjryp7OsRg2TNLGmPtZqKOqQjgmI0CZWDzYknEs,136575 +wcwidth/table_grapheme_overrides/_known_45d92e98.py,sha256=uPZsJx5W4hR6cupZjQfFTGvIgTcBwq8xQie9mM88gvU,51607 +wcwidth/table_grapheme_overrides/_known_4cdf59ce.py,sha256=bW2Y423MiB6FReHiKEiEXxAMTxIQidGdgo48KMGMiLw,765 +wcwidth/table_grapheme_overrides/_known_50bf0759.py,sha256=3_GowjW__MXRH9rHMYzX4iY1r5N2wl6mRWS63nDKZL8,68625 +wcwidth/table_grapheme_overrides/_known_529fbb4a.py,sha256=sgKp59oNyL5HHGbCSuAVTvv8X0MA7I2ixlhZkPbNJ3s,32688 +wcwidth/table_grapheme_overrides/_known_5bfac390.py,sha256=2jq-xZomcr3h8O3vasC6ga0pspBGPNVJZiNqtWS0iI0,68627 +wcwidth/table_grapheme_overrides/_known_813fee16.py,sha256=Xo4wfErdfjI29__louWWDuKRT2PFdUHG13KSZ7N_3p8,68676 +wcwidth/table_grapheme_overrides/_known_8589765c.py,sha256=LoC4aCq7et__ZPS0kdyrwvRfOc37y42SguTpCBwiV4k,29939 +wcwidth/table_grapheme_overrides/_known_8f94b404.py,sha256=-ofVLh5vaX_CgcrjTA8x92brEWT-DXWlX_JueH6H6Rw,68912 +wcwidth/table_grapheme_overrides/_known_970dbe10.py,sha256=lMhyuNZa_sqT_FQb6l4cmyXtqXBplO5pvRYutkjxCdY,73644 +wcwidth/table_grapheme_overrides/_known_c0a2cdbf.py,sha256=biqjh4bNchd69nj4stlgWlVEQGOFx850RoXOfCEirCM,74224 +wcwidth/table_grapheme_overrides/_known_c2157f7e.py,sha256=VBeqLmgdZLTsNB7KcEQfUyI61d8UsqWbr4UjkB4SMz8,27654 +wcwidth/table_grapheme_overrides/_known_c3db41c0.py,sha256=vOO11MnpUBToRr3iY1htpQIyfZkR0S3JScT3osmVVAU,79410 +wcwidth/table_grapheme_overrides/_known_da9ceb0a.py,sha256=9b3wl440EA9A1fz-RQZELUU7zDTxJJAY0JOKXjLCbNI,35916 +wcwidth/table_grapheme_overrides/_known_e08bd75e.py,sha256=bE7lS_opr6Tgjqgc13VBYfXgNR3-r70A2XbvaodNGW8,70275 +wcwidth/table_grapheme_overrides/_known_e22030f3.py,sha256=0iVZSWhfiKBIyeE6c-YqdPFbiMlSmIoiPXQ40Kly0V8,140399 +wcwidth/table_grapheme_overrides/_known_fcc05a0f.py,sha256=_o-awCawgZtYGBKs8kEXWxoyT_SYmfXYsse9Dq3t-Co,144464 +wcwidth/table_grapheme_overrides/_known_fd9d4c44.py,sha256=wuopQh2JYG-mYB_vi5iD26TONCWu8xVAqd4y0bwVeHg,75702 +wcwidth/table_grapheme_overrides/_registry.py,sha256=ZezIfARE9ZuEo9DE_PZtsXtI3PImtKMfhWMFkJm1C4U,785 +wcwidth/table_mc.py,sha256=c6vXPjdBAcppbCITgOs6Z5-zaCR_6w_pKWhc4hyisNY,13960 +wcwidth/table_overrides.py,sha256=Ly_7z0HCu10T1_oWbeU8eoGXklBxNfcCrVbx9axwneA,41321 +wcwidth/table_term_programs.py,sha256=IHKTJeN5ilOvUvXmu6-Kgz9_rdQ_lWX9fWBtkYGBM-U,1044 +wcwidth/table_vs15.py,sha256=O2IqhGijNTfoNwzGWZ8q6vTn2rexs0_X4gKp2CRcnUY,5170 +wcwidth/table_vs16.py,sha256=pMRZHhQFRqLGvQWT83eccbt3QvXW0zBwJstmulB4AJg,6857 +wcwidth/table_wide.py,sha256=wWoKvuyCAOc5TUFdA4LYbWWnxn9tybM0WypbXpYu380,8958 +wcwidth/table_zero.py,sha256=pJjjW4wcDBJXkUX867AZOI3ZXe6jLc2wTU1krmMVUJk,25609 +wcwidth/text_sizing.py,sha256=z_EeB8rnJMhqJ9CGM7j1T3ok0NbBc6-KyRR5p_LnjHY,7980 +wcwidth/textwrap.py,sha256=R2G7XeB2ZsZaVeQ0oNsZulyaxNd3Gx16ZE_PPQy8Iuc,29541 +wcwidth/unicode_versions.py,sha256=72JoIArE1C5rjDOg9T9mLcb3tcuX0HVngEI4HeLX4jY,508 +wcwidth/wcwidth.py,sha256=WX66lOPtigH_axqcvU0JMTFd950jxV4MGrZEBHoGkEs,2858 diff --git a/libs/werkzeug-3.1.6.dist-info/REQUESTED b/libs/wcwidth-0.8.1.dist-info/REQUESTED similarity index 100% rename from libs/werkzeug-3.1.6.dist-info/REQUESTED rename to libs/wcwidth-0.8.1.dist-info/REQUESTED diff --git a/libs/wcwidth-0.8.1.dist-info/WHEEL b/libs/wcwidth-0.8.1.dist-info/WHEEL new file mode 100644 index 000000000..bd8422674 --- /dev/null +++ b/libs/wcwidth-0.8.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/libs/wcwidth-0.6.0.dist-info/licenses/LICENSE b/libs/wcwidth-0.8.1.dist-info/licenses/LICENSE similarity index 100% rename from libs/wcwidth-0.6.0.dist-info/licenses/LICENSE rename to libs/wcwidth-0.8.1.dist-info/licenses/LICENSE diff --git a/libs/wcwidth/__init__.py b/libs/wcwidth/__init__.py index 400c8a619..2e619eb06 100644 --- a/libs/wcwidth/__init__.py +++ b/libs/wcwidth/__init__.py @@ -1,43 +1,81 @@ """ -Wcwidth module. +Python 'wcwidth' module. https://github.com/jquast/wcwidth """ -# re-export all functions & definitions, even private ones, from top-level -# module path, to allow for 'from wcwidth import _private_func'. Of course, -# user beware that any _private functions or variables not exported by __all__ -# may disappear or change signature at any future version. + +# re-export common and outermost functions & definitions, even a few private +# ones, some for convenience, others for legacy, only the items in __all__ are +# documented as public API + +__lazy_modules__ = [ + "wcwidth._clip", + "wcwidth._wcswidth", + "wcwidth._wcwidth", + "wcwidth._width", + "wcwidth.align", + "wcwidth.bisearch", + "wcwidth.escape_sequences", + "wcwidth.grapheme", + "wcwidth.hyperlink", + "wcwidth.sgr_state", + "wcwidth.table_ambiguous", + "wcwidth.table_vs16", + "wcwidth.table_wide", + "wcwidth.table_zero", + "wcwidth.text_sizing", + "wcwidth.textwrap", + "wcwidth.unicode_versions", +] # local -from .wcwidth import ZERO_WIDTH # noqa -from .wcwidth import (WIDE_EASTASIAN, - AMBIGUOUS_EASTASIAN, - VS16_NARROW_TO_WIDE, - clip, - ljust, - rjust, - width, - center, - wcwidth, - wcswidth, - list_versions, - iter_sequences, - strip_sequences, - _wcmatch_version, - _wcversion_value) +from ._clip import clip +from .align import ljust, rjust, center +from ._width import width from .bisearch import bisearch as _bisearch -from .grapheme import grapheme_boundary_before # noqa -from .grapheme import iter_graphemes, iter_graphemes_reverse +from .grapheme import iter_graphemes, iter_graphemes_reverse, grapheme_boundary_before from .textwrap import SequenceTextWrapper, wrap +from ._wcswidth import wcswidth, wcstwidth +from .hyperlink import Hyperlink, HyperlinkParams from .sgr_state import propagate_sgr +from ._constants import list_term_programs +from .table_vs16 import VS16_NARROW_TO_WIDE +from .table_wide import WIDE_EASTASIAN +from .table_zero import ZERO_WIDTH +from .text_sizing import TextSizing, TextSizingParams +from .table_ambiguous import AMBIGUOUS_EASTASIAN +from .escape_sequences import iter_sequences, strip_sequences +from .unicode_versions import list_versions + +# NOTE: this sort order is important for legacy import API compatibility before release 0.7.0 +# +# On Python < 3.15 the legacy submodule is eagerly pre-imported for backward compatibility +# (populates sys.modules['wcwidth.wcwidth']). On 3.15+ __lazy_modules__ handles all submodules; the +# legacy shim loads on-demand via file discovery when ``from wcwidth.wcwidth import ...`` is used. +if __import__('sys').version_info < (3, 15): + # Pre-import the legacy submodule so that sys.modules['wcwidth.wcwidth'] is populated during + # package initialization. Without this, a later downstream dependent ``import wcwidth.wcwidth`` + # triggers on-disk file discovery which rebinds wcwidth.wcwidth from the function to the module + # object. + # + # this is just a lot of carefulness for the original release that contained all functions in a + # single 'wcwidth.py' file. Even though we always exposed our API at the top-level the preferred + # 'from wcwidth import wcswidth', it was always possible to import them more directly, + # 'from wcwidth.wcwidth import wcswidth' + # -- and we make a lot of effort to allow any such import statements to continue to function. + from . import wcwidth as _wcwidth_module # isort:skip +from ._wcwidth import wcwidth, _wcmatch_version, _wcversion_value # isort:skip # pylint: disable=wrong-import-position + # The __all__ attribute defines the items exported from statement, # 'from wcwidth import *', but also to say, "This is the public API". -__all__ = ('wcwidth', 'wcswidth', 'width', 'iter_sequences', 'iter_graphemes', +__all__ = ('wcwidth', 'wcswidth', 'wcstwidth', 'width', 'iter_sequences', 'iter_graphemes', 'iter_graphemes_reverse', 'grapheme_boundary_before', 'ljust', 'rjust', 'center', 'wrap', 'clip', 'strip_sequences', - 'list_versions', 'propagate_sgr') + 'list_versions', 'list_term_programs', 'propagate_sgr', + 'Hyperlink', 'HyperlinkParams', 'TextSizing', 'TextSizingParams') # Using 'hatchling', it does not seem to provide the pyproject.toml nicety, "dynamic = ['version']" # like flit_core, maybe there is some better way but for now we have to duplicate it in both places -__version__ = '0.6.0' +# Prefer the installed distribution version when available (helps test environments) +__version__ = '0.8.1' # don't forget to also update pyproject.toml:version diff --git a/libs/wcwidth/_clip.py b/libs/wcwidth/_clip.py new file mode 100644 index 000000000..cbdb02c60 --- /dev/null +++ b/libs/wcwidth/_clip.py @@ -0,0 +1,835 @@ +"""This is a python implementation of clip().""" +from __future__ import annotations + +# std imports +import enum +from itertools import islice + +from typing import Literal, Callable, Optional, NamedTuple + +# local +from ._width import width +from .grapheme import iter_graphemes +from .hyperlink import Hyperlink, HyperlinkParams +from .sgr_state import (_SGR_STATE_DEFAULT, + _SGRState, + _sgr_state_update, + _sgr_state_is_active, + _sgr_state_to_sequence) +from .text_sizing import TextSizing, TextSizingParams +from .escape_sequences import (_SEQUENCE_CLASSIFY, + _HORIZONTAL_CURSOR_MOVEMENT, + INDETERMINATE_EFFECT_SEQUENCE) + + +class _HyperlinkAction(enum.Enum): + """Outcome of processing an OSC 8 hyperlink unit.""" + + NO_CLOSE = enum.auto() # open sequence without matching close + EMPTY = enum.auto() # hyperlink with no visible inner text + OUTSIDE = enum.auto() # hyperlink entirely outside the clip window + VISIBLE = enum.auto() # hyperlink overlaps the clip window + + +class _HyperlinkResult(NamedTuple): + """ + Result of processing an OSC 8 hyperlink. + + Only the fields relevant to each action are populated. + """ + + action: _HyperlinkAction + close_end: int = 0 + inner_width: int = 0 + open_seq: str = '' + clipped_inner: str = '' + close_seq: str = '' + clipped_width: int = 0 + hl_col_end: int = 0 + + +def _apply_sgr_wrap(result: str, captured_style: Optional[_SGRState]) -> str: + """ + Apply SGR prefix/suffix around *result*. + + If an SGR state was captured at the first visible character, prefix the result with the + corresponding SGR sequence and suffix with a reset if any styles are active. + """ + if captured_style is not None: + if prefix := _sgr_state_to_sequence(captured_style): + result = prefix + result + if _sgr_state_is_active(captured_style): + result += '\x1b[0m' + return result + + +def _process_hyperlink( + text: str, + start: int, + end: int, + fillchar: str, + tabsize: int, + ambiguous_width: int, + term_program: bool | str, + control_codes: Literal['parse', 'strict', 'ignore'], + *, + params: HyperlinkParams, + match_end: int, + col: int, +) -> _HyperlinkResult: + """ + Process an OSC 8 hyperlink unit. + + Finds the matching close sequence, measures the inner text width, and determines whether the + hyperlink is empty, outside the clip window, or visible (requiring inner-text clipping). + """ + # pylint: disable=too-many-locals,too-many-positional-arguments,too-many-arguments + close_start, close_end = Hyperlink.find_close(text, match_end) + if (close_start, close_end) == (-1, -1): + return _HyperlinkResult(_HyperlinkAction.NO_CLOSE) + inner_text = text[match_end:close_start] + inner_width = width( + inner_text, control_codes=control_codes, + tabsize=tabsize, ambiguous_width=ambiguous_width, + term_program=term_program, + ) + + if inner_width == 0: + return _HyperlinkResult(_HyperlinkAction.EMPTY, close_end=close_end) + + hl_col_end = col + inner_width + + if hl_col_end <= start or col >= end: + return _HyperlinkResult(_HyperlinkAction.OUTSIDE, close_end=close_end, + inner_width=inner_width) + + inner_clip_start = max(0, start - col) + inner_clip_end = end - col + + clipped_inner = clip( + inner_text, inner_clip_start, inner_clip_end, + fillchar=fillchar, tabsize=tabsize, + ambiguous_width=ambiguous_width, + term_program=term_program, + propagate_sgr=False, + control_codes=control_codes, + ) + + clipped_width = width( + clipped_inner, control_codes=control_codes, + tabsize=tabsize, ambiguous_width=ambiguous_width, + term_program=term_program, + ) + + return _HyperlinkResult( + _HyperlinkAction.VISIBLE, + close_end=close_end, + inner_width=inner_width, + open_seq=params.make_open(), + clipped_inner=clipped_inner, + close_seq=params.make_close(), + clipped_width=clipped_width, + hl_col_end=hl_col_end, + ) + + +def _reconstruct_painter( + cells: dict[int, tuple[str, int]], + sequences: list[tuple[int, int, str]], + start: int, + end: int, + fillchar: str, +) -> str: + """ + Reconstruct the output string from painter's algorithm state. + + Walks columns left-to-right, interleaving escape sequences and cell content, filling gaps with + *fillchar*. + """ + # pylint: disable=too-many-locals + # Group and sort sequences by column, preserving insertion order within each. + seqs_by_col: dict[int, list[tuple[int, str]]] = {} + for col_pos, order, seq_text in sequences: + seqs_by_col.setdefault(col_pos, []).append((order, seq_text)) + for entries in seqs_by_col.values(): + entries.sort() + + max_cell_col = max(cells.keys()) if cells else -1 + max_seq_col = max(seqs_by_col.keys()) if seqs_by_col else -1 + max_col = max(max_cell_col, max_seq_col) + + parts: list[str] = [] + walk_col = 0 + col_limit = min(max_col, end) + while walk_col <= col_limit: + # Emit any sequences anchored at this column. + for _, seq_text in seqs_by_col.get(walk_col, ()): + parts.append(seq_text) + + if walk_col >= end: + walk_col += 1 + continue + + if walk_col in cells: + cell_text, cell_w = cells[walk_col] + parts.append(cell_text) + walk_col += cell_w + else: + if start <= walk_col <= max_cell_col: + parts.append(fillchar) + walk_col += 1 + + # Emit sequences anchored beyond the visible region. + for c in sorted(seqs_by_col.keys()): + if c > col_limit: + for _, seq_text in seqs_by_col[c]: + parts.append(seq_text) + + return ''.join(parts) + + +def _clip_simple( + text: str, + start: int, + end: int, + *, + propagate_sgr: bool, + ambiguous_width: int, + term_program: bool | str, + fillchar: str, + tabsize: int, + strict: bool, + control_codes: Literal['parse', 'strict', 'ignore'], +) -> tuple[str, Optional[_SGRState]]: + """ + Clip text without cursor movement (simple append-to-output path). + + Returns ``(result, captured_style)``. The caller applies SGR wrapping. + """ + # pylint: disable=too-complex,too-many-locals,too-many-branches,too-many-statements + # pylint: disable=too-many-nested-blocks + # code length and complexity traded for performance, to allow this to be used as a "hot path" + + output: list[str] = [] + col = 0 + idx = 0 + # captured_style is a frozen snapshot of current_style taken at the first + # visible character emitted within the clip window (start, end). It stays + # None until that point. current_style, by contrast, is continuously + # updated by SGR sequences throughout the scan. The snapshot is what the + # caller uses to wrap the result in the correct SGR state. + # + # When propagate_sgr is False, current_style (and therefore captured_style) + # remain None, and SGR sequences pass through as literal text. + captured_style: Optional[_SGRState] = None + current_style = _SGR_STATE_DEFAULT if propagate_sgr else None + + while idx < len(text): + char = text[idx] + + # Early exit: past visible region. + if col >= end and char not in '\r\x08\t\x1b': + if captured_style is not None: + break + # propagate_sgr is always False here: with propagate_sgr=True, + # captured_style is set on the first visible emission in the + # clip window and we would have broken above. The skip-ahead + # optimization is only needed (and safe) when SGR tracking is off. + next_esc = text.find('\x1b', idx + 1) + if next_esc == -1: + break + idx = next_esc + continue + + if char == '\x1b': + m = _SEQUENCE_CLASSIFY.match(text, idx) + if not m: + output.append(char) + idx += 1 + continue + + # SGR: update current_style, do not emit. + if m.group('sgr_params') is not None and propagate_sgr and current_style is not None: + current_style = _sgr_state_update(current_style, m.group()) + idx = m.end() + continue + + # OSC 8 hyperlink. + if hl_state := HyperlinkParams.parse(m.group()): + r = _process_hyperlink( + text, start, end, fillchar, tabsize, ambiguous_width, + term_program, + control_codes, + params=hl_state, match_end=m.end(), col=col, + ) + if r.action is _HyperlinkAction.NO_CLOSE: + output.append(m.group()) + idx = m.end() + elif r.action is _HyperlinkAction.EMPTY: + idx = r.close_end + elif r.action is _HyperlinkAction.OUTSIDE: + col += r.inner_width + idx = r.close_end + else: + output.append(r.open_seq) + output.append(r.clipped_inner) + output.append(r.close_seq) + if propagate_sgr and captured_style is None: + captured_style = current_style + col += r.inner_width + idx = r.close_end + continue + + # OSC 66 Text Sizing. + if (ts_meta := m.group('ts_meta')) is not None: + ts_text = m.group('ts_text') + ts_term = m.group('ts_term') + assert ts_text is not None and ts_term is not None + ts = TextSizing( + TextSizingParams.from_params(ts_meta, control_codes=control_codes), + ts_text, ts_term) + ts_width = ts.display_width(ambiguous_width) + + if col >= start and col + ts_width <= end: + output.append(ts.make_sequence()) + if propagate_sgr and captured_style is None: + captured_style = current_style + col += ts_width + elif col < end and col + ts_width > start: + ts_parts: list[str] = [] + + def _ts_write(s: str, _w: int, _col: int) -> None: + ts_parts.append(s) + col = _text_sizing_clip( + ts, col, start, end, fillchar, ambiguous_width, + term_program, + _ts_write) + output.extend(ts_parts) + if propagate_sgr and captured_style is None: + captured_style = current_style + else: + col += ts_width + idx = m.end() + continue + + # Indeterminate-effect sequences: raise in strict mode. + seq = m.group() + if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq): + raise ValueError( + f"Indeterminate cursor sequence at position {idx}, " + f"{seq!r}" + ) + + # Any other recognized sequence: preserve as-is. + output.append(seq) + idx = m.end() + continue + + if char == '\t': + # Expand tab, filling clip window with spaces. + if tabsize > 0: + next_tab = col + (tabsize - (col % tabsize)) + while col < next_tab: + if start <= col < end: + output.append(' ') + if propagate_sgr and captured_style is None: + captured_style = current_style + col += 1 + else: + output.append('\t') + idx += 1 + continue + + grapheme = next(iter_graphemes(text, start=idx)) + grapheme_w = width(grapheme, ambiguous_width=ambiguous_width, + term_program=term_program) + + # Emit grapheme or fillchar depending on visibility within clip window. + if grapheme_w == 0: + if start <= col < end: + output.append(grapheme) + elif col >= start and col + grapheme_w <= end: + output.append(grapheme) + if propagate_sgr and captured_style is None: + captured_style = current_style + elif col < end and col + grapheme_w > start: + output.append(fillchar * (min(end, col + grapheme_w) - max(start, col))) + if propagate_sgr and captured_style is None: + captured_style = current_style + + col += grapheme_w + idx += len(grapheme) + + return ''.join(output), captured_style + + +def _text_sizing_clip( + ts: TextSizing, + col: int, + start: int, + end: int, + fillchar: str, + ambiguous_width: int, + term_program: bool | str, + write_cells: Callable[[str, int, int], None], +) -> int: + """ + Emit tokens for a text-sizing (OSC 66) sequence, clipped to (start, end). + + Calls *write_cells(text, width, col)* for each emitted cell or sequence. Returns new column + position. + """ + # pylint: disable=too-many-locals,too-many-branches,too-many-positional-arguments,too-complex + ts_width = ts.display_width(ambiguous_width) + + # Fully visible: emit entire sequence + if col >= start and col + ts_width <= end: + write_cells(ts.make_sequence(), ts_width, col) + return col + ts_width + # Fully outside: just advance column + if col >= end or col + ts_width <= start: + return col + ts_width + + # Partial overlap: decompose + rel_start = max(0, start - col) + rel_end = min(end, col + ts_width) - col + scale = ts.params.scale + + units: list[tuple[str, int]] = [] + if ts.params.width > 0: + for g in islice(iter_graphemes(ts.text), ts.params.width): + units.append((g, scale)) + for _ in range(ts.params.width - len(units)): + units.append(('', scale)) + else: + for g in iter_graphemes(ts.text): + units.append( + (g, width(g, ambiguous_width=ambiguous_width, + term_program=term_program) * scale)) + + pending_units: list[tuple[str, int]] = [] + + def flush(flush_col: int) -> None: + if not pending_units: + return + texts = [u[0] for u in pending_units] + total_w = sum(u[1] for u in pending_units) + params = TextSizingParams( + scale, + len(texts) if ts.params.width > 0 else 0, + ts.params.numerator, ts.params.denominator, + ts.params.vertical_align, ts.params.horizontal_align) + write_cells( + TextSizing(params, ''.join(texts), ts.terminator).make_sequence(), + total_w, + flush_col) + pending_units.clear() + + flush_col_pos = col + rel_start + unit_pos = 0 + for unit_text, unit_w in units: + unit_end = unit_pos + unit_w + if unit_end <= rel_start: + unit_pos = unit_end + continue + if unit_pos >= rel_end: + break + + overlap = min(unit_end, rel_end) - max(unit_pos, rel_start) + if overlap == unit_w and unit_w > 0: + if not pending_units: + flush_col_pos = col + max(unit_pos, rel_start) + pending_units.append((unit_text, unit_w)) + else: + flush(flush_col_pos) + abs_start = col + max(unit_pos, rel_start) + for i in range(overlap): + write_cells(fillchar, 1, abs_start + i) + unit_pos = unit_end + + flush(flush_col_pos) + return col + ts_width + + +def _clip_painter( + text: str, + start: int, + end: int, + *, + propagate_sgr: bool, + ambiguous_width: int, + term_program: bool | str, + fillchar: str, + tabsize: int, + strict: bool, + control_codes: Literal['parse', 'strict', 'ignore'], +) -> tuple[str, Optional[_SGRState]]: + """ + Clip text with cursor movement (painter's algorithm path). + + Returns ``(result, captured_style)``. The caller applies SGR wrapping. + """ + # pylint: disable=too-complex,too-many-locals,too-many-branches + # pylint: disable=too-many-statements,too-many-nested-blocks + # code length and complexity traded for performance, to allow this to be used as a "hot path" + + cells: dict[int, tuple[str, int]] = {} + hyperlink_cells: set[int] = set() + sequences: list[tuple[int, int, str]] = [] + seq_order = 0 + + col = 0 + idx = 0 + # captured_style is a frozen snapshot of current_style taken at the first + # visible character emitted within the clip window (start, end). It stays + # None until that point. current_style, by contrast, is continuously + # updated by SGR sequences throughout the scan. + # + # When propagate_sgr is False, current_style (and therefore captured_style) + # remain None, and SGR sequences pass through as literal text. + captured_style: Optional[_SGRState] = None + current_style = _SGR_STATE_DEFAULT if propagate_sgr else None + + def _write_cells(s: str, w: int, write_col: int, + is_hyperlink: bool = False) -> None: + """Write *w* cells of text *s* at *write_col*, handling wide-char splitting.""" + nonlocal captured_style + for offset in range(w): + src_col = write_col + offset + if src_col > 0 and cells.get(src_col - 1, ('', 0))[1] == 2: + cells[src_col - 1] = (fillchar, 1) + hyperlink_cells.discard(src_col - 1) + if cells.get(src_col, ('', 0))[1] == 2: + cells[src_col + 1] = (fillchar, 1) + hyperlink_cells.discard(src_col + 1) + cells.pop(src_col, None) + hyperlink_cells.discard(src_col) + cells[write_col] = (s, w) + if is_hyperlink: + for offset in range(w): + hyperlink_cells.add(write_col + offset) + if propagate_sgr and captured_style is None: + captured_style = current_style + + while idx < len(text): + char = text[idx] + + # Early exit: past visible region, SGR captured, no escape ahead. + if col >= end and captured_style is not None and char != '\x1b': + break + + if char == '\x1b': + m = _SEQUENCE_CLASSIFY.match(text, idx) + if not m: + # Record lone ESC as a zero-width sequence at current column. + sequences.append((col, seq_order, char)) + seq_order += 1 + if propagate_sgr and captured_style is None: + captured_style = current_style + idx += 1 + continue + + # SGR: update current_style, do not emit. + if m.group('sgr_params') is not None and propagate_sgr and current_style is not None: + current_style = _sgr_state_update(current_style, m.group()) + idx = m.end() + continue + + # OSC 8 hyperlink. + if hl_state := HyperlinkParams.parse(m.group()): + r = _process_hyperlink( + text, start, end, fillchar, tabsize, ambiguous_width, + term_program, + control_codes, + params=hl_state, match_end=m.end(), col=col, + ) + if r.action is _HyperlinkAction.NO_CLOSE: + sequences.append((col, seq_order, m.group())) + seq_order += 1 + if propagate_sgr and captured_style is None: + captured_style = current_style + idx = m.end() + elif r.action is _HyperlinkAction.EMPTY: + idx = r.close_end + elif r.action is _HyperlinkAction.OUTSIDE: + col += r.inner_width + idx = r.close_end + else: + sequences.append((col, seq_order, r.open_seq)) + seq_order += 1 + if propagate_sgr and captured_style is None: + captured_style = current_style + _write_cells(r.clipped_inner, r.clipped_width, col, + is_hyperlink=True) + col += r.clipped_width + sequences.append((col, seq_order, r.close_seq)) + seq_order += 1 + col = r.hl_col_end + idx = r.close_end + continue + + # OSC 66 Text Sizing. + if (ts_meta := m.group('ts_meta')) is not None: + ts_text = m.group('ts_text') + ts_term = m.group('ts_term') + assert ts_text is not None and ts_term is not None + ts = TextSizing( + TextSizingParams.from_params(ts_meta, control_codes=control_codes), + ts_text, ts_term) + col = _text_sizing_clip( + ts, col, start, end, fillchar, ambiguous_width, + term_program, + _write_cells) + if propagate_sgr and captured_style is None: + captured_style = current_style + idx = m.end() + continue + + # Indeterminate-effect sequences: raise in strict mode. + seq = m.group() + if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq): + raise ValueError( + f"Indeterminate cursor sequence at position {idx}, " + f"{seq!r}" + ) + + # Horizontal Position Absolute (CSI n G). + if (hpa_n := m.group('hpa_n')) is not None: + col = int(hpa_n) - 1 if hpa_n else 0 + idx = m.end() + continue + + # Cursor Forward (CSI n C). + if (cforward_n := m.group('cforward_n')) is not None: + n_forward = int(cforward_n) if cforward_n else 1 + move_end = col + n_forward + if col < end and move_end > start: + for i in range(max(col, start), min(move_end, end)): + _write_cells(fillchar, 1, i) + col = move_end + idx = m.end() + continue + + # Cursor Backward (CSI n D). + if (cbackward_n := m.group('cbackward_n')) is not None: + n_backward = int(cbackward_n) if cbackward_n else 1 + if strict and n_backward > col: + raise ValueError( + f"Cursor left movement at position {idx} would move " + f"{n_backward} cells left from column {col}, " + f"exceeding string start" + ) + col -= n_backward + if col < 0: + col = 0 + idx = m.end() + continue + + # Any other recognized sequence: preserve as-is. + sequences.append((col, seq_order, m.group())) + seq_order += 1 + if propagate_sgr and captured_style is None: + captured_style = current_style + idx = m.end() + continue + + # Carriage return. + if char == '\r': + col = 0 + idx += 1 + continue + + # Backspace. + if char == '\x08': + if col > 0: + col -= 1 + idx += 1 + continue + + # Tab expansion. + if char == '\t': + if tabsize > 0: + next_tab = col + (tabsize - (col % tabsize)) + while col < next_tab: + if start <= col < end: + _write_cells(fillchar, 1, col) + col += 1 + else: + sequences.append((col, seq_order, '\t')) + seq_order += 1 + if propagate_sgr and captured_style is None: + captured_style = current_style + idx += 1 + continue + + # Grapheme cluster. + grapheme = next(iter_graphemes(text, start=idx)) + grapheme_w = width(grapheme, ambiguous_width=ambiguous_width, + term_program=term_program) + + # Emit grapheme or fillchar depending on visibility within clip window. + if grapheme_w == 0: + if start <= col < end: + sequences.append((col, seq_order, grapheme)) + seq_order += 1 + if propagate_sgr and captured_style is None: + captured_style = current_style + elif col >= start and col + grapheme_w <= end: + _write_cells(grapheme, grapheme_w, col) + elif col < end and col + grapheme_w > start: + clip_start = max(start, col) + for offset in range(min(end, col + grapheme_w) - clip_start): + _write_cells(fillchar, 1, clip_start + offset) + + col += grapheme_w + idx += len(grapheme) + + return _reconstruct_painter(cells, sequences, start, end, fillchar), captured_style + + +def clip( + text: str, + start: int, + end: int, + *, + fillchar: str = ' ', + tabsize: int = 8, + ambiguous_width: int = 1, + propagate_sgr: bool = True, + control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', + overtyping: Optional[bool] = None, + term_program: bool | str = False, +) -> str: + r""" + Clip text to display columns (start, end) while preserving all terminal sequences. + + This function extracts a substring based on visible column positions rather than + character indices. Terminal escape sequences are preserved in the output since + they have zero display width. If a wide character (width 2) is split at + either boundary, it is replaced with ``fillchar``. + + TAB characters (``\t``) are expanded to spaces up to the next tab stop, + controlled by the ``tabsize`` parameter. When cursor movement is detected, + a "painter's algorithm" is used, cursor movements actively change the write + position, allowing cursor-left and carriage return to overwrite previously + written cells. It is assumed that ``text`` begins at column 0. + + **OSC 8 hyperlinks** are handled specially: the visible text inside a hyperlink + is clipped to the requested column range, and the hyperlink is rebuilt around + the clipped text. Empty hyperlinks (those with no remaining visible text after + clipping) are removed:: + + >>> clip('\x1b]8;;http://example.com\x07Click This link\x1b]8;;\x07', 6, 10) + '\x1b]8;;http://example.com\x07This\x1b]8;;\x07' + + :param text: String to clip, may contain terminal escape sequences. + :param start: Absolute starting column (inclusive, 0-indexed). + :param end: Absolute ending column (exclusive). + :param fillchar: Character to use when a wide character must be split at + a boundary (default space). Must have display width of 1. + :param tabsize: Tab stop width (default 8). Set to 0 to pass tabs through + as zero-width (preserved in output but don't advance column position). + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :param propagate_sgr: If True (default), SGR (terminal styling) sequences + are propagated. The result begins with any active style at the start + position and ends with a reset sequence if styles are active. + :param control_codes: How to handle control characters and sequences: + + - ``'parse'`` (default): Track horizontal cursor movement and clip + hyperlink text. Cursor overwrite is always allowed, with best effort + results; indeterminate sequences (home, clear, reset, etc.) are + preserved as zero-width. + - ``'strict'``: Like ``parse``, but raises :exc:`ValueError` on + sequences with indeterminate effects (cursor home, clear screen, + reset, vertical movement, etc.) matching :func:`width` behavior. + Also raises on out-of-bounds horizontal cursor movement. + - ``'ignore'``: All control characters are treated as zero-width. + Cursor movement is not tracked (fastest path). + + :param overtyping: Whether to use the painter's algorithm for cursor + movement (``\b`` backspace, ``\r`` carriage return, and CSI cursor + left/right/position sequences). When ``None`` (default), auto-detects + by scanning for these characters in *text*. Set to ``False`` for improved + performance when the caller knows *text* contains no cursor movement + characters. Set to ``True`` to force the painter's algorithm (useful + for testing). Has no effect when ``control_codes='ignore'``. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 + + :returns: Substring of ``text`` spanning display columns (start, end), + with all terminal sequences preserved and wide characters at boundaries + replaced with ``fillchar``. + + :raises ValueError: If ``control_codes='strict'`` and an indeterminate-effect + sequence or out-of-bounds cursor movement is encountered. + + SGR (terminal styling) sequences are propagated by default. The result + begins with any active style and ends with a reset:: + + >>> clip('\x1b[1;34mHello world\x1b[0m', 6, 11) + '\x1b[1;34mworld\x1b[0m' + + Set ``propagate_sgr=False`` to disable this behavior. + + .. versionadded:: 0.3.0 + + .. versionchanged:: 0.5.0 + Added ``propagate_sgr`` parameter (default True). + + .. versionchanged:: 0.7.0 + Added ``control_codes`` parameter (default 'parse'). + OSC 8 hyperlink-aware clipping. OSC 66 text sizing protocol support. + Added ``overtyping`` parameter (default None, auto-detect). + + Example:: + + >>> clip('hello world', 0, 5) + 'hello' + >>> clip('中文字', 0, 3) # Wide char split at column 3 + '中 ' + >>> clip('a\tb', 0, 10) # Tab expanded to spaces + 'a b' + """ + start = max(start, 0) + if end <= start: + return '' + + # Fast path: printable ASCII only. + if text.isascii() and text.isprintable(): + return text[start:end] + + # No escape sequences => no SGR tracking needed. + has_esc = '\x1b' in text + if propagate_sgr and not has_esc: + propagate_sgr = False + + # Determine whether painter's algorithm is needed. + if overtyping is None: + # Auto-detect: scan for cursor movement characters. + overtyping = ( + control_codes != 'ignore' and + ('\x08' in text or '\r' in text or + (has_esc and bool(_HORIZONTAL_CURSOR_MOVEMENT.search(text)))) + ) + elif overtyping and control_codes == 'ignore': + overtyping = False # control_codes='ignore' overrides + fn_clip = _clip_painter if overtyping else _clip_simple + + return _apply_sgr_wrap(*fn_clip( + text=text, + start=start, + end=end, + propagate_sgr=propagate_sgr, + ambiguous_width=ambiguous_width, + term_program=term_program, + fillchar=fillchar, + tabsize=tabsize, + strict=(control_codes == 'strict'), + control_codes=control_codes, + )) diff --git a/libs/wcwidth/_constants.py b/libs/wcwidth/_constants.py new file mode 100644 index 000000000..443d27eec --- /dev/null +++ b/libs/wcwidth/_constants.py @@ -0,0 +1,164 @@ +"""Shared data tables and constants for wcwidth.py, _wcwidth.py, and _wcswidth.py.""" +from __future__ import annotations + +# std imports +import os +from functools import lru_cache + +from typing import Tuple, NamedTuple + +# local +from .table_mc import CATEGORY_MC +from .table_wide import WIDE_EASTASIAN +from .table_zero import ZERO_WIDTH +from .table_grapheme import (ISC_VIRAMA, + EXTENDED_PICTOGRAPHIC, + ISC_INVISIBLE_STACKER, + GRAPHEME_REGIONAL_INDICATOR) +from .table_ambiguous import AMBIGUOUS_EASTASIAN +from .table_overrides import (SFZ_OVERRIDES, + SRI_OVERRIDES, + VS15_OVERRIDES, + VS16_OVERRIDES, + WIDE_OVERRIDES, + NARROW_OVERRIDES) +from .unicode_versions import list_versions +from .table_term_programs import ALIASES, KNOWN_TERMINALS + +_RangeTuple = Tuple[Tuple[int, int], ...] + + +__all__ = ( + "_REGIONAL_INDICATOR_SET", + "_ISC_VIRAMA_SET", + "_LATEST_VERSION", + "_CATEGORY_MC_TABLE", + "_EMOJI_ZWJ_SET", + "_FITZPATRICK_RANGE", + "_ZERO_WIDTH_TABLE", + "_WIDE_EASTASIAN_TABLE", + "_AMBIGUOUS_TABLE", + "resolve_terminal", + "get_term_overrides", + "list_term_programs", +) + +_REGIONAL_INDICATOR_SET = frozenset( + range(GRAPHEME_REGIONAL_INDICATOR[0][0], GRAPHEME_REGIONAL_INDICATOR[0][1] + 1) +) +_ISC_VIRAMA_SET = frozenset( + cp for lo, hi in (*ISC_VIRAMA, *ISC_INVISIBLE_STACKER) + for cp in range(lo, hi + 1) +) +# pylint: disable=invalid-name +_LATEST_VERSION = list_versions()[-1] +_CATEGORY_MC_TABLE = CATEGORY_MC[_LATEST_VERSION] +_EMOJI_ZWJ_SET = frozenset( + cp for lo, hi in EXTENDED_PICTOGRAPHIC for cp in range(lo, hi + 1) +) | _REGIONAL_INDICATOR_SET +_FITZPATRICK_RANGE = (0x1F3FB, 0x1F3FF) + +_ZERO_WIDTH_TABLE = ZERO_WIDTH[_LATEST_VERSION] +_WIDE_EASTASIAN_TABLE = WIDE_EASTASIAN[_LATEST_VERSION] +_AMBIGUOUS_TABLE = AMBIGUOUS_EASTASIAN[_LATEST_VERSION] + + +def list_term_programs() -> tuple[str, ...]: + """ + Return all recognized values for the ``term_program`` argument. + + Includes canonical terminal names and their TERM/TERM_PROGRAM aliases. + + .. versionadded:: 0.8.0 + """ + return tuple(sorted(KNOWN_TERMINALS | ALIASES.keys())) + + +def _merge_ranges(*tuples: _RangeTuple) -> _RangeTuple: + """Merge multiple sorted range tuples into one sorted, non-overlapping tuple.""" + all_ranges: list[tuple[int, int]] = [] + for t in tuples: + all_ranges.extend(t) + if not all_ranges: + return () + all_ranges.sort(key=lambda r: r[0]) + merged = [all_ranges[0]] + for lo, hi in all_ranges[1:]: + _, prev_hi = merged[-1] + if lo <= prev_hi: + merged[-1] = (merged[-1][0], max(prev_hi, hi)) + else: + merged.append((lo, hi)) + return tuple(merged) + + +class TerminalOverrides(NamedTuple): + """Pre-merged override range tuples for a single terminal.""" + + narrower: _RangeTuple + vs16_narrower: _RangeTuple + vs15_wider: _RangeTuple + zeroer: _RangeTuple + narrow_wider: _RangeTuple + narrow_zeroer: _RangeTuple + + +_EMPTY_OVERRIDES = TerminalOverrides((), (), (), (), (), ()) + + +@lru_cache(maxsize=32) +def get_term_overrides(term_canonical: str) -> TerminalOverrides: + """Return a TerminalOverrides, with all empty tuples when there are no overrides.""" + # wide, sri, sfz: all narrow characters Unicode expects wide (no 'wider' data exists) + narrower = _merge_ranges( + WIDE_OVERRIDES.get(term_canonical, {}).get('narrower', ()), + SRI_OVERRIDES.get(term_canonical, {}).get('narrower', ()), + SFZ_OVERRIDES.get(term_canonical, {}).get('narrower', ()), + ) + vs16_narrower = VS16_OVERRIDES.get(term_canonical, {}).get('narrower', ()) + vs15_wider = VS15_OVERRIDES.get(term_canonical, {}).get('wider', ()) + zeroer = _merge_ranges( + WIDE_OVERRIDES.get(term_canonical, {}).get('zeroer', ()), + SRI_OVERRIDES.get(term_canonical, {}).get('zeroer', ()), + SFZ_OVERRIDES.get(term_canonical, {}).get('zeroer', ()), + ) + narrow_wider = NARROW_OVERRIDES.get(term_canonical, {}).get('wider', ()) + narrow_zeroer = NARROW_OVERRIDES.get(term_canonical, {}).get('narrow_zeroer', ()) + # vs15_narrower intentionally excluded: no known terminal narrows VS15 + # vs16_wider intentionally excluded: any 'wider' entries in emoji_vs16_results + # ucs-detect YAML are from the vs16n baseline test (base char without VS16), + # not actual VS16 correction data. + + if not (narrower or vs16_narrower or vs15_wider or zeroer + or narrow_wider or narrow_zeroer): + return _EMPTY_OVERRIDES + return TerminalOverrides(narrower, vs16_narrower, vs15_wider, zeroer, + narrow_wider, narrow_zeroer) + + +@lru_cache(maxsize=32) +def resolve_terminal(term_program: bool | str = False) -> str | None: + """ + Resolve a terminal identifier to its canonical name. + + :param term_program: Terminal identifier. ``False`` (default) disables override lookup. + ``True`` reads the ``TERM_PROGRAM`` environment variable, falling back to ``TERM``. + A string value is used directly (canonical name, alias, XTVERSION/ENQ result, etc.). + :returns: Canonical terminal name if recognized, ``None`` otherwise. + + The auto-detection path (``term_program=True``) reads environment variables at call time + and caches the result. The environment is assumed immutable for the process lifetime; + callers that change ``TERM`` or ``TERM_PROGRAM`` mid-process must call + :func:`resolve_terminal.cache_clear` afterward. + """ + if term_program is False: + return None + if term_program is True: + term_program = os.environ.get('TERM_PROGRAM', '') or os.environ.get('TERM', '') + if not term_program: + return None + key = term_program.strip().lower() + canonical = ALIASES.get(key, key) + if canonical not in KNOWN_TERMINALS: + return None + return canonical diff --git a/libs/wcwidth/_wcswidth.py b/libs/wcwidth/_wcswidth.py new file mode 100644 index 000000000..4b473fa76 --- /dev/null +++ b/libs/wcwidth/_wcswidth.py @@ -0,0 +1,439 @@ +"""This is a python implementation of wcswidth().""" + +from __future__ import annotations + +from typing import Optional + +__lazy_modules__ = [ + "wcwidth._constants", + "wcwidth._wcwidth", + "wcwidth.bisearch", + "wcwidth.table_grapheme", + "wcwidth.table_vs16", +] +# local +from . import table_grapheme_overrides +from ._wcwidth import wcwidth +from .bisearch import bisearch +from ._constants import (_EMOJI_ZWJ_SET, + _ISC_VIRAMA_SET, + _CATEGORY_MC_TABLE, + _FITZPATRICK_RANGE, + _REGIONAL_INDICATOR_SET, + resolve_terminal, + get_term_overrides) +from .table_vs15 import VS15_WIDE_TO_NARROW +from .table_vs16 import VS16_NARROW_TO_WIDE +from .table_grapheme import GRAPHEME_EXTEND + + +def _scan_zwj_cluster_end(text: str, start: int, end: int) -> int: + """ + Scan forward from *start* (base character) to end of a ZWJ grapheme cluster. + + Follows the UAX #29 GB11 pattern (ExtPict Extend* ZWJ x ExtPict) chained repeatedly until no + more ZWJ joins are found. + """ + idx = start + 1 + # Skip Extend characters (Fitzpatrick modifiers, etc.) before first ZWJ + while idx < end and bisearch(ord(text[idx]), GRAPHEME_EXTEND): + idx += 1 + # Follow ZWJ chains + while idx < end: + if ord(text[idx]) != 0x200D: + break + idx += 1 + # GB11: \p{ExtPict} Extend* ZWJ × \p{ExtPict} + # Extend modifiers (VS16, Fitzpatrick skin tones, etc.) attach to + # the ExtPict *before* the ZWJ, not after it. After ZWJ the next + # codepoint is always an ExtPict directly, no Extend skip needed. + if idx < end and ord(text[idx]) in _EMOJI_ZWJ_SET: + idx += 1 + # Skip trailing Extend (VS16, etc.) after ExtPict before next ZWJ + while idx < end and bisearch(ord(text[idx]), GRAPHEME_EXTEND): + idx += 1 + continue + break + return idx + + +def wcswidth( + pwcs: str, + n: Optional[int] = None, + unicode_version: str = 'auto', + ambiguous_width: int = 1, +) -> int: + """ + Given a unicode string, return its printable length on a terminal. + + See :ref:`Specification` for details of cell measurement. + + This implementation differs from Markus Khun's original POSIX C implementation, in that this + ``wcswidth()`` processes graphemes strings yielded by :func:`wcwidth.iter_graphemes` defined by + `Unicode Standard Annex #29`_. POSIX wcswidth(3) is not grapheme-aware and does not measure many + kinds of Emojis or complex scripts correctly. + + :param pwcs: Measure width of given unicode string. + :param n: When ``n`` is None (default), return the length of the entire + string, otherwise only the first ``n`` characters are measured. + :param unicode_version: Ignored. Retained for backwards compatibility. + + .. deprecated:: 0.3.0 + Only the latest Unicode version is now shipped. + + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :returns: The width, in cells, needed to display the first ``n`` characters + of the unicode string ``pwcs``. Returns ``-1`` for C0 and C1 control + characters! + + .. _`Unicode Standard Annex #29`: https://www.unicode.org/reports/tr29/ + """ + # pylint: disable=unused-argument,too-many-locals,too-many-statements,redefined-variable-type + # pylint: disable=too-complex,too-many-branches,duplicate-code,too-many-nested-blocks + + # Fast path: pure ASCII printable strings are always width == length + if n is None and pwcs.isascii() and pwcs.isprintable(): + return len(pwcs) + + _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) + + end = len(pwcs) if n is None else n + total_width = 0 + idx = 0 + + last_measured_idx = -2 # -2 sentinel blocks VS16/VS15 (no base available) + last_measured_ucs = -1 + last_measured_w = 0 + prev_was_virama = False + cluster_width = 0 + vs16_nw_table = VS16_NARROW_TO_WIDE['9.0.0'] + vs15_wn_table = VS15_WIDE_TO_NARROW['9.0.0'] + _bisearch = bisearch + + while idx < end: + char = pwcs[idx] + ucs = ord(char) + + # 5. ZWJ (U+200D): consumed without contributing width. + # Virama codepoints are treated as zero-width combining marks (Mn). When a + # virama+consonant sequence forms a conjunct, its width is capped at 2 cells. + + # ZWJ (U+200D) + if ucs == 0x200D: + if prev_was_virama: + idx += 1 + elif idx + 1 < end: + last_measured_w = 0 + prev_was_virama = False + idx += 2 + else: + prev_was_virama = False + idx += 1 + continue + + # 6. VS16 (U+FE0F): converts preceding narrow character to wide. + if ucs == 0xFE0F and last_measured_idx >= 0: + if _bisearch(last_measured_ucs, vs16_nw_table): + cluster_width = 2 + last_measured_idx = -2 + idx += 1 + continue + + # VS15 (U+FE0E): text variation selector, requests narrow presentation. + if ucs == 0xFE0E and last_measured_idx >= 0: + if bisearch(last_measured_ucs, vs15_wn_table) and last_measured_w == 2: + total_width -= 1 + idx += 1 + continue + + # 7. Regional Indicator & Fitzpatrick (both above BMP) + if ucs > 0xFFFF: + if ucs in _REGIONAL_INDICATOR_SET: + ri_before = 0 + j = idx - 1 + while j >= 0 and ord(pwcs[j]) in _REGIONAL_INDICATOR_SET: + ri_before += 1 + j -= 1 + if ri_before % 2 == 1: + last_measured_ucs = ucs + idx += 1 + continue + elif (_FITZPATRICK_RANGE[0] <= ucs <= _FITZPATRICK_RANGE[1] + and last_measured_ucs in _EMOJI_ZWJ_SET): + idx += 1 + continue + + # 8. Normal character: measure with wcwidth + w = _wcwidth(char) + if w < 0: + return -1 + if w > 0: + if prev_was_virama: + cluster_width = 2 + elif cluster_width: + total_width += cluster_width + cluster_width = w + else: + cluster_width = w + + last_measured_idx = idx + last_measured_ucs = ucs + last_measured_w = w + prev_was_virama = False + elif ucs in _ISC_VIRAMA_SET: + prev_was_virama = True + elif last_measured_idx >= 0 and _bisearch(ucs, _CATEGORY_MC_TABLE): + cluster_width = 2 + last_measured_idx = -2 + prev_was_virama = False + else: + prev_was_virama = False + idx += 1 + + if cluster_width: + total_width += cluster_width + return total_width + + +def wcstwidth( + pwcs: str, + n: Optional[int] = None, + unicode_version: str = 'auto', + ambiguous_width: int = 1, + term_program: bool | str = True, +) -> int: + """ + Given a unicode string, return its printable length on a terminal given by ``term_program``. + + See :ref:`Specification` for details of cell measurement. + + Unlike :func:`wcswidth`, this function applies per-terminal correction tables for + emoji presentation and grapheme clusters. + + :param pwcs: Measure width of given unicode string. + :param n: When ``n`` is None (default), return the length of the entire + string, otherwise only the first ``n`` characters are measured. + :param unicode_version: Ignored. Retained for backwards compatibility. + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :param term_program: Terminal software identifier for table correction. + ``True`` (default) reads the ``TERM_PROGRAM`` or ``TERM`` environment + variable for auto-detection. ``False`` disables override lookup. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 + :returns: The width, in cells, needed to display the first ``n`` characters + of the unicode string ``pwcs``. Returns ``-1`` for C0 and C1 control + characters! + """ + # pylint: disable=unused-argument,too-many-locals,too-many-statements,redefined-variable-type + # pylint: disable=too-complex,too-many-branches,duplicate-code,too-many-nested-blocks + # This function intentionally keeps all logic inline for performance. + + # Fast path: pure ASCII printable strings are always width == length + if n is None and pwcs.isascii() and pwcs.isprintable(): + return len(pwcs) + + # Resolve terminal software for override lookup + term_canonical = resolve_terminal(term_program) + + # Skip override lookup when no terminal detected (avoids lru_cache call overhead). + # Extract locals for hot-loop performance (NamedTuple attribute access is slow). + if term_canonical: + overrides = get_term_overrides(term_canonical) + _narrower = overrides.narrower + _vs16_narrower = overrides.vs16_narrower + _vs15_wider = overrides.vs15_wider + _zeroer = overrides.zeroer + _narrow_wider = overrides.narrow_wider + _narrow_zeroer = overrides.narrow_zeroer + _grapheme_overrides = table_grapheme_overrides.get(term_canonical) + else: + _narrower = () + _vs16_narrower = () + _vs15_wider = () + _zeroer = () + _narrow_wider = () + _narrow_zeroer = () + _grapheme_overrides = {} + + # Select wcwidth call pattern for best lru_cache performance + _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) + + end = len(pwcs) if n is None else n + total_width = 0 + idx = 0 + + # grapheme-clustering state and local re-binding for performance. + # Widths accumulate in cluster_width and flush at boundaries. A cluster is a base character + # plus combining marks, deferring the flush lets grapheme overrides replace the measured width + # retrospectively. + last_measured_idx = -2 # -2 sentinel blocks VS16/VS15 (no base available) + last_measured_ucs = -1 + last_measured_w = 0 + prev_was_virama = False + cluster_start = -1 + total_before_cluster = 0 + cluster_width = 0 + vs16_nw_table = VS16_NARROW_TO_WIDE['9.0.0'] + vs15_wn_table = VS15_WIDE_TO_NARROW['9.0.0'] + _bisearch = bisearch + + while idx < end: + char = pwcs[idx] + ucs = ord(char) + + # + # Much of the logic below matches the logic in width(), but is repeated for improved + # performance, they are given matching index reference numbers (starting at #5). + # + # 5. ZWJ (U+200D): consumed without contributing width. + # Virama codepoints are treated as zero-width combining marks (Mn). When a + # virama+consonant sequence forms a conjunct, its width is capped at 2 cells + # matching behavior of popular terminals (PR #224) + + # ZWJ (U+200D) + if ucs == 0x200D: + if prev_was_virama: + idx += 1 + elif idx + 1 < end: + # Check for terminal grapheme override when base char is ExtPict/RI + if (_grapheme_overrides + and last_measured_idx >= 0 + and last_measured_ucs in _EMOJI_ZWJ_SET): + cluster_end = _scan_zwj_cluster_end(pwcs, last_measured_idx, end) + cluster = pwcs[last_measured_idx:cluster_end] + override_w = _grapheme_overrides.get(cluster) + if override_w is not None: + total_width += (override_w - last_measured_w) + last_measured_idx = -2 + last_measured_ucs = -1 + last_measured_w = 0 + prev_was_virama = False + cluster_start = -1 + idx = cluster_end + continue + # No override; ZWJ breaks VS adjacency. + # VS16 already set last_measured_idx = -2, blocking further VS16. + last_measured_w = 0 + prev_was_virama = False + idx += 2 + else: + prev_was_virama = False + idx += 1 + continue + + # 6. VS16 (U+FE0F): converts preceding narrow character to wide. + if ucs == 0xFE0F and last_measured_idx >= 0: + if _vs16_narrower and _bisearch(last_measured_ucs, _vs16_narrower): + pass + elif _bisearch(last_measured_ucs, vs16_nw_table): + cluster_width = 2 + last_measured_idx = -2 # prevent double application + idx += 1 + continue + + # VS15 (U+FE0E): text variation selector, requests narrow presentation. + if ucs == 0xFE0E and last_measured_idx >= 0: + base_ucs = last_measured_ucs + vs15_narrow = bisearch(base_ucs, vs15_wn_table) + if _vs15_wider and bisearch(base_ucs, _vs15_wider): + vs15_narrow = False + if vs15_narrow and last_measured_w == 2: + total_width -= 1 + idx += 1 + continue + + # 7. Regional Indicator & Fitzpatrick (both above BMP) + if ucs > 0xFFFF: + if ucs in _REGIONAL_INDICATOR_SET: + ri_before = 0 + j = idx - 1 + while j >= 0 and ord(pwcs[j]) in _REGIONAL_INDICATOR_SET: + ri_before += 1 + j -= 1 + if ri_before % 2 == 1: + last_measured_ucs = ucs + idx += 1 + continue + elif (_FITZPATRICK_RANGE[0] <= ucs <= _FITZPATRICK_RANGE[1] + and last_measured_ucs in _EMOJI_ZWJ_SET): + idx += 1 + continue + + # 8. Normal character: measure with wcwidth + w = _wcwidth(char) + if w < 0: + # C0/C1 control character + return -1 + # Apply single-codepoint terminal overrides (pre-merged tuples) + if w == 2 and _narrower and bisearch(ucs, _narrower): + w = 1 + elif w == 2 and _zeroer and bisearch(ucs, _zeroer): + w = 0 + if w == 1 and _narrow_wider and bisearch(ucs, _narrow_wider): + w = 2 + elif w == 1 and _narrow_zeroer and bisearch(ucs, _narrow_zeroer): + w = 0 + if w > 0: + # virama+consonant extends current cluster; otherwise start new + if prev_was_virama: + cluster_width = 2 + elif cluster_width: + # flush previous cluster, check for grapheme overrides + flushed = False + if _grapheme_overrides and cluster_start >= 0: + # Two-phase override lookup: candidate (cluster+current) catches Lo+Lo pairs + # where both chars bear width (Thai KO KAI + SARA AM). cluster_text (cluster + # alone) catches C+Mc clusters where the override key is shorter. + candidate = pwcs[cluster_start:idx + 1] + override_w = _grapheme_overrides.get(candidate) + if override_w is not None: + total_width = total_before_cluster + override_w + flushed = True + cluster_width = 0 + else: + cluster_text = pwcs[cluster_start:idx] + override_w = _grapheme_overrides.get(cluster_text) + if override_w is not None: + total_width = total_before_cluster + override_w + else: + total_width += cluster_width + else: + total_width += cluster_width + if not flushed: + cluster_width = w + cluster_start = idx + total_before_cluster = total_width + else: + cluster_width = w + cluster_start = idx + total_before_cluster = total_width + last_measured_idx = idx + last_measured_ucs = ucs + last_measured_w = w + prev_was_virama = False + elif ucs in _ISC_VIRAMA_SET: + prev_was_virama = True + elif last_measured_idx >= 0 and _bisearch(ucs, _CATEGORY_MC_TABLE): + # Spacing Combining Mark (Mc) following a base character + cluster_width = 2 + last_measured_idx = -2 + prev_was_virama = False + else: + prev_was_virama = False + idx += 1 + + if cluster_width: + if _grapheme_overrides and cluster_start >= 0: + cluster_text = pwcs[cluster_start:end] + override_w = _grapheme_overrides.get(cluster_text) + if override_w is not None: + total_width = total_before_cluster + override_w + else: + total_width += cluster_width + else: + total_width += cluster_width + return total_width diff --git a/libs/wcwidth/_wcwidth.py b/libs/wcwidth/_wcwidth.py new file mode 100644 index 000000000..58877bcb2 --- /dev/null +++ b/libs/wcwidth/_wcwidth.py @@ -0,0 +1,162 @@ +""" +This is a python implementation of wcwidth() and wcswidth(). + +https://github.com/jquast/wcwidth + +Derived from Markus Kuhn's C code, + +This is an implementation of wcwidth() and wcswidth() (defined in +IEEE Std 1002.1-2001) for Unicode. + +http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html +http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html + +In fixed-width output devices, Latin characters all occupy a single +"cell" position of equal width, whereas ideographic CJK characters +occupy two such cells. Interoperability between terminal-line +applications and (teletype-style) character terminals using the +UTF-8 encoding requires agreement on which character should advance +the cursor by how many cell positions. No established formal +standards exist at present on which Unicode character shall occupy +how many cell positions on character terminals. These routines are +a first attempt of defining such behavior based on simple rules +applied to data provided by the Unicode Consortium. + +For some graphical characters, the Unicode standard explicitly +defines a character-cell width via the definition of the East Asian +FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes. +In all these cases, there is no ambiguity about which width a +terminal shall use. For characters in the East Asian Ambiguous (A) +class, the width choice depends purely on a preference of backward +compatibility with either historic CJK or Western practice. +Choosing single-width for these characters is easy to justify as +the appropriate long-term solution, as the CJK practice of +displaying these characters as double-width comes from historic +implementation simplicity (8-bit encoded characters were displayed +single-width and 16-bit ones double-width, even for Greek, +Cyrillic, etc.) and not any typographic considerations. + +Much less clear is the choice of width for the Not East Asian +(Neutral) class. Existing practice does not dictate a width for any +of these characters. It would nevertheless make sense +typographically to allocate two character cells to characters such +as for instance EM SPACE or VOLUME INTEGRAL, which cannot be +represented adequately with a single-width glyph. The following +routines at present merely assign a single-cell width to all +neutral characters, in the interest of simplicity. This is not +entirely satisfactory and should be reconsidered before +establishing a formal standard in this area. At the moment, the +decision which Not East Asian (Neutral) characters should be +represented by double-width glyphs cannot yet be answered by +applying a simple rule from the Unicode database content. Setting +up a proper standard for the behavior of UTF-8 character terminals +will require a careful analysis not only of each Unicode character, +but also of each presentation form, something the author of these +routines has avoided to do so far. + +http://www.unicode.org/unicode/reports/tr11/ + +Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c +""" + +from __future__ import annotations + +# std imports +from functools import lru_cache + +__lazy_modules__ = [ + "wcwidth.bisearch", + "wcwidth._constants", +] +# local +from .bisearch import bisearch +from ._constants import _LATEST_VERSION, _AMBIGUOUS_TABLE, _ZERO_WIDTH_TABLE, _WIDE_EASTASIAN_TABLE + + +@lru_cache(maxsize=128) +def _wcversion_value(ver_string: str) -> tuple[int, ...]: # pragma: no cover + """ + Integer-mapped value of given dotted version string. + + .. deprecated:: 0.3.0 + + This function is no longer used internally by wcwidth but is retained + for API compatibility with external tools. + + :param ver_string: Unicode version string, of form ``n.n.n``. + :returns: tuple of digit tuples, ``tuple(int, [...])``. + """ + retval = tuple(map(int, (ver_string.split('.')))) + return retval + + +@lru_cache(maxsize=8) +def _wcmatch_version(given_version: str) -> str: # pylint: disable=unused-argument + """ + Return the supported Unicode version level. + + .. deprecated:: 0.3.0 + This function now always returns the latest version. + + This function is no longer used internally by wcwidth but is retained + for API compatibility with external tools. + + :param given_version: Ignored. Any value is accepted for compatibility. + :returns: The latest unicode version string. + """ + return _LATEST_VERSION + + +# maxsize=1024: western scripts need ~64 unique codepoints per session, but +# CJK sessions may use ~2000 of ~3500 common hanzi/kanji. 1024 accommodates +# heavy CJK use. Performance floor at 32; bisearch is ~100ns per miss. + +@lru_cache(maxsize=1024) +def wcwidth(wc: str, unicode_version: str = 'auto', ambiguous_width: int = 1) -> int: # pylint: disable=unused-argument + r""" + Given one Unicode codepoint, return its printable length on a terminal. + + :param wc: A single Unicode character. + :param unicode_version: Ignored. Retained for backwards compatibility. + + .. deprecated:: 0.3.0 + Only the latest Unicode version is now shipped. + + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts + where ambiguous characters display as double-width. See + :ref:`ambiguous_width` for details. + :returns: The width, in cells, necessary to display the character of + Unicode string character, ``wc``. Returns 0 if the ``wc`` argument has + no printable effect on a terminal (such as NUL '\0'), -1 if ``wc`` is + not printable, or has an indeterminate effect on the terminal, such as + a control character. Otherwise, the number of column positions the + character occupies on a graphic terminal (1 or 2) is returned. + + See :ref:`Specification` for details of cell measurement. + """ + ucs = ord(wc) if wc else 0 + + # small optimization: early return of 1 for printable ASCII, this provides + # approximately 40% performance improvement for mostly-ascii documents, with + # less than 1% impact to others. + if 32 <= ucs < 0x7f: + return 1 + + # C0/C1 control characters are -1 for compatibility with POSIX-like calls + if ucs and ucs < 32 or 0x07F <= ucs < 0x0A0: + return -1 + + # Zero width + if bisearch(ucs, _ZERO_WIDTH_TABLE): + return 0 + + # Wide (F/W categories) + if bisearch(ucs, _WIDE_EASTASIAN_TABLE): + return 2 + + # Ambiguous width (A category) - only when ambiguous_width=2 + if ambiguous_width == 2 and bisearch(ucs, _AMBIGUOUS_TABLE): + return 2 + + return 1 diff --git a/libs/wcwidth/_width.py b/libs/wcwidth/_width.py new file mode 100644 index 000000000..08d31b57f --- /dev/null +++ b/libs/wcwidth/_width.py @@ -0,0 +1,515 @@ +"""This is a high-level width() supporting terminal output.""" + +from __future__ import annotations + +from typing import Literal + +__lazy_modules__ = [ + "wcwidth._constants", + "wcwidth._wcswidth", + "wcwidth._wcwidth", + "wcwidth.bisearch", + "wcwidth.control_codes", + "wcwidth.escape_sequences", + "wcwidth.table_grapheme", + "wcwidth.table_vs16", + "wcwidth.text_sizing", +] +# local +from . import table_grapheme_overrides +from ._wcwidth import wcwidth +from .bisearch import bisearch +from ._wcswidth import wcswidth, wcstwidth, _scan_zwj_cluster_end +from ._constants import (_EMOJI_ZWJ_SET, + _ISC_VIRAMA_SET, + _CATEGORY_MC_TABLE, + _FITZPATRICK_RANGE, + _REGIONAL_INDICATOR_SET, + resolve_terminal, + get_term_overrides) +from .table_vs15 import VS15_WIDE_TO_NARROW +from .table_vs16 import VS16_NARROW_TO_WIDE +from .text_sizing import TextSizing, TextSizingParams +from .control_codes import ILLEGAL_CTRL, VERTICAL_CTRL, HORIZONTAL_CTRL, ZERO_WIDTH_CTRL +from .escape_sequences import (_SEQUENCE_CLASSIFY, + TEXT_SIZING_PATTERN, + CURSOR_MOVEMENT_SEQUENCE, + INDETERMINATE_EFFECT_SEQUENCE, + strip_sequences) + +# In 'parse' mode, strings longer than this are checked for cursor-movement +# controls (BS, TAB, CR, cursor sequences); when absent, mode downgrades to +# 'ignore' to skip character-by-character parsing. The detection scan cost is +# negligible for long strings but wasted on short ones like labels or headings. +_WIDTH_FAST_PATH_MIN_LEN = 20 + +# Translation table to strip C0/C1 control characters for fast 'ignore' mode. +_CONTROL_CHAR_TABLE = str.maketrans('', '', ( + ''.join(chr(c) for c in range(0x00, 0x20)) + # C0: NUL through US (including tab) + '\x7f' + # DEL + ''.join(chr(c) for c in range(0x80, 0xa0)) # C1: U+0080-U+009F +)) + + +def _width_ignored_codes(text: str, ambiguous_width: int = 1, + term_program: bool | str = False) -> int: + """ + Fast path for width() with control_codes='ignore'. + + Strips escape sequences and control characters, then measures remaining text. + """ + if term_program is False: + return wcswidth( + strip_sequences(text).translate(_CONTROL_CHAR_TABLE), + ambiguous_width=ambiguous_width, + ) + return wcstwidth( + strip_sequences(text).translate(_CONTROL_CHAR_TABLE), + ambiguous_width=ambiguous_width, + term_program=term_program, + ) + + +def width( + text: str, + *, + control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', + tabsize: int = 8, + ambiguous_width: int = 1, + term_program: bool | str = False, +) -> int: + r""" + Return printable width of text containing many kinds of control codes and sequences. + + Unlike :func:`wcswidth`, this function handles most control characters and many popular terminal + output sequences. Never returns -1. + + :param text: String to measure. + :param control_codes: How to handle control characters and sequences: + + - ``'parse'`` (default): Track horizontal cursor movement like BS ``\b``, CR ``\r``, TAB + ``\t``, cursor left and right movement sequences. Vertical movement (LF, VT, FF) and + indeterminate terminal sequences are zero-width. OSC 66 Kitty Text Sizing protocol, OSC 8 + Hyperlink, and many other kinds of output sequences are parsed for displayed measurements. + - ``'strict'``: Like parse, but raises :exc:`ValueError` on control characters with + indeterminate results of the screen or cursor, like clear or vertical movement. Generally, + these should be handled with a virtual terminal emulator (like 'pyte'). + - ``'ignore'``: All C0 and C1 control characters and escape sequences are measured as + width 0. This is the fastest measurement for text already filtered or known not to contain + any kinds of control codes or sequences. TAB ``\t`` is zero-width; to ensure + tab expansion, pre-process text using :func:`str.expandtabs`. + + :param tabsize: Tab stop width for ``'parse'`` and ``'strict'`` modes. Default is 8. Must be + positive. Has no effect when ``control_codes='ignore'``. + :param ambiguous_width: Width to use for East Asian Ambiguous (A) characters. Default is ``1`` + (narrow). Set to ``2`` for CJK contexts. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 + :returns: Maximum cursor position reached, "extent", accounting for cursor movement sequences + present in ``text`` according to given parameters. This represents the rightmost column the + cursor reaches. Always a non-negative integer. + :raises ValueError: If ``control_codes='strict'`` and control characters with indeterminate + effects, such as vertical movement or clear sequences are encountered, or on unexpected + C0 or C1 control code. Also raised when ``control_codes`` is not one of the valid values. + + .. versionadded:: 0.3.0 + + .. versionchanged:: 0.7.0 + Expanded strict-mode to raise :exc:`ValueError` when cursor-left movement + (CSI D) would move beyond the beginning of the string. Previously, cursor-left + was silently clamped to column 0 in all modes. + + Support horizontal cursor sequences (``cub``, ``cuf``, ``hpa``). Cursor-left (``cub``) or + backspace (``\b``) now overwrites text. ``column_address`` (``hpa``) and carriage return + (``\r``) are now parsed, and some values conditionally raise ``ValueError`` when + ``control_codes='parse'``. + + Examples:: + + >>> width('hello') + 5 + >>> width('コンニチハ') + 10 + >>> width('\x1b[31mred\x1b[0m') + 3 + >>> width('\x1b[31mred\x1b[0m', control_codes='ignore') # same result (ignored) + 3 + >>> width('123\b4') # backspace overwrites previous cell (outputs '124') + 3 + >>> width('abc\t') # tab caused cursor to move to column 8 + 8 + >>> width('1\x1b[10C') # '1' + cursor right 10, cursor ends on column 11 + 11 + >>> width('1\x1b[10C', control_codes='ignore') # faster but wrong in this case + 1 + """ + # pylint: disable=too-complex,too-many-branches,too-many-statements,too-many-locals,redefined-variable-type,too-many-nested-blocks + # This could be broken into sub-functions (#1, #3, and #6 especially), but for reduced overhead + # in consideration of this function a likely "hot path", they are inline, breaking many pylint + # complexity rules. + + # Fast path for ASCII printable (no tabs, escapes, or control chars) + if text.isascii() and text.isprintable(): + return len(text) + + # Fast parse: if no horizontal cursor movements are possible, switch to 'ignore' mode. + # Only check longer strings - the detection overhead hurts short string performance. + if control_codes == 'parse' and len(text) > _WIDTH_FAST_PATH_MIN_LEN: + # Check for cursor-affecting control characters + if '\b' not in text and '\t' not in text and '\r' not in text: + # Check for escape sequences, if none contain cursor movement or + # text sizing, downgrade to 'ignore' + if '\x1b' not in text or ( + not CURSOR_MOVEMENT_SEQUENCE.search(text) + and not TEXT_SIZING_PATTERN.search(text) + ): + control_codes = 'ignore' + + # Fast path for ignore mode, useful if you know the text is already free of control codes + if control_codes == 'ignore': + return _width_ignored_codes(text, ambiguous_width, term_program=term_program) + + # Resolve terminal software for override lookup + term_canonical = resolve_terminal(term_program) + + # Skip override lookup when no terminal detected (avoids lru_cache call overhead). + # Extract locals for hot-loop performance (NamedTuple attribute access is slow). + if term_canonical: + overrides = get_term_overrides(term_canonical) + _narrower = overrides.narrower + _vs16_narrower = overrides.vs16_narrower + _vs15_wider = overrides.vs15_wider + _zeroer = overrides.zeroer + _narrow_wider = overrides.narrow_wider + _narrow_zeroer = overrides.narrow_zeroer + _grapheme_overrides = table_grapheme_overrides.get(term_canonical) + else: + _narrower = () + _vs16_narrower = () + _vs15_wider = () + _zeroer = () + _narrow_wider = () + _narrow_zeroer = () + _grapheme_overrides = {} + + strict = control_codes == 'strict' + # Track absolute positions: tab stops need modulo on absolute column, CR resets to 0. + # Initialize max_extent to 0 so backward movement (CR, BS) won't yield negative width. + current_col = 0 + max_extent = 0 + idx = 0 + text_len = len(text) + + # Select wcwidth call pattern for best lru_cache performance: + # - ambiguous_width=1 (default): single-arg calls share cache with direct wcwidth() calls + # - ambiguous_width=2: full positional args needed (results differ, separate cache is correct) + _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) + + # grapheme-clustering state and local re-binding for performance. + # Widths accumulate in cluster_width and flush at boundaries (see _wcswidth.py) + last_measured_idx = -2 # -2 sentinel blocks VS16/VS15 (no base available) + last_measured_ucs = -1 + last_measured_w = 0 + prev_was_virama = False + _max_extent_before = 0 + cluster_start = -1 + col_before_cluster = 0 + max_extent_before_cluster = 0 + cluster_width = 0 + vs16_nw_table = VS16_NARROW_TO_WIDE['9.0.0'] + vs15_wn_table = VS15_WIDE_TO_NARROW['9.0.0'] + _bisearch = bisearch + + while idx < text_len: + char = text[idx] + + # 1. ESC sequences + if char == '\x1b': + # Flush pending cluster before processing escape sequence + if cluster_width: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + cluster_width = 0 + m = _SEQUENCE_CLASSIFY.match(text, idx) + if not m: + # 1a. Errant ESC or unknown sequence: only the first character is zero-width + idx += 1 + else: + seq = m.group() + if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq): + raise ValueError(f"Indeterminate cursor sequence at position {idx}, {seq!r}") + + # 2b. horizontal position absolute (before forward/backward to + # avoid other_seq match in _SEQUENCE_CLASSIFY) + if (hpa_n := m.group('hpa_n')) is not None: + target_col = int(hpa_n) if hpa_n else 1 + if strict: + raise ValueError( + f"Indeterminate horizontal position at position {idx}, " + f"{seq!r} (absolute column unknown)" + ) + current_col = target_col - 1 # HPA is 1-indexed, convert to 0-indexed + # 2c. cursor forward, backward + elif (cforward_n := m.group('cforward_n')) is not None: + current_col += int(cforward_n) if cforward_n else 1 + elif (cbackward_n := m.group('cbackward_n')) is not None: + n_backward = int(cbackward_n) if cbackward_n else 1 + if strict and n_backward > current_col: + raise ValueError( + f"Cursor left movement at position {idx} would move " + f"{n_backward} cells left from column {current_col}, " + f"exceeding string start" + ) + current_col -= n_backward + if current_col < 0: + current_col = 0 + # 2d. OSC 66 Text Sizing — has positive display width + elif (ts_meta := m.group('ts_meta')) is not None: + ts_text = m.group('ts_text') + ts_term = m.group('ts_term') + assert ts_text is not None and ts_term is not None + text_size = TextSizing( + TextSizingParams.from_params(ts_meta, control_codes=control_codes), + ts_text, ts_term) + current_col += text_size.display_width(ambiguous_width) + # 2e. SGR and other zero-width sequences -- no column advance + idx = m.end() + # Escape sequences break VS16 adjacency: reset last-measured state + last_measured_idx = -2 + last_measured_ucs = -1 + cluster_start = -1 + if current_col > max_extent: + max_extent = current_col + continue + + # 2. Vertical or Illegal control characters zero width or error when 'strict' + if char in ILLEGAL_CTRL: + if strict: + raise ValueError(f"Illegal control character {ord(char):#x} at position {idx}") + if cluster_width: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + cluster_width = 0 + idx += 1 + last_measured_idx = -2 + last_measured_ucs = -1 + cluster_start = -1 + continue + + if char in VERTICAL_CTRL: + if strict: + raise ValueError(f"Vertical movement character {ord(char):#x} at position {idx}") + if cluster_width: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + cluster_width = 0 + idx += 1 + last_measured_idx = -2 + last_measured_ucs = -1 + cluster_start = -1 + continue + + # 3. Horizontal movement characters + if char in HORIZONTAL_CTRL: + if cluster_width: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + cluster_width = 0 + if char == '\t' and tabsize > 0: + current_col += tabsize - (current_col % tabsize) + elif char == '\b': + if current_col > 0: + current_col -= 1 + elif char == '\r': + if strict: + raise ValueError( + f"Horizontal movement character \\r at position {idx}: " + "indeterminate starting column" + ) + current_col = 0 + if current_col > max_extent: + max_extent = current_col + idx += 1 + last_measured_idx = -2 + last_measured_ucs = -1 + cluster_start = -1 + continue + + # 4. Zero-width control characters + if char in ZERO_WIDTH_CTRL: + if cluster_width: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + cluster_width = 0 + idx += 1 + last_measured_idx = -2 + last_measured_ucs = -1 + cluster_start = -1 + continue + + # 5. Inline grapheme-clustering: ZWJ, Virama, VS16, Regional Indicators, + # Fitzpatrick, Mc, wcwidth + ucs = ord(char) + + # ZWJ (U+200D) + if ucs == 0x200D: + if prev_was_virama: + idx += 1 + elif idx + 1 < text_len: + # Check for terminal grapheme override when base char is ExtPict/RI + if (_grapheme_overrides + and last_measured_idx >= 0 + and last_measured_ucs in _EMOJI_ZWJ_SET): + cluster_end = _scan_zwj_cluster_end(text, last_measured_idx, text_len) + cluster = text[last_measured_idx:cluster_end] + override_w = _grapheme_overrides.get(cluster) + if override_w is not None: + current_col += (override_w - last_measured_w) + max_extent = max(max_extent, current_col) + last_measured_idx = -2 + last_measured_ucs = -1 + last_measured_w = 0 + prev_was_virama = False + cluster_start = -1 + idx = cluster_end + continue + # No override; ZWJ breaks VS adjacency. + # VS16 already set last_measured_idx = -2, blocking further VS16. + last_measured_w = 0 + prev_was_virama = False + idx += 2 + else: + prev_was_virama = False + idx += 1 + continue + + # 6. VS16 (U+FE0F): converts preceding narrow character to wide. + if ucs == 0xFE0F and last_measured_idx >= 0: + if _vs16_narrower and _bisearch(last_measured_ucs, _vs16_narrower): + pass + elif _bisearch(last_measured_ucs, vs16_nw_table): + cluster_width = 2 + last_measured_idx = -2 # prevent double application + idx += 1 + continue + + # VS15 (U+FE0E): text variation selector, requests narrow presentation. + if ucs == 0xFE0E and last_measured_idx >= 0: + base_ucs = last_measured_ucs + vs15_narrow = bisearch(base_ucs, vs15_wn_table) + if _vs15_wider and bisearch(base_ucs, _vs15_wider): + vs15_narrow = False + if vs15_narrow and last_measured_w == 2: + current_col -= 1 + max_extent = max(_max_extent_before, current_col) + idx += 1 + continue + + # 7. Regional Indicator & Fitzpatrick (both above BMP) + if ucs > 0xFFFF: + if ucs in _REGIONAL_INDICATOR_SET: + ri_before = 0 + j = idx - 1 + while j >= 0 and ord(text[j]) in _REGIONAL_INDICATOR_SET: + ri_before += 1 + j -= 1 + if ri_before % 2 == 1: + last_measured_ucs = ucs + idx += 1 + continue + elif (_FITZPATRICK_RANGE[0] <= ucs <= _FITZPATRICK_RANGE[1] + and last_measured_ucs in _EMOJI_ZWJ_SET): + idx += 1 + continue + + # 8. Normal character: measure with wcwidth + w = _wcwidth(char) + # Apply single-codepoint terminal overrides (pre-merged tuples) + if w == 2 and _narrower and bisearch(ucs, _narrower): + w = 1 + elif w == 2 and _zeroer and bisearch(ucs, _zeroer): + w = 0 + if w == 1 and _narrow_wider and bisearch(ucs, _narrow_wider): + w = 2 + elif w == 1 and _narrow_zeroer and bisearch(ucs, _narrow_zeroer): + w = 0 + if w > 0: + # virama+consonant extends current cluster; otherwise start new + if prev_was_virama: + cluster_width = 2 + elif cluster_width: + # flush previous cluster, check for grapheme overrides + flushed = False + if _grapheme_overrides and cluster_start >= 0: + # Two-phase override lookup (see _wcswidth.py) + candidate = text[cluster_start:idx + 1] + override_w = _grapheme_overrides.get(candidate) + if override_w is not None: + current_col = col_before_cluster + override_w + max_extent = max(max_extent_before_cluster, current_col) + flushed = True + cluster_width = 0 + else: + cluster_text = text[cluster_start:idx] + override_w = _grapheme_overrides.get(cluster_text) + if override_w is not None: + current_col = col_before_cluster + override_w + max_extent = max(max_extent_before_cluster, current_col) + else: + current_col += cluster_width + else: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + if not flushed: + cluster_width = w + cluster_start = idx + col_before_cluster = current_col + max_extent_before_cluster = max_extent + else: + cluster_width = w + cluster_start = idx + col_before_cluster = current_col + max_extent_before_cluster = max_extent + last_measured_idx = idx + last_measured_ucs = ucs + last_measured_w = w + _max_extent_before = max_extent + prev_was_virama = False + elif ucs in _ISC_VIRAMA_SET: + prev_was_virama = True + elif last_measured_idx >= 0 and _bisearch(ucs, _CATEGORY_MC_TABLE): + # Spacing Combining Mark (Mc) following a base character + cluster_width = 2 + last_measured_idx = -2 + prev_was_virama = False + else: + prev_was_virama = False + idx += 1 + + if cluster_width: + if _grapheme_overrides and cluster_start >= 0: + cluster_text = text[cluster_start:text_len] + override_w = _grapheme_overrides.get(cluster_text) + if override_w is not None: + current_col = col_before_cluster + override_w + max_extent = max(max_extent_before_cluster, current_col) + else: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + else: + current_col += cluster_width + if current_col > max_extent: + max_extent = current_col + return max_extent diff --git a/libs/wcwidth/align.py b/libs/wcwidth/align.py new file mode 100644 index 000000000..efe5acdd0 --- /dev/null +++ b/libs/wcwidth/align.py @@ -0,0 +1,165 @@ +"""Python grapheme, emoji, and sequence-aware ljust, rjust, center().""" +from __future__ import annotations + +from typing import Literal + +# local +from ._width import width + + +def ljust( + text: str, + dest_width: int, + fillchar: str = ' ', + *, + control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', + ambiguous_width: int = 1, + term_program: bool | str = False, +) -> str: + r""" + Return text left-justified in a string of given display width. + + :param text: String to justify, may contain terminal sequences. + :param dest_width: Total display width of result in terminal cells. + :param fillchar: Single character for padding (default space). Must have + display width of 1 (not wide, not zero-width, not combining). Unicode + characters like ``'·'`` are acceptable. The width is not validated. + :param control_codes: How to handle control sequences when measuring. + Passed to :func:`width` for measurement. + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 + :returns: Text padded on the right to reach ``dest_width``. + + .. versionadded:: 0.3.0 + + Example:: + + >>> wcwidth.ljust('hi', 5) + 'hi ' + >>> wcwidth.ljust('\x1b[31mhi\x1b[0m', 5) + '\x1b[31mhi\x1b[0m ' + >>> wcwidth.ljust('\U0001F468\u200D\U0001F469\u200D\U0001F467', 6) + '👨‍👩‍👧 ' + """ + if text.isascii() and text.isprintable(): + text_width = len(text) + else: + text_width = width(text, control_codes=control_codes, ambiguous_width=ambiguous_width, + term_program=term_program) + padding_cells = max(0, dest_width - text_width) + return text + fillchar * padding_cells + + +def rjust( + text: str, + dest_width: int, + fillchar: str = ' ', + *, + control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', + ambiguous_width: int = 1, + term_program: bool | str = False, +) -> str: + r""" + Return text right-justified in a string of given display width. + + :param text: String to justify, may contain terminal sequences. + :param dest_width: Total display width of result in terminal cells. + :param fillchar: Single character for padding (default space). Must have + display width of 1 (not wide, not zero-width, not combining). Unicode + characters like ``'·'`` are acceptable. The width is not validated. + :param control_codes: How to handle control sequences when measuring. + Passed to :func:`width` for measurement. + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 + :returns: Text padded on the left to reach ``dest_width``. + + .. versionadded:: 0.3.0 + + Example:: + + >>> wcwidth.rjust('hi', 5) + ' hi' + >>> wcwidth.rjust('\x1b[31mhi\x1b[0m', 5) + ' \x1b[31mhi\x1b[0m' + >>> wcwidth.rjust('\U0001F468\u200D\U0001F469\u200D\U0001F467', 6) + ' 👨‍👩‍👧' + """ + if text.isascii() and text.isprintable(): + text_width = len(text) + else: + text_width = width(text, control_codes=control_codes, ambiguous_width=ambiguous_width, + term_program=term_program) + padding_cells = max(0, dest_width - text_width) + return fillchar * padding_cells + text + + +def center( + text: str, + dest_width: int, + fillchar: str = ' ', + *, + control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', + ambiguous_width: int = 1, + term_program: bool | str = False, +) -> str: + r""" + Return text centered in a string of given display width. + + :param text: String to center, may contain terminal sequences. + :param dest_width: Total display width of result in terminal cells. + :param fillchar: Single character for padding (default space). Must have + display width of 1 (not wide, not zero-width, not combining). Unicode + characters like ``'·'`` are acceptable. The width is not validated. + :param control_codes: How to handle control sequences when measuring. + Passed to :func:`width` for measurement. + :param ambiguous_width: Width to use for East Asian Ambiguous (A) + characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 + :returns: Text padded on both sides to reach ``dest_width``. + + For odd-width padding, the extra cell fills in the same cell position as + Python's :meth:`str.center` behavior (the left side when ``dest_width`` is + odd, the right side when ``dest_width`` is even). + See `the eccentric str.center `_. + + .. versionadded:: 0.3.0 + + Example:: + + >>> wcwidth.center('hi', 6) + ' hi ' + >>> wcwidth.center('\x1b[31mhi\x1b[0m', 6) + ' \x1b[31mhi\x1b[0m ' + >>> wcwidth.center('\U0001F468\u200D\U0001F469\u200D\U0001F467', 6) + ' 👨‍👩‍👧 ' + """ + if text.isascii() and text.isprintable(): + text_width = len(text) + else: + text_width = width(text, control_codes=control_codes, ambiguous_width=ambiguous_width, + term_program=term_program) + total_padding = max(0, dest_width - text_width) + # matching https://jazcap53.github.io/pythons-eccentric-strcenter.html + left_pad = total_padding // 2 + (total_padding & dest_width & 1) + right_pad = total_padding - left_pad + return fillchar * left_pad + text + fillchar * right_pad diff --git a/libs/wcwidth/bisearch.py b/libs/wcwidth/bisearch.py index becfe86a9..e95c51b8d 100644 --- a/libs/wcwidth/bisearch.py +++ b/libs/wcwidth/bisearch.py @@ -1,4 +1,5 @@ """Binary search function for Unicode interval tables.""" + from __future__ import annotations @@ -7,8 +8,8 @@ def bisearch(ucs: int, table: tuple[tuple[int, int], ...]) -> int: Binary search in interval table. :param ucs: Ordinal value of unicode character. - :param table: Tuple of starting and ending ranges of ordinal values, - in form of ``((start, end), ...)``. + :param table: Tuple of starting and ending ranges of ordinal values, in form of ``((start, end), + ...)``. :returns: 1 if ordinal value ucs is found within lookup table, else 0. """ lbound = 0 diff --git a/libs/wcwidth/escape_sequences.py b/libs/wcwidth/escape_sequences.py index d4ac6cc36..77d2b9b3c 100644 --- a/libs/wcwidth/escape_sequences.py +++ b/libs/wcwidth/escape_sequences.py @@ -5,15 +5,27 @@ This module provides regex patterns for matching terminal escape sequences. All sequences that begin with ESC (``\x1b``). Before calling re.match with these patterns, callers should first check that the character at the current position is ESC for optimal performance. """ + # std imports import re +import typing + +# local +from .sgr_state import _SGR_PATTERN + +# Text Sizing Protocol (OSC 66), https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ +TEXT_SIZING_PATTERN = re.compile( + r'\x1b\]66;([^;\x07\x1b]*);([^\x07\x1b]*)(\x07|\x1b\\)' +) + # Zero-width escape sequences (SGR, OSC, CSI, etc.). This table, like INDETERMINATE_EFFECT_SEQUENCE, # originated from the 'blessed' library. ZERO_WIDTH_PATTERN = re.compile( # CSI sequences r'\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]|' - # OSC sequences + # OSC sequences, note that text sizing protocol (OSC 66) is special case in width() and clip(), + # and contrary to the variable name, it is positive width. r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|' # APC sequences r'\x1b_[^\x1b\x07]*(?:\x07|\x1b\\)|' @@ -21,12 +33,16 @@ ZERO_WIDTH_PATTERN = re.compile( r'\x1bP[^\x1b\x07]*(?:\x07|\x1b\\)|' # PM sequences r'\x1b\^[^\x1b\x07]*(?:\x07|\x1b\\)|' - # Character set designation + # Character set designation (subset of nF, handled separately for clarity) r'\x1b[()].|' - # Fe sequences + # nF sequences: ESC + one or more intermediate bytes (0x20-0x2F) + final byte (0x30-0x7E) + r'\x1b[\x20-\x2f]+[\x30-\x7e]|' + # Fe sequences (C1 controls) r'\x1b[\x40-\x5f]|' - # Fp sequences - r'\x1b[78=>g]' + # Fp sequences (private use) + r'\x1b[\x30-\x3f]|' + # Fs sequences (independent functions) + r'\x1b[\x60-\x7e]' ) # Cursor right movement: CSI [n] C, parameter may be parsed by width() @@ -35,6 +51,31 @@ CURSOR_RIGHT_SEQUENCE = re.compile(r'\x1b\[(\d*)C') # Cursor left movement: CSI [n] D, parameter may be parsed by width() CURSOR_LEFT_SEQUENCE = re.compile(r'\x1b\[(\d*)D') +# Horizontal position absolute: CSI [n] G, parameter may be parsed by width() +CURSOR_HPA_SEQUENCE = re.compile(r'\x1b\[(\d*)G') + +# Combined cursor movement: single regex for fast-path detection of any +# horizontal cursor movement (left, right, hpa). Avoids two separate search() +# calls in hot-path width() and clip() pre-checks. +CURSOR_MOVEMENT_SEQUENCE = re.compile(r'\x1b\[(\d*)[CDG]') + +# Combined horizontal cursor movement: matches BS, CR, and CSI C/D/G cursor sequences +# in a single regex pass. Used by clip() to decide between the simple append path +# and the painter's algorithm. +_HORIZONTAL_CURSOR_MOVEMENT = re.compile(r'[\x08\r]|\x1b\[(\d*)[CDG]') + +# Combined pattern: a single regex that matches any zero-width escape sequence +# and classifies it via named groups, aprox 2x faster than redundant re.matches +# in clip() and width(). +_SEQUENCE_CLASSIFY = re.compile( + _SGR_PATTERN.pattern.replace('(', '(?P', 1) + + '|' + CURSOR_HPA_SEQUENCE.pattern.replace('(', '(?P', 1) + + '|' + CURSOR_RIGHT_SEQUENCE.pattern.replace('(', '(?P', 1) + + '|' + CURSOR_LEFT_SEQUENCE.pattern.replace('(', '(?P', 1) + + '|' + r'\x1b\]66;(?P[^;\x07\x1b]*);(?P[^\x07\x1b]*)(?P\x07|\x1b\\)' + + '|' + r'(?P(?:' + ZERO_WIDTH_PATTERN.pattern + '))' +) + # Indeterminate effect sequences - raise ValueError in 'strict' mode. The effects of these sequences # are likely to be undesirable, moving the cursor vertically or to any unknown position, and # otherwise not managed by the 'width' method of this library. @@ -47,7 +88,6 @@ INDETERMINATE_EFFECT_SEQUENCE = re.compile( r'\x1b\[\d+;\d+r', # change_scroll_region r'\x1b\[\d*K', # erase_in_line (clr_eol, clr_bol) r'\x1b\[\d*J', # erase_in_display (clr_eos, erase_display) - r'\x1b\[\d*G', # column_address r'\x1b\[\d+;\d+H', # cursor_address r'\x1b\[\d*H', # cursor_home r'\x1b\[\d*A', # cursor_up @@ -65,5 +105,90 @@ INDETERMINATE_EFFECT_SEQUENCE = re.compile( r'\x1b8', # restore_cursor r'\x1bD', # scroll_forward (index) r'\x1bM', # scroll_reverse (reverse index) + r'\x1bc', # full_reset (RIS) )) ) + + +def iter_sequences(text: str) -> typing.Iterator[typing.Tuple[str, bool]]: + r""" + Iterate through text, yielding segments with sequence identification. + + This generator yields tuples of ``(segment, is_sequence)`` for each part + of the input text, where ``is_sequence`` is ``True`` if the segment is + a recognized terminal escape sequence. + + :param text: String to iterate through. + :returns: Iterator of (segment, is_sequence) tuples. + + .. versionadded:: 0.3.0 + + Example:: + + >>> list(iter_sequences('hello')) + [('hello', False)] + >>> list(iter_sequences('\x1b[31mred')) + [('\x1b[31m', True), ('red', False)] + >>> list(iter_sequences('\x1b[1m\x1b[31m')) + [('\x1b[1m', True), ('\x1b[31m', True)] + """ + idx = 0 + text_len = len(text) + segment_start = 0 + + while idx < text_len: + char = text[idx] + + if char == '\x1b': + # Yield any accumulated non-sequence text + if idx > segment_start: + yield (text[segment_start:idx], False) + + # Try to match an escape sequence + match = ZERO_WIDTH_PATTERN.match(text, idx) + if match: + yield (match.group(), True) + idx = match.end() + else: + # Lone ESC or unrecognized - yield as sequence anyway + yield (char, True) + idx += 1 + segment_start = idx + else: + idx += 1 + + # Yield any remaining text + if segment_start < text_len: + yield (text[segment_start:], False) + + +def strip_sequences(text: str) -> str: + r""" + Return text with all terminal escape sequences removed. + + Unknown or incomplete ESC sequences are preserved. + + :param text: String that may contain terminal escape sequences. + :returns: The input text with all escape sequences stripped. + + .. versionadded:: 0.3.0 + + .. versionchanged:: 0.7.0 + Inner text of OSC 66 (Text sizing protocol) is preserved. + + Example:: + + >>> strip_sequences('\x1b[31mred\x1b[0m') + 'red' + >>> strip_sequences('hello') + 'hello' + >>> strip_sequences('\x1b[1m\x1b[31mbold red\x1b[0m text') + 'bold red text' + >>> strip_sequences('\x1b]66;s=2;hello\x07') + 'hello' + >>> strip_sequences('\x1b]8;id=34;https://example.com\x1b\\[view]\x1b]8;;\x1b\\') + '[view]' + """ + if '\x1b]66;' in text: + text = TEXT_SIZING_PATTERN.sub(r'\2', text) + return ZERO_WIDTH_PATTERN.sub('', text) diff --git a/libs/wcwidth/grapheme.py b/libs/wcwidth/grapheme.py index 7befc9205..9ecfb672e 100644 --- a/libs/wcwidth/grapheme.py +++ b/libs/wcwidth/grapheme.py @@ -10,11 +10,17 @@ https://www.unicode.org/reports/tr29/ from __future__ import annotations # std imports +import sys +import unicodedata from enum import IntEnum from functools import lru_cache -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Optional, NamedTuple +__lazy_modules__ = [ + "wcwidth.bisearch", + "wcwidth.table_grapheme", +] # local from .bisearch import bisearch as _bisearch from .table_grapheme import (GRAPHEME_L, @@ -36,6 +42,12 @@ if TYPE_CHECKING: # pragma: no cover # std imports from collections.abc import Iterator +# check for python 3.15 for new iter_graphemes() function +_HAS_PYTHON315_ITER_GRAPHEMES = ( + sys.version_info >= (3, 15) + and hasattr(unicodedata, 'iter_graphemes') +) + # Maximum backward scan distance when finding grapheme cluster boundaries. # Covers all known Unicode grapheme clusters with margin; longer sequences are pathological. MAX_GRAPHEME_SCAN = 32 @@ -130,7 +142,7 @@ class BreakResult(NamedTuple): @lru_cache(maxsize=1024) -def _simple_break_check(prev_gcb: GCB, curr_gcb: GCB) -> BreakResult | None: +def _simple_break_check(prev_gcb: GCB, curr_gcb: GCB) -> Optional[BreakResult]: """ Check simple GCB-pair-based break rules (cacheable). @@ -245,13 +257,13 @@ def _should_break( return BreakResult(should_break=True, ri_count=ri_count) -def iter_graphemes( +def _iter_graphemes_stdlib( unistr: str, start: int = 0, - end: int | None = None, + end: Optional[int] = None, ) -> Iterator[str]: r""" - Iterate over grapheme clusters in a Unicode string. + Iterate over grapheme clusters using :func:`unicodedata.iter_graphemes`. Grapheme clusters are "user-perceived characters" - what a user would consider a single character, which may consist of multiple Unicode @@ -266,9 +278,55 @@ def iter_graphemes( >>> list(iter_graphemes('cafe\u0301')) ['c', 'a', 'f', 'e\u0301'] - >>> list(iter_graphemes('\U0001F468\u200D\U0001F469\u200D\U0001F467')) + >>> list(iter_graphemes('ok\U0001F468\u200D\U0001F469\u200D\U0001F467')) ['o', 'k', '\U0001F468\u200D\U0001F469\u200D\U0001F467'] - >>> list(iter_graphemes('\U0001F1FA\U0001F1F8')) + >>> list(iter_graphemes('ok\U0001F1FA\U0001F1F8')) + ['o', 'k', '\U0001F1FA\U0001F1F8'] + + .. versionadded:: 0.3.0 + """ + if not unistr: + return + + length = len(unistr) + + if end is None: + end = length + + if start >= end or start >= length: + return + + end = min(end, length) + + full_segment = unistr[start:end] + for seg in unicodedata.iter_graphemes(full_segment): # type: ignore[attr-defined] # pylint: disable=no-member + yield full_segment[seg.start:seg.end] + + +def _iter_graphemes_python( + unistr: str, + start: int = 0, + end: int | None = None, +) -> Iterator[str]: + r""" + Iterate over grapheme clusters using :func:`unicodedata.iter_graphemes`. + + Grapheme clusters are "user-perceived characters" - what a user would + consider a single character, which may consist of multiple Unicode + codepoints (e.g., a base character with combining marks, emoji sequences). + + :param unistr: The Unicode string to segment. + :param start: Starting index (default 0). + :param end: Ending index (default len(unistr)). + :yields: Grapheme cluster substrings. + + Example:: + + >>> list(iter_graphemes('cafe\u0301')) + ['c', 'a', 'f', 'e\u0301'] + >>> list(iter_graphemes('ok\U0001F468\u200D\U0001F469\u200D\U0001F467')) + ['o', 'k', '\U0001F468\u200D\U0001F469\u200D\U0001F467'] + >>> list(iter_graphemes('ok\U0001F1FA\U0001F1F8')) ['o', 'k', '\U0001F1FA\U0001F1F8'] .. versionadded:: 0.3.0 @@ -390,7 +448,7 @@ def grapheme_boundary_before(unistr: str, pos: int) -> int: def iter_graphemes_reverse( unistr: str, start: int = 0, - end: int | None = None, + end: Optional[int] = None, ) -> Iterator[str]: r""" Iterate over grapheme clusters in reverse order (last to first). @@ -426,3 +484,10 @@ def iter_graphemes_reverse( break yield unistr[cluster_start:pos] pos = cluster_start + + +# Bind iter_graphemes at module level to avoid per-call dispatch overhead. +iter_graphemes = ( + _iter_graphemes_stdlib if _HAS_PYTHON315_ITER_GRAPHEMES + else _iter_graphemes_python +) diff --git a/libs/wcwidth/hyperlink.py b/libs/wcwidth/hyperlink.py new file mode 100644 index 000000000..da7a3aa08 --- /dev/null +++ b/libs/wcwidth/hyperlink.py @@ -0,0 +1,142 @@ +""" +OSC 8 hyperlink parsing and measurement. + +.. versionadded:: 0.7.0 +""" + +from __future__ import annotations + +# std imports +import re + +import typing + +# local +from ._width import width as _width +from .escape_sequences import _SEQUENCE_CLASSIFY + +HYPERLINK_OPEN_RE = re.compile(r'\x1b]8;([^;]*);([^\x07\x1b]*)(\x07|\x1b\\)') +HYPERLINK_CLOSE_RE = re.compile(r'\x1b]8;;(\x07|\x1b\\)') + + +class HyperlinkParams(typing.NamedTuple): + r""" + Parsed parameters from an OSC 8 hyperlink open sequence. + + :param url: The hyperlink URL. + :param params: Colon-separated metadata string (often empty). + :param terminator: Sequence terminator (``\x07`` or ``\x1b\\``). + """ + + url: str + params: str = '' + terminator: str = '\x07' + + @classmethod + def parse(cls, seq: str) -> HyperlinkParams | None: + r""" + Parse an OSC 8 open sequence string. + + Returns ``None`` if *seq* is not a valid OSC 8 open. + + Example:: + + >>> HyperlinkParams.parse('\x1b]8;;http://example.com\x07') + HyperlinkParams(url='http://example.com', params='', terminator='\\x07') + """ + m = HYPERLINK_OPEN_RE.match(seq) + if m is None: + return None + return cls(url=m.group(2), params=m.group(1), terminator=m.group(3)) + + def make_open(self) -> str: + """Generate the OSC 8 open escape sequence.""" + return f'\x1b]8;{self.params};{self.url}{self.terminator}' + + def make_close(self) -> str: + """Generate the OSC 8 close escape sequence.""" + return f'\x1b]8;;{self.terminator}' + + +class Hyperlink(typing.NamedTuple): + """ + A complete OSC 8 hyperlink with target and inner text. + + :param params: Parsed open sequence parameters. + :param text: Inner text between the open and close sequences. + """ + + params: HyperlinkParams + text: str + + @classmethod + def find_close(cls, text: str, open_end: int) -> tuple[int, int]: + """ + Find the matching OSC 8 close sequence. + + Searches 'text' starting at 'open_end', the position just past the open + sequence. Returns position of close sequence ``(close_start, + close_end)`` or ``(-1, -1)`` if not found. + + Per the OSC 8 specification, terminal emulators treat hyperlinks as a + state attribute, not as nested HTML anchors. A close sequence closes + the current hyperlink regardless of how many open sequences preceded it. + """ + m = HYPERLINK_CLOSE_RE.search(text, open_end) + if m is None: + return (-1, -1) + return (m.start(), m.end()) + + def display_width( + self, + *, + control_codes: typing.Literal['parse', 'strict', 'ignore'] = 'parse', + tabsize: int = 8, + ambiguous_width: int = 1, + ) -> int: + r""" + Measure the display width of the hyperlink's inner text. + + Delegates to :func:`wcwidth.width` with the given parameters. + + Example:: + + >>> hl = Hyperlink.parse('\x1b]8;;http://example.com\x07Hello\x1b]8;;\x07', 0) + >>> hl.display_width() + 5 + """ + return _width( + self.text, + control_codes=control_codes, + tabsize=tabsize, + ambiguous_width=ambiguous_width, + ) + + @classmethod + def parse(cls, text: str, start: int = 0) -> Hyperlink | None: + r""" + Parse a complete OSC 8 hyperlink unit from *text* at position *start*. + + Locates the open sequence, finds the matching close, and returns a + ``Hyperlink`` containing the parsed parameters and inner text. Returns + ``None`` if the text at *start* is not a complete OSC 8 hyperlink. + + Example:: + + >>> Hyperlink.parse('\x1b]8;;http://example.com\x07Hello\x1b]8;;\x07') + Hyperlink(params=HyperlinkParams(url='http://example.com', ...), text='Hello') + """ + m = _SEQUENCE_CLASSIFY.match(text, start) + if m is None: + return None + params = HyperlinkParams.parse(m.group()) + if params is None: + return None + close_start, close_end = cls.find_close(text, m.end()) + if (close_start, close_end) == (-1, -1): + return None + return cls(params=params, text=text[m.end():close_start]) + + def make_sequence(self) -> str: + """Rebuild the complete OSC 8 hyperlink escape sequence.""" + return self.params.make_open() + self.text + self.params.make_close() diff --git a/libs/wcwidth/sgr_state.py b/libs/wcwidth/sgr_state.py index b0c864843..8e6e5ccfe 100644 --- a/libs/wcwidth/sgr_state.py +++ b/libs/wcwidth/sgr_state.py @@ -5,6 +5,7 @@ This module provides functions for tracking and propagating terminal styling (bo etc.) via public API propagate_sgr(), and its dependent functions, cut() and wrap(). It only has attributes necessary to perform its functions, eg 'RED' and 'BLUE' attributes are not defined. """ + from __future__ import annotations # std imports @@ -307,7 +308,7 @@ def propagate_sgr(lines: Sequence[str]) -> list[str]: ['\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m'] This is useful in cases of making special editors and viewers, and is used for the - default modes (propagate_sgr=True) of :func:`wcwidth.width` and :func:`wcwidth.clip`. + default modes (propagate_sgr=True) of :func:`wcwidth.wrap` and :func:`wcwidth.clip`. When wrapping and clipping text containing SGR sequences, maybe a previous line enabled the BLUE color--if we are viewing *only* the line following, we would want the carry over the BLUE color, diff --git a/libs/wcwidth/table_ambiguous.py b/libs/wcwidth/table_ambiguous.py index e3dc0b1c3..d2fdd6b87 100644 --- a/libs/wcwidth/table_ambiguous.py +++ b/libs/wcwidth/table_ambiguous.py @@ -1,7 +1,7 @@ """ Exports AMBIGUOUS_EASTASIAN table keyed by supporting unicode version level. -This code generated by wcwidth/bin/update-tables.py on 2026-01-18 23:27:15 UTC. +This code generated by python wcwidth project. """ # pylint: disable=duplicate-code AMBIGUOUS_EASTASIAN = { diff --git a/libs/wcwidth/table_grapheme.py b/libs/wcwidth/table_grapheme.py index 42fd19e03..55e573ab2 100644 --- a/libs/wcwidth/table_grapheme.py +++ b/libs/wcwidth/table_grapheme.py @@ -4,7 +4,7 @@ Exports grapheme cluster break property tables for Unicode version 17.0.0. This module provides lookup tables for Unicode grapheme cluster break properties as defined in UAX #29: Unicode Text Segmentation. -This code generated by wcwidth/bin/update-tables.py on 2026-01-29 23:33:42 UTC. +This code generated by python wcwidth project. """ # pylint: disable=duplicate-code @@ -2292,3 +2292,56 @@ ISC_CONSONANT = ( (0x16101, 0x1611d,), # Gurung Khema Letter Ka ..Gurung Khema Letter Sa (0x16d43, 0x16d62,), # Kirat Rai Letter A ..Kirat Rai Letter Ha ) + +ISC_VIRAMA = ( + # Source: IndicSyllabicCategory + # Date: see file + # + (0x0094d, 0x0094d,), # Devanagari Sign Virama + (0x009cd, 0x009cd,), # Bengali Sign Virama + (0x00a4d, 0x00a4d,), # Gurmukhi Sign Virama + (0x00acd, 0x00acd,), # Gujarati Sign Virama + (0x00b4d, 0x00b4d,), # Oriya Sign Virama + (0x00bcd, 0x00bcd,), # Tamil Sign Virama + (0x00c4d, 0x00c4d,), # Telugu Sign Virama + (0x00ccd, 0x00ccd,), # Kannada Sign Virama + (0x00d4d, 0x00d4d,), # Malayalam Sign Virama + (0x00dca, 0x00dca,), # Sinhala Sign Al-lakuna + (0x01b44, 0x01b44,), # Balinese Adeg Adeg + (0x0a806, 0x0a806,), # Syloti Nagri Sign Hasanta + (0x0a8c4, 0x0a8c4,), # Saurashtra Sign Virama + (0x0a9c0, 0x0a9c0,), # Javanese Pangkon + (0x11046, 0x11046,), # Brahmi Virama + (0x110b9, 0x110b9,), # Kaithi Sign Virama + (0x111c0, 0x111c0,), # Sharada Sign Virama + (0x11235, 0x11235,), # Khojki Sign Virama + (0x1134d, 0x1134d,), # Grantha Sign Virama + (0x11442, 0x11442,), # Newa Sign Virama + (0x114c2, 0x114c2,), # Tirhuta Sign Virama + (0x115bf, 0x115bf,), # Siddham Sign Virama + (0x1163f, 0x1163f,), # Modi Sign Virama + (0x116b6, 0x116b6,), # Takri Sign Virama + (0x11839, 0x11839,), # Dogra Sign Virama + (0x119e0, 0x119e0,), # Nandinagari Sign Virama + (0x11c3f, 0x11c3f,), # Bhaiksuki Sign Virama +) + +ISC_INVISIBLE_STACKER = ( + # Source: IndicSyllabicCategory + # Date: see file + # + (0x01039, 0x01039,), # Myanmar Sign Virama + (0x017d2, 0x017d2,), # Khmer Sign Coeng + (0x01a60, 0x01a60,), # Tai Tham Sign Sakot + (0x01bab, 0x01bab,), # Sundanese Sign Virama + (0x0aaf6, 0x0aaf6,), # Meetei Mayek Virama + (0x10a3f, 0x10a3f,), # Kharoshthi Virama + (0x11133, 0x11133,), # Chakma Virama + (0x113d0, 0x113d0,), # Tulu-tigalari Conjoiner + (0x1193e, 0x1193e,), # Dives Akuru Virama + (0x11a47, 0x11a47,), # Zanabazar Square Subjoiner + (0x11a99, 0x11a99,), # Soyombo Subjoiner + (0x11d45, 0x11d45,), # Masaram Gondi Virama + (0x11d97, 0x11d97,), # Gunjala Gondi Virama + (0x11f42, 0x11f42,), # Kawi Conjoiner +) diff --git a/libs/wcwidth/table_grapheme_overrides/__init__.py b/libs/wcwidth/table_grapheme_overrides/__init__.py new file mode 100644 index 000000000..beee01189 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/__init__.py @@ -0,0 +1,37 @@ +""" +Lazy-loading per-terminal grapheme override tables. + +This minimizes memory for the 99.9% of use-cases of a single 'term_program'. +""" +from __future__ import annotations + +# std imports +import importlib +from functools import lru_cache + +# local +from ._registry import _REGISTRY + + +@lru_cache(maxsize=32) +def get(term_canonical: str) -> dict[str, int]: + """ + Return grapheme override dict for a terminal, or an empty dict. + + The per-terminal module is imported on first access and cached in ``sys.modules``; subsequent + calls for the same terminal return immediately via lru_cache. + """ + hash_key = _REGISTRY.get(term_canonical) + if hash_key is None: + return {} + + try: + mod = importlib.import_module(f'._known_{hash_key}', __package__) + result: dict[str, int] = getattr(mod, 'GRAPHEMES') + return result + except ImportError: + # This can occur during a program re-install when the registry and files are out of sync + # (filesystem vs. in-memory copy differ), this can happen with a "pip upgrade" of a running + # service, or if the upgrade tool itself uses wcwidth. Not being able to provide terminal + # overrides in this situation is not important, continue without them. + return {} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_27e0693f.py b/libs/wcwidth/table_grapheme_overrides/_known_27e0693f.py new file mode 100644 index 000000000..f7df06fc7 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_27e0693f.py @@ -0,0 +1,2677 @@ +""" +Grapheme overrides for _known_27e0693f. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: warp +""" +# pylint: skip-file +GRAPHEMES = { + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কা': 1, + 'কা‌': 1, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খা': 1, + 'গ্রে': 3, + 'ঘা': 1, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চা': 1, + 'চ্ছে': 3, + 'ছা': 1, + 'জা': 1, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'টা': 1, + 'ট্রে': 3, + 'ড়া': 1, + 'ণা': 1, + 'ণ্ডে': 3, + 'তা': 1, + 'তাঁ': 1, + 'তা‌': 1, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'থা': 1, + 'দা': 1, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্রো': 3, + 'ধা': 1, + 'ধ্যে': 3, + 'না': 1, + 'না‌': 1, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্যে': 3, + 'পা': 1, + 'পা‌': 1, + 'বা': 1, + 'বা‌': 1, + 'ব্ধি': 3, + 'ভা': 1, + 'মা': 1, + 'মা‌': 1, + 'য়া': 1, + 'যা': 1, + 'রা': 1, + 'র্থে': 3, + 'র্দো': 3, + 'র্নী': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'লা': 1, + 'ল্যে': 3, + 'ল্লে': 3, + 'শা': 1, + 'শ্চি': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'ষা': 1, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সা': 1, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থ্য': 3, + 'স্বী': 3, + 'স্যে': 3, + 'হা': 1, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'கா': 1, + 'சா': 1, + 'டா': 1, + 'ணா': 1, + 'தா': 1, + 'நா': 1, + 'னா': 1, + 'பா': 1, + 'மா': 1, + 'யா': 1, + 'ரா': 1, + 'றா': 1, + 'லா': 1, + 'ளா': 1, + 'வா': 1, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕೀ': 1, + 'ಕುಂ': 3, + 'ಕೂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೀ': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೋ': 1, + 'ಚೀ': 1, + 'ಜೀ': 1, + 'ಟುಂ': 3, + 'ಟೇ': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣೈ': 1, + 'ತೀ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ದೂ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಾಂ': 3, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭೀ': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮೀ': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯುಂ': 3, + 'ಯೂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಾಂ': 3, + 'ರೀ': 1, + 'ರೂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲೀ': 1, + 'ಲೂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳೂ': 1, + 'ವೀ': 1, + 'ವುಂ': 3, + 'ವೂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾಂ': 3, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷೇ': 1, + 'ಸಾಂ': 3, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೋ': 1, + 'കാ': 1, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഗ്ഗം': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ചാ': 1, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജാ': 1, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ടാ': 1, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണാ': 1, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'താ': 1, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദാ': 1, + 'ദ്ധി': 3, + 'ധാ': 1, + 'നാ': 1, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്യം': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പാ': 1, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'ബാ': 1, + 'ബ്ലി': 3, + 'ഭാ': 1, + 'മാ': 1, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യാ': 1, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യി': 3, + 'രാ': 1, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ലാ': 1, + 'ല്ലാം': 3, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ളാ': 1, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വാ': 1, + 'വ്യം': 3, + 'ശാ': 1, + 'ഷ്ട്യാ': 3, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'സാ': 1, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വീ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹാ': 1, + 'ഹിം': 3, + 'කා': 1, + 'ගා': 1, + 'චා': 1, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ඨා': 1, + 'ඩා': 1, + 'තා': 1, + 'ථා': 1, + 'දා': 1, + 'ධා': 1, + 'නා': 1, + 'ඳා': 1, + 'පා': 1, + 'බා': 1, + 'භා': 1, + 'මා': 1, + 'යා': 1, + 'රා': 1, + 'ලා': 1, + 'වා': 1, + 'ෂා': 1, + 'සා': 1, + 'හා': 1, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 5, + '⛹️‍♀️': 5, + '⛹️‍♂️': 5, + '⛹🏻‍♀️': 6, + '⛹🏻‍♂️': 6, + '⛹🏼‍♀️': 6, + '⛹🏼‍♂️': 6, + '⛹🏽‍♀️': 6, + '⛹🏽‍♂️': 6, + '⛹🏾‍♀️': 6, + '⛹🏾‍♂️': 6, + '⛹🏿‍♀️': 6, + '⛹🏿‍♂️': 6, + '❤️‍🔥': 5, + '❤️‍🩹': 5, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾': 1, + '𑌕𑍍': 2, + '𑌗𑌾': 1, + '𑌗𑍍': 2, + '𑌘𑌾': 1, + '𑌚𑌾': 1, + '𑌚𑍍': 2, + '𑌛𑌾': 1, + '𑌜𑌾': 1, + '𑌜𑍍': 2, + '𑌞𑌾': 1, + '𑌞𑍍': 2, + '𑌟𑌾': 1, + '𑌟𑍍': 2, + '𑌠𑌾': 1, + '𑌠𑍈𑌃': 3, + '𑌣𑌾': 1, + '𑌣𑍍': 2, + '𑌤𑌾': 1, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾': 1, + '𑌦𑌾': 1, + '𑌦𑍍': 2, + '𑌧𑌾': 1, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾': 1, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑌾': 1, + '𑌪𑍍': 2, + '𑌬𑌾': 1, + '𑌬𑍍': 2, + '𑌭𑌾': 1, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌾': 1, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾': 1, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾': 1, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾': 1, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾': 1, + '𑌵𑍍': 2, + '𑌶𑌾': 1, + '𑌶𑍍': 2, + '𑌷𑌾': 1, + '𑌷𑍍': 2, + '𑌸𑌾': 1, + '𑌸𑍍': 2, + '𑌹𑌾': 1, + '🍄‍🟫': 5, + '🍋‍🟩': 5, + '🏃‍♀️': 5, + '🏃‍♀️‍➡️': 8, + '🏃‍♂️': 5, + '🏃‍♂️‍➡️': 8, + '🏃‍➡️': 5, + '🏃🏻‍♀️': 7, + '🏃🏻‍♀️‍➡️': 10, + '🏃🏻‍♂️': 7, + '🏃🏻‍♂️‍➡️': 10, + '🏃🏻‍➡️': 7, + '🏃🏼‍♀️': 7, + '🏃🏼‍♀️‍➡️': 10, + '🏃🏼‍♂️': 7, + '🏃🏼‍♂️‍➡️': 10, + '🏃🏼‍➡️': 7, + '🏃🏽‍♀️': 7, + '🏃🏽‍♀️‍➡️': 10, + '🏃🏽‍♂️': 7, + '🏃🏽‍♂️‍➡️': 10, + '🏃🏽‍➡️': 7, + '🏃🏾‍♀️': 7, + '🏃🏾‍♀️‍➡️': 10, + '🏃🏾‍♂️': 7, + '🏃🏾‍♂️‍➡️': 10, + '🏃🏾‍➡️': 7, + '🏃🏿‍♀️': 7, + '🏃🏿‍♀️‍➡️': 10, + '🏃🏿‍♂️': 7, + '🏃🏿‍♂️‍➡️': 10, + '🏃🏿‍➡️': 7, + '🏄‍♀️': 5, + '🏄‍♂️': 5, + '🏄🏻‍♀️': 7, + '🏄🏻‍♂️': 7, + '🏄🏼‍♀️': 7, + '🏄🏼‍♂️': 7, + '🏄🏽‍♀️': 7, + '🏄🏽‍♂️': 7, + '🏄🏾‍♀️': 7, + '🏄🏾‍♂️': 7, + '🏄🏿‍♀️': 7, + '🏄🏿‍♂️': 7, + '🏊‍♀️': 5, + '🏊‍♂️': 5, + '🏊🏻‍♀️': 7, + '🏊🏻‍♂️': 7, + '🏊🏼‍♀️': 7, + '🏊🏼‍♂️': 7, + '🏊🏽‍♀️': 7, + '🏊🏽‍♂️': 7, + '🏊🏾‍♀️': 7, + '🏊🏾‍♂️': 7, + '🏊🏿‍♀️': 7, + '🏊🏿‍♂️': 7, + '🏋️‍♀️': 5, + '🏋️‍♂️': 5, + '🏋🏻‍♀️': 6, + '🏋🏻‍♂️': 6, + '🏋🏼‍♀️': 6, + '🏋🏼‍♂️': 6, + '🏋🏽‍♀️': 6, + '🏋🏽‍♂️': 6, + '🏋🏾‍♀️': 6, + '🏋🏾‍♂️': 6, + '🏋🏿‍♀️': 6, + '🏋🏿‍♂️': 6, + '🏌️‍♀️': 5, + '🏌️‍♂️': 5, + '🏌🏻‍♀️': 6, + '🏌🏻‍♂️': 6, + '🏌🏼‍♀️': 6, + '🏌🏼‍♂️': 6, + '🏌🏽‍♀️': 6, + '🏌🏽‍♂️': 6, + '🏌🏾‍♀️': 6, + '🏌🏾‍♂️': 6, + '🏌🏿‍♀️': 6, + '🏌🏿‍♂️': 6, + '🏳️‍⚧️': 5, + '🏳️‍🌈': 5, + '🏴‍☠️': 5, + '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 3, + '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 3, + '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 3, + '🐈‍⬛': 5, + '🐕‍🦺': 5, + '🐦‍⬛': 5, + '🐦‍🔥': 5, + '🐻‍❄️': 5, + '👁️‍🗨️': 5, + '👨‍⚕️': 5, + '👨‍⚖️': 5, + '👨‍✈️': 5, + '👨‍❤️‍👨': 8, + '👨‍❤️‍💋‍👨': 11, + '👨‍🌾': 5, + '👨‍🍳': 5, + '👨‍🍼': 5, + '👨‍🎓': 5, + '👨‍🎤': 5, + '👨‍🎨': 5, + '👨‍🏫': 5, + '👨‍🏭': 5, + '👨‍👦': 5, + '👨‍👦‍👦': 8, + '👨‍👧': 5, + '👨‍👧‍👦': 8, + '👨‍👧‍👧': 8, + '👨‍👨‍👦': 8, + '👨‍👨‍👦‍👦': 11, + '👨‍👨‍👧': 8, + '👨‍👨‍👧‍👦': 11, + '👨‍👨‍👧‍👧': 11, + '👨‍👩‍👦': 8, + '👨‍👩‍👦‍👦': 11, + '👨‍👩‍👧': 8, + '👨‍👩‍👧‍👦': 11, + '👨‍👩‍👧‍👧': 11, + '👨‍💻': 5, + '👨‍💼': 5, + '👨‍🔧': 5, + '👨‍🔬': 5, + '👨‍🚀': 5, + '👨‍🚒': 5, + '👨‍🦯': 5, + '👨‍🦯‍➡️': 8, + '👨‍🦰': 5, + '👨‍🦱': 5, + '👨‍🦲': 5, + '👨‍🦳': 5, + '👨‍🦼': 5, + '👨‍🦼‍➡️': 8, + '👨‍🦽': 5, + '👨‍🦽‍➡️': 8, + '👨🏻‍⚕️': 7, + '👨🏻‍⚖️': 7, + '👨🏻‍✈️': 7, + '👨🏻‍❤️‍👨🏻': 12, + '👨🏻‍❤️‍👨🏽': 12, + '👨🏻‍❤️‍👨🏿': 12, + '👨🏻‍❤️‍💋‍👨🏻': 15, + '👨🏻‍❤️‍💋‍👨🏽': 15, + '👨🏻‍❤️‍💋‍👨🏾': 15, + '👨🏻‍❤️‍💋‍👨🏿': 15, + '👨🏻‍🌾': 7, + '👨🏻‍🍳': 7, + '👨🏻‍🍼': 7, + '👨🏻‍🎓': 7, + '👨🏻‍🎤': 7, + '👨🏻‍🎨': 7, + '👨🏻‍🏫': 7, + '👨🏻‍🏭': 7, + '👨🏻‍🐰‍👨🏼': 12, + '👨🏻‍🐰‍👨🏽': 12, + '👨🏻‍🐰‍👨🏾': 12, + '👨🏻‍🐰‍👨🏿': 12, + '👨🏻‍💻': 7, + '👨🏻‍💼': 7, + '👨🏻‍🔧': 7, + '👨🏻‍🔬': 7, + '👨🏻‍🚀': 7, + '👨🏻‍🚒': 7, + '👨🏻‍🤝‍👨🏼': 12, + '👨🏻‍🤝‍👨🏽': 12, + '👨🏻‍🤝‍👨🏾': 12, + '👨🏻‍🤝‍👨🏿': 12, + '👨🏻‍🦯': 7, + '👨🏻‍🦯‍➡️': 10, + '👨🏻‍🦰': 7, + '👨🏻‍🦱': 7, + '👨🏻‍🦲': 7, + '👨🏻‍🦳': 7, + '👨🏻‍🦼': 7, + '👨🏻‍🦼‍➡️': 10, + '👨🏻‍🦽': 7, + '👨🏻‍🦽‍➡️': 10, + '👨🏻‍🫯‍👨🏼': 10, + '👨🏻‍🫯‍👨🏽': 10, + '👨🏻‍🫯‍👨🏾': 10, + '👨🏻‍🫯‍👨🏿': 10, + '👨🏼‍⚕️': 7, + '👨🏼‍⚖️': 7, + '👨🏼‍✈️': 7, + '👨🏼‍❤️‍👨🏼': 12, + '👨🏼‍❤️‍💋‍👨🏼': 15, + '👨🏼‍❤️‍💋‍👨🏿': 15, + '👨🏼‍🌾': 7, + '👨🏼‍🍳': 7, + '👨🏼‍🍼': 7, + '👨🏼‍🎓': 7, + '👨🏼‍🎤': 7, + '👨🏼‍🎨': 7, + '👨🏼‍🏫': 7, + '👨🏼‍🏭': 7, + '👨🏼‍🐰‍👨🏻': 12, + '👨🏼‍🐰‍👨🏽': 12, + '👨🏼‍🐰‍👨🏿': 12, + '👨🏼‍💻': 7, + '👨🏼‍💼': 7, + '👨🏼‍🔧': 7, + '👨🏼‍🔬': 7, + '👨🏼‍🚀': 7, + '👨🏼‍🚒': 7, + '👨🏼‍🤝‍👨🏻': 12, + '👨🏼‍🤝‍👨🏽': 12, + '👨🏼‍🤝‍👨🏿': 12, + '👨🏼‍🦯': 7, + '👨🏼‍🦯‍➡️': 10, + '👨🏼‍🦰': 7, + '👨🏼‍🦱': 7, + '👨🏼‍🦲': 7, + '👨🏼‍🦳': 7, + '👨🏼‍🦼': 7, + '👨🏼‍🦽': 7, + '👨🏼‍🦽‍➡️': 10, + '👨🏼‍🫯‍👨🏻': 10, + '👨🏼‍🫯‍👨🏽': 10, + '👨🏼‍🫯‍👨🏾': 10, + '👨🏼‍🫯‍👨🏿': 10, + '👨🏽‍⚕️': 7, + '👨🏽‍⚖️': 7, + '👨🏽‍✈️': 7, + '👨🏽‍❤️‍👨🏻': 12, + '👨🏽‍❤️‍👨🏽': 12, + '👨🏽‍❤️‍👨🏿': 12, + '👨🏽‍❤️‍💋‍👨🏻': 15, + '👨🏽‍❤️‍💋‍👨🏽': 15, + '👨🏽‍❤️‍💋‍👨🏿': 15, + '👨🏽‍🌾': 7, + '👨🏽‍🍳': 7, + '👨🏽‍🍼': 7, + '👨🏽‍🎓': 7, + '👨🏽‍🎤': 7, + '👨🏽‍🎨': 7, + '👨🏽‍🏫': 7, + '👨🏽‍🏭': 7, + '👨🏽‍🐰‍👨🏻': 12, + '👨🏽‍🐰‍👨🏼': 12, + '👨🏽‍🐰‍👨🏾': 12, + '👨🏽‍🐰‍👨🏿': 12, + '👨🏽‍💻': 7, + '👨🏽‍💼': 7, + '👨🏽‍🔧': 7, + '👨🏽‍🔬': 7, + '👨🏽‍🚀': 7, + '👨🏽‍🚒': 7, + '👨🏽‍🤝‍👨🏻': 12, + '👨🏽‍🤝‍👨🏼': 12, + '👨🏽‍🤝‍👨🏾': 12, + '👨🏽‍🤝‍👨🏿': 12, + '👨🏽‍🦯': 7, + '👨🏽‍🦯‍➡️': 10, + '👨🏽‍🦰': 7, + '👨🏽‍🦱': 7, + '👨🏽‍🦲': 7, + '👨🏽‍🦳': 7, + '👨🏽‍🦼': 7, + '👨🏽‍🦼‍➡️': 10, + '👨🏽‍🦽': 7, + '👨🏽‍🦽‍➡️': 10, + '👨🏽‍🫯‍👨🏻': 10, + '👨🏽‍🫯‍👨🏼': 10, + '👨🏽‍🫯‍👨🏾': 10, + '👨🏽‍🫯‍👨🏿': 10, + '👨🏾‍⚕️': 7, + '👨🏾‍⚖️': 7, + '👨🏾‍✈️': 7, + '👨🏾‍❤️‍👨🏾': 12, + '👨🏾‍❤️‍💋‍👨🏻': 15, + '👨🏾‍❤️‍💋‍👨🏾': 15, + '👨🏾‍❤️‍💋‍👨🏿': 15, + '👨🏾‍🌾': 7, + '👨🏾‍🍳': 7, + '👨🏾‍🍼': 7, + '👨🏾‍🎓': 7, + '👨🏾‍🎤': 7, + '👨🏾‍🎨': 7, + '👨🏾‍🏫': 7, + '👨🏾‍🏭': 7, + '👨🏾‍🐰‍👨🏻': 12, + '👨🏾‍🐰‍👨🏽': 12, + '👨🏾‍🐰‍👨🏿': 12, + '👨🏾‍💻': 7, + '👨🏾‍💼': 7, + '👨🏾‍🔧': 7, + '👨🏾‍🔬': 7, + '👨🏾‍🚀': 7, + '👨🏾‍🚒': 7, + '👨🏾‍🤝‍👨🏻': 12, + '👨🏾‍🤝‍👨🏽': 12, + '👨🏾‍🤝‍👨🏿': 12, + '👨🏾‍🦯': 7, + '👨🏾‍🦯‍➡️': 10, + '👨🏾‍🦰': 7, + '👨🏾‍🦱': 7, + '👨🏾‍🦲': 7, + '👨🏾‍🦳': 7, + '👨🏾‍🦼': 7, + '👨🏾‍🦽': 7, + '👨🏾‍🦽‍➡️': 10, + '👨🏾‍🫯‍👨🏻': 10, + '👨🏾‍🫯‍👨🏼': 10, + '👨🏾‍🫯‍👨🏽': 10, + '👨🏾‍🫯‍👨🏿': 10, + '👨🏿‍⚕️': 7, + '👨🏿‍⚖️': 7, + '👨🏿‍✈️': 7, + '👨🏿‍❤️‍👨🏻': 12, + '👨🏿‍❤️‍👨🏽': 12, + '👨🏿‍❤️‍👨🏿': 12, + '👨🏿‍❤️‍💋‍👨🏻': 15, + '👨🏿‍❤️‍💋‍👨🏼': 15, + '👨🏿‍❤️‍💋‍👨🏽': 15, + '👨🏿‍❤️‍💋‍👨🏾': 15, + '👨🏿‍❤️‍💋‍👨🏿': 15, + '👨🏿‍🌾': 7, + '👨🏿‍🍳': 7, + '👨🏿‍🍼': 7, + '👨🏿‍🎓': 7, + '👨🏿‍🎤': 7, + '👨🏿‍🎨': 7, + '👨🏿‍🏫': 7, + '👨🏿‍🏭': 7, + '👨🏿‍🐰‍👨🏻': 12, + '👨🏿‍🐰‍👨🏼': 12, + '👨🏿‍🐰‍👨🏽': 12, + '👨🏿‍🐰‍👨🏾': 12, + '👨🏿‍💻': 7, + '👨🏿‍💼': 7, + '👨🏿‍🔧': 7, + '👨🏿‍🔬': 7, + '👨🏿‍🚀': 7, + '👨🏿‍🚒': 7, + '👨🏿‍🤝‍👨🏻': 12, + '👨🏿‍🤝‍👨🏼': 12, + '👨🏿‍🤝‍👨🏽': 12, + '👨🏿‍🤝‍👨🏾': 12, + '👨🏿‍🦯': 7, + '👨🏿‍🦯‍➡️': 10, + '👨🏿‍🦰': 7, + '👨🏿‍🦱': 7, + '👨🏿‍🦲': 7, + '👨🏿‍🦳': 7, + '👨🏿‍🦼': 7, + '👨🏿‍🦼‍➡️': 10, + '👨🏿‍🦽': 7, + '👨🏿‍🦽‍➡️': 10, + '👨🏿‍🫯‍👨🏻': 10, + '👨🏿‍🫯‍👨🏼': 10, + '👨🏿‍🫯‍👨🏽': 10, + '👨🏿‍🫯‍👨🏾': 10, + '👩‍⚕️': 5, + '👩‍⚖️': 5, + '👩‍✈️': 5, + '👩‍❤️‍👨': 8, + '👩‍❤️‍👩': 8, + '👩‍❤️‍💋‍👨': 11, + '👩‍❤️‍💋‍👩': 11, + '👩‍🌾': 5, + '👩‍🍳': 5, + '👩‍🍼': 5, + '👩‍🎓': 5, + '👩‍🎤': 5, + '👩‍🎨': 5, + '👩‍🏫': 5, + '👩‍🏭': 5, + '👩‍👦': 5, + '👩‍👦‍👦': 8, + '👩‍👧': 5, + '👩‍👧‍👦': 8, + '👩‍👧‍👧': 8, + '👩‍👩‍👦': 8, + '👩‍👩‍👦‍👦': 11, + '👩‍👩‍👧': 8, + '👩‍👩‍👧‍👦': 11, + '👩‍👩‍👧‍👧': 11, + '👩‍💻': 5, + '👩‍💼': 5, + '👩‍🔧': 5, + '👩‍🔬': 5, + '👩‍🚀': 5, + '👩‍🚒': 5, + '👩‍🦯': 5, + '👩‍🦯‍➡️': 8, + '👩‍🦰': 5, + '👩‍🦱': 5, + '👩‍🦲': 5, + '👩‍🦳': 5, + '👩‍🦼': 5, + '👩‍🦼‍➡️': 8, + '👩‍🦽': 5, + '👩‍🦽‍➡️': 8, + '👩🏻‍⚕️': 7, + '👩🏻‍⚖️': 7, + '👩🏻‍✈️': 7, + '👩🏻‍❤️‍👨🏻': 12, + '👩🏻‍❤️‍👨🏿': 12, + '👩🏻‍❤️‍👩🏻': 12, + '👩🏻‍❤️‍💋‍👨🏻': 15, + '👩🏻‍❤️‍💋‍👨🏽': 15, + '👩🏻‍❤️‍💋‍👨🏿': 15, + '👩🏻‍❤️‍💋‍👩🏻': 15, + '👩🏻‍❤️‍💋‍👩🏽': 15, + '👩🏻‍❤️‍💋‍👩🏿': 15, + '👩🏻‍🌾': 7, + '👩🏻‍🍳': 7, + '👩🏻‍🍼': 7, + '👩🏻‍🎓': 7, + '👩🏻‍🎤': 7, + '👩🏻‍🎨': 7, + '👩🏻‍🏫': 7, + '👩🏻‍🏭': 7, + '👩🏻‍🐰‍👩🏼': 12, + '👩🏻‍🐰‍👩🏽': 12, + '👩🏻‍🐰‍👩🏾': 12, + '👩🏻‍🐰‍👩🏿': 12, + '👩🏻‍💻': 7, + '👩🏻‍💼': 7, + '👩🏻‍🔧': 7, + '👩🏻‍🔬': 7, + '👩🏻‍🚀': 7, + '👩🏻‍🚒': 7, + '👩🏻‍🤝‍👨🏽': 12, + '👩🏻‍🤝‍👨🏿': 12, + '👩🏻‍🤝‍👩🏼': 12, + '👩🏻‍🤝‍👩🏽': 12, + '👩🏻‍🤝‍👩🏾': 12, + '👩🏻‍🤝‍👩🏿': 12, + '👩🏻‍🦯': 7, + '👩🏻‍🦯‍➡️': 10, + '👩🏻‍🦰': 7, + '👩🏻‍🦱': 7, + '👩🏻‍🦲': 7, + '👩🏻‍🦳': 7, + '👩🏻‍🦼': 7, + '👩🏻‍🦼‍➡️': 10, + '👩🏻‍🦽': 7, + '👩🏻‍🦽‍➡️': 10, + '👩🏻‍🫯‍👩🏼': 10, + '👩🏻‍🫯‍👩🏽': 10, + '👩🏻‍🫯‍👩🏾': 10, + '👩🏻‍🫯‍👩🏿': 10, + '👩🏼‍⚕️': 7, + '👩🏼‍⚖️': 7, + '👩🏼‍✈️': 7, + '👩🏼‍❤️‍👨🏼': 12, + '👩🏼‍❤️‍👩🏼': 12, + '👩🏼‍❤️‍💋‍👨🏼': 15, + '👩🏼‍❤️‍💋‍👩🏼': 15, + '👩🏼‍🌾': 7, + '👩🏼‍🍳': 7, + '👩🏼‍🍼': 7, + '👩🏼‍🎓': 7, + '👩🏼‍🎤': 7, + '👩🏼‍🎨': 7, + '👩🏼‍🏫': 7, + '👩🏼‍🏭': 7, + '👩🏼‍🐰‍👩🏻': 12, + '👩🏼‍🐰‍👩🏿': 12, + '👩🏼‍💻': 7, + '👩🏼‍💼': 7, + '👩🏼‍🔧': 7, + '👩🏼‍🔬': 7, + '👩🏼‍🚀': 7, + '👩🏼‍🚒': 7, + '👩🏼‍🤝‍👩🏻': 12, + '👩🏼‍🤝‍👩🏽': 12, + '👩🏼‍🤝‍👩🏿': 12, + '👩🏼‍🦯': 7, + '👩🏼‍🦯‍➡️': 10, + '👩🏼‍🦰': 7, + '👩🏼‍🦱': 7, + '👩🏼‍🦲': 7, + '👩🏼‍🦳': 7, + '👩🏼‍🦼': 7, + '👩🏼‍🦽': 7, + '👩🏼‍🫯‍👩🏻': 10, + '👩🏼‍🫯‍👩🏽': 10, + '👩🏼‍🫯‍👩🏾': 10, + '👩🏼‍🫯‍👩🏿': 10, + '👩🏽‍⚕️': 7, + '👩🏽‍⚖️': 7, + '👩🏽‍✈️': 7, + '👩🏽‍❤️‍👨🏽': 12, + '👩🏽‍❤️‍👩🏽': 12, + '👩🏽‍❤️‍💋‍👨🏻': 15, + '👩🏽‍❤️‍💋‍👨🏽': 15, + '👩🏽‍❤️‍💋‍👨🏿': 15, + '👩🏽‍❤️‍💋‍👩🏻': 15, + '👩🏽‍❤️‍💋‍👩🏽': 15, + '👩🏽‍❤️‍💋‍👩🏿': 15, + '👩🏽‍🌾': 7, + '👩🏽‍🍳': 7, + '👩🏽‍🍼': 7, + '👩🏽‍🎓': 7, + '👩🏽‍🎤': 7, + '👩🏽‍🎨': 7, + '👩🏽‍🏫': 7, + '👩🏽‍🏭': 7, + '👩🏽‍🐰‍👩🏻': 12, + '👩🏽‍🐰‍👩🏾': 12, + '👩🏽‍🐰‍👩🏿': 12, + '👩🏽‍💻': 7, + '👩🏽‍💼': 7, + '👩🏽‍🔧': 7, + '👩🏽‍🔬': 7, + '👩🏽‍🚀': 7, + '👩🏽‍🚒': 7, + '👩🏽‍🤝‍👨🏻': 12, + '👩🏽‍🤝‍👨🏿': 12, + '👩🏽‍🤝‍👩🏻': 12, + '👩🏽‍🤝‍👩🏼': 12, + '👩🏽‍🤝‍👩🏾': 12, + '👩🏽‍🤝‍👩🏿': 12, + '👩🏽‍🦯': 7, + '👩🏽‍🦯‍➡️': 10, + '👩🏽‍🦰': 7, + '👩🏽‍🦱': 7, + '👩🏽‍🦲': 7, + '👩🏽‍🦳': 7, + '👩🏽‍🦼': 7, + '👩🏽‍🦼‍➡️': 10, + '👩🏽‍🦽': 7, + '👩🏽‍🦽‍➡️': 10, + '👩🏽‍🫯‍👩🏻': 10, + '👩🏽‍🫯‍👩🏼': 10, + '👩🏽‍🫯‍👩🏾': 10, + '👩🏽‍🫯‍👩🏿': 10, + '👩🏾‍⚕️': 7, + '👩🏾‍⚖️': 7, + '👩🏾‍✈️': 7, + '👩🏾‍❤️‍👨🏾': 12, + '👩🏾‍❤️‍👩🏾': 12, + '👩🏾‍❤️‍💋‍👨🏾': 15, + '👩🏾‍❤️‍💋‍👩🏾': 15, + '👩🏾‍🌾': 7, + '👩🏾‍🍳': 7, + '👩🏾‍🍼': 7, + '👩🏾‍🎓': 7, + '👩🏾‍🎤': 7, + '👩🏾‍🎨': 7, + '👩🏾‍🏫': 7, + '👩🏾‍🏭': 7, + '👩🏾‍🐰‍👩🏻': 12, + '👩🏾‍🐰‍👩🏽': 12, + '👩🏾‍🐰‍👩🏿': 12, + '👩🏾‍💻': 7, + '👩🏾‍💼': 7, + '👩🏾‍🔧': 7, + '👩🏾‍🔬': 7, + '👩🏾‍🚀': 7, + '👩🏾‍🚒': 7, + '👩🏾‍🤝‍👩🏻': 12, + '👩🏾‍🤝‍👩🏽': 12, + '👩🏾‍🤝‍👩🏿': 12, + '👩🏾‍🦯': 7, + '👩🏾‍🦯‍➡️': 10, + '👩🏾‍🦰': 7, + '👩🏾‍🦱': 7, + '👩🏾‍🦲': 7, + '👩🏾‍🦳': 7, + '👩🏾‍🦼': 7, + '👩🏾‍🦽': 7, + '👩🏾‍🦽‍➡️': 10, + '👩🏾‍🫯‍👩🏻': 10, + '👩🏾‍🫯‍👩🏼': 10, + '👩🏾‍🫯‍👩🏽': 10, + '👩🏾‍🫯‍👩🏿': 10, + '👩🏿‍⚕️': 7, + '👩🏿‍⚖️': 7, + '👩🏿‍✈️': 7, + '👩🏿‍❤️‍👨🏻': 12, + '👩🏿‍❤️‍👨🏿': 12, + '👩🏿‍❤️‍👩🏿': 12, + '👩🏿‍❤️‍💋‍👨🏻': 15, + '👩🏿‍❤️‍💋‍👨🏽': 15, + '👩🏿‍❤️‍💋‍👨🏿': 15, + '👩🏿‍❤️‍💋‍👩🏻': 15, + '👩🏿‍❤️‍💋‍👩🏽': 15, + '👩🏿‍❤️‍💋‍👩🏿': 15, + '👩🏿‍🌾': 7, + '👩🏿‍🍳': 7, + '👩🏿‍🍼': 7, + '👩🏿‍🎓': 7, + '👩🏿‍🎤': 7, + '👩🏿‍🎨': 7, + '👩🏿‍🏫': 7, + '👩🏿‍🏭': 7, + '👩🏿‍🐰‍👩🏻': 12, + '👩🏿‍🐰‍👩🏼': 12, + '👩🏿‍🐰‍👩🏽': 12, + '👩🏿‍🐰‍👩🏾': 12, + '👩🏿‍💻': 7, + '👩🏿‍💼': 7, + '👩🏿‍🔧': 7, + '👩🏿‍🔬': 7, + '👩🏿‍🚀': 7, + '👩🏿‍🚒': 7, + '👩🏿‍🤝‍👨🏻': 12, + '👩🏿‍🤝‍👨🏽': 12, + '👩🏿‍🤝‍👩🏻': 12, + '👩🏿‍🤝‍👩🏼': 12, + '👩🏿‍🤝‍👩🏽': 12, + '👩🏿‍🤝‍👩🏾': 12, + '👩🏿‍🦯': 7, + '👩🏿‍🦯‍➡️': 10, + '👩🏿‍🦰': 7, + '👩🏿‍🦱': 7, + '👩🏿‍🦲': 7, + '👩🏿‍🦳': 7, + '👩🏿‍🦼': 7, + '👩🏿‍🦼‍➡️': 10, + '👩🏿‍🦽': 7, + '👩🏿‍🦽‍➡️': 10, + '👩🏿‍🫯‍👩🏻': 10, + '👩🏿‍🫯‍👩🏼': 10, + '👩🏿‍🫯‍👩🏽': 10, + '👩🏿‍🫯‍👩🏾': 10, + '👮‍♀️': 5, + '👮‍♂️': 5, + '👮🏻‍♀️': 7, + '👮🏻‍♂️': 7, + '👮🏼‍♀️': 7, + '👮🏼‍♂️': 7, + '👮🏽‍♀️': 7, + '👮🏽‍♂️': 7, + '👮🏾‍♀️': 7, + '👮🏾‍♂️': 7, + '👮🏿‍♀️': 7, + '👮🏿‍♂️': 7, + '👯‍♀️': 5, + '👯‍♂️': 5, + '👯🏻‍♀️': 7, + '👯🏻‍♂️': 7, + '👯🏼‍♀️': 7, + '👯🏼‍♂️': 7, + '👯🏽‍♀️': 7, + '👯🏽‍♂️': 7, + '👯🏾‍♀️': 7, + '👯🏾‍♂️': 7, + '👯🏿‍♀️': 7, + '👯🏿‍♂️': 7, + '👰‍♀️': 5, + '👰‍♂️': 5, + '👰🏻‍♀️': 7, + '👰🏻‍♂️': 7, + '👰🏼‍♀️': 7, + '👰🏼‍♂️': 7, + '👰🏽‍♀️': 7, + '👰🏽‍♂️': 7, + '👰🏾‍♀️': 7, + '👰🏾‍♂️': 7, + '👰🏿‍♀️': 7, + '👰🏿‍♂️': 7, + '👱‍♀️': 5, + '👱‍♂️': 5, + '👱🏻‍♀️': 7, + '👱🏻‍♂️': 7, + '👱🏼‍♀️': 7, + '👱🏼‍♂️': 7, + '👱🏽‍♀️': 7, + '👱🏽‍♂️': 7, + '👱🏾‍♀️': 7, + '👱🏾‍♂️': 7, + '👱🏿‍♀️': 7, + '👱🏿‍♂️': 7, + '👳‍♀️': 5, + '👳‍♂️': 5, + '👳🏻‍♀️': 7, + '👳🏻‍♂️': 7, + '👳🏼‍♀️': 7, + '👳🏼‍♂️': 7, + '👳🏽‍♀️': 7, + '👳🏽‍♂️': 7, + '👳🏾‍♀️': 7, + '👳🏾‍♂️': 7, + '👳🏿‍♀️': 7, + '👳🏿‍♂️': 7, + '👷‍♀️': 5, + '👷‍♂️': 5, + '👷🏻‍♀️': 7, + '👷🏻‍♂️': 7, + '👷🏼‍♀️': 7, + '👷🏼‍♂️': 7, + '👷🏽‍♀️': 7, + '👷🏽‍♂️': 7, + '👷🏾‍♀️': 7, + '👷🏾‍♂️': 7, + '👷🏿‍♀️': 7, + '👷🏿‍♂️': 7, + '💁‍♀️': 5, + '💁‍♂️': 5, + '💁🏻‍♀️': 7, + '💁🏻‍♂️': 7, + '💁🏼‍♀️': 7, + '💁🏼‍♂️': 7, + '💁🏽‍♀️': 7, + '💁🏽‍♂️': 7, + '💁🏾‍♀️': 7, + '💁🏾‍♂️': 7, + '💁🏿‍♀️': 7, + '💁🏿‍♂️': 7, + '💂‍♀️': 5, + '💂‍♂️': 5, + '💂🏻‍♀️': 7, + '💂🏻‍♂️': 7, + '💂🏼‍♀️': 7, + '💂🏼‍♂️': 7, + '💂🏽‍♀️': 7, + '💂🏽‍♂️': 7, + '💂🏾‍♀️': 7, + '💂🏾‍♂️': 7, + '💂🏿‍♀️': 7, + '💂🏿‍♂️': 7, + '💆‍♀️': 5, + '💆‍♂️': 5, + '💆🏻‍♀️': 7, + '💆🏻‍♂️': 7, + '💆🏼‍♀️': 7, + '💆🏼‍♂️': 7, + '💆🏽‍♀️': 7, + '💆🏽‍♂️': 7, + '💆🏾‍♀️': 7, + '💆🏾‍♂️': 7, + '💆🏿‍♀️': 7, + '💆🏿‍♂️': 7, + '💇‍♀️': 5, + '💇‍♂️': 5, + '💇🏻‍♀️': 7, + '💇🏻‍♂️': 7, + '💇🏼‍♀️': 7, + '💇🏼‍♂️': 7, + '💇🏽‍♀️': 7, + '💇🏽‍♂️': 7, + '💇🏾‍♀️': 7, + '💇🏾‍♂️': 7, + '💇🏿‍♀️': 7, + '💇🏿‍♂️': 7, + '🕵️‍♀️': 5, + '🕵️‍♂️': 5, + '🕵🏻‍♀️': 6, + '🕵🏻‍♂️': 6, + '🕵🏼‍♀️': 6, + '🕵🏼‍♂️': 6, + '🕵🏽‍♀️': 6, + '🕵🏽‍♂️': 6, + '🕵🏾‍♀️': 6, + '🕵🏾‍♂️': 6, + '🕵🏿‍♀️': 6, + '🕵🏿‍♂️': 6, + '😮‍💨': 5, + '😵‍💫': 5, + '😶‍🌫️': 5, + '🙂‍↔️': 5, + '🙂‍↕️': 5, + '🙅‍♀️': 5, + '🙅‍♂️': 5, + '🙅🏻‍♀️': 7, + '🙅🏻‍♂️': 7, + '🙅🏼‍♀️': 7, + '🙅🏼‍♂️': 7, + '🙅🏽‍♀️': 7, + '🙅🏽‍♂️': 7, + '🙅🏾‍♀️': 7, + '🙅🏾‍♂️': 7, + '🙅🏿‍♀️': 7, + '🙅🏿‍♂️': 7, + '🙆‍♀️': 5, + '🙆‍♂️': 5, + '🙆🏻‍♀️': 7, + '🙆🏻‍♂️': 7, + '🙆🏼‍♀️': 7, + '🙆🏼‍♂️': 7, + '🙆🏽‍♀️': 7, + '🙆🏽‍♂️': 7, + '🙆🏾‍♀️': 7, + '🙆🏾‍♂️': 7, + '🙆🏿‍♀️': 7, + '🙆🏿‍♂️': 7, + '🙇‍♀️': 5, + '🙇‍♂️': 5, + '🙇🏻‍♀️': 7, + '🙇🏻‍♂️': 7, + '🙇🏼‍♀️': 7, + '🙇🏼‍♂️': 7, + '🙇🏽‍♀️': 7, + '🙇🏽‍♂️': 7, + '🙇🏾‍♀️': 7, + '🙇🏾‍♂️': 7, + '🙇🏿‍♀️': 7, + '🙇🏿‍♂️': 7, + '🙋‍♀️': 5, + '🙋‍♂️': 5, + '🙋🏻‍♀️': 7, + '🙋🏻‍♂️': 7, + '🙋🏼‍♀️': 7, + '🙋🏼‍♂️': 7, + '🙋🏽‍♀️': 7, + '🙋🏽‍♂️': 7, + '🙋🏾‍♀️': 7, + '🙋🏾‍♂️': 7, + '🙋🏿‍♀️': 7, + '🙋🏿‍♂️': 7, + '🙍‍♀️': 5, + '🙍‍♂️': 5, + '🙍🏻‍♀️': 7, + '🙍🏻‍♂️': 7, + '🙍🏼‍♀️': 7, + '🙍🏼‍♂️': 7, + '🙍🏽‍♀️': 7, + '🙍🏽‍♂️': 7, + '🙍🏾‍♀️': 7, + '🙍🏾‍♂️': 7, + '🙍🏿‍♀️': 7, + '🙍🏿‍♂️': 7, + '🙎‍♀️': 5, + '🙎‍♂️': 5, + '🙎🏻‍♀️': 7, + '🙎🏻‍♂️': 7, + '🙎🏼‍♀️': 7, + '🙎🏼‍♂️': 7, + '🙎🏽‍♀️': 7, + '🙎🏽‍♂️': 7, + '🙎🏾‍♀️': 7, + '🙎🏾‍♂️': 7, + '🙎🏿‍♀️': 7, + '🙎🏿‍♂️': 7, + '🚣‍♀️': 5, + '🚣‍♂️': 5, + '🚣🏻‍♀️': 7, + '🚣🏻‍♂️': 7, + '🚣🏼‍♀️': 7, + '🚣🏼‍♂️': 7, + '🚣🏽‍♀️': 7, + '🚣🏽‍♂️': 7, + '🚣🏾‍♀️': 7, + '🚣🏾‍♂️': 7, + '🚣🏿‍♀️': 7, + '🚣🏿‍♂️': 7, + '🚴‍♀️': 5, + '🚴‍♂️': 5, + '🚴🏻‍♀️': 7, + '🚴🏻‍♂️': 7, + '🚴🏼‍♀️': 7, + '🚴🏼‍♂️': 7, + '🚴🏽‍♀️': 7, + '🚴🏽‍♂️': 7, + '🚴🏾‍♀️': 7, + '🚴🏾‍♂️': 7, + '🚴🏿‍♀️': 7, + '🚴🏿‍♂️': 7, + '🚵‍♀️': 5, + '🚵‍♂️': 5, + '🚵🏻‍♀️': 7, + '🚵🏻‍♂️': 7, + '🚵🏼‍♀️': 7, + '🚵🏼‍♂️': 7, + '🚵🏽‍♀️': 7, + '🚵🏽‍♂️': 7, + '🚵🏾‍♀️': 7, + '🚵🏾‍♂️': 7, + '🚵🏿‍♀️': 7, + '🚵🏿‍♂️': 7, + '🚶‍♀️': 5, + '🚶‍♀️‍➡️': 8, + '🚶‍♂️': 5, + '🚶‍♂️‍➡️': 8, + '🚶‍➡️': 5, + '🚶🏻‍♀️': 7, + '🚶🏻‍♀️‍➡️': 10, + '🚶🏻‍♂️': 7, + '🚶🏻‍♂️‍➡️': 10, + '🚶🏻‍➡️': 7, + '🚶🏼‍♀️': 7, + '🚶🏼‍♀️‍➡️': 10, + '🚶🏼‍♂️': 7, + '🚶🏼‍♂️‍➡️': 10, + '🚶🏼‍➡️': 7, + '🚶🏽‍♀️': 7, + '🚶🏽‍♀️‍➡️': 10, + '🚶🏽‍♂️': 7, + '🚶🏽‍♂️‍➡️': 10, + '🚶🏽‍➡️': 7, + '🚶🏾‍♀️': 7, + '🚶🏾‍♀️‍➡️': 10, + '🚶🏾‍♂️': 7, + '🚶🏾‍♂️‍➡️': 10, + '🚶🏾‍➡️': 7, + '🚶🏿‍♀️': 7, + '🚶🏿‍♀️‍➡️': 10, + '🚶🏿‍♂️': 7, + '🚶🏿‍♂️‍➡️': 10, + '🚶🏿‍➡️': 7, + '🤦‍♀️': 5, + '🤦‍♂️': 5, + '🤦🏻‍♀️': 7, + '🤦🏻‍♂️': 7, + '🤦🏼‍♀️': 7, + '🤦🏼‍♂️': 7, + '🤦🏽‍♀️': 7, + '🤦🏽‍♂️': 7, + '🤦🏾‍♀️': 7, + '🤦🏾‍♂️': 7, + '🤦🏿‍♀️': 7, + '🤦🏿‍♂️': 7, + '🤵‍♀️': 5, + '🤵‍♂️': 5, + '🤵🏻‍♀️': 7, + '🤵🏻‍♂️': 7, + '🤵🏼‍♀️': 7, + '🤵🏼‍♂️': 7, + '🤵🏽‍♀️': 7, + '🤵🏽‍♂️': 7, + '🤵🏾‍♀️': 7, + '🤵🏾‍♂️': 7, + '🤵🏿‍♀️': 7, + '🤵🏿‍♂️': 7, + '🤷‍♀️': 5, + '🤷‍♂️': 5, + '🤷🏻‍♀️': 7, + '🤷🏻‍♂️': 7, + '🤷🏼‍♀️': 7, + '🤷🏼‍♂️': 7, + '🤷🏽‍♀️': 7, + '🤷🏽‍♂️': 7, + '🤷🏾‍♀️': 7, + '🤷🏾‍♂️': 7, + '🤷🏿‍♀️': 7, + '🤷🏿‍♂️': 7, + '🤸‍♀️': 5, + '🤸‍♂️': 5, + '🤸🏻‍♀️': 7, + '🤸🏻‍♂️': 7, + '🤸🏼‍♀️': 7, + '🤸🏼‍♂️': 7, + '🤸🏽‍♀️': 7, + '🤸🏽‍♂️': 7, + '🤸🏾‍♀️': 7, + '🤸🏾‍♂️': 7, + '🤸🏿‍♀️': 7, + '🤸🏿‍♂️': 7, + '🤹‍♀️': 5, + '🤹‍♂️': 5, + '🤹🏻‍♀️': 7, + '🤹🏻‍♂️': 7, + '🤹🏼‍♀️': 7, + '🤹🏼‍♂️': 7, + '🤹🏽‍♀️': 7, + '🤹🏽‍♂️': 7, + '🤹🏾‍♀️': 7, + '🤹🏾‍♂️': 7, + '🤹🏿‍♀️': 7, + '🤹🏿‍♂️': 7, + '🤼‍♀️': 5, + '🤼‍♂️': 5, + '🤼🏻‍♀️': 7, + '🤼🏻‍♂️': 7, + '🤼🏼‍♀️': 7, + '🤼🏼‍♂️': 7, + '🤼🏽‍♀️': 7, + '🤼🏽‍♂️': 7, + '🤼🏾‍♀️': 7, + '🤼🏾‍♂️': 7, + '🤼🏿‍♀️': 7, + '🤼🏿‍♂️': 7, + '🤽‍♀️': 5, + '🤽‍♂️': 5, + '🤽🏻‍♀️': 7, + '🤽🏻‍♂️': 7, + '🤽🏼‍♀️': 7, + '🤽🏼‍♂️': 7, + '🤽🏽‍♀️': 7, + '🤽🏽‍♂️': 7, + '🤽🏾‍♀️': 7, + '🤽🏾‍♂️': 7, + '🤽🏿‍♀️': 7, + '🤽🏿‍♂️': 7, + '🤾‍♀️': 5, + '🤾‍♂️': 5, + '🤾🏻‍♀️': 7, + '🤾🏻‍♂️': 7, + '🤾🏼‍♀️': 7, + '🤾🏼‍♂️': 7, + '🤾🏽‍♀️': 7, + '🤾🏽‍♂️': 7, + '🤾🏾‍♀️': 7, + '🤾🏾‍♂️': 7, + '🤾🏿‍♀️': 7, + '🤾🏿‍♂️': 7, + '🦸‍♀️': 5, + '🦸‍♂️': 5, + '🦸🏻‍♀️': 7, + '🦸🏻‍♂️': 7, + '🦸🏼‍♀️': 7, + '🦸🏼‍♂️': 7, + '🦸🏽‍♀️': 7, + '🦸🏽‍♂️': 7, + '🦸🏾‍♀️': 7, + '🦸🏾‍♂️': 7, + '🦸🏿‍♀️': 7, + '🦸🏿‍♂️': 7, + '🦹‍♀️': 5, + '🦹‍♂️': 5, + '🦹🏻‍♀️': 7, + '🦹🏻‍♂️': 7, + '🦹🏼‍♀️': 7, + '🦹🏼‍♂️': 7, + '🦹🏽‍♀️': 7, + '🦹🏽‍♂️': 7, + '🦹🏾‍♀️': 7, + '🦹🏾‍♂️': 7, + '🦹🏿‍♀️': 7, + '🦹🏿‍♂️': 7, + '🧍‍♀️': 5, + '🧍‍♂️': 5, + '🧍🏻‍♀️': 7, + '🧍🏻‍♂️': 7, + '🧍🏼‍♀️': 7, + '🧍🏼‍♂️': 7, + '🧍🏽‍♀️': 7, + '🧍🏽‍♂️': 7, + '🧍🏾‍♀️': 7, + '🧍🏾‍♂️': 7, + '🧍🏿‍♀️': 7, + '🧍🏿‍♂️': 7, + '🧎‍♀️': 5, + '🧎‍♀️‍➡️': 8, + '🧎‍♂️': 5, + '🧎‍♂️‍➡️': 8, + '🧎‍➡️': 5, + '🧎🏻‍♀️': 7, + '🧎🏻‍♀️‍➡️': 10, + '🧎🏻‍♂️': 7, + '🧎🏻‍♂️‍➡️': 10, + '🧎🏻‍➡️': 7, + '🧎🏼‍♀️': 7, + '🧎🏼‍♀️‍➡️': 10, + '🧎🏼‍♂️': 7, + '🧎🏼‍♂️‍➡️': 10, + '🧎🏼‍➡️': 7, + '🧎🏽‍♀️': 7, + '🧎🏽‍♀️‍➡️': 10, + '🧎🏽‍♂️': 7, + '🧎🏽‍♂️‍➡️': 10, + '🧎🏽‍➡️': 7, + '🧎🏾‍♀️': 7, + '🧎🏾‍♀️‍➡️': 10, + '🧎🏾‍♂️': 7, + '🧎🏾‍♂️‍➡️': 10, + '🧎🏾‍➡️': 7, + '🧎🏿‍♀️': 7, + '🧎🏿‍♀️‍➡️': 10, + '🧎🏿‍♂️': 7, + '🧎🏿‍♂️‍➡️': 10, + '🧎🏿‍➡️': 7, + '🧏‍♀️': 5, + '🧏‍♂️': 5, + '🧏🏻‍♀️': 7, + '🧏🏻‍♂️': 7, + '🧏🏼‍♀️': 7, + '🧏🏼‍♂️': 7, + '🧏🏽‍♀️': 7, + '🧏🏽‍♂️': 7, + '🧏🏾‍♀️': 7, + '🧏🏾‍♂️': 7, + '🧏🏿‍♀️': 7, + '🧏🏿‍♂️': 7, + '🧑‍⚕️': 5, + '🧑‍⚖️': 5, + '🧑‍✈️': 5, + '🧑‍🌾': 5, + '🧑‍🍳': 5, + '🧑‍🍼': 5, + '🧑‍🎄': 5, + '🧑‍🎓': 5, + '🧑‍🎤': 5, + '🧑‍🎨': 5, + '🧑‍🏫': 5, + '🧑‍🏭': 5, + '🧑‍💻': 5, + '🧑‍💼': 5, + '🧑‍🔧': 5, + '🧑‍🔬': 5, + '🧑‍🚀': 5, + '🧑‍🚒': 5, + '🧑‍🤝‍🧑': 8, + '🧑‍🦯': 5, + '🧑‍🦯‍➡️': 8, + '🧑‍🦰': 5, + '🧑‍🦱': 5, + '🧑‍🦲': 5, + '🧑‍🦳': 5, + '🧑‍🦼': 5, + '🧑‍🦼‍➡️': 8, + '🧑‍🦽': 5, + '🧑‍🦽‍➡️': 8, + '🧑‍🧑‍🧒': 8, + '🧑‍🧑‍🧒‍🧒': 11, + '🧑‍🧒': 5, + '🧑‍🧒‍🧒': 8, + '🧑‍🩰': 5, + '🧑🏻‍⚕️': 7, + '🧑🏻‍⚖️': 7, + '🧑🏻‍✈️': 7, + '🧑🏻‍❤️‍💋‍🧑🏿': 15, + '🧑🏻‍🌾': 7, + '🧑🏻‍🍳': 7, + '🧑🏻‍🍼': 7, + '🧑🏻‍🎄': 7, + '🧑🏻‍🎓': 7, + '🧑🏻‍🎤': 7, + '🧑🏻‍🎨': 7, + '🧑🏻‍🏫': 7, + '🧑🏻‍🏭': 7, + '🧑🏻‍🐰‍🧑🏽': 12, + '🧑🏻‍🐰‍🧑🏾': 12, + '🧑🏻‍🐰‍🧑🏿': 12, + '🧑🏻‍💻': 7, + '🧑🏻‍💼': 7, + '🧑🏻‍🔧': 7, + '🧑🏻‍🔬': 7, + '🧑🏻‍🚀': 7, + '🧑🏻‍🚒': 7, + '🧑🏻‍🤝‍🧑🏻': 12, + '🧑🏻‍🤝‍🧑🏼': 12, + '🧑🏻‍🤝‍🧑🏽': 12, + '🧑🏻‍🤝‍🧑🏾': 12, + '🧑🏻‍🤝‍🧑🏿': 12, + '🧑🏻‍🦯': 7, + '🧑🏻‍🦯‍➡️': 10, + '🧑🏻‍🦰': 7, + '🧑🏻‍🦱': 7, + '🧑🏻‍🦲': 7, + '🧑🏻‍🦳': 7, + '🧑🏻‍🦼': 7, + '🧑🏻‍🦼‍➡️': 10, + '🧑🏻‍🦽': 7, + '🧑🏻‍🦽‍➡️': 10, + '🧑🏻‍🩰': 7, + '🧑🏻‍🫯‍🧑🏼': 10, + '🧑🏻‍🫯‍🧑🏽': 10, + '🧑🏻‍🫯‍🧑🏾': 10, + '🧑🏻‍🫯‍🧑🏿': 10, + '🧑🏼‍⚕️': 7, + '🧑🏼‍⚖️': 7, + '🧑🏼‍✈️': 7, + '🧑🏼‍🌾': 7, + '🧑🏼‍🍳': 7, + '🧑🏼‍🍼': 7, + '🧑🏼‍🎄': 7, + '🧑🏼‍🎓': 7, + '🧑🏼‍🎤': 7, + '🧑🏼‍🎨': 7, + '🧑🏼‍🏫': 7, + '🧑🏼‍🏭': 7, + '🧑🏼‍🐰‍🧑🏿': 12, + '🧑🏼‍💻': 7, + '🧑🏼‍💼': 7, + '🧑🏼‍🔧': 7, + '🧑🏼‍🔬': 7, + '🧑🏼‍🚀': 7, + '🧑🏼‍🚒': 7, + '🧑🏼‍🤝‍🧑🏻': 12, + '🧑🏼‍🤝‍🧑🏼': 12, + '🧑🏼‍🤝‍🧑🏽': 12, + '🧑🏼‍🤝‍🧑🏿': 12, + '🧑🏼‍🦯': 7, + '🧑🏼‍🦯‍➡️': 10, + '🧑🏼‍🦰': 7, + '🧑🏼‍🦱': 7, + '🧑🏼‍🦲': 7, + '🧑🏼‍🦳': 7, + '🧑🏼‍🦼': 7, + '🧑🏼‍🦽': 7, + '🧑🏼‍🩰': 7, + '🧑🏼‍🫯‍🧑🏻': 10, + '🧑🏼‍🫯‍🧑🏽': 10, + '🧑🏼‍🫯‍🧑🏿': 10, + '🧑🏽‍⚕️': 7, + '🧑🏽‍⚖️': 7, + '🧑🏽‍✈️': 7, + '🧑🏽‍❤️‍💋‍🧑🏿': 15, + '🧑🏽‍🌾': 7, + '🧑🏽‍🍳': 7, + '🧑🏽‍🍼': 7, + '🧑🏽‍🎄': 7, + '🧑🏽‍🎓': 7, + '🧑🏽‍🎤': 7, + '🧑🏽‍🎨': 7, + '🧑🏽‍🏫': 7, + '🧑🏽‍🏭': 7, + '🧑🏽‍🐰‍🧑🏻': 12, + '🧑🏽‍🐰‍🧑🏿': 12, + '🧑🏽‍💻': 7, + '🧑🏽‍💼': 7, + '🧑🏽‍🔧': 7, + '🧑🏽‍🔬': 7, + '🧑🏽‍🚀': 7, + '🧑🏽‍🚒': 7, + '🧑🏽‍🤝‍🧑🏻': 12, + '🧑🏽‍🤝‍🧑🏼': 12, + '🧑🏽‍🤝‍🧑🏽': 12, + '🧑🏽‍🤝‍🧑🏾': 12, + '🧑🏽‍🤝‍🧑🏿': 12, + '🧑🏽‍🦯': 7, + '🧑🏽‍🦯‍➡️': 10, + '🧑🏽‍🦰': 7, + '🧑🏽‍🦱': 7, + '🧑🏽‍🦲': 7, + '🧑🏽‍🦳': 7, + '🧑🏽‍🦼': 7, + '🧑🏽‍🦼‍➡️': 10, + '🧑🏽‍🦽': 7, + '🧑🏽‍🦽‍➡️': 10, + '🧑🏽‍🩰': 7, + '🧑🏽‍🫯‍🧑🏻': 10, + '🧑🏽‍🫯‍🧑🏼': 10, + '🧑🏽‍🫯‍🧑🏾': 10, + '🧑🏽‍🫯‍🧑🏿': 10, + '🧑🏾‍⚕️': 7, + '🧑🏾‍⚖️': 7, + '🧑🏾‍✈️': 7, + '🧑🏾‍🌾': 7, + '🧑🏾‍🍳': 7, + '🧑🏾‍🍼': 7, + '🧑🏾‍🎄': 7, + '🧑🏾‍🎓': 7, + '🧑🏾‍🎤': 7, + '🧑🏾‍🎨': 7, + '🧑🏾‍🏫': 7, + '🧑🏾‍🏭': 7, + '🧑🏾‍🐰‍🧑🏻': 12, + '🧑🏾‍🐰‍🧑🏿': 12, + '🧑🏾‍💻': 7, + '🧑🏾‍💼': 7, + '🧑🏾‍🔧': 7, + '🧑🏾‍🔬': 7, + '🧑🏾‍🚀': 7, + '🧑🏾‍🚒': 7, + '🧑🏾‍🤝‍🧑🏻': 12, + '🧑🏾‍🤝‍🧑🏽': 12, + '🧑🏾‍🤝‍🧑🏾': 12, + '🧑🏾‍🤝‍🧑🏿': 12, + '🧑🏾‍🦯': 7, + '🧑🏾‍🦯‍➡️': 10, + '🧑🏾‍🦰': 7, + '🧑🏾‍🦱': 7, + '🧑🏾‍🦲': 7, + '🧑🏾‍🦳': 7, + '🧑🏾‍🦼': 7, + '🧑🏾‍🦽': 7, + '🧑🏾‍🩰': 7, + '🧑🏾‍🫯‍🧑🏻': 10, + '🧑🏾‍🫯‍🧑🏽': 10, + '🧑🏾‍🫯‍🧑🏿': 10, + '🧑🏿‍⚕️': 7, + '🧑🏿‍⚖️': 7, + '🧑🏿‍✈️': 7, + '🧑🏿‍❤️‍💋‍🧑🏻': 15, + '🧑🏿‍❤️‍💋‍🧑🏽': 15, + '🧑🏿‍🌾': 7, + '🧑🏿‍🍳': 7, + '🧑🏿‍🍼': 7, + '🧑🏿‍🎄': 7, + '🧑🏿‍🎓': 7, + '🧑🏿‍🎤': 7, + '🧑🏿‍🎨': 7, + '🧑🏿‍🏫': 7, + '🧑🏿‍🏭': 7, + '🧑🏿‍🐰‍🧑🏻': 12, + '🧑🏿‍🐰‍🧑🏼': 12, + '🧑🏿‍🐰‍🧑🏽': 12, + '🧑🏿‍🐰‍🧑🏾': 12, + '🧑🏿‍💻': 7, + '🧑🏿‍💼': 7, + '🧑🏿‍🔧': 7, + '🧑🏿‍🔬': 7, + '🧑🏿‍🚀': 7, + '🧑🏿‍🚒': 7, + '🧑🏿‍🤝‍🧑🏻': 12, + '🧑🏿‍🤝‍🧑🏼': 12, + '🧑🏿‍🤝‍🧑🏽': 12, + '🧑🏿‍🤝‍🧑🏾': 12, + '🧑🏿‍🤝‍🧑🏿': 12, + '🧑🏿‍🦯': 7, + '🧑🏿‍🦯‍➡️': 10, + '🧑🏿‍🦰': 7, + '🧑🏿‍🦱': 7, + '🧑🏿‍🦲': 7, + '🧑🏿‍🦳': 7, + '🧑🏿‍🦼': 7, + '🧑🏿‍🦼‍➡️': 10, + '🧑🏿‍🦽': 7, + '🧑🏿‍🦽‍➡️': 10, + '🧑🏿‍🩰': 7, + '🧑🏿‍🫯‍🧑🏻': 10, + '🧑🏿‍🫯‍🧑🏼': 10, + '🧑🏿‍🫯‍🧑🏽': 10, + '🧑🏿‍🫯‍🧑🏾': 10, + '🧔‍♀️': 5, + '🧔‍♂️': 5, + '🧔🏻‍♀️': 7, + '🧔🏻‍♂️': 7, + '🧔🏼‍♀️': 7, + '🧔🏼‍♂️': 7, + '🧔🏽‍♀️': 7, + '🧔🏽‍♂️': 7, + '🧔🏾‍♀️': 7, + '🧔🏾‍♂️': 7, + '🧔🏿‍♀️': 7, + '🧔🏿‍♂️': 7, + '🧖‍♀️': 5, + '🧖‍♂️': 5, + '🧖🏻‍♀️': 7, + '🧖🏻‍♂️': 7, + '🧖🏼‍♀️': 7, + '🧖🏼‍♂️': 7, + '🧖🏽‍♀️': 7, + '🧖🏽‍♂️': 7, + '🧖🏾‍♀️': 7, + '🧖🏾‍♂️': 7, + '🧖🏿‍♀️': 7, + '🧖🏿‍♂️': 7, + '🧗‍♀️': 5, + '🧗‍♂️': 5, + '🧗🏻‍♀️': 7, + '🧗🏻‍♂️': 7, + '🧗🏼‍♀️': 7, + '🧗🏼‍♂️': 7, + '🧗🏽‍♀️': 7, + '🧗🏽‍♂️': 7, + '🧗🏾‍♀️': 7, + '🧗🏾‍♂️': 7, + '🧗🏿‍♀️': 7, + '🧗🏿‍♂️': 7, + '🧘‍♀️': 5, + '🧘‍♂️': 5, + '🧘🏻‍♀️': 7, + '🧘🏻‍♂️': 7, + '🧘🏼‍♀️': 7, + '🧘🏼‍♂️': 7, + '🧘🏽‍♀️': 7, + '🧘🏽‍♂️': 7, + '🧘🏾‍♀️': 7, + '🧘🏾‍♂️': 7, + '🧘🏿‍♀️': 7, + '🧘🏿‍♂️': 7, + '🧙‍♀️': 5, + '🧙‍♂️': 5, + '🧙🏻‍♀️': 7, + '🧙🏻‍♂️': 7, + '🧙🏼‍♀️': 7, + '🧙🏼‍♂️': 7, + '🧙🏽‍♀️': 7, + '🧙🏽‍♂️': 7, + '🧙🏾‍♀️': 7, + '🧙🏾‍♂️': 7, + '🧙🏿‍♀️': 7, + '🧙🏿‍♂️': 7, + '🧚‍♀️': 5, + '🧚‍♂️': 5, + '🧚🏻‍♀️': 7, + '🧚🏻‍♂️': 7, + '🧚🏼‍♀️': 7, + '🧚🏼‍♂️': 7, + '🧚🏽‍♀️': 7, + '🧚🏽‍♂️': 7, + '🧚🏾‍♀️': 7, + '🧚🏾‍♂️': 7, + '🧚🏿‍♀️': 7, + '🧚🏿‍♂️': 7, + '🧛‍♀️': 5, + '🧛‍♂️': 5, + '🧛🏻‍♀️': 7, + '🧛🏻‍♂️': 7, + '🧛🏼‍♀️': 7, + '🧛🏼‍♂️': 7, + '🧛🏽‍♀️': 7, + '🧛🏽‍♂️': 7, + '🧛🏾‍♀️': 7, + '🧛🏾‍♂️': 7, + '🧛🏿‍♀️': 7, + '🧛🏿‍♂️': 7, + '🧜‍♀️': 5, + '🧜‍♂️': 5, + '🧜🏻‍♀️': 7, + '🧜🏻‍♂️': 7, + '🧜🏼‍♀️': 7, + '🧜🏼‍♂️': 7, + '🧜🏽‍♀️': 7, + '🧜🏽‍♂️': 7, + '🧜🏾‍♀️': 7, + '🧜🏾‍♂️': 7, + '🧜🏿‍♀️': 7, + '🧜🏿‍♂️': 7, + '🧝‍♀️': 5, + '🧝‍♂️': 5, + '🧝🏻‍♀️': 7, + '🧝🏻‍♂️': 7, + '🧝🏼‍♀️': 7, + '🧝🏼‍♂️': 7, + '🧝🏽‍♀️': 7, + '🧝🏽‍♂️': 7, + '🧝🏾‍♀️': 7, + '🧝🏾‍♂️': 7, + '🧝🏿‍♀️': 7, + '🧝🏿‍♂️': 7, + '🧞‍♀️': 5, + '🧞‍♂️': 5, + '🧟‍♀️': 5, + '🧟‍♂️': 5, + '🫱🏻‍🫲🏼': 9, + '🫱🏻‍🫲🏽': 9, + '🫱🏻‍🫲🏾': 9, + '🫱🏻‍🫲🏿': 9, + '🫱🏼‍🫲🏻': 9, + '🫱🏼‍🫲🏽': 9, + '🫱🏼‍🫲🏾': 9, + '🫱🏼‍🫲🏿': 9, + '🫱🏽‍🫲🏻': 9, + '🫱🏽‍🫲🏼': 9, + '🫱🏽‍🫲🏾': 9, + '🫱🏽‍🫲🏿': 9, + '🫱🏾‍🫲🏻': 9, + '🫱🏾‍🫲🏼': 9, + '🫱🏾‍🫲🏽': 9, + '🫱🏾‍🫲🏿': 9, + '🫱🏿‍🫲🏻': 9, + '🫱🏿‍🫲🏼': 9, + '🫱🏿‍🫲🏽': 9, + '🫱🏿‍🫲🏾': 9, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_3d4826b8.py b/libs/wcwidth/table_grapheme_overrides/_known_3d4826b8.py new file mode 100644 index 000000000..a8daae5e5 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_3d4826b8.py @@ -0,0 +1,6462 @@ +""" +Grapheme overrides for _known_3d4826b8. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: mintty +""" +# pylint: skip-file +GRAPHEMES = { + ' ̃': 2, + ' ̣': 2, + ' ̱́': 3, + ' ོ': 2, + '-́': 2, + '.̃': 2, + 'à': 2, + 'À': 2, + 'á': 2, + 'â': 2, + 'ã': 2, + 'Ā': 2, + 'ā': 2, + 'ä': 2, + 'ả': 2, + 'ǎ': 2, + 'ạ': 2, + 'a̱': 2, + 'A̱': 2, + 'A̸': 2, + 'b̃': 2, + 'c̈': 2, + 'ç': 2, + 'Ç': 2, + 'c̱': 2, + 'ḍ': 2, + 'ḏ': 2, + 'è': 2, + 'È': 2, + 'É': 2, + 'é': 2, + 'Ê': 2, + 'ê': 2, + 'ẽ': 2, + 'ē': 2, + 'ẻ': 2, + 'e̊': 2, + 'ě': 2, + 'ẹ': 2, + 'e̱': 2, + 'E̱': 2, + 'ğ': 2, + 'ḥ': 2, + 'Ḥ': 2, + 'ì': 2, + 'í': 2, + 'Í': 2, + 'î': 2, + 'ĩ': 2, + 'ī': 2, + 'İ': 2, + 'ï': 2, + 'ỉ': 2, + 'ǐ': 2, + 'ị': 2, + 'I̱': 2, + 'i̱': 2, + 'k̀': 2, + 'm̃': 2, + 'ǹ': 2, + 'ñ': 2, + 'n̄': 2, + 'ṅ': 2, + 'ṉ': 2, + 'ò': 2, + 'ó': 2, + 'ô': 2, + 'õ': 2, + 'ō': 2, + 'Ö': 2, + 'ö': 2, + 'ỏ': 2, + 'ǒ': 2, + 'ọ': 2, + 'O̱': 2, + 'o̱': 2, + 'p̃': 2, + 'p̱': 2, + 'p̱̱': 3, + 'r̃': 2, + 'ṛ': 2, + 'S̈': 2, + 's̈': 2, + 'Ṣ': 2, + 'ṣ': 2, + 'Ş': 2, + 'ş': 2, + 'T̃': 2, + 't̃': 2, + 'ṭ': 2, + 'ṯ': 2, + 'ù': 2, + 'Ú': 2, + 'ú': 2, + 'û': 2, + 'ũ': 2, + 'ū': 2, + 'Ü': 2, + 'ü': 2, + 'ủ': 2, + 'ụ': 2, + 'u̱': 2, + 'U̱': 2, + 'ỳ': 2, + 'Ý': 2, + 'ý': 2, + 'ỹ': 2, + 'ỷ': 2, + 'ỵ': 2, + 'ẓ': 2, + 'á̱': 2, + 'ầ': 2, + 'ấ': 2, + 'ẫ': 2, + 'ẩ': 2, + 'ậ': 2, + 'â̱': 2, + 'ã̱': 2, + 'ä́': 2, + 'ä̱': 2, + 'ề': 2, + 'ế': 2, + 'ễ': 2, + 'ể': 2, + 'ệ': 2, + 'ë́': 2, + 'ë̱': 2, + 'í̱': 2, + 'ï̱': 2, + 'ó̱': 2, + 'ồ': 2, + 'ố': 2, + 'ỗ': 2, + 'ổ': 2, + 'ộ': 2, + 'ö́': 2, + 'ö̱': 2, + 'Ü̃': 2, + 'ü̃': 2, + 'ü̃́': 3, + 'ü̱̃': 3, + 'ü̱': 2, + 'ằ': 2, + 'ắ': 2, + 'ẳ': 2, + 'ặ': 2, + 'ą́': 2, + 'ę́': 2, + 'į́': 2, + 'ờ': 2, + 'ớ': 2, + 'ỡ': 2, + 'ở': 2, + 'ợ': 2, + 'ừ': 2, + 'ứ': 2, + 'ữ': 2, + 'ử': 2, + 'ự': 2, + 'ǎ̱': 2, + 'ǫ́': 2, + 'ɔ̀': 2, + 'ɔ́': 2, + 'Ɔ́': 2, + 'ɔ̂': 2, + 'ɔ̃': 2, + 'ɔ̈': 2, + 'ɔ̌': 2, + 'ə̀': 2, + 'ə́': 2, + 'ɛ̀': 2, + 'ɛ́': 2, + 'ɛ̂': 2, + 'ɛ̃': 2, + 'ɛ̈': 2, + 'ɛ̌': 2, + 'Ɨ́': 2, + 'ɨ́': 2, + 'ɨ̂': 2, + 'ɨ̈': 2, + 'ɨ̌': 2, + 'ɨ̱': 2, + 'ɨ̱́': 3, + 'ɨ̱̈': 3, + 'ɩ́': 2, + 'ʉ́': 2, + 'ʉ̌': 2, + 'ʊ́': 2, + 'а̄': 2, + 'е̄': 2, + 'ё': 2, + 'ӣ': 2, + 'ӣ̄': 3, + 'й': 2, + 'о̄': 2, + 'о̄̄': 3, + 'Р̌': 2, + 'р̌': 2, + 'ӯ': 2, + 'ы̄': 2, + 'ы̄̄': 3, + 'э̄': 2, + 'э̄̄': 3, + 'ю̄': 2, + 'я̄': 2, + 'ө̄': 2, + 'אַ': 2, + 'אָ': 2, + 'בֿ': 2, + 'וּ': 2, + 'יִ': 2, + 'כּ': 2, + 'פּ': 2, + 'פֿ': 2, + 'תּ': 2, + 'ײַ': 2, + 'اً': 2, + 'اُ': 2, + 'اّ': 2, + 'أ': 2, + 'إ': 2, + 'بَ': 2, + 'تُ': 2, + 'ت‌': 2, + 'ثّ': 2, + 'جُ': 2, + 'دّ': 2, + 'رً': 2, + 'رِ': 2, + 'رّ': 2, + 'صّ': 2, + 'ضً': 2, + 'فّ': 2, + 'قِ': 2, + 'لِ': 2, + 'مً': 2, + 'مِ': 2, + 'مّ': 2, + 'نِ': 2, + 'نّ': 2, + 'ن‌': 2, + 'هٔ': 2, + 'ه‌': 2, + 'وَ': 2, + 'وّ': 2, + 'ؤ': 2, + 'يُ': 2, + 'پُ': 2, + 'چّ': 2, + 'کُ': 2, + 'کِ': 2, + 'ھُ': 2, + 'ھٔ': 2, + 'ہِ': 2, + 'ہٰ': 2, + 'یِ': 2, + 'یّ': 2, + 'یٔ': 2, + 'یٰ': 2, + 'ی‌': 2, + 'ے‌': 2, + 'ܒ݂': 2, + 'ܓ̰': 2, + 'ܓ݂': 2, + 'ܝܼ': 2, + 'ܟ̰': 2, + 'ܟ݂': 2, + 'ܡܹ': 2, + 'ܬܵ': 2, + 'ހަ': 2, + 'ހާ': 2, + 'ހި': 2, + 'ހީ': 2, + 'ހު': 2, + 'ހެ': 2, + 'ހޭ': 2, + 'ހޮ': 2, + 'ހޯ': 2, + 'ށަ': 2, + 'ށާ': 2, + 'ށި': 2, + 'ށު': 2, + 'ށެ': 2, + 'ށޭ': 2, + 'ށް': 2, + 'ނަ': 2, + 'ނާ': 2, + 'ނި': 2, + 'ނީ': 2, + 'ނު': 2, + 'ނޫ': 2, + 'ނެ': 2, + 'ނޭ': 2, + 'ން': 2, + 'ރަ': 2, + 'ރާ': 2, + 'ރި': 2, + 'ރީ': 2, + 'ރު': 2, + 'ރޫ': 2, + 'ރެ': 2, + 'ރޭ': 2, + 'ރޮ': 2, + 'ބަ': 2, + 'ބާ': 2, + 'ބި': 2, + 'ބީ': 2, + 'ބު': 2, + 'ބޫ': 2, + 'ބެ': 2, + 'ބޭ': 2, + 'ބޮ': 2, + 'ބް': 2, + 'ޅަ': 2, + 'ޅާ': 2, + 'ޅި': 2, + 'ޅު': 2, + 'ޅެ': 2, + 'ޅޭ': 2, + 'ކަ': 2, + 'ކާ': 2, + 'ކި': 2, + 'ކީ': 2, + 'ކު': 2, + 'ކެ': 2, + 'ކޮ': 2, + 'ކް': 2, + 'އަ': 2, + 'އާ': 2, + 'އި': 2, + 'އީ': 2, + 'އު': 2, + 'އޫ': 2, + 'އެ': 2, + 'އޭ': 2, + 'އޮ': 2, + 'އޯ': 2, + 'އް': 2, + 'ވަ': 2, + 'ވާ': 2, + 'ވި': 2, + 'ވީ': 2, + 'ވު': 2, + 'ވެ': 2, + 'ވޭ': 2, + 'ވޮ': 2, + 'ވޯ': 2, + 'މަ': 2, + 'މާ': 2, + 'މި': 2, + 'މީ': 2, + 'މު': 2, + 'މުި': 3, + 'މެ': 2, + 'މޭ': 2, + 'މް': 2, + 'ފަ': 2, + 'ފާ': 2, + 'ފި': 2, + 'ފީ': 2, + 'ފު': 2, + 'ފޫ': 2, + 'ފެ': 2, + 'ފޯ': 2, + 'ފް': 2, + 'ދަ': 2, + 'ދާ': 2, + 'ދި': 2, + 'ދީ': 2, + 'ދު': 2, + 'ދޫ': 2, + 'ދެ': 2, + 'ދޭ': 2, + 'ދޮ': 2, + 'ތަ': 2, + 'ތާ': 2, + 'ތި': 2, + 'ތީ': 2, + 'ތު': 2, + 'ތެ': 2, + 'ތް': 2, + 'ލަ': 2, + 'ލާ': 2, + 'ލި': 2, + 'ލީ': 2, + 'ލު': 2, + 'ލޫ': 2, + 'ލެ': 2, + 'ލޭ': 2, + 'ލް': 2, + 'ގަ': 2, + 'ގާ': 2, + 'ގި': 2, + 'ގު': 2, + 'ގެ': 2, + 'ގޮ': 2, + 'ޏަ': 2, + 'ޏެ': 2, + 'ސަ': 2, + 'ސާ': 2, + 'ސި': 2, + 'ސީ': 2, + 'ސު': 2, + 'ސޫ': 2, + 'ސެ': 2, + 'ސް': 2, + 'ޑަ': 2, + 'ޑާ': 2, + 'ޑި': 2, + 'ޑު': 2, + 'ޑެ': 2, + 'ޒަ': 2, + 'ޒާ': 2, + 'ޒި': 2, + 'ޒީ': 2, + 'ޒު': 2, + 'ޒެ': 2, + 'ޓަ': 2, + 'ޓި': 2, + 'ޓީ': 2, + 'ޓު': 2, + 'ޓެ': 2, + 'ޔަ': 2, + 'ޔާ': 2, + 'ޔި': 2, + 'ޔު': 2, + 'ޔެ': 2, + 'ޔޭ': 2, + 'ޔޮ': 2, + 'ޕް': 2, + 'ޖަ': 2, + 'ޖާ': 2, + 'ޖި': 2, + 'ޖު': 2, + 'ޖޫ': 2, + 'ޖެ': 2, + 'ޖޭ': 2, + 'ޖް': 2, + 'ޗަ': 2, + 'ޗު': 2, + 'ޗެ': 2, + 'ޘަ': 2, + 'ޙަ': 2, + 'ޙާ': 2, + 'ޙި': 2, + 'ޙީ': 2, + 'ޙު': 2, + 'ޙެ': 2, + 'ޙް': 2, + 'ޚާ': 2, + 'ޚި': 2, + 'ޚު': 2, + 'ޚް': 2, + 'ޛަ': 2, + 'ޝަ': 2, + 'ޞަ': 2, + 'ޞާ': 2, + 'ޞި': 2, + 'ޞު': 2, + 'ޞޫ': 2, + 'ޞް': 2, + 'ޟު': 2, + 'ޠަ': 2, + 'ޠީ': 2, + 'ޢަ': 2, + 'ޢާ': 2, + 'ޢީ': 2, + 'ޢު': 2, + 'ޢެ': 2, + 'ޢް': 2, + 'ޣާ': 2, + 'ޤަ': 2, + 'ޤާ': 2, + 'ޤި': 2, + 'ޤީ': 2, + 'ޤު': 2, + 'ޤެ': 2, + 'ޤް': 2, + 'अं': 2, + 'आं': 2, + 'इँ': 2, + 'इं': 2, + 'ऊं': 2, + 'एं': 2, + 'ओं': 2, + 'कं': 2, + 'कां': 3, + 'किं': 3, + 'कु': 2, + 'कू': 2, + 'कृ': 2, + 'के': 2, + 'केँ': 3, + 'कें': 3, + 'केा': 3, + 'कै': 2, + 'क्': 2, + 'क्क': 3, + 'क्का': 4, + 'क्कां': 5, + 'क्को': 4, + 'क्घ्घि्': 7, + 'क्छ': 3, + 'क्त': 3, + 'क्ता': 4, + 'क्ति': 4, + 'क्ती': 4, + 'क्तीं': 5, + 'क्तुं': 5, + 'क्तेः': 5, + 'क्त्योः': 7, + 'क्ना': 4, + 'क्नि': 4, + 'क्य': 3, + 'क्या': 4, + 'क्र': 3, + 'क्रा': 4, + 'क्रि': 4, + 'क्रू': 4, + 'क्ष': 3, + 'क्षं': 4, + 'क्षा': 4, + 'क्षां': 5, + 'क्षाः': 5, + 'क्षि': 4, + 'क्षे': 4, + 'क्षै': 4, + 'क्षो': 4, + 'क्ष्ण': 5, + 'क्ष्य': 5, + 'क्ष्या': 6, + 'क्सा': 4, + 'खु': 2, + 'खे': 2, + 'खै': 2, + 'ख्त': 3, + 'ख्ने': 4, + 'ख्य': 3, + 'ख्यः': 4, + 'ख्या': 4, + 'ख्‍ने': 5, + 'गं': 2, + 'ग़': 2, + 'ग़ा': 3, + 'ग़ु': 3, + 'गाँ': 3, + 'गां': 3, + 'गु': 2, + 'गू': 2, + 'गृ': 2, + 'गे': 2, + 'गै': 2, + 'गों': 3, + 'ग्ण': 3, + 'ग्न': 3, + 'ग्ने': 4, + 'ग्भे': 4, + 'ग्य': 3, + 'ग्या': 4, + 'ग्ये': 4, + 'ग्यैः': 5, + 'ग्र': 3, + 'ग्रे': 4, + 'ग्र्य': 5, + 'घं': 2, + 'घू': 2, + 'घृ': 2, + 'घे': 2, + 'घेि': 3, + 'घ्': 2, + 'घ्ंत': 4, + 'घ्का': 4, + 'घ्त': 3, + 'घ्र': 3, + 'घ्र्व्य': 7, + 'घ्लि': 4, + 'घ्व': 3, + 'घ्श्य': 5, + 'घ्स्ट': 5, + 'ङ्ग': 3, + 'ङ्घ': 3, + 'चां': 3, + 'चु': 2, + 'चूं': 3, + 'चे': 2, + 'चै': 2, + 'च्च': 3, + 'च्चा': 4, + 'च्चे': 4, + 'च्चों': 5, + 'च्छ': 3, + 'च्छं': 4, + 'च्छा': 4, + 'च्छि': 4, + 'च्छु': 4, + 'च्छे': 4, + 'च्या': 4, + 'च्यां': 5, + 'च्यो': 4, + 'छु': 2, + 'छुी': 3, + 'छै': 2, + 'जँ': 2, + 'जिं': 3, + 'जु': 2, + 'जे': 2, + 'जेँ': 3, + 'ज्ज': 3, + 'ज्ञा': 4, + 'ज्ञे': 4, + 'ज्ने': 4, + 'ज्य': 3, + 'ज्या': 4, + 'ज्या‌': 5, + 'ज्रा': 4, + 'झे': 2, + 'ञ्च': 3, + 'ञ्चा': 4, + 'ञ्चि': 4, + 'टां': 3, + 'टुँ': 3, + 'टुं': 3, + 'टू': 2, + 'टे': 2, + 'टै': 2, + 'टों': 3, + 'ट्ज़': 4, + 'ट्टि': 4, + 'ट्टी': 4, + 'ट्या': 4, + 'ट्र': 3, + 'ट्रा': 4, + 'ट्रे': 4, + 'ठे': 2, + 'ठैः': 3, + 'ड़': 2, + 'ड़ा': 3, + 'ड़े': 3, + 'डू': 2, + 'डे़': 3, + 'ढं': 2, + 'ढ़': 2, + 'ढ़ा': 3, + 'ढ़ि': 3, + 'ढ़ी': 3, + 'ढ़ो': 3, + 'ढाँ': 3, + 'ढे': 2, + 'णं': 2, + 'णां': 3, + 'णु': 2, + 'णू': 2, + 'णे': 2, + 'णेँ': 3, + 'णों': 3, + 'ण्ड': 3, + 'ण्डं': 4, + 'ण्डः': 4, + 'ण्डि': 4, + 'ण्य': 3, + 'ण्या': 4, + 'ण्यां': 5, + 'तँ': 2, + 'तं': 2, + 'तां': 3, + 'ताः': 3, + 'तिं': 3, + 'तु': 2, + 'तुं': 3, + 'तू': 2, + 'तृ': 2, + 'ते': 2, + 'तेँ': 3, + 'तै': 2, + 'तों': 3, + 'त्': 2, + 'त्का': 4, + 'त्त': 3, + 'त्ता': 4, + 'त्ति': 4, + 'त्ते': 4, + 'त्त्थि': 6, + 'त्त्व': 5, + 'त्त्वा': 6, + 'त्त्वे': 6, + 'त्न': 3, + 'त्नी': 4, + 'त्नों': 5, + 'त्प': 3, + 'त्पा': 4, + 'त्पी': 4, + 'त्पू': 4, + 'त्म': 3, + 'त्मा': 4, + 'त्य': 3, + 'त्यं': 4, + 'त्या': 4, + 'त्यां': 5, + 'त्यि': 4, + 'त्ये': 4, + 'त्यै': 4, + 'त्र': 3, + 'त्रा': 4, + 'त्राा': 5, + 'त्राि': 5, + 'त्रि': 4, + 'त्री': 4, + 'त्रे': 4, + 'त्र्त': 5, + 'त्र्य': 5, + 'त्र्या': 6, + 'त्र्ये': 6, + 'त्व': 3, + 'त्वं': 4, + 'त्वा': 4, + 'त्वां': 5, + 'त्वे': 4, + 'त्सं': 4, + 'त्सा': 4, + 'त्सो': 4, + 'त्स्वा': 6, + 'थु': 2, + 'थे': 2, + 'थै': 2, + 'थ्य': 3, + 'दं': 2, + 'दां': 3, + 'दु': 2, + 'दू': 2, + 'दृ': 2, + 'दे': 2, + 'दै': 2, + 'द्घ': 3, + 'द्घि': 4, + 'द्घो': 4, + 'द्द': 3, + 'द्दा': 4, + 'द्दि': 4, + 'द्दी': 4, + 'द्दे': 4, + 'द्ध': 3, + 'द्धः': 4, + 'द्धा': 4, + 'द्धां': 5, + 'द्धि': 4, + 'द्धिं': 5, + 'द्धी': 4, + 'द्धीं': 5, + 'द्धैः': 5, + 'द्भ': 3, + 'द्भा': 4, + 'द्य': 3, + 'द्या': 4, + 'द्र': 3, + 'द्रो': 4, + 'द्व': 3, + 'द्वा': 4, + 'द्वि': 4, + 'द्वे': 4, + 'द्स': 3, + 'द्‌': 3, + 'धां': 3, + 'धाः': 3, + 'धु': 2, + 'धृ': 2, + 'धे': 2, + 'धेः': 3, + 'धों': 3, + 'ध्य': 3, + 'ध्या': 4, + 'ध्ये': 4, + 'ध्व': 3, + 'नं': 2, + 'नां': 3, + 'नाः': 3, + 'ना‌': 3, + 'निँ': 3, + 'निः': 3, + 'नु': 2, + 'नू': 2, + 'नृ': 2, + 'ने': 2, + 'नै': 2, + 'नैः': 3, + 'नों': 3, + 'न्': 2, + 'न्का': 4, + 'न्चि': 4, + 'न्छे': 4, + 'न्जे': 4, + 'न्ञ्या': 6, + 'न्त': 3, + 'न्तं': 4, + 'न्तः': 4, + 'न्ता': 4, + 'न्ति': 4, + 'न्तु': 4, + 'न्तुं': 5, + 'न्ते': 4, + 'न्तो': 4, + 'न्तों': 5, + 'न्त्र': 5, + 'न्त्रा': 6, + 'न्त्राः': 7, + 'न्त्रि': 6, + 'न्त्री': 6, + 'न्त्र्य': 7, + 'न्त्र्यं': 8, + 'न्त्र्या': 8, + 'न्त्र्ये': 8, + 'न्द': 3, + 'न्दा': 4, + 'न्दी': 4, + 'न्दो': 4, + 'न्ध': 3, + 'न्धा': 4, + 'न्धि': 4, + 'न्धी': 4, + 'न्धु': 4, + 'न्धों': 5, + 'न्न': 3, + 'न्ना': 4, + 'न्नाः': 5, + 'न्नि': 4, + 'न्ने': 4, + 'न्म': 3, + 'न्मा': 4, + 'न्मु': 4, + 'न्मे': 4, + 'न्य': 3, + 'न्या': 4, + 'न्ये': 4, + 'न्यै': 4, + 'न्यो': 4, + 'न्व': 3, + 'न्वे': 4, + 'न्स': 3, + 'न्हा': 4, + 'न्हें': 5, + 'न्ह्यां': 7, + 'पं': 2, + 'पाँ': 3, + 'पां': 3, + 'पु': 2, + 'पू': 2, + 'पृ': 2, + 'पे': 2, + 'पेँ': 3, + 'पै': 2, + 'पों': 3, + 'प्त': 3, + 'प्तं': 4, + 'प्ति': 4, + 'प्तुं': 5, + 'प्तौ': 4, + 'प्य': 3, + 'प्यं': 4, + 'प्या': 4, + 'प्र': 3, + 'प्रा': 4, + 'फ़': 2, + 'फाँ': 3, + 'फु': 2, + 'फे': 2, + 'फ्त': 3, + 'फ्ता': 4, + 'फ्ना': 4, + 'फ्नो': 4, + 'फ्रां': 5, + 'बँ': 2, + 'बं': 2, + 'बाँ': 3, + 'बु': 2, + 'बू': 2, + 'बृ': 2, + 'बे': 2, + 'बै': 2, + 'ब्द': 3, + 'ब्ध': 3, + 'ब्धि': 4, + 'ब्यू': 4, + 'भं': 2, + 'भिः': 3, + 'भु': 2, + 'भू': 2, + 'भे': 2, + 'भै': 2, + 'भ्य': 3, + 'भ्यः': 4, + 'भ्या': 4, + 'भ्यां': 5, + 'भ्यु': 4, + 'भ्यो': 4, + 'भ्र': 3, + 'मं': 2, + 'मां': 3, + 'मु': 2, + 'मू': 2, + 'मे': 2, + 'मेँ': 3, + 'में': 3, + 'मै': 2, + 'मों': 3, + 'म्': 2, + 'म्ति': 4, + 'म्न': 3, + 'म्नि': 4, + 'म्प': 3, + 'म्पा': 4, + 'म्प्र': 5, + 'म्ब': 3, + 'म्बु': 4, + 'म्भि': 4, + 'म्म': 3, + 'म्मा': 4, + 'म्मि': 4, + 'म्मे': 4, + 'म्या': 4, + 'म्से': 4, + 'म्ह': 3, + 'म्ही': 4, + 'म्हो': 4, + 'यं': 2, + 'यां': 3, + 'याः': 3, + 'यु': 2, + 'यू': 2, + 'ये': 2, + 'यै': 2, + 'यैः': 3, + 'यों': 3, + 'य्य': 3, + 'य्यो': 4, + 'रं': 2, + 'रां': 3, + 'राः': 3, + 'रिं': 3, + 'रु': 2, + 'रू': 2, + 'रे': 2, + 'रें': 3, + 'रै': 2, + 'रों': 3, + 'र्क': 3, + 'र्कि': 4, + 'र्ग': 3, + 'र्गी': 4, + 'र्गे': 4, + 'र्चि': 4, + 'र्छ': 3, + 'र्ज': 3, + 'र्जा': 4, + 'र्जि': 4, + 'र्जी': 4, + 'र्ट': 3, + 'र्ढ': 3, + 'र्ण': 3, + 'र्णं': 4, + 'र्णां': 5, + 'र्णी': 4, + 'र्त': 3, + 'र्ता': 4, + 'र्ति': 4, + 'र्तिः': 5, + 'र्ती': 4, + 'र्तुं': 5, + 'र्त्त': 5, + 'र्त्ति': 6, + 'र्थ': 3, + 'र्थं': 4, + 'र्थि': 4, + 'र्थी': 4, + 'र्द': 3, + 'र्दा': 4, + 'र्दि': 4, + 'र्दु': 4, + 'र्दे': 4, + 'र्दै': 4, + 'र्दो': 4, + 'र्द्ध': 5, + 'र्ध': 3, + 'र्धा': 4, + 'र्धि': 4, + 'र्न': 3, + 'र्नि': 4, + 'र्नु': 4, + 'र्नू': 4, + 'र्ने': 4, + 'र्फ': 3, + 'र्ब': 3, + 'र्बं': 4, + 'र्बा': 4, + 'र्भ': 3, + 'र्भा': 4, + 'र्म': 3, + 'र्मं': 4, + 'र्मा': 4, + 'र्मि': 4, + 'र्य': 3, + 'र्यं': 4, + 'र्या': 4, + 'र्यां': 5, + 'र्ये': 4, + 'र्यों': 5, + 'र्रा': 4, + 'र्व': 3, + 'र्वा': 4, + 'र्वां': 5, + 'र्वृ': 4, + 'र्वे': 4, + 'र्वो': 4, + 'र्श': 3, + 'र्ष': 3, + 'र्षे': 4, + 'र्स': 3, + 'र्सा': 4, + 'र्ह': 3, + 'र्हाः': 5, + 'र्हि': 4, + 'र्हो': 4, + 'र्‍या': 5, + 'लां': 3, + 'लाः': 3, + 'लिं': 3, + 'लीं': 3, + 'लु': 2, + 'लू': 2, + 'ले': 2, + 'लै': 2, + 'लों': 3, + 'ल्क': 3, + 'ल्का': 4, + 'ल्ने': 4, + 'ल्प': 3, + 'ल्पि': 4, + 'ल्म': 3, + 'ल्य': 3, + 'ल्यं': 4, + 'ल्या': 4, + 'ल्यां': 5, + 'ल्याः': 5, + 'ल्ये': 4, + 'ल्ल': 3, + 'ल्लं': 4, + 'ल्ला': 4, + 'ल्लि': 4, + 'ल्ले': 4, + 'ल्ह': 3, + 'ळे': 2, + 'वं': 2, + 'वां': 3, + 'वाः': 3, + 'वू': 2, + 'वृ': 2, + 'वे': 2, + 'वै': 2, + 'वों': 3, + 'व्य': 3, + 'व्या': 4, + 'व्ये': 4, + 'व्यों': 5, + 'शं': 2, + 'शां': 3, + 'शाः': 3, + 'शु': 2, + 'शू': 2, + 'शे': 2, + 'शों': 3, + 'श्च': 3, + 'श्चि': 4, + 'श्य': 3, + 'श्या': 4, + 'श्यि': 4, + 'श्यों': 5, + 'श्र': 3, + 'श्रा': 4, + 'श्रां': 5, + 'श्रु': 4, + 'श्व': 3, + 'श्वा': 4, + 'श्वि': 4, + 'षां': 3, + 'षाः': 3, + 'षु': 2, + 'षे': 2, + 'षों': 3, + 'ष्का': 4, + 'ष्ट': 3, + 'ष्टः': 4, + 'ष्टा': 4, + 'ष्टां': 5, + 'ष्टाः': 5, + 'ष्टि': 4, + 'ष्टी': 4, + 'ष्टीं': 5, + 'ष्ट्र': 5, + 'ष्ट्रा': 6, + 'ष्ट्रां': 7, + 'ष्ट्रि': 6, + 'ष्ट्री': 6, + 'ष्ट्रे': 6, + 'ष्ट्रो': 6, + 'ष्ट्रों': 7, + 'ष्ठा': 4, + 'ष्ठे': 4, + 'ष्ठों': 5, + 'ष्णु': 4, + 'ष्प': 3, + 'ष्प्र': 5, + 'ष्य': 3, + 'ष्यः': 4, + 'ष्यों': 5, + 'सँ': 2, + 'सं': 2, + 'साँ': 3, + 'सां': 3, + 'सु': 2, + 'सू': 2, + 'सृ': 2, + 'से': 2, + 'सै': 2, + 'स्क': 3, + 'स्का': 4, + 'स्काः': 5, + 'स्कु': 4, + 'स्कू': 4, + 'स्कृ': 4, + 'स्ट': 3, + 'स्ट्या': 6, + 'स्त': 3, + 'स्ता': 4, + 'स्ति': 4, + 'स्ती': 4, + 'स्तु': 4, + 'स्तुं': 5, + 'स्तै': 4, + 'स्तो': 4, + 'स्त्र': 5, + 'स्त्रा': 6, + 'स्त्रि': 6, + 'स्त्री': 6, + 'स्थ': 3, + 'स्था': 4, + 'स्थां': 5, + 'स्थि': 4, + 'स्थी': 4, + 'स्थे': 4, + 'स्थ्य': 5, + 'स्धी': 4, + 'स्प': 3, + 'स्पे': 4, + 'स्म': 3, + 'स्मि': 4, + 'स्मिं': 5, + 'स्मै': 4, + 'स्य': 3, + 'स्या': 4, + 'स्यां': 5, + 'स्याः': 5, + 'स्युः': 5, + 'स्यों': 5, + 'स्रो': 4, + 'स्ल': 3, + 'स्व': 3, + 'स्वं': 4, + 'स्वा': 4, + 'स्वी': 4, + 'स्वे': 4, + 'स्स': 3, + 'स्सा': 4, + 'हाँ': 3, + 'हां': 3, + 'हिं': 3, + 'हीं': 3, + 'हु': 2, + 'हुँ': 3, + 'हू': 2, + 'हे': 2, + 'है': 2, + 'हैं': 3, + 'हैः': 3, + 'हों': 3, + 'ह्या': 4, + 'ह्रा': 4, + 'फ़्ता': 4, + 'আ‌': 2, + 'ই‌': 2, + 'এ‌': 2, + 'কা‌': 3, + 'কিং': 3, + 'কু': 2, + 'কূ': 2, + 'কৃ': 2, + 'কে‌': 3, + 'ক্ত': 3, + 'ক্তি': 4, + 'ক্য': 3, + 'ক্র': 3, + 'ক্রী': 4, + 'ক্ষ': 3, + 'ক্ষা': 4, + 'ক্ষি': 4, + 'ক্ষে': 4, + 'ক্ষ্য': 5, + 'ক্ষ্যে': 6, + 'খু': 2, + 'খ্যা': 4, + 'গু': 2, + 'গৃ': 2, + 'গ্য': 3, + 'গ্র': 3, + 'গ্রা': 4, + 'গ্রে': 4, + 'ঘৃ': 2, + 'ঘো': 3, + 'ঙ্খা': 4, + 'ঙ্গ': 3, + 'ঙ্গী': 4, + 'ঙ্গে': 4, + 'ঙ্ঘ': 3, + 'চ্চ': 3, + 'চ্ছ': 3, + 'চ্ছা': 4, + 'চ্ছা‌': 5, + 'চ্ছু': 4, + 'চ্ছে': 4, + 'ছু': 2, + 'জে‌': 3, + 'জ্ঞ': 3, + 'জ্ঞা': 4, + 'জ্য': 3, + 'ঞ্চি': 4, + 'ঞ্ছি': 4, + 'ট্রে': 4, + 'ড়': 2, + 'ড়া': 3, + 'ঢ়': 2, + 'ণ্ড': 3, + 'ণ্ডে': 4, + 'ণ্য': 3, + 'তাঁ': 3, + 'তা‌': 3, + 'তি‌': 3, + 'তু': 2, + 'তৃ': 2, + 'তে‌': 3, + 'ত্ত': 3, + 'ত্তা': 4, + 'ত্তি': 4, + 'ত্ন': 3, + 'ত্ব': 3, + 'ত্বে': 4, + 'ত্ম': 3, + 'ত্য': 3, + 'ত্যা': 4, + 'ত্যি': 4, + 'ত্যে': 4, + 'ত্র': 3, + 'ত্রা': 4, + 'ত্রে': 4, + 'ত্‍': 3, + 'ত্‍প': 4, + 'ত্‍পী': 5, + 'ত্‍সা': 5, + 'থ্য': 3, + 'দৃ': 2, + 'দে‌': 3, + 'দ্দি': 4, + 'দ্দে': 4, + 'দ্ধ': 3, + 'দ্ধা': 4, + 'দ্ধি': 4, + 'দ্ধে': 4, + 'দ্বা': 4, + 'দ্ভ': 3, + 'দ্ভূ': 4, + 'দ্য': 3, + 'দ্রো': 4, + 'ধ্য': 3, + 'ধ্যে': 4, + 'না‌': 3, + 'নু': 2, + 'ন্ড': 3, + 'ন্ত': 3, + 'ন্তা': 4, + 'ন্তি': 4, + 'ন্তে': 4, + 'ন্ত্রি': 6, + 'ন্ত্রে': 6, + 'ন্থী': 4, + 'ন্ধ': 3, + 'ন্ধা': 4, + 'ন্ধু': 4, + 'ন্ন': 3, + 'ন্ম': 3, + 'ন্মা': 4, + 'ন্মু': 4, + 'ন্য': 3, + 'ন্যা': 4, + 'ন্যে': 4, + 'ন‌': 2, + 'পা‌': 3, + 'পু': 2, + 'পূ': 2, + 'পৃ': 2, + 'প্ত': 3, + 'প্তা': 4, + 'প্র': 3, + 'প্রা': 4, + 'বা‌': 3, + 'বু': 2, + 'বৃ': 2, + 'ব্ধি': 4, + 'ব্য': 3, + 'ব্যা': 4, + 'ব্র': 3, + 'ভূ': 2, + 'ভ্য': 3, + 'ভ্রা': 4, + 'মা‌': 3, + 'মু': 2, + 'মূ': 2, + 'ম্প': 3, + 'ম্পা': 4, + 'ম্ব': 3, + 'ম্ভ': 3, + 'ম্ম': 3, + 'ম্মা': 4, + 'ম্য': 3, + 'য়': 2, + 'য়া': 3, + 'য়ি': 3, + 'য়ে': 3, + 'য়ো': 3, + 'যু': 2, + 'য্য': 3, + 'রাং': 3, + 'রু': 2, + 'রূ': 2, + 'র্ক': 3, + 'র্জ': 3, + 'র্জা': 4, + 'র্ণ': 3, + 'র্ত': 3, + 'র্থ': 3, + 'র্থে': 4, + 'র্দো': 4, + 'র্ধ': 3, + 'র্ধা': 4, + 'র্নী': 4, + 'র্ব': 3, + 'র্বা': 4, + 'র্বি': 4, + 'র্বো': 4, + 'র্ব্য': 5, + 'র্ভূ': 4, + 'র্ম': 3, + 'র্মে': 4, + 'র্য': 3, + 'র্যা': 4, + 'র‌': 2, + 'লে‌': 3, + 'ল্প': 3, + 'ল্য': 3, + 'ল্যা': 4, + 'ল্যে': 4, + 'ল্লে': 4, + 'শু': 2, + 'শৃং': 3, + 'শ্চ': 3, + 'শ্চি': 4, + 'শ্বা': 4, + 'শ্বে': 4, + 'শ্য': 3, + 'শ্যে': 4, + 'শ্র': 3, + 'শ্রা': 4, + 'ষ্ট': 3, + 'ষ্টা': 4, + 'ষ্টি': 4, + 'ষ্ট্র': 5, + 'ষ্ট্রী': 6, + 'ষ্ট্রে': 6, + 'ষ্ঠা': 4, + 'ষ্ঠি': 4, + 'ষ্ঠী': 4, + 'ষ্ঠু': 4, + 'ষ্ণু': 4, + 'সাং': 3, + 'সু': 2, + 'সৃ': 2, + 'সে‌': 3, + 'স্ক': 3, + 'স্কৃ': 4, + 'স্ত': 3, + 'স্তি': 4, + 'স্তি‌': 5, + 'স্ত্র': 5, + 'স্থ': 3, + 'স্থা': 4, + 'স্থ্য': 5, + 'স্ব': 3, + 'স্বা': 4, + 'স্বী': 4, + 'স্য': 3, + 'স্যা': 4, + 'স্যে': 4, + 'হ্য': 3, + 'হ‌': 2, + 'ਅੰ': 2, + 'ਅੱ': 2, + 'ਆਂ': 2, + 'ਇੰ': 2, + 'ਉਂ': 2, + 'ਉੱ': 2, + 'ਕਾਂ': 3, + 'ਕਿ੍': 3, + 'ਕਿੱ': 3, + 'ਕੁ': 2, + 'ਕੂ': 2, + 'ਕੇ': 2, + 'ਕੋ': 2, + 'ਕੌ': 2, + 'ਕੰ': 2, + 'ਕੱ': 2, + 'ਖਾਂ': 3, + 'ਖਿ਼': 3, + 'ਖੁ': 2, + 'ਖੁੱ': 3, + 'ਖੇ': 2, + 'ਖੋ': 2, + 'ਖੋਂ': 3, + 'ਗਿ੍': 3, + 'ਗੁ': 2, + 'ਗੇ': 2, + 'ਗੈ': 2, + 'ਗੱ': 2, + 'ਘੁੰ': 3, + 'ਘੰ': 2, + 'ਚਿੱ': 3, + 'ਚੁ': 2, + 'ਚੇ': 2, + 'ਚੋ': 2, + 'ਚੋਂ': 3, + 'ਚੰ': 2, + 'ਛੁ': 2, + 'ਛੋਂ': 3, + 'ਜਾਂ': 3, + 'ਜਿ਼': 3, + 'ਜਿ਼ੰ': 4, + 'ਜੁ': 2, + 'ਜੇ': 2, + 'ਜੋ': 2, + 'ਜੋਂ': 3, + 'ਜੰ': 2, + 'ਜੱ': 2, + 'ਝੰ': 2, + 'ਟਿ੍': 3, + 'ਟੇ': 2, + 'ਢੇ': 2, + 'ਢੰ': 2, + 'ਣਾਂ': 3, + 'ਣੇ': 2, + 'ਤਾਂ': 3, + 'ਤੇ': 2, + 'ਤੋਂ': 3, + 'ਤੰ': 2, + 'ਤੱ': 2, + 'ਥੇ': 2, + 'ਥੋਂ': 3, + 'ਦਾਂ': 3, + 'ਦਿੱ': 3, + 'ਦੁ': 2, + 'ਦੂ': 2, + 'ਦੇ': 2, + 'ਦੋ': 2, + 'ਦੋਂ': 3, + 'ਦੰ': 2, + 'ਧਾਂ': 3, + 'ਧੁ': 2, + 'ਧੇ': 2, + 'ਧੱ': 2, + 'ਨਾਂ': 3, + 'ਨੀਂ': 3, + 'ਨੁ': 2, + 'ਨੁੱ': 3, + 'ਨੂੰ': 3, + 'ਨੇ': 2, + 'ਨੋ': 2, + 'ਨੌ': 2, + 'ਨ੍': 2, + 'ਨੰ': 2, + 'ਪੁ': 2, + 'ਪੂ': 2, + 'ਪੈ': 2, + 'ਪ੍': 2, + 'ਪੰ': 2, + 'ਪੱ': 2, + 'ਫੌ': 2, + 'ਬੁ': 2, + 'ਬੂ': 2, + 'ਬੇ': 2, + 'ਬੋ': 2, + 'ਬੰ': 2, + 'ਬੱ': 2, + 'ਭੂ': 2, + 'ਭੇ': 2, + 'ਮਾਂ': 3, + 'ਮੁ': 2, + 'ਮੁੱ': 3, + 'ਮੂ': 2, + 'ਮੇ': 2, + 'ਮੇਂ': 3, + 'ਮੈਂ': 3, + 'ਮੌ': 2, + 'ਮੰ': 2, + 'ਯੁ': 2, + 'ਯੂ': 2, + 'ਯੋ': 2, + 'ਰਾਂ': 3, + 'ਰੁ': 2, + 'ਰੁੱ': 3, + 'ਰੂ': 2, + 'ਰੇ': 2, + 'ਰੋ': 2, + 'ਰ੍': 2, + 'ਰੰ': 2, + 'ਰੱ': 2, + 'ਲਾਂ': 3, + 'ਲਿੰ': 3, + 'ਲੇ': 2, + 'ਲੈ': 2, + 'ਲੋ': 2, + 'ਲ੍': 2, + 'ਲੰ': 2, + 'ਵਾਂ': 3, + 'ਵਿੱ': 3, + 'ਵੇ': 2, + 'ਵੇਂ': 3, + 'ਵੈ': 2, + 'ਵੰ': 2, + 'ਵੱ': 2, + 'ਸ਼ਾਂ': 3, + 'ਸ਼ੇ': 2, + 'ਸ਼ੋ': 2, + 'ਸ਼ੌ': 2, + 'ਸਾਂ': 3, + 'ਸਿੱ': 3, + 'ਸੁ': 2, + 'ਸੁੰ': 3, + 'ਸੂ': 2, + 'ਸੇ': 2, + 'ਸੇੇ': 3, + 'ਸੈਂ': 3, + 'ਸੌ': 2, + 'ਸੰ': 2, + 'ਸੱ': 2, + 'ਹਾਂ': 3, + 'ਹਿੰ': 3, + 'ਹਿੱ': 3, + 'ਹੀਂ': 3, + 'ਹੁ': 2, + 'ਹੁੰ': 3, + 'ਹੂ': 2, + 'ਹੇ': 2, + 'ਹੈ': 2, + 'ਹੋ': 2, + 'ਹੋਂ': 3, + 'ਹੱ': 2, + 'ਜ਼ੁ': 2, + 'ੜੀਂ': 3, + 'ੜੇ': 2, + 'ੜ੍': 2, + 'ਫ਼ੁ': 2, + 'અં': 2, + 'આં': 2, + 'ઊં': 2, + 'કાં': 3, + 'કુ': 2, + 'કૃ': 2, + 'કે': 2, + 'ક્ક': 3, + 'ક્કા': 4, + 'ક્કો': 4, + 'ક્તિ': 4, + 'ક્ર': 3, + 'ક્ષ': 3, + 'ક્ષા': 4, + 'ખુ': 2, + 'ખૂ': 2, + 'ખ્ત': 3, + 'ગુ': 2, + 'ગે': 2, + 'ગ્ન': 3, + 'ગ્ય': 3, + 'ચૂં': 3, + 'ચ્ચ': 3, + 'ચ્ચા': 4, + 'ચ્ચે': 4, + 'ચ્છા': 4, + 'ચ્છે': 4, + 'છું': 3, + 'છૂ': 2, + 'છે': 2, + 'જં': 2, + 'જાં': 3, + 'જુ': 2, + 'જૂ': 2, + 'જે': 2, + 'જ્ઞા': 4, + 'જ્ય': 3, + 'જ્યો': 4, + 'ઝૂં': 3, + 'ટું': 3, + 'ટે': 2, + 'ટ્ર': 3, + 'ટ્રે': 4, + 'ડાં': 3, + 'ડે': 2, + 'ણુ': 2, + 'ણે': 2, + 'તં': 2, + 'તાં': 3, + 'તુ': 2, + 'તૃ': 2, + 'તે': 2, + 'ત્તા': 4, + 'ત્તિ': 4, + 'ત્ત્વ': 5, + 'ત્ત્વા': 6, + 'ત્ન': 3, + 'ત્મ': 3, + 'ત્ય': 3, + 'ત્યા': 4, + 'ત્યાં': 5, + 'ત્યિ': 4, + 'ત્યે': 4, + 'ત્યો': 4, + 'ત્ર': 3, + 'ત્રી': 4, + 'ત્ર્ય': 5, + 'ત્વ': 3, + 'થે': 2, + 'દુ': 2, + 'દૃ': 2, + 'દે': 2, + 'દ્ધ': 3, + 'દ્ધા': 4, + 'દ્ધાં': 5, + 'દ્ધિ': 4, + 'દ્ભ': 3, + 'દ્વા': 4, + 'ધાં': 3, + 'ધુ': 2, + 'ધ્ય': 3, + 'ધ્યા': 4, + 'નં': 2, + 'નાં': 3, + 'નુ': 2, + 'નું': 3, + 'ને': 2, + 'નૈ': 2, + 'ન્ત': 3, + 'ન્તો': 4, + 'ન્મ': 3, + 'ન્મા': 4, + 'ન્મે': 4, + 'ન્ય': 3, + 'ન્યા': 4, + 'ન્યૂં': 5, + 'પુ': 2, + 'પૂ': 2, + 'પે': 2, + 'પ્ત': 3, + 'પ્ર': 3, + 'પ્રા': 4, + 'ફે': 2, + 'બં': 2, + 'બાં': 3, + 'બે': 2, + 'બ્ધ': 3, + 'ભં': 2, + 'ભાં': 3, + 'ભૂ': 2, + 'ભે': 2, + 'ભ્ય': 3, + 'મં': 2, + 'માં': 3, + 'મુ': 2, + 'મૂ': 2, + 'મૃ': 2, + 'મે': 2, + 'મૈ': 2, + 'મ્ય': 3, + 'મ્યાં': 5, + 'યુ': 2, + 'રં': 2, + 'રુ': 2, + 'રૂ': 2, + 'રે': 2, + 'ર્જ': 3, + 'ર્ણ': 3, + 'ર્ત': 3, + 'ર્તા': 4, + 'ર્થ': 3, + 'ર્થિ': 4, + 'ર્દો': 4, + 'ર્મ': 3, + 'ર્મિ': 4, + 'ર્મે': 4, + 'ર્ય': 3, + 'ર્યા': 4, + 'ર્યો': 4, + 'ર્વ': 3, + 'લાં': 3, + 'લિં': 3, + 'લે': 2, + 'લ્મ': 3, + 'લ્ય': 3, + 'ળે': 2, + 'વં': 2, + 'વાં': 3, + 'વું': 3, + 'વૃ': 2, + 'વે': 2, + 'વૈ': 2, + 'વ્ય': 3, + 'વ્યા': 4, + 'વ્યો': 4, + 'શાં': 3, + 'શુ': 2, + 'શે': 2, + 'શ્કે': 4, + 'શ્ચ': 3, + 'શ્ય': 3, + 'શ્ર': 3, + 'શ્વ': 3, + 'ષ્ટિ': 4, + 'ષ્ટ્ર': 5, + 'ષ્ટ્રી': 6, + 'ષ્ટ્રો': 6, + 'ષ્ઠા': 4, + 'ષ્ણુ': 4, + 'ષ્પ': 3, + 'સં': 2, + 'સાં': 3, + 'સુ': 2, + 'સું': 3, + 'સૂ': 2, + 'સે': 2, + 'સ્કૃ': 4, + 'સ્ટ': 3, + 'સ્તિ': 4, + 'સ્તી': 4, + 'સ્ત્રી': 6, + 'સ્થા': 4, + 'સ્થિ': 4, + 'સ્પ': 3, + 'સ્રી': 4, + 'સ્વ': 3, + 'સ્વા': 4, + 'સ્વી': 4, + 'સ્સા': 4, + 'હુ': 2, + 'હે': 2, + 'கீ': 2, + 'க்': 2, + 'ங்': 2, + 'சீ': 2, + 'ச்': 2, + 'ஞ்': 2, + 'ட்': 2, + 'ண்': 2, + 'தீ': 2, + 'த்': 2, + 'நீ': 2, + 'ந்': 2, + 'ன்': 2, + 'ப்': 2, + 'மீ': 2, + 'ம்': 2, + 'யீ': 2, + 'ய்': 2, + 'ர்': 2, + 'ற்': 2, + 'ல்': 2, + 'ள்': 2, + 'ழ்': 2, + 'வீ': 2, + 'வ்': 2, + 'ஸ்': 2, + 'కా': 2, + 'కాం': 3, + 'కి': 2, + 'కీ': 2, + 'కుం': 3, + 'కె': 2, + 'కెం': 3, + 'కై': 2, + 'కొ': 2, + 'క్క': 3, + 'క్కు': 4, + 'క్త': 3, + 'క్తి': 4, + 'క్తీ': 4, + 'క్తు': 4, + 'క్య': 3, + 'క్ర': 3, + 'క్రాం': 5, + 'క్రి': 4, + 'క్రూ': 4, + 'క్ష': 3, + 'క్షా': 4, + 'క్షి': 4, + 'క్షిం': 5, + 'క్షే': 4, + 'క్షో': 4, + 'క్స్వా': 6, + 'ఖా': 2, + 'ఖ్య': 3, + 'ఖ్యా': 4, + 'గా': 2, + 'గి': 2, + 'గిం': 3, + 'గీ': 2, + 'గౌ': 2, + 'గ్భా': 4, + 'గ్మ': 3, + 'గ్య': 3, + 'గ్ర': 3, + 'గ్రి': 4, + 'ఘి': 2, + 'ఘో': 2, + 'చా': 2, + 'చి': 2, + 'చెం': 3, + 'చే': 2, + 'చో': 2, + 'చ్చు': 4, + 'చ్ఛ': 3, + 'చ్ఛా': 4, + 'చ్ఛే': 4, + 'ఛ్ఛా': 4, + 'జా': 2, + 'జి': 2, + 'జీ': 2, + 'జే': 2, + 'జో': 2, + 'జ్ఞ': 3, + 'జ్ఞా': 4, + 'జ్య': 3, + 'టి': 2, + 'టిం': 3, + 'టుం': 3, + 'టే': 2, + 'ట్టి': 4, + 'ట్లు': 4, + 'డి': 2, + 'డ్డ': 3, + 'డ్రు': 4, + 'ణా': 2, + 'ణిం': 3, + 'ణీ': 2, + 'తా': 2, + 'తి': 2, + 'తీ': 2, + 'తె': 2, + 'తొ': 2, + 'తో': 2, + 'త్కా': 4, + 'త్త': 3, + 'త్తి': 4, + 'త్తు': 4, + 'త్న': 3, + 'త్నిం': 5, + 'త్ప': 3, + 'త్ఫ': 3, + 'త్య': 3, + 'త్యం': 4, + 'త్యా': 4, + 'త్ర': 3, + 'త్రు': 4, + 'త్ర్య': 5, + 'త్వ': 3, + 'త్వా': 4, + 'త్స': 3, + 'థా': 2, + 'దా': 2, + 'ది': 2, + 'దిం': 3, + 'దుం': 3, + 'దే': 2, + 'దై': 2, + 'దో': 2, + 'ద్ఘో': 4, + 'ద్ది': 4, + 'ద్దు': 4, + 'ద్దే': 4, + 'ద్ధ': 3, + 'ద్ధి': 4, + 'ద్ధిం': 5, + 'ద్య': 3, + 'ద్యా': 4, + 'ద్యో': 4, + 'ద్ర్య': 5, + 'ద్వా': 4, + 'ధా': 2, + 'ధి': 2, + 'ధిం': 3, + 'ధీ': 2, + 'ధ్య': 3, + 'నా': 2, + 'నాం': 3, + 'ని': 2, + 'నిం': 3, + 'నీ': 2, + 'నుం': 3, + 'నె': 2, + 'నే': 2, + 'నై': 2, + 'నొ': 2, + 'నొం': 3, + 'న్ధ': 3, + 'న్న': 3, + 'న్నా': 4, + 'న్ని': 4, + 'న్ను': 4, + 'న్మ': 3, + 'న్య': 3, + 'న్యా': 4, + 'న్వే': 4, + 'పా': 2, + 'పిం': 3, + 'పీ': 2, + 'పె': 2, + 'పెం': 3, + 'పే': 2, + 'పై': 2, + 'పొం': 3, + 'ప్త': 3, + 'ప్ర': 3, + 'ప్రా': 4, + 'ప్రే': 4, + 'బా': 2, + 'బి': 2, + 'బో': 2, + 'బ్ర': 3, + 'భా': 2, + 'భి': 2, + 'భే': 2, + 'భ్య': 3, + 'భ్యు': 4, + 'భ్ర': 3, + 'భ్రా': 4, + 'మా': 2, + 'మి': 2, + 'మిం': 3, + 'మే': 2, + 'మై': 2, + 'మో': 2, + 'మ్య': 3, + 'యా': 2, + 'యి': 2, + 'యిం': 3, + 'యుం': 3, + 'యె': 2, + 'యే': 2, + 'యొ': 2, + 'యో': 2, + 'రా': 2, + 'రి': 2, + 'రిం': 3, + 'రై': 2, + 'రో': 2, + 'ర్': 2, + 'ర్గ': 3, + 'ర్చి': 4, + 'ర్చు': 4, + 'ర్ణ': 3, + 'ర్ణాం': 5, + 'ర్ణు': 4, + 'ర్త': 3, + 'ర్తి': 4, + 'ర్తిం': 5, + 'ర్తృ': 4, + 'ర్థ': 3, + 'ర్థి': 4, + 'ర్థిం': 5, + 'ర్ద': 3, + 'ర్దో': 4, + 'ర్ధ': 3, + 'ర్ప': 3, + 'ర్బం': 4, + 'ర్భ': 3, + 'ర్భా': 4, + 'ర్య': 3, + 'ర్యా': 4, + 'ర్రా': 4, + 'ర్వ': 3, + 'ర్వా': 4, + 'ర్హ': 3, + 'ర్హాం': 5, + 'ర్హు': 4, + 'లా': 2, + 'లి': 2, + 'లిం': 3, + 'లీ': 2, + 'లె': 2, + 'లే': 2, + 'లో': 2, + 'లౌ': 2, + 'ల్క': 3, + 'ల్గొ': 4, + 'ల్ప': 3, + 'ల్ల': 3, + 'ల్లి': 4, + 'ళా': 2, + 'వా': 2, + 'వి': 2, + 'విం': 3, + 'వీ': 2, + 'వే': 2, + 'వై': 2, + 'వ్య': 3, + 'వ్యా': 4, + 'వ్వ': 3, + 'శా': 2, + 'శాం': 3, + 'శి': 2, + 'శిం': 3, + 'శై': 2, + 'శ్చ': 3, + 'శ్చి': 4, + 'శ్య': 3, + 'శ్ర': 3, + 'శ్రాం': 5, + 'శ్లే': 4, + 'శ్వా': 4, + 'షా': 2, + 'షి': 2, + 'షిం': 3, + 'షే': 2, + 'ష్ట': 3, + 'ష్టు': 4, + 'ష్ట్ర': 5, + 'ష్ట్రీ': 6, + 'ష్ఠ': 3, + 'ష్ఠా': 4, + 'ష్ఠు': 4, + 'ష్థ': 3, + 'ష్ప': 3, + 'సా': 2, + 'సాం': 3, + 'సి': 2, + 'సీ': 2, + 'సె': 2, + 'సే': 2, + 'సౌ': 2, + 'స్కృ': 4, + 'స్తా': 4, + 'స్తి': 4, + 'స్త్ర': 5, + 'స్త్రీ': 6, + 'స్థ': 3, + 'స్థా': 4, + 'స్థి': 4, + 'స్థ్య': 5, + 'స్ప': 3, + 'స్య': 3, + 'స్వ': 3, + 'స్వా': 4, + 'స్వీ': 4, + 'స్వే': 4, + 'హా': 2, + 'హి': 2, + 'హిం': 3, + 'హో': 2, + 'ಕಾಂ': 3, + 'ಕಿಂ': 3, + 'ಕುಂ': 3, + 'ಕೆ': 2, + 'ಕೆಂ': 3, + 'ಕೊಂ': 3, + 'ಕೌ': 2, + 'ಕ್': 2, + 'ಕ್‌': 3, + 'ಖಾಂ': 3, + 'ಖ್': 2, + 'ಗಾಂ': 3, + 'ಗಿ': 2, + 'ಗೆ': 2, + 'ಗೊಂ': 3, + 'ಗೌ': 2, + 'ಗ್': 2, + 'ಘಿ': 2, + 'ಚಿ': 2, + 'ಚ್': 2, + 'ಛಿ': 2, + 'ಛೆ': 2, + 'ಜಿ': 2, + 'ಜೆ': 2, + 'ಜ್': 2, + 'ಞೆ': 2, + 'ಞೌ': 2, + 'ಟಿ': 2, + 'ಟುಂ': 3, + 'ಟ್': 2, + 'ಠಿ': 2, + 'ಡಿ': 2, + 'ಡೆ': 2, + 'ಡ್': 2, + 'ಢಿ': 2, + 'ಣಿ': 2, + 'ಣೆ': 2, + 'ತಿ': 2, + 'ತೆ': 2, + 'ತ್': 2, + 'ಥಿ': 2, + 'ಥೆ': 2, + 'ದಿ': 2, + 'ದಿಂ': 3, + 'ದೆ': 2, + 'ದೌ': 2, + 'ದ್': 2, + 'ಧಿ': 2, + 'ಧೆ': 2, + 'ಧ್': 2, + 'ನಾಂ': 3, + 'ನಿ': 2, + 'ನಿಂ': 3, + 'ನೆ': 2, + 'ನೌ': 2, + 'ನ್': 2, + 'ಪಿ': 2, + 'ಪ್': 2, + 'ಬೆ': 2, + 'ಬ್': 2, + 'ಭಿ': 2, + 'ಭೆ': 2, + 'ಭೌ': 2, + 'ಮಿ': 2, + 'ಮೆ': 2, + 'ಮ್': 2, + 'ಯಿ': 2, + 'ಯಿಂ': 3, + 'ಯುಂ': 3, + 'ಯೆ': 2, + 'ಯೆಂ': 3, + 'ಯ್': 2, + 'ರಾಂ': 3, + 'ರಿ': 2, + 'ರಿಂ': 3, + 'ರೆ': 2, + 'ರೆಂ': 3, + 'ರ್': 2, + 'ಲಿ': 2, + 'ಲೆ': 2, + 'ಲೆಂ': 3, + 'ಲ್': 2, + 'ಳಿ': 2, + 'ಳಿಂ': 3, + 'ಳೆ': 2, + 'ಳ್': 2, + 'ವಿ': 2, + 'ವುಂ': 3, + 'ವೆ': 2, + 'ವೆಂ': 3, + 'ವ್': 2, + 'ಶಾಂ': 3, + 'ಶಿ': 2, + 'ಶ್': 2, + 'ಷಿ': 2, + 'ಷೆ': 2, + 'ಷ್': 2, + 'ಸಾಂ': 3, + 'ಸಿ': 2, + 'ಸೆ': 2, + 'ಸ್': 2, + 'ಹಿ': 2, + 'ಹೆ': 2, + 'ಹೊಂ': 3, + 'കു': 2, + 'കൂ': 2, + 'കൃ': 2, + 'ക്ക': 3, + 'ക്കാ': 4, + 'ക്കി': 4, + 'ക്കു': 4, + 'ക്കും': 5, + 'ക്കെ': 4, + 'ക്കേ': 4, + 'ക്കൊ': 4, + 'ക്കോ': 4, + 'ക്ക്‌': 5, + 'ക്തി': 4, + 'ക്യ': 3, + 'ക്യം': 4, + 'ക്ര': 3, + 'ക്രൂ': 4, + 'ക്ഷ': 3, + 'ക്ഷാ': 4, + 'ക്ഷി': 4, + 'ക്ഷേ': 4, + 'ഖ്യാ': 4, + 'ഗു': 2, + 'ഗ്ഗം': 4, + 'ഗ്ദാ': 4, + 'ഗ്യ': 3, + 'ഗ്യം': 4, + 'ഗ്ര': 3, + 'ങ്കി': 4, + 'ങ്കെ': 4, + 'ങ്കേ': 4, + 'ങ്ങ': 3, + 'ങ്ങി': 4, + 'ച്ച': 3, + 'ച്ചി': 4, + 'ച്ചു': 4, + 'ച്ചും': 5, + 'ച്ചെ': 4, + 'ച്ചേ': 4, + 'ജ്യ': 3, + 'ജ്യാ': 4, + 'ഞ്ച': 3, + 'ഞ്ഞ': 3, + 'ഞ്ഞി': 4, + 'ഞ്ഞെ': 4, + 'ടു': 2, + 'ടും': 3, + 'ട്ട': 3, + 'ട്ടാ': 4, + 'ട്ടി': 4, + 'ട്ടു': 4, + 'ട്ടോ': 4, + 'ട്‌': 3, + 'ണ്ട': 3, + 'ണ്ടാ': 4, + 'ണ്ടി': 4, + 'ണ്ടു': 4, + 'ണ്ടും': 5, + 'ണ്ടെ': 4, + 'ണ്ട്‌': 5, + 'ണ്ഡ': 3, + 'ണ്ണ': 3, + 'ണ്‌': 3, + 'തു': 2, + 'തും': 3, + 'തൃ': 2, + 'ത്ത': 3, + 'ത്താ': 4, + 'ത്തി': 4, + 'ത്തു': 4, + 'ത്തും': 5, + 'ത്തെ': 4, + 'ത്തേ': 4, + 'ത്തോ': 4, + 'ത്ത്‌': 5, + 'ത്ന': 3, + 'ത്നം': 4, + 'ത്നി': 4, + 'ത്യ': 3, + 'ത്യാ': 4, + 'ത്യേ': 4, + 'ത്ര': 3, + 'ത്വ': 3, + 'ത്വം': 4, + 'ത്‌': 3, + 'ദൃ': 2, + 'ദ്ദ': 3, + 'ദ്ധ': 3, + 'ദ്ധി': 4, + 'ദ്യ': 3, + 'ദ്യാ': 4, + 'ദ്ര': 3, + 'ധൂ': 2, + 'ധ്യ': 3, + 'നു': 2, + 'നും': 3, + 'ന്ത': 3, + 'ന്തം': 4, + 'ന്തെ': 4, + 'ന്ത്ര': 5, + 'ന്ത്ര്യ': 7, + 'ന്ത്ര്യം': 8, + 'ന്ഥ': 3, + 'ന്ദ': 3, + 'ന്ധ': 3, + 'ന്ധി': 4, + 'ന്ന': 3, + 'ന്നാ': 4, + 'ന്നി': 4, + 'ന്നീ': 4, + 'ന്നു': 4, + 'ന്നും': 5, + 'ന്നെ': 4, + 'ന്നേ': 4, + 'ന്നോ': 4, + 'ന്ന്': 4, + 'ന്മ': 3, + 'ന്മാ': 4, + 'ന്മു': 4, + 'ന്മൂ': 4, + 'ന്യ': 3, + 'ന്യം': 4, + 'ന്യാ': 4, + 'ന്യേ': 4, + 'ന്യോ': 4, + 'ന്റെ': 4, + 'ന്‌': 3, + 'ന്‍': 3, + 'ന്‍കൂ': 5, + 'ന്‍പി': 5, + 'പു': 2, + 'പൂ': 2, + 'പ്പ': 3, + 'പ്പി': 4, + 'പ്പു': 4, + 'പ്പെ': 4, + 'പ്പോ': 4, + 'പ്പ്‌': 5, + 'പ്ര': 3, + 'പ്രാ': 4, + 'ബു': 2, + 'ബ്ലി': 4, + 'ഭ്യാ': 4, + 'ഭ്രാ': 4, + 'മു': 2, + 'മൃ': 2, + 'മ്പ': 3, + 'മ്പാ': 4, + 'മ്പ്ര': 5, + 'മ്മ': 3, + 'മ്മി': 4, + 'യു': 2, + 'യും': 3, + 'യ്ക്കു': 6, + 'യ്തി': 4, + 'യ്തു': 4, + 'യ്മ': 3, + 'യ്യാ': 4, + 'യ്യി': 4, + 'രു': 2, + 'രും': 3, + 'രൂ': 2, + 'ര്യ': 3, + 'ര്യം': 4, + 'ര്‍ക്കു': 7, + 'ര്‍ക്കും': 8, + 'ര്‍ക്ക്‌': 8, + 'ര്‍ഗ്ഗം': 7, + 'ര്‍ച്ച': 6, + 'ര്‍ണ്ണ': 6, + 'ര്‍ത്ത': 6, + 'ര്‍ത്തി': 7, + 'ര്‍ത്തു': 7, + 'ര്‍ത്തേ': 7, + 'ര്‍ദേ': 5, + 'ര്‍ദ്ദ': 6, + 'ര്‍പ്പെ': 7, + 'ര്‍ബ': 4, + 'ര്‍ഭ': 4, + 'ര്‍മ്മി': 7, + 'ര്‍രാ': 5, + 'ര്‍വ്വ': 6, + 'ര്‍ഹ': 4, + 'ര്‍ഹി': 5, + 'റു': 2, + 'റ്റ': 3, + 'റ്റം': 4, + 'റ്റു': 4, + 'റ്റേ': 4, + 'ലു': 2, + 'ലും': 3, + 'ല്യ': 3, + 'ല്യാ': 4, + 'ല്ല': 3, + 'ല്ലാ': 4, + 'ല്ലാം': 5, + 'ല്ലെ': 4, + 'ല്‍': 3, + 'ല്‍കി': 5, + 'ല്‍കു': 5, + 'ല്‍കേ': 5, + 'ല്‍ത': 4, + 'ല്‍നി': 5, + 'ല്‍പ്പ': 6, + 'ളു': 2, + 'ളും': 3, + 'ള്ള': 3, + 'ള്ളാ': 4, + 'ള്ളി': 4, + 'ള്ളൂ': 4, + 'ള്‍': 3, + 'ള്‍കൊ': 5, + 'ള്‍ക്ക': 6, + 'ള്‍ക്കി': 7, + 'ള്‍ക്കു': 7, + 'ള്‍ക്കും': 8, + 'ള്‍ക്കെ': 7, + 'ള്‍ക്ക്‌': 8, + 'ഴു': 2, + 'വു': 2, + 'വും': 3, + 'വൃ': 2, + 'വ്യ': 3, + 'വ്യം': 4, + 'വ്യാ': 4, + 'വ്വ': 3, + 'ശു': 2, + 'ശ്യ': 3, + 'ശ്ര': 3, + 'ശ്വാ': 4, + 'ഷ്ട': 3, + 'ഷ്ട്യാ': 6, + 'ഷ്ട്ര': 5, + 'ഷ്ട്രീ': 6, + 'ഷ്ണു': 4, + 'ഷ്യ': 3, + 'ഷ്യാ': 4, + 'സാം': 3, + 'സു': 2, + 'സൃ': 2, + 'സ്കാ': 4, + 'സ്തു': 4, + 'സ്ത്രം': 6, + 'സ്ത്രീ': 6, + 'സ്ഥ': 3, + 'സ്ഥാ': 4, + 'സ്ഥി': 4, + 'സ്യ': 3, + 'സ്റ്റ്‌': 7, + 'സ്വ': 3, + 'സ്വാ': 4, + 'സ്വീ': 4, + 'സ്സ': 3, + 'സ്സാ': 4, + 'സ്സി': 4, + 'സ്സും': 5, + 'സ്സോ': 4, + 'ഹിം': 3, + 'ഹു': 2, + 'ഹൃ': 2, + 'ൻ്': 2, + 'ක්': 2, + 'ක්‍': 3, + 'කි': 2, + 'කී': 2, + 'කු': 2, + 'කූ': 2, + 'ඛ්‍': 3, + 'ගි': 2, + 'ගී': 2, + 'ගු': 2, + 'ඟි': 2, + 'ඟී': 2, + 'ච්': 2, + 'චි': 2, + 'ජ්‍': 3, + 'ජි': 2, + 'ජී': 2, + 'ටි': 2, + 'ටී': 2, + 'ටු': 2, + 'ඩි': 2, + 'ඩු': 2, + 'ණ්': 2, + 'ණි': 2, + 'ණී': 2, + 'ණු': 2, + 'ඬු': 2, + 'ත්': 2, + 'ත්‍': 3, + 'ති': 2, + 'තී': 2, + 'තු': 2, + 'ථි': 2, + 'ද්': 2, + 'ද්‍': 3, + 'දි': 2, + 'දිං': 3, + 'දී': 2, + 'දු': 2, + 'ධ්‍': 3, + 'ධි': 2, + 'ධී': 2, + 'න්': 2, + 'න්‍': 3, + 'නි': 2, + 'නී': 2, + 'නු': 2, + 'ඳි': 2, + 'ඳී': 2, + 'ඳු': 2, + 'ප්': 2, + 'ප්‍': 3, + 'පි': 2, + 'පී': 2, + 'පු': 2, + 'පූ': 2, + 'බි': 2, + 'බී': 2, + 'භු': 2, + 'ම්': 2, + 'මි': 2, + 'මී': 2, + 'මු': 2, + 'මූ': 2, + 'ඹී': 2, + 'ඹු': 2, + 'යි': 2, + 'යු': 2, + 'ර්': 2, + 'රි': 2, + 'රී': 2, + 'රු': 2, + 'රූ': 2, + 'ල්': 2, + 'ලි': 2, + 'ලී': 2, + 'ලු': 2, + 'ව්': 2, + 'ව්‍': 3, + 'වි': 2, + 'වී': 2, + 'වු': 2, + 'වූ': 2, + 'ශ්': 2, + 'ශ්‍': 3, + 'ශී': 2, + 'ෂ්': 2, + 'ෂ්‍': 3, + 'ෂි': 2, + 'ස්': 2, + 'සි': 2, + 'සී': 2, + 'සු': 2, + 'හි': 2, + 'හිං': 3, + 'හී': 2, + 'හු': 2, + 'ළි': 2, + 'ළු': 2, + 'กั': 2, + 'กิ': 2, + 'กี': 2, + 'กุ': 2, + 'ก็': 2, + 'ก่': 2, + 'ก้': 2, + 'ขั': 2, + 'ขั้': 3, + 'ขี่': 3, + 'ขึ้': 3, + 'ขื': 2, + 'ข็': 2, + 'ข่': 2, + 'ข้': 2, + 'คั': 2, + 'คิ': 2, + 'คุ': 2, + 'คุ้': 3, + 'คู่': 3, + 'ค่': 2, + 'ค้': 2, + 'ค์': 2, + 'งิ': 2, + 'งื่': 3, + 'จั': 2, + 'จิ': 2, + 'จึ': 2, + 'จุ': 2, + 'จ็': 2, + 'จ่': 2, + 'จ้': 2, + 'ฉั': 2, + 'ชั้': 3, + 'ชิ': 2, + 'ชี': 2, + 'ชื่': 3, + 'ชื้': 3, + 'ชุ': 2, + 'ช่': 2, + 'ช้': 2, + 'ซี่': 3, + 'ซึ่': 3, + 'ญิ': 2, + 'ฎิ': 2, + 'ฏิ': 2, + 'ฒิ': 2, + 'ณิ': 2, + 'ณี': 2, + 'ณ์': 2, + 'ดั': 2, + 'ดั่': 3, + 'ดิ': 2, + 'ดิ์': 3, + 'ดี': 2, + 'ดู': 2, + 'ด็': 2, + 'ด้': 2, + 'ตั': 2, + 'ตั้': 3, + 'ติ': 2, + 'ตี': 2, + 'ตุ': 2, + 'ต็': 2, + 'ต่': 2, + 'ต่ำ': 3, + 'ต้': 2, + 'ถิ่': 3, + 'ถี': 2, + 'ถึ': 2, + 'ถื': 2, + 'ถื่': 3, + 'ถุ': 2, + 'ถู': 2, + 'ถ้': 2, + 'ทั': 2, + 'ทั่': 3, + 'ทั้': 3, + 'ทิ': 2, + 'ที': 2, + 'ที่': 3, + 'ทุ': 2, + 'ท่': 2, + 'ท้': 2, + 'ธั': 2, + 'ธิ': 2, + 'ธิ์': 3, + 'ธี': 2, + 'ธุ์': 3, + 'ธ์': 2, + 'นั': 2, + 'นั้': 3, + 'นิ': 2, + 'นี้': 3, + 'นึ': 2, + 'นึ่': 3, + 'นื': 2, + 'นื่': 3, + 'นุ': 2, + 'นุ่': 3, + 'นู': 2, + 'น่': 2, + 'น้': 2, + 'น์': 2, + 'บั': 2, + 'บั่': 3, + 'บิ': 2, + 'บี': 2, + 'บื้': 3, + 'บุ': 2, + 'บู': 2, + 'บ่': 2, + 'ปั': 2, + 'ปิ': 2, + 'ป็': 2, + 'ป่': 2, + 'ผิ': 2, + 'ผู้': 3, + 'ผ่': 2, + 'พั': 2, + 'พิ': 2, + 'พิ่': 3, + 'พี': 2, + 'พื่': 3, + 'พื้': 3, + 'พู': 2, + 'พ้': 2, + 'ภั': 2, + 'มั': 2, + 'มั่': 3, + 'มิ': 2, + 'มิ่': 3, + 'มี': 2, + 'มื': 2, + 'มุ่': 3, + 'มู': 2, + 'มู่': 3, + 'ม่': 2, + 'ม้': 2, + 'ยั': 2, + 'ยิ': 2, + 'ยิ่': 3, + 'ยี': 2, + 'ยึ': 2, + 'ยื': 2, + 'ยุ': 2, + 'ยู่': 3, + 'ย่': 2, + 'ย่ำ': 3, + 'ย้': 2, + 'ย์': 2, + 'รั': 2, + 'รั้': 3, + 'ริ': 2, + 'รี': 2, + 'รื': 2, + 'รื่': 3, + 'รุ': 2, + 'รู': 2, + 'ร็': 2, + 'ร่': 2, + 'ร้': 2, + 'ร์': 2, + 'ลั': 2, + 'ลิ': 2, + 'ลี่': 3, + 'ลี้': 3, + 'ลึ': 2, + 'ลื': 2, + 'ลื่': 3, + 'ลุ': 2, + 'ลุ่': 3, + 'ลู่': 3, + 'ล่': 2, + 'ล้': 2, + 'วั': 2, + 'วิ': 2, + 'วุ': 2, + 'ว่': 2, + 'ว้': 2, + 'ศั': 2, + 'ศิ': 2, + 'ศี': 2, + 'ศึ': 2, + 'ษ์': 2, + 'สั': 2, + 'สิ': 2, + 'สิ่': 3, + 'สิ้': 3, + 'สี': 2, + 'สื': 2, + 'สื่': 3, + 'สุ': 2, + 'สู': 2, + 'สู่': 3, + 'สู้': 3, + 'ส่': 2, + 'หั': 2, + 'ห็': 2, + 'ห่': 2, + 'ห้': 2, + 'อั': 2, + 'อิ': 2, + 'อี': 2, + 'อื่': 3, + 'อื้': 3, + 'อ้': 2, + 'ກັ': 2, + 'ກິ': 2, + 'ກີ': 2, + 'ກຸ': 2, + 'ກຸ່': 3, + 'ກົ': 2, + 'ກ່': 2, + 'ກ້': 2, + 'ກໍ': 2, + 'ກໍ່': 3, + 'ຂັ': 2, + 'ຂັ້': 3, + 'ຂີ່': 3, + 'ຂື້': 3, + 'ຂົ': 2, + 'ຂົ່': 3, + 'ຂົ້': 3, + 'ຂ່': 2, + 'ຂ້': 2, + 'ຂໍ': 2, + 'ຂໍ້': 3, + 'ຄັ': 2, + 'ຄິ': 2, + 'ຄື': 2, + 'ຄື່': 3, + 'ຄຸ': 2, + 'ຄຸ້': 3, + 'ຄູ່': 3, + 'ຄົ': 2, + 'ຄົ້': 3, + 'ຄ່': 2, + 'ຄ້': 2, + 'ງົ': 2, + 'ຈັ': 2, + 'ຈັ້': 3, + 'ຈິ': 2, + 'ຈື່': 3, + 'ຈຸ': 2, + 'ຈົ': 2, + 'ຈົ່': 3, + 'ຈົ້': 3, + 'ຈ່': 2, + 'ຈ້': 2, + 'ຊັ': 2, + 'ຊັ່': 3, + 'ຊັ້': 3, + 'ຊິ': 2, + 'ຊີ': 2, + 'ຊີ້': 3, + 'ຊື່': 3, + 'ຊື້': 3, + 'ຊຸ': 2, + 'ຊູ': 2, + 'ຊົ': 2, + 'ຊົ່': 3, + 'ຊ່': 2, + 'ຊ້': 2, + 'ຍິ': 2, + 'ຍີ່': 3, + 'ຍຸ': 2, + 'ຍົ': 2, + 'ຍ່': 2, + 'ຍ້': 2, + 'ດັ': 2, + 'ດັ່': 3, + 'ດິ': 2, + 'ດີ': 2, + 'ດົ': 2, + 'ດ້': 2, + 'ຕັ': 2, + 'ຕັ້': 3, + 'ຕິ': 2, + 'ຕີ': 2, + 'ຕົ': 2, + 'ຕົ້': 3, + 'ຕ່': 2, + 'ຕ້': 2, + 'ຕໍ່': 3, + 'ຖິ': 2, + 'ຖື': 2, + 'ຖື່': 3, + 'ຖຸ': 2, + 'ຖົ້': 3, + 'ຖ້': 2, + 'ທັ': 2, + 'ທິ': 2, + 'ທີ': 2, + 'ທີ່': 3, + 'ທຸ': 2, + 'ທົ່': 3, + 'ທ່': 2, + 'ທ້': 2, + 'ທໍ': 2, + 'ນັ': 2, + 'ນັ້': 3, + 'ນິ': 2, + 'ນີ': 2, + 'ນີ້': 3, + 'ນື': 2, + 'ນື່': 3, + 'ນື້': 3, + 'ນຸ': 2, + 'ນຸ່': 3, + 'ນູ': 2, + 'ນົ': 2, + 'ນ້': 2, + 'ບັ': 2, + 'ບີ': 2, + 'ບຸ': 2, + 'ບູ': 2, + 'ບົ': 2, + 'ບ່': 2, + 'ບໍ': 2, + 'ບໍ່': 3, + 'ປັ': 2, + 'ປີ': 2, + 'ປົ': 2, + 'ປ່': 2, + 'ປ້': 2, + 'ຜັ': 2, + 'ຜິ': 2, + 'ຜີ': 2, + 'ຜູ້': 3, + 'ຜົ': 2, + 'ຜ່': 2, + 'ພັ': 2, + 'ພິ': 2, + 'ພີ': 2, + 'ພີ່': 3, + 'ພຶ': 2, + 'ພື່': 3, + 'ພູ': 2, + 'ພົ': 2, + 'ພົ້': 3, + 'ພ້': 2, + 'ພໍ': 2, + 'ພໍ່': 3, + 'ຟັ': 2, + 'ຟ້': 2, + 'ມັ': 2, + 'ມິ': 2, + 'ມີ': 2, + 'ມື': 2, + 'ມື່': 3, + 'ມຸ້': 3, + 'ມູ': 2, + 'ມົ': 2, + 'ມ່': 2, + 'ຢັ': 2, + 'ຢື': 2, + 'ຢູ່': 3, + 'ຢູ້': 3, + 'ຢ່': 2, + 'ຢ້': 2, + 'ລັ': 2, + 'ລິ': 2, + 'ລີ': 2, + 'ລື': 2, + 'ລຸ': 2, + 'ລຸ້': 3, + 'ລູ': 2, + 'ລົ': 2, + 'ລ້': 2, + 'ວັ': 2, + 'ວັ່': 3, + 'ວິ': 2, + 'ວຸ': 2, + 'ວົ': 2, + 'ວົ້': 3, + 'ວ່': 2, + 'ວ້': 2, + 'ສັ': 2, + 'ສິ': 2, + 'ສີ່': 3, + 'ສີ້': 3, + 'ສຶ': 2, + 'ສື': 2, + 'ສຸ': 2, + 'ສູ້': 3, + 'ສົ': 2, + 'ສ່': 2, + 'ສ້': 2, + 'ຫັ': 2, + 'ຫຼ': 2, + 'ຫຼັ': 3, + 'ຫຼິ': 3, + 'ຫຼື': 3, + 'ຫຼົ': 3, + 'ຫຼົ່': 4, + 'ຫ່': 2, + 'ຫ້': 2, + 'ອັ': 2, + 'ອິ': 2, + 'ອີ': 2, + 'ອື່': 3, + 'ອຸ': 2, + 'ອົ': 2, + 'ອ່': 2, + 'ອ້': 2, + 'ຮັ': 2, + 'ຮີ': 2, + 'ຮູ້': 3, + 'ຮົ': 2, + 'ຮົ່': 3, + 'ຮ່': 2, + 'ຮ້': 2, + 'ໜັ': 2, + 'ໜີ່': 3, + 'ໜື່': 3, + 'ໜູ': 2, + 'ໜ່': 2, + 'ໜ້': 2, + 'ໝັ້': 3, + 'ໝີ': 2, + 'ໝູ່': 3, + 'ໝ່': 2, + 'ໝ້': 2, + 'ཀུ': 2, + 'ཀོ': 2, + 'ཀྱ': 2, + 'ཀྱི': 3, + 'ཀླ': 2, + 'ཀློ': 3, + 'ཁུ': 2, + 'ཁེ': 2, + 'ཁོ': 2, + 'ཁྱ': 2, + 'ཁྱི': 3, + 'ཁྲི': 3, + 'ཁྲོ': 3, + 'གི': 2, + 'གུ': 2, + 'གེ': 2, + 'གོ': 2, + 'གྱ': 2, + 'གྱི': 3, + 'གྱུ': 3, + 'གྱོ': 3, + 'གྲ': 2, + 'གྲི': 3, + 'གྲུ': 3, + 'གྲེ': 3, + 'གྲོ': 3, + 'གླ': 2, + 'གླི': 3, + 'གླེ': 3, + 'ངུ': 2, + 'ངེ': 2, + 'ངོ': 2, + 'ཅི': 2, + 'ཅུ': 2, + 'ཅོ': 2, + 'ཆུ': 2, + 'ཆེ': 2, + 'ཆོ': 2, + 'ཇི': 2, + 'ཇུ': 2, + 'ཇོ': 2, + 'ཉི': 2, + 'ཉེ': 2, + 'ཉོ': 2, + 'ཏུ': 2, + 'ཏེ': 2, + 'ཏོ': 2, + 'ཐི': 2, + 'ཐུ': 2, + 'ཐེ': 2, + 'ཐོ': 2, + 'དི': 2, + 'དུ': 2, + 'དེ': 2, + 'དོ': 2, + 'དྲ': 2, + 'དྲི': 3, + 'དྲུ': 3, + 'དྲོ': 3, + 'ནི': 2, + 'ནུ': 2, + 'ནོ': 2, + 'པེ': 2, + 'པོ': 2, + 'པྱ': 2, + 'པྱོ': 3, + 'ཕེ': 2, + 'ཕོ': 2, + 'ཕྱི': 3, + 'ཕྱེ': 3, + 'ཕྱོ': 3, + 'ཕྲུ': 3, + 'ཕྲོ': 3, + 'བུ': 2, + 'བེ': 2, + 'བོ': 2, + 'བྱ': 2, + 'བྱི': 3, + 'བྱུ': 3, + 'བྱེ': 3, + 'བྱོ': 3, + 'བྲ': 2, + 'བྲེ': 3, + 'བླ': 2, + 'བློ': 3, + 'མི': 2, + 'མེ': 2, + 'མོ': 2, + 'མྱོ': 3, + 'ཙུ': 2, + 'ཙོ': 2, + 'ཚུ': 2, + 'ཚེ': 2, + 'ཚོ': 2, + 'ཛི': 2, + 'ཛུ': 2, + 'ཛོ': 2, + 'ཞི': 2, + 'ཞུ': 2, + 'ཟི': 2, + 'ཟུ': 2, + 'ཟེ': 2, + 'ཟོ': 2, + 'ཟླ': 2, + 'ཟློ': 3, + 'འི': 2, + 'འུ': 2, + 'འོ': 2, + 'ཡི': 2, + 'ཡུ': 2, + 'ཡོ': 2, + 'རི': 2, + 'རུ': 2, + 'རེ': 2, + 'རོ': 2, + 'རྐྱ': 3, + 'རྐྱེ': 4, + 'རྒ': 2, + 'རྒོ': 3, + 'རྒྱ': 3, + 'རྒྱུ': 4, + 'རྗེ': 3, + 'རྗོ': 3, + 'རྟ': 2, + 'རྟེ': 3, + 'རྟོ': 3, + 'རྡ': 2, + 'རྣ': 2, + 'རྩ': 2, + 'རྩི': 3, + 'རྩུ': 3, + 'རྩོ': 3, + 'རྫ': 2, + 'ལཱ': 2, + 'ལུ': 2, + 'ལེ': 2, + 'ལོ': 2, + 'ལྔ': 2, + 'ལྕི': 3, + 'ལྕོ': 3, + 'ལྟ': 2, + 'ལྟེ': 3, + 'ལྡ': 2, + 'ལྡེ': 3, + 'ལྷ': 2, + 'ལྷུ': 3, + 'ལྷོ': 3, + 'ཤི': 2, + 'ཤུ': 2, + 'ཤེ': 2, + 'ཤོ': 2, + 'སུ': 2, + 'སེ': 2, + 'སོ': 2, + 'སྐ': 2, + 'སྐི': 3, + 'སྐུ': 3, + 'སྐོ': 3, + 'སྐྱ': 3, + 'སྐྱི': 4, + 'སྐྱུ': 4, + 'སྐྱེ': 4, + 'སྐྱོ': 4, + 'སྒེ': 3, + 'སྒོ': 3, + 'སྒྱུ': 4, + 'སྒྲ': 3, + 'སྒྲི': 4, + 'སྒྲུ': 4, + 'སྔ': 2, + 'སྔོ': 3, + 'སྙི': 3, + 'སྟ': 2, + 'སྟུ': 3, + 'སྟེ': 3, + 'སྟོ': 3, + 'སྡ': 2, + 'སྡི': 3, + 'སྡུ': 3, + 'སྡེ': 3, + 'སྡོ': 3, + 'སྣ': 2, + 'སྣོ': 3, + 'སྤུ': 3, + 'སྤེ': 3, + 'སྤྱ': 3, + 'སྤྱི': 4, + 'སྤྱོ': 4, + 'སྤྲ': 3, + 'སྤྲོ': 4, + 'སྦེ': 3, + 'སྦྱ': 3, + 'སྦྱི': 4, + 'སྦྱོ': 4, + 'སྦྲེ': 4, + 'སྨ': 2, + 'སྨྲ': 3, + 'སྲ': 2, + 'སྲི': 3, + 'སྲུ': 3, + 'སྲོ': 3, + 'སླ': 2, + 'སློ': 3, + 'ཧྲི': 3, + 'ཧྲེ': 3, + 'ཨི': 2, + 'ཨུ': 2, + 'ကိ': 2, + 'ကို': 3, + 'ကီု': 3, + 'ကု': 2, + 'ကူ': 2, + 'ကေု': 3, + 'ကဲ့': 3, + 'ကဵု': 3, + 'ကံ': 2, + 'က္က': 3, + 'က္ခ': 3, + 'က္ဍ': 3, + 'က္ဍို': 5, + 'က်': 2, + 'က်ျ': 3, + 'ကျို': 4, + 'ကျူ': 3, + 'ကျေ': 3, + 'ကျွ': 3, + 'ကြို': 4, + 'ကြီ': 3, + 'ကြု': 3, + 'ကြေ': 3, + 'ကြံ': 3, + 'ကြွေ': 4, + 'ကွ': 2, + 'ကၟ': 2, + 'ကၟိ': 3, + 'ကၟို': 4, + 'ကၟဲ': 3, + 'ကၠ': 2, + 'ကၠို': 4, + 'ကၠီု': 4, + 'ကၠု': 3, + 'ကၠေ': 3, + 'ကၠေံ': 4, + 'ကၠဳ': 3, + 'ခိ': 2, + 'ခို': 3, + 'ခု': 2, + 'ခဲ့': 3, + 'ခဳ': 2, + 'ခံ': 2, + 'ချိ': 3, + 'ချို': 4, + 'ချို့': 5, + 'ချီ': 3, + 'ချု': 3, + 'ချေ': 3, + 'ချဳ': 3, + 'ခြုံ': 4, + 'ခြေ': 3, + 'ခွ': 2, + 'ခွဲ': 3, + 'ခၞံ': 3, + 'ခၠ': 2, + 'ဂိ': 2, + 'ဂို': 3, + 'ဂီု': 3, + 'ဂု': 2, + 'ဂဵု': 3, + 'ဂ္ဂ': 3, + 'ဂ္ဂို': 5, + 'ဂျို': 4, + 'ဂွံ': 3, + 'ဂှ်': 3, + 'ဂၞ': 2, + 'ဂၞဴ': 3, + 'ဂၟံ': 3, + 'ဂၠ': 2, + 'ဂၠိ': 3, + 'ဂၠို': 4, + 'ဂၠေ': 3, + 'ဂၠံ': 3, + 'ငိ': 2, + 'ငို': 3, + 'ငဲ့': 3, + 'ငံ': 2, + 'င့်': 3, + 'င်': 2, + 'င်္ဂ': 4, + 'ငြိ': 3, + 'ငွေ': 3, + 'ငှ': 2, + 'စိ': 2, + 'စို': 3, + 'စီ': 2, + 'စု': 2, + 'စုံ': 3, + 'စဳ': 2, + 'စဴ': 2, + 'စဵု': 3, + 'စံ': 2, + 'စ္စ': 3, + 'စ်': 2, + 'စွ': 2, + 'စွဲ': 3, + 'စွံ': 3, + 'စှ်': 3, + 'စှ်ေ': 4, + 'စၞ': 2, + 'စၞေ': 3, + 'စၟ': 2, + 'စၠေ': 3, + 'ဆို': 3, + 'ဆီ': 2, + 'ဆုဲ': 3, + 'ဆုံ': 3, + 'ဆံ': 2, + 'ဆွဲ': 3, + 'ဇို': 3, + 'ဇူ': 2, + 'ဇြေ': 3, + 'ဇြဳ': 3, + 'ဇၞ': 2, + 'ဇၞေ': 3, + 'ဇၟ': 2, + 'ဉ်': 2, + 'ညိ': 2, + 'ညီ': 2, + 'ညဳ': 2, + 'ညံ': 2, + 'ည်': 2, + 'ည့်': 3, + 'ညှ': 2, + 'ဋိ': 2, + 'ဋ္ဌ': 3, + 'ဍိ': 2, + 'ဍို': 3, + 'ဍု': 2, + 'ဍဵု': 3, + 'ဏီ': 2, + 'ဏံ': 2, + 'ဏ်': 2, + 'ဏှ': 2, + 'တိ': 2, + 'တို': 3, + 'တို့': 4, + 'တိူ': 3, + 'တီ': 2, + 'တု': 2, + 'တုဲ': 3, + 'တူ': 2, + 'တေံ': 3, + 'တဲ': 2, + 'တဴ': 2, + 'တဵ': 2, + 'တံ': 2, + 'တံ့': 3, + 'တ္တ': 3, + 'တ္ၚဲ': 4, + 'တ်': 2, + 'တ်ို': 4, + 'တြုံ': 4, + 'တွ': 2, + 'တွေ': 3, + 'တွဵု': 4, + 'တှ်ေ': 4, + 'တၞ': 2, + 'တၟ': 2, + 'တၟိ': 3, + 'တၟို': 4, + 'တၟေ': 3, + 'တၟံ': 3, + 'တၠု': 3, + 'တႂ်': 3, + 'တႅ': 2, + 'ထိ': 2, + 'ထို': 3, + 'ထို့': 4, + 'ထီ': 2, + 'ထု': 2, + 'ထုဲ': 3, + 'ထူ': 2, + 'ထဵ': 2, + 'ထံ': 2, + 'ထ္ၜ': 3, + 'ထွ': 2, + 'ထွေ': 3, + 'ဒို': 3, + 'ဒီ': 2, + 'ဒီု': 3, + 'ဒု': 2, + 'ဒေံ': 3, + 'ဒဳ': 2, + 'ဒဵု': 3, + 'ဒ္ဂေ': 4, + 'ဒ်': 2, + 'ဒှ်': 3, + 'ဒၞ': 2, + 'ဒၟံ': 3, + 'ဓိ': 2, + 'ဓု': 2, + 'နိ': 2, + 'နို': 3, + 'နု': 2, + 'နူ': 2, + 'နေ့': 3, + 'န္ဒ': 3, + 'န္နိ': 4, + 'န်': 2, + 'န့်': 3, + 'နွ': 2, + 'နွံ': 3, + 'နှ': 2, + 'နှိ': 3, + 'နှီ': 3, + 'နှေ': 3, + 'ပိ': 2, + 'ပို': 3, + 'ပိုဲ': 4, + 'ပိူ': 3, + 'ပီ': 2, + 'ပု': 2, + 'ပုံ': 3, + 'ပူ': 2, + 'ပေဲ': 3, + 'ပဲ': 2, + 'ပဵ': 2, + 'ပံ': 2, + 'ပံ့': 3, + 'ပ္ဍဲ': 4, + 'ပ္တံ': 4, + 'ပ္ပ': 3, + 'ပ္ပံ': 4, + 'ပ်': 2, + 'ပျေ': 3, + 'ပျဲ': 3, + 'ပြီ': 3, + 'ပြု': 3, + 'ပြေ': 3, + 'ပြဳ': 3, + 'ပြံ': 3, + 'ပွ': 2, + 'ပွဲ': 3, + 'ပွႆ': 3, + 'ပၟိ': 3, + 'ပၠ': 2, + 'ပႂ်': 3, + 'ပႅ': 2, + 'ပႆ': 2, + 'ဖိ': 2, + 'ဖီ': 2, + 'ဖဵု': 3, + 'ဖံ': 2, + 'ဖျု': 3, + 'ဖျေံ': 4, + 'ဖြို': 4, + 'ဖွ': 2, + 'ဖွဲ့': 4, + 'ဖွံ့': 4, + 'ဗီု': 3, + 'ဗု': 2, + 'ဗ္တေ': 4, + 'ဗ္ၜေ': 4, + 'ဗျဵု': 4, + 'ဗြေ': 3, + 'ဗြဴ': 3, + 'ဗွို': 4, + 'ဗွဲ': 3, + 'ဗၠ': 2, + 'ဗၠေ': 3, + 'ဘူ': 2, + 'ဘဲ': 2, + 'ဘဴ': 2, + 'မိ': 2, + 'မို': 3, + 'မိူ': 3, + 'မီ': 2, + 'မု': 2, + 'မူ': 2, + 'မဲ': 2, + 'မဲ့': 3, + 'မဳ': 2, + 'မဵု': 3, + 'မံ': 2, + 'မ္ဂ': 3, + 'မ္တီ': 4, + 'မ္ဘ': 3, + 'မ္မ': 3, + 'မ္ၚဵု': 5, + 'မ်': 2, + 'မ့်': 3, + 'မျို': 4, + 'မျီု': 4, + 'မျှ': 3, + 'မြေ': 3, + 'မြှ': 3, + 'မွ': 2, + 'မွေ': 3, + 'မွေ့': 4, + 'မွဲ': 3, + 'မှ': 2, + 'မှီ': 3, + 'မှု': 3, + 'မှေ': 3, + 'မၞိ': 3, + 'မၞု': 3, + 'မၞုံ': 4, + 'မၠို': 4, + 'မၠေ': 3, + 'မႂ်': 3, + 'မႅ': 2, + 'မႆ': 2, + 'ယိ': 2, + 'ယို': 3, + 'ယိူ': 3, + 'ယု': 2, + 'ယုံ': 3, + 'ယူ': 2, + 'ယဲ': 2, + 'ယဵ': 2, + 'ယံ': 2, + 'ယ်': 2, + 'ယျဵု': 4, + 'ယြဴ': 3, + 'ယွ': 2, + 'ယှ': 2, + 'ယႂ်': 3, + 'ယႅ': 2, + 'ရို': 3, + 'ရီု': 3, + 'ရု': 2, + 'ရုဲ': 3, + 'ရုံ': 3, + 'ရူ': 2, + 'ရေံ': 3, + 'ရဲ': 2, + 'ရဴ': 2, + 'ရံ': 2, + 'ရ်': 2, + 'ရွ': 2, + 'ရွေ': 3, + 'ရွံ့': 4, + 'ရွှေ့': 5, + 'ရှ': 2, + 'ရှိ': 3, + 'ရှေ': 3, + 'ရှေ့': 4, + 'ရှ်ေ': 4, + 'လိ': 2, + 'လို': 3, + 'လို့': 4, + 'လိူ': 3, + 'လီ': 2, + 'လီု': 3, + 'လု': 2, + 'လုံ': 3, + 'လူ': 2, + 'လူ့': 3, + 'လဲ': 2, + 'လဵ': 2, + 'လဵု': 3, + 'လံ': 2, + 'လ္ပ': 3, + 'လ္ၚ': 3, + 'လ်': 2, + 'လျို': 4, + 'လျေ': 3, + 'လျှ': 3, + 'လျှို့': 6, + 'လျှေ': 4, + 'လွ': 2, + 'လွဳ': 3, + 'လွှ': 3, + 'လွှဲ': 4, + 'လှ': 2, + 'လှုံ': 4, + 'လၟေ': 3, + 'လႂ်': 3, + 'လႅ': 2, + 'လႆ': 2, + 'ဝူ': 2, + 'ဝဲ': 2, + 'ဝဳ': 2, + 'ဝဵု': 3, + 'ဝ်': 2, + 'ဝွံ': 3, + 'ဝှ': 2, + 'ဝႆ': 2, + 'သိ': 2, + 'သို': 3, + 'သို့': 4, + 'သိူ': 3, + 'သီ': 2, + 'သီု': 3, + 'သု': 2, + 'သုံ': 3, + 'သူ': 2, + 'သူ့': 3, + 'သဳ': 2, + 'သဵ': 2, + 'သဵု': 3, + 'သ္က': 3, + 'သ္ကံ': 4, + 'သ္ဂု': 4, + 'သ္ဂေ': 4, + 'သ္ဇို': 5, + 'သ္ဒ': 3, + 'သ္ပ': 3, + 'သ္ၚိ': 4, + 'သ်': 2, + 'သြို': 4, + 'သွ': 2, + 'သွံ': 3, + 'သှ်': 3, + 'သှ်ေ': 4, + 'သၞ': 2, + 'သၞေ': 3, + 'သၟ': 2, + 'သၠ': 2, + 'သၠု': 3, + 'သႂ်': 3, + 'သႅ': 2, + 'ဟို': 3, + 'ဟီု': 3, + 'ဟု': 2, + 'ဟူ': 2, + 'ဟ်': 2, + 'ဟွံ': 3, + 'အိ': 2, + 'အို': 3, + 'အု': 2, + 'အံ': 2, + 'အ်': 2, + 'ဢိ': 2, + 'ဢို': 3, + 'ဢွ': 2, + 'ဿီု': 3, + 'ၚု': 2, + 'ၚ်': 2, + 'ၜို': 3, + 'ၵိ': 2, + 'ၵို': 3, + 'ၵိူ': 3, + 'ၵု': 2, + 'ၵူ': 2, + 'ၵဵ': 2, + 'ၵ်': 2, + 'ၵွ': 2, + 'ၵႂ': 2, + 'ၵႅ': 2, + 'ၶိ': 2, + 'ၶို': 3, + 'ၶိူ': 3, + 'ၶီ': 2, + 'ၶု': 2, + 'ၶူ': 2, + 'ၶွ': 2, + 'ၶႂ': 2, + 'ၶႅ': 2, + 'ၶႆ': 2, + 'ၸိ': 2, + 'ၸို': 3, + 'ၸိူ': 3, + 'ၸု': 2, + 'ၸူ': 2, + 'ၸဵ': 2, + 'ၸွ': 2, + 'ၸွႆ': 3, + 'ၸႂ်': 3, + 'ၸႅ': 2, + 'ၺ်': 2, + 'ၺွ': 2, + 'ၼို': 3, + 'ၼိူ': 3, + 'ၼု': 2, + 'ၼဵ': 2, + 'ၼ်': 2, + 'ၼွ': 2, + 'ၼႂ်': 3, + 'ၼႆ': 2, + 'ၽိ': 2, + 'ၽို': 3, + 'ၽု': 2, + 'ၽူ': 2, + 'ၽဵ': 2, + 'ၽွ': 2, + 'ၽႂ်': 3, + 'ၾိ': 2, + 'ႁို': 3, + 'ႁိူ': 3, + 'ႁု': 2, + 'ႁူ': 2, + 'ႁဵ': 2, + 'ႁွ': 2, + 'ႁႂ်': 3, + 'ႁႅ': 2, + 'ᜄ᜔': 2, + 'ᜆᜒ': 2, + 'ᜆ᜔': 2, + 'ᜇᜒ': 2, + 'ᜇᜓ': 2, + 'ᜇ᜔': 2, + 'ᜈᜒ': 2, + 'ᜈ᜔': 2, + 'ᜉᜒ': 2, + 'ᜊᜓ': 2, + 'ᜋ᜔': 2, + 'ᜌ᜔': 2, + 'ᜎᜓ': 2, + 'ᜏᜒ': 2, + 'ᜐᜒ': 2, + 'ᜑᜒ': 2, + 'កិ': 2, + 'កុ': 2, + 'កុំ': 3, + 'កំ': 2, + 'ក់': 2, + 'ក៏': 2, + 'ក្ខ': 3, + 'ក្ដិ': 4, + 'ក្ដី': 4, + 'ក្ដៅ': 4, + 'ក្នុ': 4, + 'ក្រ': 3, + 'ក្រុ': 4, + 'ក្រោ': 4, + 'ក្ស': 3, + 'ក្សា': 4, + 'ខិ': 2, + 'ខុ': 2, + 'ខំ': 2, + 'ខ្ជា': 4, + 'ខ្ព': 3, + 'ខ្លា': 4, + 'ខ្លួ': 4, + 'គាំ': 3, + 'គិ': 2, + 'គឺ': 2, + 'គំ': 2, + 'គ៌': 2, + 'គ្គ': 3, + 'គ្នា': 4, + 'គ្មា': 4, + 'គ្រ': 3, + 'គ្រា': 4, + 'គ្រឹះ': 5, + 'គ្រួ': 4, + 'ឃុំ': 3, + 'ឃ្ន': 3, + 'ងឺ': 2, + 'ង់': 2, + 'ង្ក': 3, + 'ង្កើ': 4, + 'ង្ខិ': 4, + 'ង្ខំ': 4, + 'ង្គ': 3, + 'ង្រី': 4, + 'ង្រឹ': 4, + 'ង្រៀ': 4, + 'ង្ហា': 4, + 'ង្អ': 3, + 'ចាំ': 3, + 'ចិ': 2, + 'ចុ': 2, + 'ចូ': 2, + 'ចំ': 2, + 'ច់': 2, + 'ច្ច': 3, + 'ច្ចេ': 4, + 'ច្បា': 4, + 'ឆាំ': 3, + 'ឆ្នាំ': 5, + 'ឆ្នោ': 4, + 'ឆ្ល': 3, + 'ឆ្លើ': 4, + 'ជិ': 2, + 'ជី': 2, + 'ជុំ': 3, + 'ជំ': 2, + 'ជ្ជ': 3, + 'ជ្ជា': 4, + 'ជ្ញា': 4, + 'ជ្យ': 3, + 'ជ្រ': 3, + 'ជ្រើ': 4, + 'ជ្រៀ': 4, + 'ជ្រែ': 4, + 'ឈ្មោះ': 5, + 'ញុះ': 3, + 'ញ្ចូ': 4, + 'ញ្ជា': 4, + 'ញ្ញ': 3, + 'ញ្ញា': 4, + 'ញ្ញៈ': 4, + 'ដី': 2, + 'ដឹ': 2, + 'ដូ': 2, + 'ដំ': 2, + 'ដ៏': 2, + 'ដ្ឋ': 3, + 'ដ្ឋា': 4, + 'ឌ្ឍ': 3, + 'ណិ': 2, + 'ណី': 2, + 'ណោះ': 3, + 'ណ៌': 2, + 'ណ៍': 2, + 'ណ្ដា': 4, + 'ណ្ដុះ': 5, + 'ណ្ឌ': 3, + 'ណ្ណោះ': 5, + 'តិ': 2, + 'តី': 2, + 'តុ': 2, + 'តំ': 2, + 'ត់': 2, + 'ត់់': 3, + 'ត៌': 2, + 'ត្ដ': 3, + 'ត្ដិ': 4, + 'ត្ថ': 3, + 'ត្ថិ': 4, + 'ត្យ': 3, + 'ត្រ': 3, + 'ត្រា': 4, + 'ត្រឹ': 4, + 'ត្រូ': 4, + 'ត្ស': 3, + 'ថ្ងៃ': 4, + 'ថ្នា': 4, + 'ថ្នូ': 4, + 'ថ្មី': 4, + 'ថ្លៃ': 4, + 'ទាំ': 3, + 'ទិ': 2, + 'ទិ៍': 3, + 'ទី': 2, + 'ទឹ': 2, + 'ទុ': 2, + 'ទូ': 2, + 'ទួ': 2, + 'ទោះ': 3, + 'ទំ': 2, + 'ទ្ធ': 3, + 'ទ្ធិ': 4, + 'ទ្យា': 4, + 'ទ្រ': 3, + 'ធិ': 2, + 'ធី': 2, + 'ធ្ង': 3, + 'ធ្នា': 4, + 'ធ្នូ': 4, + 'ធ្យា': 4, + 'ធ្យោ': 4, + 'ធ្វើ': 4, + 'នាំ': 3, + 'និ': 2, + 'នី': 2, + 'នឹ': 2, + 'នុ': 2, + 'នូ': 2, + 'នួ': 2, + 'នេះ': 3, + 'នោះ': 3, + 'ន់': 2, + 'ន៍': 2, + 'ន្ដ': 3, + 'ន្ដិ': 4, + 'ន្ថែ': 4, + 'ន្ថោ': 4, + 'ន្ទា': 4, + 'ន្ទៈ': 4, + 'ន្ធ': 3, + 'ន្និ': 4, + 'ន្ម': 3, + 'ន្យា': 4, + 'ន្លែ': 4, + 'បិ': 2, + 'បី': 2, + 'បុ': 2, + 'បូ': 2, + 'បោះ': 3, + 'បំ': 2, + 'ប៉ុ': 3, + 'ប៉ះ': 3, + 'ប់': 2, + 'ប្ដី': 4, + 'ប្ដូ': 4, + 'ប្ដេ': 4, + 'ប្ប': 3, + 'ប្បី': 4, + 'ប្រ': 3, + 'ប្រា': 4, + 'ប្រឹ': 4, + 'ប្រុ': 4, + 'ប្រើ': 4, + 'ប្រែ': 4, + 'ប្អូ': 4, + 'ផុ': 2, + 'ផ្ដ': 3, + 'ផ្ដា': 4, + 'ផ្ទា': 4, + 'ផ្ទុ': 4, + 'ផ្ទេ': 4, + 'ផ្នែ': 4, + 'ផ្លា': 4, + 'ផ្លិ': 4, + 'ផ្ស': 3, + 'ផ្សា': 4, + 'ផ្សេ': 4, + 'ផ្សៃ': 4, + 'ពិ': 2, + 'ពី': 2, + 'ពុ': 2, + 'ពូ': 2, + 'ពោះ': 3, + 'ព័': 2, + 'ព្យ': 3, + 'ព្យា': 4, + 'ព្រ': 3, + 'ព្រឹ': 4, + 'ព្រៀ': 4, + 'ព្រៃ': 4, + 'ព្រំ': 4, + 'ព្វ': 3, + 'ភិ': 2, + 'ភ័': 2, + 'មិ': 2, + 'មុ': 2, + 'មូ': 2, + 'មួ': 2, + 'ម៉ា': 3, + 'ម៉ោ': 3, + 'ម៌': 2, + 'ម័': 2, + 'ម្ងា': 4, + 'ម្ចា': 4, + 'ម្ដា': 4, + 'ម្ដែ': 4, + 'ម្នា': 4, + 'ម្នេ': 4, + 'ម្ប': 3, + 'ម្បី': 4, + 'ម្បុ': 4, + 'ម្ភា': 4, + 'ម្ម': 3, + 'ម្រ': 3, + 'ម្រា': 4, + 'ម្រិ': 4, + 'ម្រូ': 4, + 'ម្រើ': 4, + 'ម្រេ': 4, + 'ម្លៀ': 4, + 'ម្លៃ': 4, + 'យុ': 2, + 'យ៉ា': 3, + 'យ្យ': 3, + 'រាំ': 3, + 'រិ': 2, + 'រី': 2, + 'រុ': 2, + 'រូ': 2, + 'រួ': 2, + 'រំ': 2, + 'រ្ដិ៍': 5, + 'លិ': 2, + 'លុះ': 3, + 'លូ': 2, + 'លំ': 2, + 'ល់': 2, + 'ល្បៈ': 4, + 'ល្មើ': 4, + 'វិ': 2, + 'សិ': 2, + 'សី': 2, + 'សុ': 2, + 'សុំ': 3, + 'សំ': 2, + 'ស់': 2, + 'ស័': 2, + 'ស្គា': 4, + 'ស្ដី': 4, + 'ស្ដ្រី': 6, + 'ស្ត្រ': 5, + 'ស្ថា': 4, + 'ស្ថិ': 4, + 'ស្ទួ': 4, + 'ស្នា': 4, + 'ស្មា': 4, + 'ស្មើ': 4, + 'ស្រ': 3, + 'ស្រា': 4, + 'ស្រី': 4, + 'ស្រ័': 4, + 'ស្លា': 4, + 'ស្វែ': 4, + 'ស្វ័': 4, + 'ស្ស': 3, + 'ហូ': 2, + 'ហ៍': 2, + 'ហ្ម': 3, + 'ឡុ': 2, + 'អំ': 2, + 'អ្ន': 3, + 'អ្វី': 4, + 'ឱ្យ': 3, + 'ᨠ᩠ᨠ': 3, + 'ᨠ᩠ᨦᩢᩢ': 5, + 'ᨠ᩠ᨦᩨ᩵': 5, + 'ᨠ᩠ᨭᩫ': 4, + 'ᨠ᩠ᨯᩢ': 4, + 'ᨠ᩠ᨶᩢ': 4, + 'ᨠ᩠ᨶᩥ': 4, + 'ᨠ᩠ᩅ': 3, + 'ᨠ᩠ᩅᩢ': 4, + 'ᨠ᩠ᩅᩫ': 4, + 'ᨠ᩠ᩋ᩵': 4, + 'ᨠᩢ': 2, + 'ᨠᩢ᩠ᨷ': 4, + 'ᨠᩥ': 2, + 'ᨠᩨ': 2, + 'ᨠᩩ': 2, + 'ᨠᩮ᩠': 3, + 'ᨠᩮᩢ᩵': 4, + 'ᨠᩮ᩵': 3, + 'ᨠᩯ᩠': 3, + 'ᨠᩯ᩵': 3, + 'ᨠᩳ᩵': 3, + 'ᨠ᩵': 2, + 'ᨠ᩵ᩴ': 3, + 'ᨡ᩠ᨦᩢ': 4, + 'ᨡ᩠ᨯᩢ': 4, + 'ᨡ᩠ᨶ': 3, + 'ᨡ᩠ᨶᩢ': 4, + 'ᨡ᩠ᨶᩨ᩶': 5, + 'ᨡ᩠ᩅ': 3, + 'ᨡ᩠ᩋ': 3, + 'ᨡᩢ': 2, + 'ᨡᩮ᩠': 3, + 'ᨡᩮᩢ': 3, + 'ᨡᩮᩢᩢ': 4, + 'ᨡᩯ᩠': 3, + 'ᨡᩱᩢ': 3, + 'ᨡᩳ᩶': 3, + 'ᨡ᩵': 2, + 'ᨣᩕ᩠': 3, + 'ᨣᩕᩮ᩠': 4, + 'ᨣ᩠ᨣ': 3, + 'ᨣ᩠ᨯᩧ': 4, + 'ᨣ᩠ᨶᩢ': 4, + 'ᨣ᩠ᨾᩫ': 4, + 'ᨣ᩠ᩅ': 3, + 'ᨣ᩠ᩋᩢ': 4, + 'ᨣᩢ': 2, + 'ᨣᩢ᩠ᨷ': 4, + 'ᨣᩢᩴ': 3, + 'ᨣᩩ': 2, + 'ᨣᩩᩢ': 3, + 'ᨣᩮᩢᩢ': 4, + 'ᨣᩯ᩠': 3, + 'ᨣᩳ᩶': 3, + 'ᨣᩴ': 2, + 'ᨤ᩠ᨯᩥ': 4, + 'ᨤ᩠ᨶᩨ': 4, + 'ᨤ᩠ᨶᩫ': 4, + 'ᨤ᩠ᩅᩫ': 4, + 'ᨤ᩠ᩋ': 3, + 'ᨤᩢ᩠ᨬ': 4, + 'ᨤᩮᩢ': 3, + 'ᨤᩱ᩵': 3, + 'ᨥᩢ': 2, + 'ᨦᩢ': 2, + 'ᨦᩢᩢ': 3, + 'ᨦᩨ': 2, + 'ᨦᩨ᩵': 3, + 'ᨦᩨ᩶': 3, + 'ᨦ᩵': 2, + 'ᨧ᩠ᨦᩥ᩵': 5, + 'ᨧ᩠ᨯᩢ': 4, + 'ᨧᩢ': 2, + 'ᨧᩮ᩠': 3, + 'ᨧᩮᩢᩢ': 4, + 'ᨧᩱ᩠': 3, + 'ᨧᩴ': 2, + 'ᨧ᩵ᩴ': 3, + 'ᨨ᩠ᨶᩢ': 4, + 'ᨩ᩠ᨶᩢᩢ': 5, + 'ᨩ᩠ᨶ᩼': 4, + 'ᨩᩢ': 2, + 'ᨩᩦ': 2, + 'ᨩᩨ᩵': 3, + 'ᨩᩩ': 2, + 'ᨩᩭ᩵': 3, + 'ᨩᩮ᩠': 3, + 'ᨩᩱᩢ': 3, + 'ᨩ᩵ᩭ': 3, + 'ᨪ᩠ᨦᩨ᩵': 5, + 'ᨪᩨ᩵': 3, + 'ᨪᩩ': 2, + 'ᨪᩮᩢ': 3, + 'ᨬ᩠ᨬ': 3, + 'ᨭᩛ': 2, + 'ᨭᩥ': 2, + 'ᨯ᩠ᨦᩢ᩵': 5, + 'ᨯ᩠ᨰ': 3, + 'ᨯ᩠ᨶᩥ': 4, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 6, + 'ᨯᩢ': 2, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 6, + 'ᨯᩦ': 2, + 'ᨯᩨ': 2, + 'ᨯᩪ': 2, + 'ᨯᩪᩯ': 3, + 'ᨯᩯ᩠': 3, + 'ᨯᩰ᩠': 3, + 'ᨯᩱᩢ': 3, + 'ᨯᩴ᩠ᨿ': 4, + 'ᨱ᩠ᨱ': 3, + 'ᨲᩕᩦ': 3, + 'ᨲᩕ᩼': 3, + 'ᨲ᩠ᨠᩫ': 4, + 'ᨲ᩠ᨦᩢᩢ': 5, + 'ᨲ᩠ᨯᩢ': 4, + 'ᨲ᩠ᨲᩥ': 4, + 'ᨲ᩠ᨳᩩ': 4, + 'ᨲ᩠ᨶᩫ': 4, + 'ᨲ᩠ᨶᩫᩢ': 5, + 'ᨲ᩠ᩅᩫ': 4, + 'ᨲ᩠ᩋᩢ': 4, + 'ᨲᩥ': 2, + 'ᨲᩩ': 2, + 'ᨲᩮ᩠': 3, + 'ᨲᩯ᩠': 3, + 'ᨲᩯ᩵': 3, + 'ᨲᩱ᩠': 3, + 'ᨲᩱᩢ': 3, + 'ᨲᩳ᩵': 3, + 'ᨲ᩵': 2, + 'ᨲ᩵ᩴ': 3, + 'ᨳ᩠ᨦᩨ': 4, + 'ᨳ᩠ᩅᩢ': 4, + 'ᨳᩨ': 2, + 'ᨳᩪ': 2, + 'ᨳᩫ᩠ᨷ': 4, + 'ᨳᩮ᩠': 3, + 'ᨳᩮᩢᩢ': 4, + 'ᨳᩯ᩠': 3, + 'ᨴ᩠ᨦᩢ': 4, + 'ᨴ᩠ᨦᩨ': 4, + 'ᨴ᩠ᨵᩥ': 4, + 'ᨴ᩠ᨿ': 3, + 'ᨴ᩠ᨿ᩵': 4, + 'ᨴ᩠ᩅᩢ': 4, + 'ᨴ᩠ᩅᩫ᩵': 5, + 'ᨴᩦ᩵': 3, + 'ᨴᩩ': 2, + 'ᨴᩮ᩠': 3, + 'ᨴᩮᩢᩢ': 4, + 'ᨴᩮᩢ᩵': 4, + 'ᨴᩯ᩠': 3, + 'ᨴᩯᩢ': 3, + 'ᨴᩰ᩠': 3, + 'ᨵᩥ': 2, + 'ᨵᩦ': 2, + 'ᨶ᩠ᨲᩥ': 4, + 'ᨶ᩠ᨶᩢᩢ': 5, + 'ᨶ᩠ᨶᩥ': 4, + 'ᨶ᩠ᩋ': 3, + 'ᨶ᩠ᩋᩢ': 4, + 'ᨶᩢ': 2, + 'ᨶᩢ᩠ᨷ': 4, + 'ᨶᩦ᩶': 3, + 'ᨶᩨ': 2, + 'ᨶᩨ᩵': 3, + 'ᨶᩩ': 2, + 'ᨶᩩ᩵': 3, + 'ᨶᩯ᩠': 3, + 'ᨶ᩵': 2, + 'ᨷ᩠ᨦᩢ': 4, + 'ᨷ᩠ᨯᩢ': 4, + 'ᨷ᩠ᨲᩢ': 4, + 'ᨷ᩠ᨲᩥ': 4, + 'ᨷ᩠ᨴᩫ': 4, + 'ᨷ᩠ᨿ': 3, + 'ᨷ᩠ᩅᩫ': 4, + 'ᨷ᩠ᩋ': 3, + 'ᨷᩢ': 2, + 'ᨷᩥ': 2, + 'ᨷᩩ': 2, + 'ᨷᩪ': 2, + 'ᨷᩮ᩠': 3, + 'ᨸ᩠ᨠᩫ': 4, + 'ᨸ᩠ᨯᩨ': 4, + 'ᨸ᩠ᨶᩢ': 4, + 'ᨸ᩠ᩋ': 3, + 'ᨸᩮ᩠': 3, + 'ᨸᩮᩢ᩵': 4, + 'ᨸ᩵': 2, + 'ᨸ᩠᩵ᨿ': 4, + 'ᨹ᩠ᨯᩥ': 4, + 'ᨹ᩠ᨿ᩵': 4, + 'ᨹ᩠ᩅᩥ': 4, + 'ᨹᩪᩢ': 3, + 'ᨹᩪᩢᩱ': 4, + 'ᨹᩫ᩠ᩃ': 4, + 'ᨹᩮᩨ᩠': 4, + 'ᨹ᩵': 2, + 'ᨻᩕ᩠': 3, + 'ᨻ᩠ᨦ': 3, + 'ᨻ᩠ᨶᩨ᩶': 5, + 'ᨻ᩠ᨶᩫ᩶': 5, + 'ᨻ᩠ᨿ': 3, + 'ᨻ᩠ᩋ': 3, + 'ᨻᩥ': 2, + 'ᨻᩦ᩵': 3, + 'ᨻᩮ᩠': 3, + 'ᨻᩳ': 2, + 'ᨽᩱ᩠': 3, + 'ᨾ᩠ᨲᩥ': 4, + 'ᨾ᩠ᨾ': 3, + 'ᨾ᩠ᨾ᩼': 4, + 'ᨾ᩠ᩅ': 3, + 'ᨾ᩠ᩅ᩵': 4, + 'ᨾᩢ': 2, + 'ᨾᩦ': 2, + 'ᨾᩨ': 2, + 'ᨾᩨᩨ': 3, + 'ᨾᩨ᩵': 3, + 'ᨾᩩ': 2, + 'ᨾᩪ': 2, + 'ᨾᩮ᩠': 3, + 'ᨾᩯ᩵': 3, + 'ᨾ᩵': 2, + 'ᨿ᩠ᨦᩢ': 4, + 'ᨿ᩠ᨦᩢᩢ': 5, + 'ᨿ᩠ᨦᩥ': 4, + 'ᨿ᩠ᨦᩥ᩵': 5, + 'ᨿ᩠ᩋ': 3, + 'ᨿᩢ᩠ᨷ': 4, + 'ᨿᩩ': 2, + 'ᨿᩫ᩠ᩈ': 4, + 'ᨿᩮ᩠': 3, + 'ᨿ᩵': 2, + 'ᩀᩪ᩵': 3, + 'ᩀ᩵': 2, + 'ᩁ᩠ᨠᩢ': 4, + 'ᩁ᩠ᨮᩢ': 4, + 'ᩁ᩠ᨾᩫ': 4, + 'ᩁ᩠ᨿ': 3, + 'ᩁ᩠ᩅ': 3, + 'ᩁ᩠ᩅ᩵': 4, + 'ᩁ᩠ᩋ': 3, + 'ᩁᩢ': 2, + 'ᩁᩢ᩠ᨷ': 4, + 'ᩁᩢᩭ': 3, + 'ᩁᩥ': 2, + 'ᩁᩦ': 2, + 'ᩁᩪᩢ': 3, + 'ᩁᩫ᩠ᨷ': 4, + 'ᩁᩮ᩠': 3, + 'ᩁᩮᩨ᩠': 4, + 'ᩁᩯ᩠': 3, + 'ᩃ᩠ᨦᩫ': 4, + 'ᩃ᩠ᩅ᩵': 4, + 'ᩃᩢ᩠ᨿ': 4, + 'ᩃᩢᩴ': 3, + 'ᩃᩥ': 2, + 'ᩃᩨ᩠ᨷ': 4, + 'ᩃᩩ': 2, + 'ᩃᩪ': 2, + 'ᩃᩮ᩠': 3, + 'ᩃᩰ᩠': 3, + 'ᩅ᩠ᨲᩥ': 4, + 'ᩅ᩠ᨶᩢ': 4, + 'ᩅ᩠ᨿ': 3, + 'ᩅ᩠ᩅ᩵': 4, + 'ᩅᩢ': 2, + 'ᩅᩥ': 2, + 'ᩅᩮ᩠': 3, + 'ᩅᩱᩢ': 3, + 'ᩈ᩠ᨠᩢ': 4, + 'ᩈ᩠ᨠᩧ': 4, + 'ᩈ᩠ᨦᩢ': 4, + 'ᩈ᩠ᨦᩥ᩵': 5, + 'ᩈ᩠ᨦᩫ': 4, + 'ᩈ᩠ᨶᩫ': 4, + 'ᩈ᩠ᨾᩫ': 4, + 'ᩈ᩠ᨿ': 3, + 'ᩈ᩠ᩅ᩵': 4, + 'ᩈ᩠ᩋ': 3, + 'ᩈᩢ': 2, + 'ᩈᩢ᩠ᨿ': 4, + 'ᩈᩥ': 2, + 'ᩈᩦ': 2, + 'ᩈᩨ᩵': 3, + 'ᩈᩩ': 2, + 'ᩈᩪ': 2, + 'ᩈᩪᩢ': 3, + 'ᩈᩫ᩠ᨾ': 4, + 'ᩈᩮ᩠': 3, + 'ᩈᩯ᩠': 3, + 'ᩈᩱ᩠': 3, + 'ᩈᩳ᩵': 3, + 'ᩈ᩵ᩴ': 3, + 'ᩉᩕᩨ': 3, + 'ᩉᩖ': 2, + 'ᩉᩖᩢ': 3, + 'ᩉ᩠ᨶᩢ': 4, + 'ᩉ᩠ᨶᩦ': 4, + 'ᩉ᩠ᨶᩧ': 4, + 'ᩉ᩠ᨶᩧ᩵': 5, + 'ᩉ᩠ᨶᩫ': 4, + 'ᩉ᩠ᨾ': 3, + 'ᩉ᩠ᨾᩢ': 4, + 'ᩉ᩠ᨾᩢᩢ': 5, + 'ᩉ᩠ᨾᩪ᩵': 5, + 'ᩉ᩠ᨾᩳ': 4, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 6, + 'ᩉ᩠ᨿᩩ': 4, + 'ᩉ᩠ᩅᩫ': 4, + 'ᩉ᩠ᩅ᩵': 4, + 'ᩉᩢ': 2, + 'ᩉᩨᩢ': 3, + 'ᩉᩨ᩶': 3, + 'ᩉᩮᩖᩨ': 4, + 'ᩉᩮ᩠': 3, + 'ᩉᩯ᩠': 3, + 'ᩉᩯ᩵': 3, + 'ᩉᩱ᩠': 3, + 'ᩉᩳ': 2, + 'ᩋ᩠ᨶᩢ': 4, + 'ᩋ᩠ᨶᩨ᩵': 5, + 'ᩋ᩠ᩋ': 3, + 'ᩋᩢ': 2, + 'ᩋᩨ': 2, + 'ᩋᩨ᩵': 3, + 'ᩋᩨ᩶': 3, + 'ᩋᩪᩢ': 3, + 'ᩋᩴ': 2, + 'ᩐᩢ': 2, + 'ᩓ᩠ᩅᩢ': 4, + 'ᩓᩢ': 2, + 'ᩔ᩼': 2, + 'ᵾ́': 2, + 'Ẹ̀': 2, + 'ẹ̀': 2, + 'ẹ́': 2, + 'ẽ́': 2, + 'ẽ̱': 2, + 'Ọ̀': 2, + 'ọ̀': 2, + 'Ọ́': 2, + 'ọ́': 2, + '⛓️‍💥': 5, + '⛹️‍♀️': 5, + '⛹️‍♂️': 5, + '⛹🏻‍♀️': 6, + '⛹🏻‍♂️': 6, + '⛹🏼‍♀️': 6, + '⛹🏼‍♂️': 6, + '⛹🏽‍♀️': 6, + '⛹🏽‍♂️': 6, + '⛹🏾‍♀️': 6, + '⛹🏾‍♂️': 6, + '⛹🏿‍♀️': 6, + '⛹🏿‍♂️': 6, + '❤️‍🔥': 5, + '❤️‍🩹': 5, + 'ꦉꦁ': 2, + 'ꦏꦁ': 2, + 'ꦏꦂ': 2, + 'ꦏꦶ': 2, + 'ꦏꦶꦂ': 3, + 'ꦏꦸ': 2, + 'ꦏꦸꦂ': 3, + 'ꦏꦺꦴ': 3, + 'ꦏꦼ': 2, + 'ꦏꦿꦁ': 3, + 'ꦏꦿꦂ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 4, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 4, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 4, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 4, + 'ꦏ꧀ꦠꦸ': 4, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 4, + 'ꦏ꧀ꦢꦶ': 4, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 5, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 4, + 'ꦏ꧀ꦥꦿꦶ': 5, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 4, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 4, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 4, + 'ꦏ꧀ꦱꦶ': 4, + 'ꦏ꧀ꦱꦸ': 4, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 4, + 'ꦏ꧀ꦲꦶꦁ': 5, + 'ꦏ꧀ꦲꦸ': 4, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦁ': 2, + 'ꦒꦂ': 2, + 'ꦒꦸ': 2, + 'ꦒꦸꦁ': 3, + 'ꦒꦸꦂ': 3, + 'ꦒꦸꦃ': 3, + 'ꦒꦺꦴ': 3, + 'ꦒꦼ': 2, + 'ꦒꦼꦂ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦁ': 2, + 'ꦔꦶ': 2, + 'ꦔꦶꦁ': 3, + 'ꦔꦸ': 2, + 'ꦔꦺꦴ': 3, + 'ꦔꦼ': 2, + 'ꦔꦼꦂ': 3, + 'ꦔꦽ': 2, + 'ꦔ꧀ꦒꦼ': 4, + 'ꦔ꧀ꦒꦿꦼꦁ': 6, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 4, + 'ꦔ꧀ꦭꦸ': 4, + 'ꦔ꧀ꦭꦼ': 4, + 'ꦕꦶ': 2, + 'ꦕꦶꦁ': 3, + 'ꦕꦸ': 2, + 'ꦕꦺꦴ': 3, + 'ꦗꦶ': 2, + 'ꦗꦸ': 2, + 'ꦗꦼ': 2, + 'ꦗꦿꦺꦴ': 4, + 'ꦚꦁ': 2, + 'ꦚꦶꦁ': 3, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 4, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 4, + 'ꦚ꧀ꦗꦸꦁ': 5, + 'ꦚ꧀ꦗꦸꦂ': 5, + 'ꦝꦁ': 2, + 'ꦝꦂ': 2, + 'ꦝꦶ': 2, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 4, + 'ꦝꦼ': 2, + 'ꦝꦼꦁ': 3, + 'ꦠꦁ': 2, + 'ꦠꦶ': 2, + 'ꦠꦸ': 2, + 'ꦠꦸꦁ': 3, + 'ꦠꦸꦂ': 3, + 'ꦠꦸꦃ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠꦼ': 2, + 'ꦠꦼꦂ': 3, + 'ꦠꦽ': 2, + 'ꦠꦿꦶ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 4, + 'ꦠ꧀ꦏꦸ': 4, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 4, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 4, + 'ꦠ꧀ꦠꦶꦁ': 5, + 'ꦠ꧀ꦠꦸ': 4, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 4, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 4, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 5, + 'ꦠ꧀ꦱꦶꦃ': 5, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 5, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦂ': 2, + 'ꦢꦶ': 2, + 'ꦢꦸ': 2, + 'ꦢꦸꦂ': 3, + 'ꦢꦺꦴ': 3, + 'ꦢꦼ': 2, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 5, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦁ': 2, + 'ꦤꦂ': 2, + 'ꦤꦶ': 2, + 'ꦤꦶꦁ': 3, + 'ꦤꦸ': 2, + 'ꦤꦸꦁ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤꦼ': 2, + 'ꦤꦼꦁ': 3, + 'ꦤꦼꦂ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 4, + 'ꦤ꧀ꦏꦸ': 4, + 'ꦤ꧀ꦏꦼ': 4, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 4, + 'ꦤ꧀ꦔꦶ': 4, + 'ꦤ꧀ꦔꦸ': 4, + 'ꦤ꧀ꦔꦸꦂ': 5, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 4, + 'ꦤ꧀ꦔꦿꦸ': 5, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 4, + 'ꦤ꧀ꦗꦼ': 4, + 'ꦤ꧀ꦚꦶ': 4, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 4, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 4, + 'ꦤ꧀ꦝꦶ': 4, + 'ꦤ꧀ꦝꦸ': 4, + 'ꦤ꧀ꦝꦸꦁ': 5, + 'ꦤ꧀ꦝꦺꦁ': 5, + 'ꦤ꧀ꦝꦺꦴꦁ': 6, + 'ꦤ꧀ꦝꦼ': 4, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 4, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 4, + 'ꦤ꧀ꦠꦸ': 4, + 'ꦤ꧀ꦠꦸꦁ': 5, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 4, + 'ꦤ꧀ꦠꦼꦂ': 5, + 'ꦤ꧀ꦠꦽ': 4, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 4, + 'ꦤ꧀ꦢꦂ': 4, + 'ꦤ꧀ꦢꦶ': 4, + 'ꦤ꧀ꦢꦸ': 4, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 4, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 4, + 'ꦤ꧀ꦤꦶꦁ': 5, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 4, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 4, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 7, + 'ꦤ꧀ꦥꦶ': 4, + 'ꦤ꧀ꦥꦸ': 4, + 'ꦤ꧀ꦥꦼ': 4, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 5, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 4, + 'ꦤ꧀ꦧꦶ': 4, + 'ꦤ꧀ꦧꦸ': 4, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 4, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 4, + 'ꦤ꧀ꦩꦶ': 4, + 'ꦤ꧀ꦩꦸꦂ': 5, + 'ꦤ꧀ꦩꦼ': 4, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 4, + 'ꦤ꧀ꦭꦸ': 4, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 4, + 'ꦤ꧀ꦮꦶ': 4, + 'ꦤ꧀ꦮꦸ': 4, + 'ꦤ꧀ꦮꦼ': 4, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 4, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 4, + 'ꦤ꧀ꦱꦶꦁ': 5, + 'ꦤ꧀ꦱꦸ': 4, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 4, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 4, + 'ꦤ꧀ꦲꦶ': 4, + 'ꦤ꧀ꦲꦶꦁ': 5, + 'ꦤ꧀ꦲꦸ': 4, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥꦁ': 2, + 'ꦥ꦳ꦼ': 3, + 'ꦥ꦳꧀': 3, + 'ꦥꦶ': 2, + 'ꦥꦸ': 2, + 'ꦥꦸꦁ': 3, + 'ꦥꦺꦴ': 3, + 'ꦥꦼ': 2, + 'ꦥꦼꦂ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦶ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 4, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 4, + 'ꦥ꧀ꦢꦸ': 4, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 4, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 4, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 4, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 4, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 5, + 'ꦥ꧀ꦱꦼ': 4, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦁ': 2, + 'ꦧꦶ': 2, + 'ꦧꦸ': 2, + 'ꦧꦸꦂ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧꦼ': 2, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 4, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 4, + 'ꦩꦁ': 2, + 'ꦩꦂ': 2, + 'ꦩꦶ': 2, + 'ꦩꦸ': 2, + 'ꦩꦸꦁ': 3, + 'ꦩꦸꦂ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦼ': 2, + 'ꦩꦼꦂ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 4, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 4, + 'ꦩ꧀ꦔꦼ': 4, + 'ꦩ꧀ꦠꦸ': 4, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 5, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 4, + 'ꦩ꧀ꦥꦸ': 4, + 'ꦩ꧀ꦥꦼ': 4, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 4, + 'ꦩ꧀ꦧꦂ': 4, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 4, + 'ꦩ꧀ꦧꦸ': 4, + 'ꦩ꧀ꦧꦸꦁ': 5, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 5, + 'ꦩ꧀ꦧꦼꦂ': 5, + 'ꦩ꧀ꦧꦾꦸꦂ': 6, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 4, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 4, + 'ꦪꦁ': 2, + 'ꦪꦶ': 2, + 'ꦪꦸ': 2, + 'ꦪꦸꦁ': 3, + 'ꦪꦸꦃ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦁ': 2, + 'ꦫꦶ': 2, + 'ꦫꦶꦁ': 3, + 'ꦫꦶꦃ': 3, + 'ꦫꦸ': 2, + 'ꦫꦸꦁ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦁ': 2, + 'ꦭꦂ': 2, + 'ꦭꦶ': 2, + 'ꦭꦶꦁ': 3, + 'ꦭꦶꦃ': 3, + 'ꦭꦸ': 2, + 'ꦭꦸꦁ': 3, + 'ꦭꦸꦂ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 4, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 4, + 'ꦭ꧀ꦚꦼꦁ': 5, + 'ꦭ꧀ꦠꦸ': 4, + 'ꦭ꧀ꦧꦸ': 4, + 'ꦭ꧀ꦩꦸ': 4, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 4, + 'ꦭ꧀ꦭꦶꦁ': 5, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 4, + 'ꦭ꧀ꦱꦶꦁ': 5, + 'ꦭ꧀ꦱꦸ': 4, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 4, + 'ꦭ꧀ꦲꦶꦁ': 5, + 'ꦭ꧀ꦲꦸ': 4, + 'ꦮꦂ': 2, + 'ꦮ꦳ꦼꦂ': 4, + 'ꦮꦶ': 2, + 'ꦮꦶꦃ': 3, + 'ꦮꦸ': 2, + 'ꦮꦸꦁ': 3, + 'ꦮꦸꦃ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 4, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦼ': 2, + 'ꦮꦼꦁ': 3, + 'ꦮꦼꦂ': 3, + 'ꦮꦿꦸꦃ': 4, + 'ꦱꦂ': 2, + 'ꦱꦶ': 2, + 'ꦱꦶꦁ': 3, + 'ꦱꦶꦂ': 3, + 'ꦱꦶꦃ': 3, + 'ꦱꦸ': 2, + 'ꦱꦸꦁ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦼ': 2, + 'ꦱꦼꦁ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 4, + 'ꦱ꧀ꦏꦿꦶ': 5, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 4, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 4, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 4, + 'ꦱ꧀ꦢꦶ': 4, + 'ꦱ꧀ꦢꦸ': 4, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 4, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 4, + 'ꦱ꧀ꦩꦼ': 4, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 4, + 'ꦱ꧀ꦱꦶ': 4, + 'ꦱ꧀ꦱꦶꦁ': 5, + 'ꦱ꧀ꦱꦸ': 4, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 4, + 'ꦱ꧀ꦲꦶ': 4, + 'ꦱ꧀ꦲꦶꦁ': 5, + 'ꦱ꧀ꦲꦸ': 4, + 'ꦲꦁ': 2, + 'ꦲꦶ': 2, + 'ꦲꦶꦁ': 3, + 'ꦲꦶꦂ': 3, + 'ꦲꦸ': 2, + 'ꦲꦸꦂ': 3, + 'ꦲꦺꦴ': 3, + 'ꦲꦼ': 2, + 'ꪀꪰ': 2, + 'ꪀꪲ': 2, + 'ꪀꪳ': 2, + 'ꪀꪴ': 2, + 'ꪀꪷ': 2, + 'ꪀꪾ': 2, + 'ꪁꪴ': 2, + 'ꪁꪷ': 2, + 'ꪁꪸ': 2, + 'ꪁꪾ': 2, + 'ꪄꪰ': 2, + 'ꪄꪲ': 2, + 'ꪄꪳ': 2, + 'ꪄꪴ': 2, + 'ꪄꪷ': 2, + 'ꪅꪰ': 2, + 'ꪈꪲ': 2, + 'ꪈꪸ': 2, + 'ꪈꪾ': 2, + 'ꪉꪷ': 2, + 'ꪉꪸ': 2, + 'ꪊꪰ': 2, + 'ꪊꪲ': 2, + 'ꪊꪴ': 2, + 'ꪊꪸ': 2, + 'ꪋꪲ': 2, + 'ꪋꪳ': 2, + 'ꪋꪴ': 2, + 'ꪋꪷ': 2, + 'ꪋꪸ': 2, + 'ꪎꪰ': 2, + 'ꪎꪲ': 2, + 'ꪎꪳ': 2, + 'ꪎꪴ': 2, + 'ꪎꪷ': 2, + 'ꪎꪸ': 2, + 'ꪏꪴ': 2, + 'ꪏꪷ': 2, + 'ꪏꪸ': 2, + 'ꪑꪰ': 2, + 'ꪑꪲ': 2, + 'ꪑꪷ': 2, + 'ꪒꪰ': 2, + 'ꪒꪲ': 2, + 'ꪒꪴ': 2, + 'ꪒꪷ': 2, + 'ꪒꪸ': 2, + 'ꪔꪰ': 2, + 'ꪔꪲ': 2, + 'ꪔꪳ': 2, + 'ꪔꪷ': 2, + 'ꪔꪸ': 2, + 'ꪔꪾ': 2, + 'ꪕꪰ': 2, + 'ꪕꪲ': 2, + 'ꪕꪳ': 2, + 'ꪕꪴ': 2, + 'ꪕꪷ': 2, + 'ꪕꪸ': 2, + 'ꪕꪾ': 2, + 'ꪖꪸ': 2, + 'ꪘꪰ': 2, + 'ꪘꪳ': 2, + 'ꪙꪲ': 2, + 'ꪙꪴ': 2, + 'ꪙꪾ': 2, + 'ꪚꪰ': 2, + 'ꪚꪴ': 2, + 'ꪚꪷ': 2, + 'ꪜꪰ': 2, + 'ꪜꪲ': 2, + 'ꪜꪴ': 2, + 'ꪜꪸ': 2, + 'ꪝꪰ': 2, + 'ꪝꪲ': 2, + 'ꪝꪳ': 2, + 'ꪝꪷ': 2, + 'ꪝꪸ': 2, + 'ꪠꪲ': 2, + 'ꪠꪴ': 2, + 'ꪠꪸ': 2, + 'ꪢꪲ': 2, + 'ꪢꪴ': 2, + 'ꪢꪸ': 2, + 'ꪣꪰ': 2, + 'ꪣꪲ': 2, + 'ꪣꪳ': 2, + 'ꪣꪷ': 2, + 'ꪣꪸ': 2, + 'ꪤꪰ': 2, + 'ꪤꪴ': 2, + 'ꪤꪷ': 2, + 'ꪤꪸ': 2, + 'ꪤꪾ': 2, + 'ꪥꪰ': 2, + 'ꪥꪴ': 2, + 'ꪥꪸ': 2, + 'ꪨꪰ': 2, + 'ꪨꪷ': 2, + 'ꪨꪸ': 2, + 'ꪩꪰ': 2, + 'ꪩꪳ': 2, + 'ꪩꪴ': 2, + 'ꪩꪷ': 2, + 'ꪩꪾ': 2, + 'ꪪꪴ': 2, + 'ꪫꪸ': 2, + 'ꪬꪷ': 2, + 'ꪬꪸ': 2, + 'ꪭꪰ': 2, + 'ꪭꪲ': 2, + 'ꪭꪳ': 2, + 'ꪭꪴ': 2, + 'ꪭꪷ': 2, + 'ꪭꪸ': 2, + 'ꪮꪲ': 2, + 'ꪮꪳ': 2, + 'ꪮꪴ': 2, + 'ꪮꪸ': 2, + 'ꪯꪸ': 2, + '𑄃𑄧': 2, + '𑄃𑄧𑄁': 3, + '𑄃𑄨': 2, + '𑄃𑄪': 2, + '𑄃𑄬𑄭': 3, + '𑄃𑄮': 2, + '𑄃𑄰': 2, + '𑄃𑄳𑄃': 3, + '𑄃𑄳𑄦𑄧': 4, + '𑄃𑄳𑄦𑄨': 4, + '𑄅𑄪': 2, + '𑄅𑄫': 2, + '𑄇𑄧': 2, + '𑄇𑄧𑄧': 3, + '𑄇𑄧𑄧𑄱': 4, + '𑄇𑄨': 2, + '𑄇𑄨𑄁': 3, + '𑄇𑄪': 2, + '𑄇𑄭𑄭': 3, + '𑄇𑄳𑄠': 3, + '𑄇𑄳𑄠𑄬': 4, + '𑄇𑄳𑄠𑄴': 4, + '𑄇𑄳𑄢𑄨': 4, + '𑄇𑄳𑄦𑄧': 4, + '𑄇𑄳𑄦𑄴': 4, + '𑄇𑄴': 2, + '𑄈𑄧': 2, + '𑄈𑄪': 2, + '𑄈𑄳𑄠': 3, + '𑄈𑄳𑄠𑄬': 4, + '𑄉𑄧': 2, + '𑄉𑄨': 2, + '𑄉𑄩': 2, + '𑄉𑄪': 2, + '𑄉𑄬𑄭': 3, + '𑄉𑄭': 2, + '𑄉𑄮': 2, + '𑄉𑄳𑄠': 3, + '𑄉𑄳𑄢𑄧': 4, + '𑄊𑄧': 2, + '𑄊𑄨': 2, + '𑄋𑄧': 2, + '𑄋𑄨': 2, + '𑄌𑄧': 2, + '𑄌𑄨': 2, + '𑄌𑄮': 2, + '𑄌𑄯': 2, + '𑄌𑄳𑄠𑄬': 4, + '𑄌𑄳𑄠𑄴': 4, + '𑄌𑄴': 2, + '𑄍𑄨': 2, + '𑄍𑄪': 2, + '𑄎𑄧': 2, + '𑄎𑄨': 2, + '𑄎𑄨𑄁': 3, + '𑄎𑄩': 2, + '𑄎𑄩𑄁': 3, + '𑄎𑄪': 2, + '𑄎𑄪𑄁': 3, + '𑄎𑄬𑄭': 3, + '𑄎𑄮': 2, + '𑄎𑄳𑄠𑄧': 4, + '𑄎𑄳𑄠𑄬': 4, + '𑄏𑄨': 2, + '𑄏𑄮': 2, + '𑄑𑄨': 2, + '𑄑𑄳𑄢𑄬': 4, + '𑄒𑄨': 2, + '𑄓𑄧': 2, + '𑄓𑄨': 2, + '𑄓𑄮': 2, + '𑄕𑄧': 2, + '𑄖𑄧': 2, + '𑄖𑄧𑄧': 3, + '𑄖𑄧𑄧𑄱': 4, + '𑄖𑄨': 2, + '𑄖𑄨𑄨': 3, + '𑄖𑄨𑄨𑄪': 4, + '𑄖𑄩': 2, + '𑄖𑄪': 2, + '𑄖𑄳𑄠𑄧': 4, + '𑄖𑄳𑄠𑄬': 4, + '𑄖𑄳𑄠𑄴': 4, + '𑄖𑄳𑄢𑄨': 4, + '𑄖𑄳𑄢𑄬': 4, + '𑄖𑄳𑄦𑄴': 4, + '𑄖𑄴': 2, + '𑄗𑄧': 2, + '𑄗𑄪': 2, + '𑄗𑄭': 2, + '𑄘𑄁': 2, + '𑄘𑄧': 2, + '𑄘𑄨': 2, + '𑄘𑄪': 2, + '𑄘𑄮': 2, + '𑄘𑄳𑄠𑄧': 4, + '𑄘𑄳𑄠𑄬': 4, + '𑄙𑄧': 2, + '𑄙𑄨': 2, + '𑄙𑄩': 2, + '𑄙𑄪': 2, + '𑄙𑄮': 2, + '𑄙𑄳𑄠': 3, + '𑄙𑄳𑄠𑄧': 4, + '𑄚𑄧': 2, + '𑄚𑄧𑄧𑄱': 4, + '𑄚𑄨': 2, + '𑄚𑄩': 2, + '𑄚𑄪': 2, + '𑄚𑄬𑄭': 3, + '𑄚𑄮': 2, + '𑄚𑄳𑄠𑄧': 4, + '𑄚𑄳𑄠𑄬': 4, + '𑄚𑄳𑄠𑄴': 4, + '𑄚𑄳𑄢𑄧𑄧': 5, + '𑄚𑄳𑄦𑄧': 4, + '𑄚𑄳𑄦𑄨': 4, + '𑄚𑄳𑄦𑄴': 4, + '𑄚𑄴': 2, + '𑄛𑄧': 2, + '𑄛𑄨': 2, + '𑄛𑄪': 2, + '𑄛𑄭': 2, + '𑄛𑄮': 2, + '𑄛𑄱': 2, + '𑄛𑄳𑄢': 3, + '𑄛𑄳𑄢𑄧': 4, + '𑄛𑄳𑄦': 3, + '𑄛𑄴': 2, + '𑄜𑄧': 2, + '𑄜𑄨': 2, + '𑄜𑄪': 2, + '𑄜𑄯': 2, + '𑄝𑄧': 2, + '𑄝𑄧𑄧': 3, + '𑄝𑄨': 2, + '𑄝𑄪': 2, + '𑄝𑄮': 2, + '𑄝𑄰': 2, + '𑄝𑄳𑄠': 3, + '𑄝𑄳𑄢𑄨': 4, + '𑄝𑄳𑄢𑄬': 4, + '𑄞𑄧': 2, + '𑄞𑄨': 2, + '𑄞𑄩': 2, + '𑄞𑄪': 2, + '𑄞𑄬𑄭': 3, + '𑄞𑄮': 2, + '𑄞𑄯': 2, + '𑄟𑄧': 2, + '𑄟𑄨': 2, + '𑄟𑄪': 2, + '𑄟𑄫': 2, + '𑄟𑄮': 2, + '𑄟𑄱': 2, + '𑄟𑄳𑄠𑄬': 4, + '𑄟𑄴': 2, + '𑄠𑄧': 2, + '𑄠𑄨': 2, + '𑄠𑄬𑄭': 3, + '𑄠𑄮': 2, + '𑄠𑄴': 2, + '𑄡𑄧': 2, + '𑄡𑄨': 2, + '𑄡𑄪': 2, + '𑄡𑄬𑄭': 3, + '𑄡𑄳𑄠𑄬': 4, + '𑄢𑄁': 2, + '𑄢𑄧': 2, + '𑄢𑄨': 2, + '𑄢𑄩': 2, + '𑄢𑄪': 2, + '𑄢𑄬𑄭': 3, + '𑄢𑄮': 2, + '𑄢𑄳𑄦𑄧𑄱𑄱': 6, + '𑄢𑄴': 2, + '𑄣𑄧': 2, + '𑄣𑄨': 2, + '𑄣𑄩': 2, + '𑄣𑄪': 2, + '𑄣𑄬𑄭': 3, + '𑄣𑄮': 2, + '𑄣𑄰𑄱': 3, + '𑄣𑄳𑄦𑄬': 4, + '𑄣𑄳𑄦𑄴': 4, + '𑄣𑄴': 2, + '𑄥𑄁': 2, + '𑄥𑄧': 2, + '𑄥𑄧𑄁': 3, + '𑄥𑄨': 2, + '𑄥𑄩': 2, + '𑄥𑄪': 2, + '𑄥𑄳𑄠': 3, + '𑄥𑄳𑄠𑄧': 4, + '𑄥𑄳𑄢𑄧': 4, + '𑄥𑄳𑄢𑄨𑄁': 5, + '𑄦𑄧': 2, + '𑄦𑄨': 2, + '𑄦𑄮': 2, + '𑄦𑄰': 2, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍀': 2, + '𑌕𑍍': 2, + '𑌗𑍀': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑌼': 2, + '𑌜𑍀': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌢𑌼𑍀': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍀': 2, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍀': 2, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍀': 2, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍀': 2, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍀': 2, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍀': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍀': 2, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍀': 2, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍀': 2, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍀': 2, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍀': 2, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍀': 2, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '𑌹𑍀': 2, + '𞤀𞥄': 2, + '𞤢𞥄': 2, + '𞤣𞥆': 2, + '𞤂𞥆': 2, + '𞤤𞥆': 2, + '𞤥𞥆': 2, + '𞤦𞥆': 2, + '𞤨𞥆': 2, + '𞤩𞥆': 2, + '𞤫𞥅': 2, + '𞤭𞥅': 2, + '𞤌𞥅': 2, + '𞤮𞥅': 2, + '𞤍𞥆': 2, + '𞤯𞥆': 2, + '𞤰𞥆': 2, + '𞤲𞥆': 2, + '𞤳𞥆': 2, + '𞤵𞥅': 2, + '𞤔𞥆': 2, + '𞤶𞥆': 2, + '𞤷𞥆': 2, + '𞤺𞥆': 2, + '𞤼𞥆': 2, + '🇦🇨': 4, + '🇦🇩': 4, + '🇦🇪': 4, + '🇦🇫': 4, + '🇦🇬': 4, + '🇦🇮': 4, + '🇦🇱': 4, + '🇦🇲': 4, + '🇦🇴': 4, + '🇦🇶': 4, + '🇦🇷': 4, + '🇦🇸': 4, + '🇦🇹': 4, + '🇦🇺': 4, + '🇦🇼': 4, + '🇦🇽': 4, + '🇦🇿': 4, + '🇧🇦': 4, + '🇧🇧': 4, + '🇧🇩': 4, + '🇧🇪': 4, + '🇧🇫': 4, + '🇧🇬': 4, + '🇧🇭': 4, + '🇧🇮': 4, + '🇧🇯': 4, + '🇧🇱': 4, + '🇧🇲': 4, + '🇧🇳': 4, + '🇧🇴': 4, + '🇧🇶': 4, + '🇧🇷': 4, + '🇧🇸': 4, + '🇧🇹': 4, + '🇧🇻': 4, + '🇧🇼': 4, + '🇧🇾': 4, + '🇧🇿': 4, + '🇨🇦': 4, + '🇨🇨': 4, + '🇨🇩': 4, + '🇨🇫': 4, + '🇨🇬': 4, + '🇨🇭': 4, + '🇨🇮': 4, + '🇨🇰': 4, + '🇨🇱': 4, + '🇨🇲': 4, + '🇨🇳': 4, + '🇨🇴': 4, + '🇨🇵': 4, + '🇨🇶': 4, + '🇨🇷': 4, + '🇨🇺': 4, + '🇨🇻': 4, + '🇨🇼': 4, + '🇨🇽': 4, + '🇨🇾': 4, + '🇨🇿': 4, + '🇩🇪': 4, + '🇩🇬': 4, + '🇩🇯': 4, + '🇩🇰': 4, + '🇩🇲': 4, + '🇩🇴': 4, + '🇩🇿': 4, + '🇪🇦': 4, + '🇪🇨': 4, + '🇪🇪': 4, + '🇪🇬': 4, + '🇪🇭': 4, + '🇪🇷': 4, + '🇪🇸': 4, + '🇪🇹': 4, + '🇪🇺': 4, + '🇫🇮': 4, + '🇫🇯': 4, + '🇫🇰': 4, + '🇫🇲': 4, + '🇫🇴': 4, + '🇫🇷': 4, + '🇬🇦': 4, + '🇬🇧': 4, + '🇬🇩': 4, + '🇬🇪': 4, + '🇬🇫': 4, + '🇬🇬': 4, + '🇬🇭': 4, + '🇬🇮': 4, + '🇬🇱': 4, + '🇬🇲': 4, + '🇬🇳': 4, + '🇬🇵': 4, + '🇬🇶': 4, + '🇬🇷': 4, + '🇬🇸': 4, + '🇬🇹': 4, + '🇬🇺': 4, + '🇬🇼': 4, + '🇬🇾': 4, + '🇭🇰': 4, + '🇭🇲': 4, + '🇭🇳': 4, + '🇭🇷': 4, + '🇭🇹': 4, + '🇭🇺': 4, + '🇮🇨': 4, + '🇮🇩': 4, + '🇮🇪': 4, + '🇮🇱': 4, + '🇮🇲': 4, + '🇮🇳': 4, + '🇮🇴': 4, + '🇮🇶': 4, + '🇮🇷': 4, + '🇮🇸': 4, + '🇮🇹': 4, + '🇯🇪': 4, + '🇯🇲': 4, + '🇯🇴': 4, + '🇯🇵': 4, + '🇰🇪': 4, + '🇰🇬': 4, + '🇰🇭': 4, + '🇰🇮': 4, + '🇰🇲': 4, + '🇰🇳': 4, + '🇰🇵': 4, + '🇰🇷': 4, + '🇰🇼': 4, + '🇰🇾': 4, + '🇰🇿': 4, + '🇱🇦': 4, + '🇱🇧': 4, + '🇱🇨': 4, + '🇱🇮': 4, + '🇱🇰': 4, + '🇱🇷': 4, + '🇱🇸': 4, + '🇱🇹': 4, + '🇱🇺': 4, + '🇱🇻': 4, + '🇱🇾': 4, + '🇲🇦': 4, + '🇲🇨': 4, + '🇲🇩': 4, + '🇲🇪': 4, + '🇲🇫': 4, + '🇲🇬': 4, + '🇲🇭': 4, + '🇲🇰': 4, + '🇲🇱': 4, + '🇲🇲': 4, + '🇲🇳': 4, + '🇲🇴': 4, + '🇲🇵': 4, + '🇲🇶': 4, + '🇲🇷': 4, + '🇲🇸': 4, + '🇲🇹': 4, + '🇲🇺': 4, + '🇲🇻': 4, + '🇲🇼': 4, + '🇲🇽': 4, + '🇲🇾': 4, + '🇲🇿': 4, + '🇳🇦': 4, + '🇳🇨': 4, + '🇳🇪': 4, + '🇳🇫': 4, + '🇳🇬': 4, + '🇳🇮': 4, + '🇳🇱': 4, + '🇳🇴': 4, + '🇳🇵': 4, + '🇳🇷': 4, + '🇳🇺': 4, + '🇳🇿': 4, + '🇴🇲': 4, + '🇵🇦': 4, + '🇵🇪': 4, + '🇵🇫': 4, + '🇵🇬': 4, + '🇵🇭': 4, + '🇵🇰': 4, + '🇵🇱': 4, + '🇵🇲': 4, + '🇵🇳': 4, + '🇵🇷': 4, + '🇵🇸': 4, + '🇵🇹': 4, + '🇵🇼': 4, + '🇵🇾': 4, + '🇶🇦': 4, + '🇷🇪': 4, + '🇷🇴': 4, + '🇷🇸': 4, + '🇷🇺': 4, + '🇷🇼': 4, + '🇸🇦': 4, + '🇸🇧': 4, + '🇸🇨': 4, + '🇸🇩': 4, + '🇸🇪': 4, + '🇸🇬': 4, + '🇸🇭': 4, + '🇸🇮': 4, + '🇸🇯': 4, + '🇸🇰': 4, + '🇸🇱': 4, + '🇸🇲': 4, + '🇸🇳': 4, + '🇸🇴': 4, + '🇸🇷': 4, + '🇸🇸': 4, + '🇸🇹': 4, + '🇸🇻': 4, + '🇸🇽': 4, + '🇸🇾': 4, + '🇸🇿': 4, + '🇹🇦': 4, + '🇹🇨': 4, + '🇹🇩': 4, + '🇹🇫': 4, + '🇹🇬': 4, + '🇹🇭': 4, + '🇹🇯': 4, + '🇹🇰': 4, + '🇹🇱': 4, + '🇹🇲': 4, + '🇹🇳': 4, + '🇹🇴': 4, + '🇹🇷': 4, + '🇹🇹': 4, + '🇹🇻': 4, + '🇹🇼': 4, + '🇹🇿': 4, + '🇺🇦': 4, + '🇺🇬': 4, + '🇺🇲': 4, + '🇺🇳': 4, + '🇺🇸': 4, + '🇺🇾': 4, + '🇺🇿': 4, + '🇻🇦': 4, + '🇻🇨': 4, + '🇻🇪': 4, + '🇻🇬': 4, + '🇻🇮': 4, + '🇻🇳': 4, + '🇻🇺': 4, + '🇼🇫': 4, + '🇼🇸': 4, + '🇽🇰': 4, + '🇾🇪': 4, + '🇾🇹': 4, + '🇿🇦': 4, + '🇿🇲': 4, + '🇿🇼': 4, + '🍄‍🟫': 5, + '🍋‍🟩': 5, + '🏃‍♀️': 5, + '🏃‍♀️‍➡️': 8, + '🏃‍♂️': 5, + '🏃‍♂️‍➡️': 8, + '🏃‍➡️': 5, + '🏃🏻‍♀️': 7, + '🏃🏻‍♀️‍➡️': 10, + '🏃🏻‍♂️': 7, + '🏃🏻‍♂️‍➡️': 10, + '🏃🏻‍➡️': 7, + '🏃🏼‍♀️': 7, + '🏃🏼‍♀️‍➡️': 10, + '🏃🏼‍♂️': 7, + '🏃🏼‍♂️‍➡️': 10, + '🏃🏼‍➡️': 7, + '🏃🏽‍♀️': 7, + '🏃🏽‍♀️‍➡️': 10, + '🏃🏽‍♂️': 7, + '🏃🏽‍♂️‍➡️': 10, + '🏃🏽‍➡️': 7, + '🏃🏾‍♀️': 7, + '🏃🏾‍♀️‍➡️': 10, + '🏃🏾‍♂️': 7, + '🏃🏾‍♂️‍➡️': 10, + '🏃🏾‍➡️': 7, + '🏃🏿‍♀️': 7, + '🏃🏿‍♀️‍➡️': 10, + '🏃🏿‍♂️': 7, + '🏃🏿‍♂️‍➡️': 10, + '🏃🏿‍➡️': 7, + '🏄‍♀️': 5, + '🏄‍♂️': 5, + '🏄🏻‍♀️': 7, + '🏄🏻‍♂️': 7, + '🏄🏼‍♀️': 7, + '🏄🏼‍♂️': 7, + '🏄🏽‍♀️': 7, + '🏄🏽‍♂️': 7, + '🏄🏾‍♀️': 7, + '🏄🏾‍♂️': 7, + '🏄🏿‍♀️': 7, + '🏄🏿‍♂️': 7, + '🏊‍♀️': 5, + '🏊‍♂️': 5, + '🏊🏻‍♀️': 7, + '🏊🏻‍♂️': 7, + '🏊🏼‍♀️': 7, + '🏊🏼‍♂️': 7, + '🏊🏽‍♀️': 7, + '🏊🏽‍♂️': 7, + '🏊🏾‍♀️': 7, + '🏊🏾‍♂️': 7, + '🏊🏿‍♀️': 7, + '🏊🏿‍♂️': 7, + '🏋️‍♀️': 5, + '🏋️‍♂️': 5, + '🏋🏻‍♀️': 6, + '🏋🏻‍♂️': 6, + '🏋🏼‍♀️': 6, + '🏋🏼‍♂️': 6, + '🏋🏽‍♀️': 6, + '🏋🏽‍♂️': 6, + '🏋🏾‍♀️': 6, + '🏋🏾‍♂️': 6, + '🏋🏿‍♀️': 6, + '🏋🏿‍♂️': 6, + '🏌️‍♀️': 5, + '🏌️‍♂️': 5, + '🏌🏻‍♀️': 6, + '🏌🏻‍♂️': 6, + '🏌🏼‍♀️': 6, + '🏌🏼‍♂️': 6, + '🏌🏽‍♀️': 6, + '🏌🏽‍♂️': 6, + '🏌🏾‍♀️': 6, + '🏌🏾‍♂️': 6, + '🏌🏿‍♀️': 6, + '🏌🏿‍♂️': 6, + '🏳️‍⚧️': 5, + '🏳️‍🌈': 5, + '🏴‍☠️': 5, + '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 8, + '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 8, + '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 8, + '🐈‍⬛': 5, + '🐕‍🦺': 5, + '🐦‍⬛': 5, + '🐦‍🔥': 5, + '🐻‍❄️': 5, + '👁️‍🗨️': 5, + '👨‍⚕️': 5, + '👨‍⚖️': 5, + '👨‍✈️': 5, + '👨‍❤️‍👨': 8, + '👨‍❤️‍💋‍👨': 11, + '👨‍🌾': 5, + '👨‍🍳': 5, + '👨‍🍼': 5, + '👨‍🎓': 5, + '👨‍🎤': 5, + '👨‍🎨': 5, + '👨‍🏫': 5, + '👨‍🏭': 5, + '👨‍👦': 5, + '👨‍👦‍👦': 8, + '👨‍👧': 5, + '👨‍👧‍👦': 8, + '👨‍👧‍👧': 8, + '👨‍👨‍👦': 8, + '👨‍👨‍👦‍👦': 11, + '👨‍👨‍👧': 8, + '👨‍👨‍👧‍👦': 11, + '👨‍👨‍👧‍👧': 11, + '👨‍👩‍👦': 8, + '👨‍👩‍👦‍👦': 11, + '👨‍👩‍👧': 8, + '👨‍👩‍👧‍👦': 11, + '👨‍👩‍👧‍👧': 11, + '👨‍💻': 5, + '👨‍💼': 5, + '👨‍🔧': 5, + '👨‍🔬': 5, + '👨‍🚀': 5, + '👨‍🚒': 5, + '👨‍🦯': 5, + '👨‍🦯‍➡️': 8, + '👨‍🦰': 5, + '👨‍🦱': 5, + '👨‍🦲': 5, + '👨‍🦳': 5, + '👨‍🦼': 5, + '👨‍🦼‍➡️': 8, + '👨‍🦽': 5, + '👨‍🦽‍➡️': 8, + '👨🏻‍⚕️': 7, + '👨🏻‍⚖️': 7, + '👨🏻‍✈️': 7, + '👨🏻‍❤️‍👨🏻': 12, + '👨🏻‍❤️‍👨🏽': 12, + '👨🏻‍❤️‍👨🏿': 12, + '👨🏻‍❤️‍💋‍👨🏻': 15, + '👨🏻‍❤️‍💋‍👨🏽': 15, + '👨🏻‍❤️‍💋‍👨🏾': 15, + '👨🏻‍❤️‍💋‍👨🏿': 15, + '👨🏻‍🌾': 7, + '👨🏻‍🍳': 7, + '👨🏻‍🍼': 7, + '👨🏻‍🎓': 7, + '👨🏻‍🎤': 7, + '👨🏻‍🎨': 7, + '👨🏻‍🏫': 7, + '👨🏻‍🏭': 7, + '👨🏻‍🐰‍👨🏼': 12, + '👨🏻‍🐰‍👨🏽': 12, + '👨🏻‍🐰‍👨🏾': 12, + '👨🏻‍🐰‍👨🏿': 12, + '👨🏻‍💻': 7, + '👨🏻‍💼': 7, + '👨🏻‍🔧': 7, + '👨🏻‍🔬': 7, + '👨🏻‍🚀': 7, + '👨🏻‍🚒': 7, + '👨🏻‍🤝‍👨🏼': 12, + '👨🏻‍🤝‍👨🏽': 12, + '👨🏻‍🤝‍👨🏾': 12, + '👨🏻‍🤝‍👨🏿': 12, + '👨🏻‍🦯': 7, + '👨🏻‍🦯‍➡️': 10, + '👨🏻‍🦰': 7, + '👨🏻‍🦱': 7, + '👨🏻‍🦲': 7, + '👨🏻‍🦳': 7, + '👨🏻‍🦼': 7, + '👨🏻‍🦼‍➡️': 10, + '👨🏻‍🦽': 7, + '👨🏻‍🦽‍➡️': 10, + '👨🏻‍🫯‍👨🏼': 11, + '👨🏻‍🫯‍👨🏽': 11, + '👨🏻‍🫯‍👨🏾': 11, + '👨🏻‍🫯‍👨🏿': 11, + '👨🏼‍⚕️': 7, + '👨🏼‍⚖️': 7, + '👨🏼‍✈️': 7, + '👨🏼‍❤️‍👨🏼': 12, + '👨🏼‍❤️‍💋‍👨🏼': 15, + '👨🏼‍❤️‍💋‍👨🏿': 15, + '👨🏼‍🌾': 7, + '👨🏼‍🍳': 7, + '👨🏼‍🍼': 7, + '👨🏼‍🎓': 7, + '👨🏼‍🎤': 7, + '👨🏼‍🎨': 7, + '👨🏼‍🏫': 7, + '👨🏼‍🏭': 7, + '👨🏼‍🐰‍👨🏻': 12, + '👨🏼‍🐰‍👨🏽': 12, + '👨🏼‍🐰‍👨🏿': 12, + '👨🏼‍💻': 7, + '👨🏼‍💼': 7, + '👨🏼‍🔧': 7, + '👨🏼‍🔬': 7, + '👨🏼‍🚀': 7, + '👨🏼‍🚒': 7, + '👨🏼‍🤝‍👨🏻': 12, + '👨🏼‍🤝‍👨🏽': 12, + '👨🏼‍🤝‍👨🏿': 12, + '👨🏼‍🦯': 7, + '👨🏼‍🦯‍➡️': 10, + '👨🏼‍🦰': 7, + '👨🏼‍🦱': 7, + '👨🏼‍🦲': 7, + '👨🏼‍🦳': 7, + '👨🏼‍🦼': 7, + '👨🏼‍🦽': 7, + '👨🏼‍🦽‍➡️': 10, + '👨🏼‍🫯‍👨🏻': 11, + '👨🏼‍🫯‍👨🏽': 11, + '👨🏼‍🫯‍👨🏾': 11, + '👨🏼‍🫯‍👨🏿': 11, + '👨🏽‍⚕️': 7, + '👨🏽‍⚖️': 7, + '👨🏽‍✈️': 7, + '👨🏽‍❤️‍👨🏻': 12, + '👨🏽‍❤️‍👨🏽': 12, + '👨🏽‍❤️‍👨🏿': 12, + '👨🏽‍❤️‍💋‍👨🏻': 15, + '👨🏽‍❤️‍💋‍👨🏽': 15, + '👨🏽‍❤️‍💋‍👨🏿': 15, + '👨🏽‍🌾': 7, + '👨🏽‍🍳': 7, + '👨🏽‍🍼': 7, + '👨🏽‍🎓': 7, + '👨🏽‍🎤': 7, + '👨🏽‍🎨': 7, + '👨🏽‍🏫': 7, + '👨🏽‍🏭': 7, + '👨🏽‍🐰‍👨🏻': 12, + '👨🏽‍🐰‍👨🏼': 12, + '👨🏽‍🐰‍👨🏾': 12, + '👨🏽‍🐰‍👨🏿': 12, + '👨🏽‍💻': 7, + '👨🏽‍💼': 7, + '👨🏽‍🔧': 7, + '👨🏽‍🔬': 7, + '👨🏽‍🚀': 7, + '👨🏽‍🚒': 7, + '👨🏽‍🤝‍👨🏻': 12, + '👨🏽‍🤝‍👨🏼': 12, + '👨🏽‍🤝‍👨🏾': 12, + '👨🏽‍🤝‍👨🏿': 12, + '👨🏽‍🦯': 7, + '👨🏽‍🦯‍➡️': 10, + '👨🏽‍🦰': 7, + '👨🏽‍🦱': 7, + '👨🏽‍🦲': 7, + '👨🏽‍🦳': 7, + '👨🏽‍🦼': 7, + '👨🏽‍🦼‍➡️': 10, + '👨🏽‍🦽': 7, + '👨🏽‍🦽‍➡️': 10, + '👨🏽‍🫯‍👨🏻': 11, + '👨🏽‍🫯‍👨🏼': 11, + '👨🏽‍🫯‍👨🏾': 11, + '👨🏽‍🫯‍👨🏿': 11, + '👨🏾‍⚕️': 7, + '👨🏾‍⚖️': 7, + '👨🏾‍✈️': 7, + '👨🏾‍❤️‍👨🏾': 12, + '👨🏾‍❤️‍💋‍👨🏻': 15, + '👨🏾‍❤️‍💋‍👨🏾': 15, + '👨🏾‍❤️‍💋‍👨🏿': 15, + '👨🏾‍🌾': 7, + '👨🏾‍🍳': 7, + '👨🏾‍🍼': 7, + '👨🏾‍🎓': 7, + '👨🏾‍🎤': 7, + '👨🏾‍🎨': 7, + '👨🏾‍🏫': 7, + '👨🏾‍🏭': 7, + '👨🏾‍🐰‍👨🏻': 12, + '👨🏾‍🐰‍👨🏽': 12, + '👨🏾‍🐰‍👨🏿': 12, + '👨🏾‍💻': 7, + '👨🏾‍💼': 7, + '👨🏾‍🔧': 7, + '👨🏾‍🔬': 7, + '👨🏾‍🚀': 7, + '👨🏾‍🚒': 7, + '👨🏾‍🤝‍👨🏻': 12, + '👨🏾‍🤝‍👨🏽': 12, + '👨🏾‍🤝‍👨🏿': 12, + '👨🏾‍🦯': 7, + '👨🏾‍🦯‍➡️': 10, + '👨🏾‍🦰': 7, + '👨🏾‍🦱': 7, + '👨🏾‍🦲': 7, + '👨🏾‍🦳': 7, + '👨🏾‍🦼': 7, + '👨🏾‍🦽': 7, + '👨🏾‍🦽‍➡️': 10, + '👨🏾‍🫯‍👨🏻': 11, + '👨🏾‍🫯‍👨🏼': 11, + '👨🏾‍🫯‍👨🏽': 11, + '👨🏾‍🫯‍👨🏿': 11, + '👨🏿‍⚕️': 7, + '👨🏿‍⚖️': 7, + '👨🏿‍✈️': 7, + '👨🏿‍❤️‍👨🏻': 12, + '👨🏿‍❤️‍👨🏽': 12, + '👨🏿‍❤️‍👨🏿': 12, + '👨🏿‍❤️‍💋‍👨🏻': 15, + '👨🏿‍❤️‍💋‍👨🏼': 15, + '👨🏿‍❤️‍💋‍👨🏽': 15, + '👨🏿‍❤️‍💋‍👨🏾': 15, + '👨🏿‍❤️‍💋‍👨🏿': 15, + '👨🏿‍🌾': 7, + '👨🏿‍🍳': 7, + '👨🏿‍🍼': 7, + '👨🏿‍🎓': 7, + '👨🏿‍🎤': 7, + '👨🏿‍🎨': 7, + '👨🏿‍🏫': 7, + '👨🏿‍🏭': 7, + '👨🏿‍🐰‍👨🏻': 12, + '👨🏿‍🐰‍👨🏼': 12, + '👨🏿‍🐰‍👨🏽': 12, + '👨🏿‍🐰‍👨🏾': 12, + '👨🏿‍💻': 7, + '👨🏿‍💼': 7, + '👨🏿‍🔧': 7, + '👨🏿‍🔬': 7, + '👨🏿‍🚀': 7, + '👨🏿‍🚒': 7, + '👨🏿‍🤝‍👨🏻': 12, + '👨🏿‍🤝‍👨🏼': 12, + '👨🏿‍🤝‍👨🏽': 12, + '👨🏿‍🤝‍👨🏾': 12, + '👨🏿‍🦯': 7, + '👨🏿‍🦯‍➡️': 10, + '👨🏿‍🦰': 7, + '👨🏿‍🦱': 7, + '👨🏿‍🦲': 7, + '👨🏿‍🦳': 7, + '👨🏿‍🦼': 7, + '👨🏿‍🦼‍➡️': 10, + '👨🏿‍🦽': 7, + '👨🏿‍🦽‍➡️': 10, + '👨🏿‍🫯‍👨🏻': 11, + '👨🏿‍🫯‍👨🏼': 11, + '👨🏿‍🫯‍👨🏽': 11, + '👨🏿‍🫯‍👨🏾': 11, + '👩‍⚕️': 5, + '👩‍⚖️': 5, + '👩‍✈️': 5, + '👩‍❤️‍👨': 8, + '👩‍❤️‍👩': 8, + '👩‍❤️‍💋‍👨': 11, + '👩‍❤️‍💋‍👩': 11, + '👩‍🌾': 5, + '👩‍🍳': 5, + '👩‍🍼': 5, + '👩‍🎓': 5, + '👩‍🎤': 5, + '👩‍🎨': 5, + '👩‍🏫': 5, + '👩‍🏭': 5, + '👩‍👦': 5, + '👩‍👦‍👦': 8, + '👩‍👧': 5, + '👩‍👧‍👦': 8, + '👩‍👧‍👧': 8, + '👩‍👩‍👦': 8, + '👩‍👩‍👦‍👦': 11, + '👩‍👩‍👧': 8, + '👩‍👩‍👧‍👦': 11, + '👩‍👩‍👧‍👧': 11, + '👩‍💻': 5, + '👩‍💼': 5, + '👩‍🔧': 5, + '👩‍🔬': 5, + '👩‍🚀': 5, + '👩‍🚒': 5, + '👩‍🦯': 5, + '👩‍🦯‍➡️': 8, + '👩‍🦰': 5, + '👩‍🦱': 5, + '👩‍🦲': 5, + '👩‍🦳': 5, + '👩‍🦼': 5, + '👩‍🦼‍➡️': 8, + '👩‍🦽': 5, + '👩‍🦽‍➡️': 8, + '👩🏻‍⚕️': 7, + '👩🏻‍⚖️': 7, + '👩🏻‍✈️': 7, + '👩🏻‍❤️‍👨🏻': 12, + '👩🏻‍❤️‍👨🏿': 12, + '👩🏻‍❤️‍👩🏻': 12, + '👩🏻‍❤️‍💋‍👨🏻': 15, + '👩🏻‍❤️‍💋‍👨🏽': 15, + '👩🏻‍❤️‍💋‍👨🏿': 15, + '👩🏻‍❤️‍💋‍👩🏻': 15, + '👩🏻‍❤️‍💋‍👩🏽': 15, + '👩🏻‍❤️‍💋‍👩🏿': 15, + '👩🏻‍🌾': 7, + '👩🏻‍🍳': 7, + '👩🏻‍🍼': 7, + '👩🏻‍🎓': 7, + '👩🏻‍🎤': 7, + '👩🏻‍🎨': 7, + '👩🏻‍🏫': 7, + '👩🏻‍🏭': 7, + '👩🏻‍🐰‍👩🏼': 12, + '👩🏻‍🐰‍👩🏽': 12, + '👩🏻‍🐰‍👩🏾': 12, + '👩🏻‍🐰‍👩🏿': 12, + '👩🏻‍💻': 7, + '👩🏻‍💼': 7, + '👩🏻‍🔧': 7, + '👩🏻‍🔬': 7, + '👩🏻‍🚀': 7, + '👩🏻‍🚒': 7, + '👩🏻‍🤝‍👨🏽': 12, + '👩🏻‍🤝‍👨🏿': 12, + '👩🏻‍🤝‍👩🏼': 12, + '👩🏻‍🤝‍👩🏽': 12, + '👩🏻‍🤝‍👩🏾': 12, + '👩🏻‍🤝‍👩🏿': 12, + '👩🏻‍🦯': 7, + '👩🏻‍🦯‍➡️': 10, + '👩🏻‍🦰': 7, + '👩🏻‍🦱': 7, + '👩🏻‍🦲': 7, + '👩🏻‍🦳': 7, + '👩🏻‍🦼': 7, + '👩🏻‍🦼‍➡️': 10, + '👩🏻‍🦽': 7, + '👩🏻‍🦽‍➡️': 10, + '👩🏻‍🫯‍👩🏼': 11, + '👩🏻‍🫯‍👩🏽': 11, + '👩🏻‍🫯‍👩🏾': 11, + '👩🏻‍🫯‍👩🏿': 11, + '👩🏼‍⚕️': 7, + '👩🏼‍⚖️': 7, + '👩🏼‍✈️': 7, + '👩🏼‍❤️‍👨🏼': 12, + '👩🏼‍❤️‍👩🏼': 12, + '👩🏼‍❤️‍💋‍👨🏼': 15, + '👩🏼‍❤️‍💋‍👩🏼': 15, + '👩🏼‍🌾': 7, + '👩🏼‍🍳': 7, + '👩🏼‍🍼': 7, + '👩🏼‍🎓': 7, + '👩🏼‍🎤': 7, + '👩🏼‍🎨': 7, + '👩🏼‍🏫': 7, + '👩🏼‍🏭': 7, + '👩🏼‍🐰‍👩🏻': 12, + '👩🏼‍🐰‍👩🏿': 12, + '👩🏼‍💻': 7, + '👩🏼‍💼': 7, + '👩🏼‍🔧': 7, + '👩🏼‍🔬': 7, + '👩🏼‍🚀': 7, + '👩🏼‍🚒': 7, + '👩🏼‍🤝‍👩🏻': 12, + '👩🏼‍🤝‍👩🏽': 12, + '👩🏼‍🤝‍👩🏿': 12, + '👩🏼‍🦯': 7, + '👩🏼‍🦯‍➡️': 10, + '👩🏼‍🦰': 7, + '👩🏼‍🦱': 7, + '👩🏼‍🦲': 7, + '👩🏼‍🦳': 7, + '👩🏼‍🦼': 7, + '👩🏼‍🦽': 7, + '👩🏼‍🫯‍👩🏻': 11, + '👩🏼‍🫯‍👩🏽': 11, + '👩🏼‍🫯‍👩🏾': 11, + '👩🏼‍🫯‍👩🏿': 11, + '👩🏽‍⚕️': 7, + '👩🏽‍⚖️': 7, + '👩🏽‍✈️': 7, + '👩🏽‍❤️‍👨🏽': 12, + '👩🏽‍❤️‍👩🏽': 12, + '👩🏽‍❤️‍💋‍👨🏻': 15, + '👩🏽‍❤️‍💋‍👨🏽': 15, + '👩🏽‍❤️‍💋‍👨🏿': 15, + '👩🏽‍❤️‍💋‍👩🏻': 15, + '👩🏽‍❤️‍💋‍👩🏽': 15, + '👩🏽‍❤️‍💋‍👩🏿': 15, + '👩🏽‍🌾': 7, + '👩🏽‍🍳': 7, + '👩🏽‍🍼': 7, + '👩🏽‍🎓': 7, + '👩🏽‍🎤': 7, + '👩🏽‍🎨': 7, + '👩🏽‍🏫': 7, + '👩🏽‍🏭': 7, + '👩🏽‍🐰‍👩🏻': 12, + '👩🏽‍🐰‍👩🏾': 12, + '👩🏽‍🐰‍👩🏿': 12, + '👩🏽‍💻': 7, + '👩🏽‍💼': 7, + '👩🏽‍🔧': 7, + '👩🏽‍🔬': 7, + '👩🏽‍🚀': 7, + '👩🏽‍🚒': 7, + '👩🏽‍🤝‍👨🏻': 12, + '👩🏽‍🤝‍👨🏿': 12, + '👩🏽‍🤝‍👩🏻': 12, + '👩🏽‍🤝‍👩🏼': 12, + '👩🏽‍🤝‍👩🏾': 12, + '👩🏽‍🤝‍👩🏿': 12, + '👩🏽‍🦯': 7, + '👩🏽‍🦯‍➡️': 10, + '👩🏽‍🦰': 7, + '👩🏽‍🦱': 7, + '👩🏽‍🦲': 7, + '👩🏽‍🦳': 7, + '👩🏽‍🦼': 7, + '👩🏽‍🦼‍➡️': 10, + '👩🏽‍🦽': 7, + '👩🏽‍🦽‍➡️': 10, + '👩🏽‍🫯‍👩🏻': 11, + '👩🏽‍🫯‍👩🏼': 11, + '👩🏽‍🫯‍👩🏾': 11, + '👩🏽‍🫯‍👩🏿': 11, + '👩🏾‍⚕️': 7, + '👩🏾‍⚖️': 7, + '👩🏾‍✈️': 7, + '👩🏾‍❤️‍👨🏾': 12, + '👩🏾‍❤️‍👩🏾': 12, + '👩🏾‍❤️‍💋‍👨🏾': 15, + '👩🏾‍❤️‍💋‍👩🏾': 15, + '👩🏾‍🌾': 7, + '👩🏾‍🍳': 7, + '👩🏾‍🍼': 7, + '👩🏾‍🎓': 7, + '👩🏾‍🎤': 7, + '👩🏾‍🎨': 7, + '👩🏾‍🏫': 7, + '👩🏾‍🏭': 7, + '👩🏾‍🐰‍👩🏻': 12, + '👩🏾‍🐰‍👩🏽': 12, + '👩🏾‍🐰‍👩🏿': 12, + '👩🏾‍💻': 7, + '👩🏾‍💼': 7, + '👩🏾‍🔧': 7, + '👩🏾‍🔬': 7, + '👩🏾‍🚀': 7, + '👩🏾‍🚒': 7, + '👩🏾‍🤝‍👩🏻': 12, + '👩🏾‍🤝‍👩🏽': 12, + '👩🏾‍🤝‍👩🏿': 12, + '👩🏾‍🦯': 7, + '👩🏾‍🦯‍➡️': 10, + '👩🏾‍🦰': 7, + '👩🏾‍🦱': 7, + '👩🏾‍🦲': 7, + '👩🏾‍🦳': 7, + '👩🏾‍🦼': 7, + '👩🏾‍🦽': 7, + '👩🏾‍🦽‍➡️': 10, + '👩🏾‍🫯‍👩🏻': 11, + '👩🏾‍🫯‍👩🏼': 11, + '👩🏾‍🫯‍👩🏽': 11, + '👩🏾‍🫯‍👩🏿': 11, + '👩🏿‍⚕️': 7, + '👩🏿‍⚖️': 7, + '👩🏿‍✈️': 7, + '👩🏿‍❤️‍👨🏻': 12, + '👩🏿‍❤️‍👨🏿': 12, + '👩🏿‍❤️‍👩🏿': 12, + '👩🏿‍❤️‍💋‍👨🏻': 15, + '👩🏿‍❤️‍💋‍👨🏽': 15, + '👩🏿‍❤️‍💋‍👨🏿': 15, + '👩🏿‍❤️‍💋‍👩🏻': 15, + '👩🏿‍❤️‍💋‍👩🏽': 15, + '👩🏿‍❤️‍💋‍👩🏿': 15, + '👩🏿‍🌾': 7, + '👩🏿‍🍳': 7, + '👩🏿‍🍼': 7, + '👩🏿‍🎓': 7, + '👩🏿‍🎤': 7, + '👩🏿‍🎨': 7, + '👩🏿‍🏫': 7, + '👩🏿‍🏭': 7, + '👩🏿‍🐰‍👩🏻': 12, + '👩🏿‍🐰‍👩🏼': 12, + '👩🏿‍🐰‍👩🏽': 12, + '👩🏿‍🐰‍👩🏾': 12, + '👩🏿‍💻': 7, + '👩🏿‍💼': 7, + '👩🏿‍🔧': 7, + '👩🏿‍🔬': 7, + '👩🏿‍🚀': 7, + '👩🏿‍🚒': 7, + '👩🏿‍🤝‍👨🏻': 12, + '👩🏿‍🤝‍👨🏽': 12, + '👩🏿‍🤝‍👩🏻': 12, + '👩🏿‍🤝‍👩🏼': 12, + '👩🏿‍🤝‍👩🏽': 12, + '👩🏿‍🤝‍👩🏾': 12, + '👩🏿‍🦯': 7, + '👩🏿‍🦯‍➡️': 10, + '👩🏿‍🦰': 7, + '👩🏿‍🦱': 7, + '👩🏿‍🦲': 7, + '👩🏿‍🦳': 7, + '👩🏿‍🦼': 7, + '👩🏿‍🦼‍➡️': 10, + '👩🏿‍🦽': 7, + '👩🏿‍🦽‍➡️': 10, + '👩🏿‍🫯‍👩🏻': 11, + '👩🏿‍🫯‍👩🏼': 11, + '👩🏿‍🫯‍👩🏽': 11, + '👩🏿‍🫯‍👩🏾': 11, + '👮‍♀️': 5, + '👮‍♂️': 5, + '👮🏻‍♀️': 7, + '👮🏻‍♂️': 7, + '👮🏼‍♀️': 7, + '👮🏼‍♂️': 7, + '👮🏽‍♀️': 7, + '👮🏽‍♂️': 7, + '👮🏾‍♀️': 7, + '👮🏾‍♂️': 7, + '👮🏿‍♀️': 7, + '👮🏿‍♂️': 7, + '👯‍♀️': 5, + '👯‍♂️': 5, + '👯🏻‍♀️': 7, + '👯🏻‍♂️': 7, + '👯🏼‍♀️': 7, + '👯🏼‍♂️': 7, + '👯🏽‍♀️': 7, + '👯🏽‍♂️': 7, + '👯🏾‍♀️': 7, + '👯🏾‍♂️': 7, + '👯🏿‍♀️': 7, + '👯🏿‍♂️': 7, + '👰‍♀️': 5, + '👰‍♂️': 5, + '👰🏻‍♀️': 7, + '👰🏻‍♂️': 7, + '👰🏼‍♀️': 7, + '👰🏼‍♂️': 7, + '👰🏽‍♀️': 7, + '👰🏽‍♂️': 7, + '👰🏾‍♀️': 7, + '👰🏾‍♂️': 7, + '👰🏿‍♀️': 7, + '👰🏿‍♂️': 7, + '👱‍♀️': 5, + '👱‍♂️': 5, + '👱🏻‍♀️': 7, + '👱🏻‍♂️': 7, + '👱🏼‍♀️': 7, + '👱🏼‍♂️': 7, + '👱🏽‍♀️': 7, + '👱🏽‍♂️': 7, + '👱🏾‍♀️': 7, + '👱🏾‍♂️': 7, + '👱🏿‍♀️': 7, + '👱🏿‍♂️': 7, + '👳‍♀️': 5, + '👳‍♂️': 5, + '👳🏻‍♀️': 7, + '👳🏻‍♂️': 7, + '👳🏼‍♀️': 7, + '👳🏼‍♂️': 7, + '👳🏽‍♀️': 7, + '👳🏽‍♂️': 7, + '👳🏾‍♀️': 7, + '👳🏾‍♂️': 7, + '👳🏿‍♀️': 7, + '👳🏿‍♂️': 7, + '👷‍♀️': 5, + '👷‍♂️': 5, + '👷🏻‍♀️': 7, + '👷🏻‍♂️': 7, + '👷🏼‍♀️': 7, + '👷🏼‍♂️': 7, + '👷🏽‍♀️': 7, + '👷🏽‍♂️': 7, + '👷🏾‍♀️': 7, + '👷🏾‍♂️': 7, + '👷🏿‍♀️': 7, + '👷🏿‍♂️': 7, + '💁‍♀️': 5, + '💁‍♂️': 5, + '💁🏻‍♀️': 7, + '💁🏻‍♂️': 7, + '💁🏼‍♀️': 7, + '💁🏼‍♂️': 7, + '💁🏽‍♀️': 7, + '💁🏽‍♂️': 7, + '💁🏾‍♀️': 7, + '💁🏾‍♂️': 7, + '💁🏿‍♀️': 7, + '💁🏿‍♂️': 7, + '💂‍♀️': 5, + '💂‍♂️': 5, + '💂🏻‍♀️': 7, + '💂🏻‍♂️': 7, + '💂🏼‍♀️': 7, + '💂🏼‍♂️': 7, + '💂🏽‍♀️': 7, + '💂🏽‍♂️': 7, + '💂🏾‍♀️': 7, + '💂🏾‍♂️': 7, + '💂🏿‍♀️': 7, + '💂🏿‍♂️': 7, + '💆‍♀️': 5, + '💆‍♂️': 5, + '💆🏻‍♀️': 7, + '💆🏻‍♂️': 7, + '💆🏼‍♀️': 7, + '💆🏼‍♂️': 7, + '💆🏽‍♀️': 7, + '💆🏽‍♂️': 7, + '💆🏾‍♀️': 7, + '💆🏾‍♂️': 7, + '💆🏿‍♀️': 7, + '💆🏿‍♂️': 7, + '💇‍♀️': 5, + '💇‍♂️': 5, + '💇🏻‍♀️': 7, + '💇🏻‍♂️': 7, + '💇🏼‍♀️': 7, + '💇🏼‍♂️': 7, + '💇🏽‍♀️': 7, + '💇🏽‍♂️': 7, + '💇🏾‍♀️': 7, + '💇🏾‍♂️': 7, + '💇🏿‍♀️': 7, + '💇🏿‍♂️': 7, + '🕵️‍♀️': 5, + '🕵️‍♂️': 5, + '🕵🏻‍♀️': 6, + '🕵🏻‍♂️': 6, + '🕵🏼‍♀️': 6, + '🕵🏼‍♂️': 6, + '🕵🏽‍♀️': 6, + '🕵🏽‍♂️': 6, + '🕵🏾‍♀️': 6, + '🕵🏾‍♂️': 6, + '🕵🏿‍♀️': 6, + '🕵🏿‍♂️': 6, + '😮‍💨': 5, + '😵‍💫': 5, + '😶‍🌫️': 5, + '🙂‍↔️': 5, + '🙂‍↕️': 5, + '🙅‍♀️': 5, + '🙅‍♂️': 5, + '🙅🏻‍♀️': 7, + '🙅🏻‍♂️': 7, + '🙅🏼‍♀️': 7, + '🙅🏼‍♂️': 7, + '🙅🏽‍♀️': 7, + '🙅🏽‍♂️': 7, + '🙅🏾‍♀️': 7, + '🙅🏾‍♂️': 7, + '🙅🏿‍♀️': 7, + '🙅🏿‍♂️': 7, + '🙆‍♀️': 5, + '🙆‍♂️': 5, + '🙆🏻‍♀️': 7, + '🙆🏻‍♂️': 7, + '🙆🏼‍♀️': 7, + '🙆🏼‍♂️': 7, + '🙆🏽‍♀️': 7, + '🙆🏽‍♂️': 7, + '🙆🏾‍♀️': 7, + '🙆🏾‍♂️': 7, + '🙆🏿‍♀️': 7, + '🙆🏿‍♂️': 7, + '🙇‍♀️': 5, + '🙇‍♂️': 5, + '🙇🏻‍♀️': 7, + '🙇🏻‍♂️': 7, + '🙇🏼‍♀️': 7, + '🙇🏼‍♂️': 7, + '🙇🏽‍♀️': 7, + '🙇🏽‍♂️': 7, + '🙇🏾‍♀️': 7, + '🙇🏾‍♂️': 7, + '🙇🏿‍♀️': 7, + '🙇🏿‍♂️': 7, + '🙋‍♀️': 5, + '🙋‍♂️': 5, + '🙋🏻‍♀️': 7, + '🙋🏻‍♂️': 7, + '🙋🏼‍♀️': 7, + '🙋🏼‍♂️': 7, + '🙋🏽‍♀️': 7, + '🙋🏽‍♂️': 7, + '🙋🏾‍♀️': 7, + '🙋🏾‍♂️': 7, + '🙋🏿‍♀️': 7, + '🙋🏿‍♂️': 7, + '🙍‍♀️': 5, + '🙍‍♂️': 5, + '🙍🏻‍♀️': 7, + '🙍🏻‍♂️': 7, + '🙍🏼‍♀️': 7, + '🙍🏼‍♂️': 7, + '🙍🏽‍♀️': 7, + '🙍🏽‍♂️': 7, + '🙍🏾‍♀️': 7, + '🙍🏾‍♂️': 7, + '🙍🏿‍♀️': 7, + '🙍🏿‍♂️': 7, + '🙎‍♀️': 5, + '🙎‍♂️': 5, + '🙎🏻‍♀️': 7, + '🙎🏻‍♂️': 7, + '🙎🏼‍♀️': 7, + '🙎🏼‍♂️': 7, + '🙎🏽‍♀️': 7, + '🙎🏽‍♂️': 7, + '🙎🏾‍♀️': 7, + '🙎🏾‍♂️': 7, + '🙎🏿‍♀️': 7, + '🙎🏿‍♂️': 7, + '🚣‍♀️': 5, + '🚣‍♂️': 5, + '🚣🏻‍♀️': 7, + '🚣🏻‍♂️': 7, + '🚣🏼‍♀️': 7, + '🚣🏼‍♂️': 7, + '🚣🏽‍♀️': 7, + '🚣🏽‍♂️': 7, + '🚣🏾‍♀️': 7, + '🚣🏾‍♂️': 7, + '🚣🏿‍♀️': 7, + '🚣🏿‍♂️': 7, + '🚴‍♀️': 5, + '🚴‍♂️': 5, + '🚴🏻‍♀️': 7, + '🚴🏻‍♂️': 7, + '🚴🏼‍♀️': 7, + '🚴🏼‍♂️': 7, + '🚴🏽‍♀️': 7, + '🚴🏽‍♂️': 7, + '🚴🏾‍♀️': 7, + '🚴🏾‍♂️': 7, + '🚴🏿‍♀️': 7, + '🚴🏿‍♂️': 7, + '🚵‍♀️': 5, + '🚵‍♂️': 5, + '🚵🏻‍♀️': 7, + '🚵🏻‍♂️': 7, + '🚵🏼‍♀️': 7, + '🚵🏼‍♂️': 7, + '🚵🏽‍♀️': 7, + '🚵🏽‍♂️': 7, + '🚵🏾‍♀️': 7, + '🚵🏾‍♂️': 7, + '🚵🏿‍♀️': 7, + '🚵🏿‍♂️': 7, + '🚶‍♀️': 5, + '🚶‍♀️‍➡️': 8, + '🚶‍♂️': 5, + '🚶‍♂️‍➡️': 8, + '🚶‍➡️': 5, + '🚶🏻‍♀️': 7, + '🚶🏻‍♀️‍➡️': 10, + '🚶🏻‍♂️': 7, + '🚶🏻‍♂️‍➡️': 10, + '🚶🏻‍➡️': 7, + '🚶🏼‍♀️': 7, + '🚶🏼‍♀️‍➡️': 10, + '🚶🏼‍♂️': 7, + '🚶🏼‍♂️‍➡️': 10, + '🚶🏼‍➡️': 7, + '🚶🏽‍♀️': 7, + '🚶🏽‍♀️‍➡️': 10, + '🚶🏽‍♂️': 7, + '🚶🏽‍♂️‍➡️': 10, + '🚶🏽‍➡️': 7, + '🚶🏾‍♀️': 7, + '🚶🏾‍♀️‍➡️': 10, + '🚶🏾‍♂️': 7, + '🚶🏾‍♂️‍➡️': 10, + '🚶🏾‍➡️': 7, + '🚶🏿‍♀️': 7, + '🚶🏿‍♀️‍➡️': 10, + '🚶🏿‍♂️': 7, + '🚶🏿‍♂️‍➡️': 10, + '🚶🏿‍➡️': 7, + '🤦‍♀️': 5, + '🤦‍♂️': 5, + '🤦🏻‍♀️': 7, + '🤦🏻‍♂️': 7, + '🤦🏼‍♀️': 7, + '🤦🏼‍♂️': 7, + '🤦🏽‍♀️': 7, + '🤦🏽‍♂️': 7, + '🤦🏾‍♀️': 7, + '🤦🏾‍♂️': 7, + '🤦🏿‍♀️': 7, + '🤦🏿‍♂️': 7, + '🤵‍♀️': 5, + '🤵‍♂️': 5, + '🤵🏻‍♀️': 7, + '🤵🏻‍♂️': 7, + '🤵🏼‍♀️': 7, + '🤵🏼‍♂️': 7, + '🤵🏽‍♀️': 7, + '🤵🏽‍♂️': 7, + '🤵🏾‍♀️': 7, + '🤵🏾‍♂️': 7, + '🤵🏿‍♀️': 7, + '🤵🏿‍♂️': 7, + '🤷‍♀️': 5, + '🤷‍♂️': 5, + '🤷🏻‍♀️': 7, + '🤷🏻‍♂️': 7, + '🤷🏼‍♀️': 7, + '🤷🏼‍♂️': 7, + '🤷🏽‍♀️': 7, + '🤷🏽‍♂️': 7, + '🤷🏾‍♀️': 7, + '🤷🏾‍♂️': 7, + '🤷🏿‍♀️': 7, + '🤷🏿‍♂️': 7, + '🤸‍♀️': 5, + '🤸‍♂️': 5, + '🤸🏻‍♀️': 7, + '🤸🏻‍♂️': 7, + '🤸🏼‍♀️': 7, + '🤸🏼‍♂️': 7, + '🤸🏽‍♀️': 7, + '🤸🏽‍♂️': 7, + '🤸🏾‍♀️': 7, + '🤸🏾‍♂️': 7, + '🤸🏿‍♀️': 7, + '🤸🏿‍♂️': 7, + '🤹‍♀️': 5, + '🤹‍♂️': 5, + '🤹🏻‍♀️': 7, + '🤹🏻‍♂️': 7, + '🤹🏼‍♀️': 7, + '🤹🏼‍♂️': 7, + '🤹🏽‍♀️': 7, + '🤹🏽‍♂️': 7, + '🤹🏾‍♀️': 7, + '🤹🏾‍♂️': 7, + '🤹🏿‍♀️': 7, + '🤹🏿‍♂️': 7, + '🤼‍♀️': 5, + '🤼‍♂️': 5, + '🤼🏻‍♀️': 7, + '🤼🏻‍♂️': 7, + '🤼🏼‍♀️': 7, + '🤼🏼‍♂️': 7, + '🤼🏽‍♀️': 7, + '🤼🏽‍♂️': 7, + '🤼🏾‍♀️': 7, + '🤼🏾‍♂️': 7, + '🤼🏿‍♀️': 7, + '🤼🏿‍♂️': 7, + '🤽‍♀️': 5, + '🤽‍♂️': 5, + '🤽🏻‍♀️': 7, + '🤽🏻‍♂️': 7, + '🤽🏼‍♀️': 7, + '🤽🏼‍♂️': 7, + '🤽🏽‍♀️': 7, + '🤽🏽‍♂️': 7, + '🤽🏾‍♀️': 7, + '🤽🏾‍♂️': 7, + '🤽🏿‍♀️': 7, + '🤽🏿‍♂️': 7, + '🤾‍♀️': 5, + '🤾‍♂️': 5, + '🤾🏻‍♀️': 7, + '🤾🏻‍♂️': 7, + '🤾🏼‍♀️': 7, + '🤾🏼‍♂️': 7, + '🤾🏽‍♀️': 7, + '🤾🏽‍♂️': 7, + '🤾🏾‍♀️': 7, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_45d92e98.py b/libs/wcwidth/table_grapheme_overrides/_known_45d92e98.py new file mode 100644 index 000000000..f5db5a794 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_45d92e98.py @@ -0,0 +1,1969 @@ +""" +Grapheme overrides for _known_45d92e98. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: alacritty +""" +# pylint: skip-file +GRAPHEMES = { + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কা': 1, + 'কা‌': 1, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খা': 1, + 'গ্রে': 3, + 'ঘা': 1, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চা': 1, + 'চ্ছে': 3, + 'ছা': 1, + 'জা': 1, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'টা': 1, + 'ট্রে': 3, + 'ড়া': 1, + 'ণা': 1, + 'ণ্ডে': 3, + 'তা': 1, + 'তাঁ': 1, + 'তা‌': 1, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'থা': 1, + 'দা': 1, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্রো': 3, + 'ধা': 1, + 'ধ্যে': 3, + 'না': 1, + 'না‌': 1, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্যে': 3, + 'পা': 1, + 'পা‌': 1, + 'বা': 1, + 'বা‌': 1, + 'ব্ধি': 3, + 'ভা': 1, + 'মা': 1, + 'মা‌': 1, + 'য়া': 1, + 'যা': 1, + 'রা': 1, + 'র্থে': 3, + 'র্দো': 3, + 'র্নী': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'লা': 1, + 'ল্যে': 3, + 'ল্লে': 3, + 'শা': 1, + 'শ্চি': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'ষা': 1, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সা': 1, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থ্য': 3, + 'স্বী': 3, + 'স্যে': 3, + 'হা': 1, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'கா': 1, + 'சா': 1, + 'டா': 1, + 'ணா': 1, + 'தா': 1, + 'நா': 1, + 'னா': 1, + 'பா': 1, + 'மா': 1, + 'யா': 1, + 'ரா': 1, + 'றா': 1, + 'லா': 1, + 'ளா': 1, + 'வா': 1, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕೀ': 1, + 'ಕುಂ': 3, + 'ಕೂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೀ': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೋ': 1, + 'ಚೀ': 1, + 'ಜೀ': 1, + 'ಟುಂ': 3, + 'ಟೇ': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣೈ': 1, + 'ತೀ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ದೂ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಾಂ': 3, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭೀ': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮೀ': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯುಂ': 3, + 'ಯೂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಾಂ': 3, + 'ರೀ': 1, + 'ರೂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲೀ': 1, + 'ಲೂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳೂ': 1, + 'ವೀ': 1, + 'ವುಂ': 3, + 'ವೂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾಂ': 3, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷೇ': 1, + 'ಸಾಂ': 3, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೋ': 1, + 'കാ': 1, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഗ്ഗം': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ചാ': 1, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജാ': 1, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ടാ': 1, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണാ': 1, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'താ': 1, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദാ': 1, + 'ദ്ധി': 3, + 'ധാ': 1, + 'നാ': 1, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്യം': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പാ': 1, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'ബാ': 1, + 'ബ്ലി': 3, + 'ഭാ': 1, + 'മാ': 1, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യാ': 1, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യി': 3, + 'രാ': 1, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ലാ': 1, + 'ല്ലാം': 3, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ളാ': 1, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വാ': 1, + 'വ്യം': 3, + 'ശാ': 1, + 'ഷ്ട്യാ': 3, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'സാ': 1, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വീ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹാ': 1, + 'ഹിം': 3, + 'කා': 1, + 'ගා': 1, + 'චා': 1, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ඨා': 1, + 'ඩා': 1, + 'තා': 1, + 'ථා': 1, + 'දා': 1, + 'ධා': 1, + 'නා': 1, + 'ඳා': 1, + 'පා': 1, + 'බා': 1, + 'භා': 1, + 'මා': 1, + 'යා': 1, + 'රා': 1, + 'ලා': 1, + 'වා': 1, + 'ෂා': 1, + 'සා': 1, + 'හා': 1, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 3, + '⛹🏻‍♀️': 4, + '⛹🏻‍♂️': 4, + '⛹🏼‍♀️': 4, + '⛹🏼‍♂️': 4, + '⛹🏽‍♀️': 4, + '⛹🏽‍♂️': 4, + '⛹🏾‍♀️': 4, + '⛹🏾‍♂️': 4, + '⛹🏿‍♀️': 4, + '⛹🏿‍♂️': 4, + '❤️‍🔥': 3, + '❤️‍🩹': 3, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀ꦏꦺ': 3, + 'ꦏ꧀ꦔꦿ': 3, + 'ꦏ꧀ꦠꦺ': 3, + 'ꦏ꧀ꦥꦿꦶ': 3, + 'ꦏ꧀ꦧꦺ': 3, + 'ꦏ꧀ꦱꦺ': 3, + 'ꦏ꧀ꦲꦺ': 3, + 'ꦏ꧀ꦲꦺꦴ': 4, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀ꦠꦺ': 3, + 'ꦠ꧀ꦭꦺ': 3, + 'ꦠ꧀ꦱꦶꦃ': 3, + 'ꦠ꧀ꦱꦾ': 3, + 'ꦠ꧀ꦱ꧀ꦮ': 3, + 'ꦠ꧀ꦲꦺꦴ': 4, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀ꦩꦺꦴ': 4, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀ꦔꦺ': 3, + 'ꦤ꧀ꦔꦿꦸ': 3, + 'ꦤ꧀ꦛꦃ': 3, + 'ꦤ꧀ꦝꦺꦁ': 3, + 'ꦤ꧀ꦝꦺꦴꦁ': 4, + 'ꦤ꧀ꦠꦃ': 3, + 'ꦤ꧀ꦠꦺ': 3, + 'ꦤ꧀ꦢꦺ': 3, + 'ꦤ꧀ꦤꦺ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 4, + 'ꦤ꧀ꦥꦿ': 3, + 'ꦤ꧀ꦥꦿꦶ': 3, + 'ꦤ꧀ꦧꦺ': 3, + 'ꦤ꧀ꦧꦺꦴ': 4, + 'ꦤ꧀ꦱꦃ': 3, + 'ꦤ꧀ꦱꦺꦴ': 4, + 'ꦤ꧀ꦲꦺꦴ': 4, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀ꦥꦺ': 3, + 'ꦥ꧀ꦧꦺ': 3, + 'ꦥ꧀ꦫꦺ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀ꦧꦺ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀ꦠꦺꦴ': 4, + 'ꦩ꧀ꦧꦃ': 3, + 'ꦩ꧀ꦧꦺ': 3, + 'ꦩ꧀ꦧꦺꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 3, + 'ꦩ꧀ꦫꦃ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀ꦭꦺ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀ꦏꦿꦶ': 3, + 'ꦱ꧀ꦔꦿ': 3, + 'ꦱ꧀ꦠꦿ': 3, + 'ꦱ꧀ꦩꦿ': 3, + 'ꦱ꧀ꦱꦺ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾': 1, + '𑌗𑌾': 1, + '𑌘𑌾': 1, + '𑌚𑌾': 1, + '𑌛𑌾': 1, + '𑌜𑌾': 1, + '𑌞𑌾': 1, + '𑌟𑌾': 1, + '𑌠𑌾': 1, + '𑌠𑍈𑌃': 3, + '𑌣𑌾': 1, + '𑌤𑌾': 1, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌥𑌾': 1, + '𑌦𑌾': 1, + '𑌧𑌾': 1, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌨𑌾': 1, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌪𑌾': 1, + '𑌬𑌾': 1, + '𑌭𑌾': 1, + '𑌭𑌿𑌃': 3, + '𑌮𑌾': 1, + '𑌮𑌿𑌂': 3, + '𑌯𑌾': 1, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌰𑌾': 1, + '𑌰𑌿𑌂': 3, + '𑌲𑌾': 1, + '𑌲𑌿𑌂': 3, + '𑌵𑌾': 1, + '𑌶𑌾': 1, + '𑌷𑌾': 1, + '𑌸𑌾': 1, + '𑌹𑌾': 1, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 3, + '🏃‍♀️‍➡️': 4, + '🏃‍♂️': 3, + '🏃‍♂️‍➡️': 4, + '🏃‍➡️': 3, + '🏃🏻‍♀️': 5, + '🏃🏻‍♀️‍➡️': 6, + '🏃🏻‍♂️': 5, + '🏃🏻‍♂️‍➡️': 6, + '🏃🏻‍➡️': 5, + '🏃🏼‍♀️': 5, + '🏃🏼‍♀️‍➡️': 6, + '🏃🏼‍♂️': 5, + '🏃🏼‍♂️‍➡️': 6, + '🏃🏼‍➡️': 5, + '🏃🏽‍♀️': 5, + '🏃🏽‍♀️‍➡️': 6, + '🏃🏽‍♂️': 5, + '🏃🏽‍♂️‍➡️': 6, + '🏃🏽‍➡️': 5, + '🏃🏾‍♀️': 5, + '🏃🏾‍♀️‍➡️': 6, + '🏃🏾‍♂️': 5, + '🏃🏾‍♂️‍➡️': 6, + '🏃🏾‍➡️': 5, + '🏃🏿‍♀️': 5, + '🏃🏿‍♀️‍➡️': 6, + '🏃🏿‍♂️': 5, + '🏃🏿‍♂️‍➡️': 6, + '🏃🏿‍➡️': 5, + '🏄‍♀️': 3, + '🏄‍♂️': 3, + '🏄🏻‍♀️': 5, + '🏄🏻‍♂️': 5, + '🏄🏼‍♀️': 5, + '🏄🏼‍♂️': 5, + '🏄🏽‍♀️': 5, + '🏄🏽‍♂️': 5, + '🏄🏾‍♀️': 5, + '🏄🏾‍♂️': 5, + '🏄🏿‍♀️': 5, + '🏄🏿‍♂️': 5, + '🏊‍♀️': 3, + '🏊‍♂️': 3, + '🏊🏻‍♀️': 5, + '🏊🏻‍♂️': 5, + '🏊🏼‍♀️': 5, + '🏊🏼‍♂️': 5, + '🏊🏽‍♀️': 5, + '🏊🏽‍♂️': 5, + '🏊🏾‍♀️': 5, + '🏊🏾‍♂️': 5, + '🏊🏿‍♀️': 5, + '🏊🏿‍♂️': 5, + '🏋🏻‍♀️': 4, + '🏋🏻‍♂️': 4, + '🏋🏼‍♀️': 4, + '🏋🏼‍♂️': 4, + '🏋🏽‍♀️': 4, + '🏋🏽‍♂️': 4, + '🏋🏾‍♀️': 4, + '🏋🏾‍♂️': 4, + '🏋🏿‍♀️': 4, + '🏋🏿‍♂️': 4, + '🏌🏻‍♀️': 4, + '🏌🏻‍♂️': 4, + '🏌🏼‍♀️': 4, + '🏌🏼‍♂️': 4, + '🏌🏽‍♀️': 4, + '🏌🏽‍♂️': 4, + '🏌🏾‍♀️': 4, + '🏌🏾‍♂️': 4, + '🏌🏿‍♀️': 4, + '🏌🏿‍♂️': 4, + '🏳️‍🌈': 3, + '🏴‍☠️': 3, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 3, + '👨‍⚕️': 3, + '👨‍⚖️': 3, + '👨‍✈️': 3, + '👨‍❤️‍👨': 5, + '👨‍❤️‍💋‍👨': 7, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 5, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 5, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 5, + '👨🏻‍⚕️': 5, + '👨🏻‍⚖️': 5, + '👨🏻‍✈️': 5, + '👨🏻‍❤️‍👨🏻': 9, + '👨🏻‍❤️‍👨🏽': 9, + '👨🏻‍❤️‍👨🏿': 9, + '👨🏻‍❤️‍💋‍👨🏻': 11, + '👨🏻‍❤️‍💋‍👨🏽': 11, + '👨🏻‍❤️‍💋‍👨🏾': 11, + '👨🏻‍❤️‍💋‍👨🏿': 11, + '👨🏻‍🌾': 6, + '👨🏻‍🍳': 6, + '👨🏻‍🍼': 6, + '👨🏻‍🎓': 6, + '👨🏻‍🎤': 6, + '👨🏻‍🎨': 6, + '👨🏻‍🏫': 6, + '👨🏻‍🏭': 6, + '👨🏻‍🐰‍👨🏼': 10, + '👨🏻‍🐰‍👨🏽': 10, + '👨🏻‍🐰‍👨🏾': 10, + '👨🏻‍🐰‍👨🏿': 10, + '👨🏻‍💻': 6, + '👨🏻‍💼': 6, + '👨🏻‍🔧': 6, + '👨🏻‍🔬': 6, + '👨🏻‍🚀': 6, + '👨🏻‍🚒': 6, + '👨🏻‍🤝‍👨🏼': 10, + '👨🏻‍🤝‍👨🏽': 10, + '👨🏻‍🤝‍👨🏾': 10, + '👨🏻‍🤝‍👨🏿': 10, + '👨🏻‍🦯': 6, + '👨🏻‍🦯‍➡️': 7, + '👨🏻‍🦰': 6, + '👨🏻‍🦱': 6, + '👨🏻‍🦲': 6, + '👨🏻‍🦳': 6, + '👨🏻‍🦼': 6, + '👨🏻‍🦼‍➡️': 7, + '👨🏻‍🦽': 6, + '👨🏻‍🦽‍➡️': 7, + '👨🏻‍🫯‍👨🏼': 10, + '👨🏻‍🫯‍👨🏽': 10, + '👨🏻‍🫯‍👨🏾': 10, + '👨🏻‍🫯‍👨🏿': 10, + '👨🏼‍⚕️': 5, + '👨🏼‍⚖️': 5, + '👨🏼‍✈️': 5, + '👨🏼‍❤️‍👨🏼': 9, + '👨🏼‍❤️‍💋‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏿': 11, + '👨🏼‍🌾': 6, + '👨🏼‍🍳': 6, + '👨🏼‍🍼': 6, + '👨🏼‍🎓': 6, + '👨🏼‍🎤': 6, + '👨🏼‍🎨': 6, + '👨🏼‍🏫': 6, + '👨🏼‍🏭': 6, + '👨🏼‍🐰‍👨🏻': 10, + '👨🏼‍🐰‍👨🏽': 10, + '👨🏼‍🐰‍👨🏿': 10, + '👨🏼‍💻': 6, + '👨🏼‍💼': 6, + '👨🏼‍🔧': 6, + '👨🏼‍🔬': 6, + '👨🏼‍🚀': 6, + '👨🏼‍🚒': 6, + '👨🏼‍🤝‍👨🏻': 10, + '👨🏼‍🤝‍👨🏽': 10, + '👨🏼‍🤝‍👨🏿': 10, + '👨🏼‍🦯': 6, + '👨🏼‍🦯‍➡️': 7, + '👨🏼‍🦰': 6, + '👨🏼‍🦱': 6, + '👨🏼‍🦲': 6, + '👨🏼‍🦳': 6, + '👨🏼‍🦼': 6, + '👨🏼‍🦽': 6, + '👨🏼‍🦽‍➡️': 7, + '👨🏼‍🫯‍👨🏻': 10, + '👨🏼‍🫯‍👨🏽': 10, + '👨🏼‍🫯‍👨🏾': 10, + '👨🏼‍🫯‍👨🏿': 10, + '👨🏽‍⚕️': 5, + '👨🏽‍⚖️': 5, + '👨🏽‍✈️': 5, + '👨🏽‍❤️‍👨🏻': 9, + '👨🏽‍❤️‍👨🏽': 9, + '👨🏽‍❤️‍👨🏿': 9, + '👨🏽‍❤️‍💋‍👨🏻': 11, + '👨🏽‍❤️‍💋‍👨🏽': 11, + '👨🏽‍❤️‍💋‍👨🏿': 11, + '👨🏽‍🌾': 6, + '👨🏽‍🍳': 6, + '👨🏽‍🍼': 6, + '👨🏽‍🎓': 6, + '👨🏽‍🎤': 6, + '👨🏽‍🎨': 6, + '👨🏽‍🏫': 6, + '👨🏽‍🏭': 6, + '👨🏽‍🐰‍👨🏻': 10, + '👨🏽‍🐰‍👨🏼': 10, + '👨🏽‍🐰‍👨🏾': 10, + '👨🏽‍🐰‍👨🏿': 10, + '👨🏽‍💻': 6, + '👨🏽‍💼': 6, + '👨🏽‍🔧': 6, + '👨🏽‍🔬': 6, + '👨🏽‍🚀': 6, + '👨🏽‍🚒': 6, + '👨🏽‍🤝‍👨🏻': 10, + '👨🏽‍🤝‍👨🏼': 10, + '👨🏽‍🤝‍👨🏾': 10, + '👨🏽‍🤝‍👨🏿': 10, + '👨🏽‍🦯': 6, + '👨🏽‍🦯‍➡️': 7, + '👨🏽‍🦰': 6, + '👨🏽‍🦱': 6, + '👨🏽‍🦲': 6, + '👨🏽‍🦳': 6, + '👨🏽‍🦼': 6, + '👨🏽‍🦼‍➡️': 7, + '👨🏽‍🦽': 6, + '👨🏽‍🦽‍➡️': 7, + '👨🏽‍🫯‍👨🏻': 10, + '👨🏽‍🫯‍👨🏼': 10, + '👨🏽‍🫯‍👨🏾': 10, + '👨🏽‍🫯‍👨🏿': 10, + '👨🏾‍⚕️': 5, + '👨🏾‍⚖️': 5, + '👨🏾‍✈️': 5, + '👨🏾‍❤️‍👨🏾': 9, + '👨🏾‍❤️‍💋‍👨🏻': 11, + '👨🏾‍❤️‍💋‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏿': 11, + '👨🏾‍🌾': 6, + '👨🏾‍🍳': 6, + '👨🏾‍🍼': 6, + '👨🏾‍🎓': 6, + '👨🏾‍🎤': 6, + '👨🏾‍🎨': 6, + '👨🏾‍🏫': 6, + '👨🏾‍🏭': 6, + '👨🏾‍🐰‍👨🏻': 10, + '👨🏾‍🐰‍👨🏽': 10, + '👨🏾‍🐰‍👨🏿': 10, + '👨🏾‍💻': 6, + '👨🏾‍💼': 6, + '👨🏾‍🔧': 6, + '👨🏾‍🔬': 6, + '👨🏾‍🚀': 6, + '👨🏾‍🚒': 6, + '👨🏾‍🤝‍👨🏻': 10, + '👨🏾‍🤝‍👨🏽': 10, + '👨🏾‍🤝‍👨🏿': 10, + '👨🏾‍🦯': 6, + '👨🏾‍🦯‍➡️': 7, + '👨🏾‍🦰': 6, + '👨🏾‍🦱': 6, + '👨🏾‍🦲': 6, + '👨🏾‍🦳': 6, + '👨🏾‍🦼': 6, + '👨🏾‍🦽': 6, + '👨🏾‍🦽‍➡️': 7, + '👨🏾‍🫯‍👨🏻': 10, + '👨🏾‍🫯‍👨🏼': 10, + '👨🏾‍🫯‍👨🏽': 10, + '👨🏾‍🫯‍👨🏿': 10, + '👨🏿‍⚕️': 5, + '👨🏿‍⚖️': 5, + '👨🏿‍✈️': 5, + '👨🏿‍❤️‍👨🏻': 9, + '👨🏿‍❤️‍👨🏽': 9, + '👨🏿‍❤️‍👨🏿': 9, + '👨🏿‍❤️‍💋‍👨🏻': 11, + '👨🏿‍❤️‍💋‍👨🏼': 11, + '👨🏿‍❤️‍💋‍👨🏽': 11, + '👨🏿‍❤️‍💋‍👨🏾': 11, + '👨🏿‍❤️‍💋‍👨🏿': 11, + '👨🏿‍🌾': 6, + '👨🏿‍🍳': 6, + '👨🏿‍🍼': 6, + '👨🏿‍🎓': 6, + '👨🏿‍🎤': 6, + '👨🏿‍🎨': 6, + '👨🏿‍🏫': 6, + '👨🏿‍🏭': 6, + '👨🏿‍🐰‍👨🏻': 10, + '👨🏿‍🐰‍👨🏼': 10, + '👨🏿‍🐰‍👨🏽': 10, + '👨🏿‍🐰‍👨🏾': 10, + '👨🏿‍💻': 6, + '👨🏿‍💼': 6, + '👨🏿‍🔧': 6, + '👨🏿‍🔬': 6, + '👨🏿‍🚀': 6, + '👨🏿‍🚒': 6, + '👨🏿‍🤝‍👨🏻': 10, + '👨🏿‍🤝‍👨🏼': 10, + '👨🏿‍🤝‍👨🏽': 10, + '👨🏿‍🤝‍👨🏾': 10, + '👨🏿‍🦯': 6, + '👨🏿‍🦯‍➡️': 7, + '👨🏿‍🦰': 6, + '👨🏿‍🦱': 6, + '👨🏿‍🦲': 6, + '👨🏿‍🦳': 6, + '👨🏿‍🦼': 6, + '👨🏿‍🦼‍➡️': 7, + '👨🏿‍🦽': 6, + '👨🏿‍🦽‍➡️': 7, + '👨🏿‍🫯‍👨🏻': 10, + '👨🏿‍🫯‍👨🏼': 10, + '👨🏿‍🫯‍👨🏽': 10, + '👨🏿‍🫯‍👨🏾': 10, + '👩‍⚕️': 3, + '👩‍⚖️': 3, + '👩‍✈️': 3, + '👩‍❤️‍👨': 5, + '👩‍❤️‍👩': 5, + '👩‍❤️‍💋‍👨': 7, + '👩‍❤️‍💋‍👩': 7, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 5, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 5, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 5, + '👩🏻‍⚕️': 5, + '👩🏻‍⚖️': 5, + '👩🏻‍✈️': 5, + '👩🏻‍❤️‍👨🏻': 9, + '👩🏻‍❤️‍👨🏿': 9, + '👩🏻‍❤️‍👩🏻': 9, + '👩🏻‍❤️‍💋‍👨🏻': 11, + '👩🏻‍❤️‍💋‍👨🏽': 11, + '👩🏻‍❤️‍💋‍👨🏿': 11, + '👩🏻‍❤️‍💋‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👩🏽': 11, + '👩🏻‍❤️‍💋‍👩🏿': 11, + '👩🏻‍🌾': 6, + '👩🏻‍🍳': 6, + '👩🏻‍🍼': 6, + '👩🏻‍🎓': 6, + '👩🏻‍🎤': 6, + '👩🏻‍🎨': 6, + '👩🏻‍🏫': 6, + '👩🏻‍🏭': 6, + '👩🏻‍🐰‍👩🏼': 10, + '👩🏻‍🐰‍👩🏽': 10, + '👩🏻‍🐰‍👩🏾': 10, + '👩🏻‍🐰‍👩🏿': 10, + '👩🏻‍💻': 6, + '👩🏻‍💼': 6, + '👩🏻‍🔧': 6, + '👩🏻‍🔬': 6, + '👩🏻‍🚀': 6, + '👩🏻‍🚒': 6, + '👩🏻‍🤝‍👨🏽': 10, + '👩🏻‍🤝‍👨🏿': 10, + '👩🏻‍🤝‍👩🏼': 10, + '👩🏻‍🤝‍👩🏽': 10, + '👩🏻‍🤝‍👩🏾': 10, + '👩🏻‍🤝‍👩🏿': 10, + '👩🏻‍🦯': 6, + '👩🏻‍🦯‍➡️': 7, + '👩🏻‍🦰': 6, + '👩🏻‍🦱': 6, + '👩🏻‍🦲': 6, + '👩🏻‍🦳': 6, + '👩🏻‍🦼': 6, + '👩🏻‍🦼‍➡️': 7, + '👩🏻‍🦽': 6, + '👩🏻‍🦽‍➡️': 7, + '👩🏻‍🫯‍👩🏼': 10, + '👩🏻‍🫯‍👩🏽': 10, + '👩🏻‍🫯‍👩🏾': 10, + '👩🏻‍🫯‍👩🏿': 10, + '👩🏼‍⚕️': 5, + '👩🏼‍⚖️': 5, + '👩🏼‍✈️': 5, + '👩🏼‍❤️‍👨🏼': 9, + '👩🏼‍❤️‍👩🏼': 9, + '👩🏼‍❤️‍💋‍👨🏼': 11, + '👩🏼‍❤️‍💋‍👩🏼': 11, + '👩🏼‍🌾': 6, + '👩🏼‍🍳': 6, + '👩🏼‍🍼': 6, + '👩🏼‍🎓': 6, + '👩🏼‍🎤': 6, + '👩🏼‍🎨': 6, + '👩🏼‍🏫': 6, + '👩🏼‍🏭': 6, + '👩🏼‍🐰‍👩🏻': 10, + '👩🏼‍🐰‍👩🏿': 10, + '👩🏼‍💻': 6, + '👩🏼‍💼': 6, + '👩🏼‍🔧': 6, + '👩🏼‍🔬': 6, + '👩🏼‍🚀': 6, + '👩🏼‍🚒': 6, + '👩🏼‍🤝‍👩🏻': 10, + '👩🏼‍🤝‍👩🏽': 10, + '👩🏼‍🤝‍👩🏿': 10, + '👩🏼‍🦯': 6, + '👩🏼‍🦯‍➡️': 7, + '👩🏼‍🦰': 6, + '👩🏼‍🦱': 6, + '👩🏼‍🦲': 6, + '👩🏼‍🦳': 6, + '👩🏼‍🦼': 6, + '👩🏼‍🦽': 6, + '👩🏼‍🫯‍👩🏻': 10, + '👩🏼‍🫯‍👩🏽': 10, + '👩🏼‍🫯‍👩🏾': 10, + '👩🏼‍🫯‍👩🏿': 10, + '👩🏽‍⚕️': 5, + '👩🏽‍⚖️': 5, + '👩🏽‍✈️': 5, + '👩🏽‍❤️‍👨🏽': 9, + '👩🏽‍❤️‍👩🏽': 9, + '👩🏽‍❤️‍💋‍👨🏻': 11, + '👩🏽‍❤️‍💋‍👨🏽': 11, + '👩🏽‍❤️‍💋‍👨🏿': 11, + '👩🏽‍❤️‍💋‍👩🏻': 11, + '👩🏽‍❤️‍💋‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👩🏿': 11, + '👩🏽‍🌾': 6, + '👩🏽‍🍳': 6, + '👩🏽‍🍼': 6, + '👩🏽‍🎓': 6, + '👩🏽‍🎤': 6, + '👩🏽‍🎨': 6, + '👩🏽‍🏫': 6, + '👩🏽‍🏭': 6, + '👩🏽‍🐰‍👩🏻': 10, + '👩🏽‍🐰‍👩🏾': 10, + '👩🏽‍🐰‍👩🏿': 10, + '👩🏽‍💻': 6, + '👩🏽‍💼': 6, + '👩🏽‍🔧': 6, + '👩🏽‍🔬': 6, + '👩🏽‍🚀': 6, + '👩🏽‍🚒': 6, + '👩🏽‍🤝‍👨🏻': 10, + '👩🏽‍🤝‍👨🏿': 10, + '👩🏽‍🤝‍👩🏻': 10, + '👩🏽‍🤝‍👩🏼': 10, + '👩🏽‍🤝‍👩🏾': 10, + '👩🏽‍🤝‍👩🏿': 10, + '👩🏽‍🦯': 6, + '👩🏽‍🦯‍➡️': 7, + '👩🏽‍🦰': 6, + '👩🏽‍🦱': 6, + '👩🏽‍🦲': 6, + '👩🏽‍🦳': 6, + '👩🏽‍🦼': 6, + '👩🏽‍🦼‍➡️': 7, + '👩🏽‍🦽': 6, + '👩🏽‍🦽‍➡️': 7, + '👩🏽‍🫯‍👩🏻': 10, + '👩🏽‍🫯‍👩🏼': 10, + '👩🏽‍🫯‍👩🏾': 10, + '👩🏽‍🫯‍👩🏿': 10, + '👩🏾‍⚕️': 5, + '👩🏾‍⚖️': 5, + '👩🏾‍✈️': 5, + '👩🏾‍❤️‍👨🏾': 9, + '👩🏾‍❤️‍👩🏾': 9, + '👩🏾‍❤️‍💋‍👨🏾': 11, + '👩🏾‍❤️‍💋‍👩🏾': 11, + '👩🏾‍🌾': 6, + '👩🏾‍🍳': 6, + '👩🏾‍🍼': 6, + '👩🏾‍🎓': 6, + '👩🏾‍🎤': 6, + '👩🏾‍🎨': 6, + '👩🏾‍🏫': 6, + '👩🏾‍🏭': 6, + '👩🏾‍🐰‍👩🏻': 10, + '👩🏾‍🐰‍👩🏽': 10, + '👩🏾‍🐰‍👩🏿': 10, + '👩🏾‍💻': 6, + '👩🏾‍💼': 6, + '👩🏾‍🔧': 6, + '👩🏾‍🔬': 6, + '👩🏾‍🚀': 6, + '👩🏾‍🚒': 6, + '👩🏾‍🤝‍👩🏻': 10, + '👩🏾‍🤝‍👩🏽': 10, + '👩🏾‍🤝‍👩🏿': 10, + '👩🏾‍🦯': 6, + '👩🏾‍🦯‍➡️': 7, + '👩🏾‍🦰': 6, + '👩🏾‍🦱': 6, + '👩🏾‍🦲': 6, + '👩🏾‍🦳': 6, + '👩🏾‍🦼': 6, + '👩🏾‍🦽': 6, + '👩🏾‍🦽‍➡️': 7, + '👩🏾‍🫯‍👩🏻': 10, + '👩🏾‍🫯‍👩🏼': 10, + '👩🏾‍🫯‍👩🏽': 10, + '👩🏾‍🫯‍👩🏿': 10, + '👩🏿‍⚕️': 5, + '👩🏿‍⚖️': 5, + '👩🏿‍✈️': 5, + '👩🏿‍❤️‍👨🏻': 9, + '👩🏿‍❤️‍👨🏿': 9, + '👩🏿‍❤️‍👩🏿': 9, + '👩🏿‍❤️‍💋‍👨🏻': 11, + '👩🏿‍❤️‍💋‍👨🏽': 11, + '👩🏿‍❤️‍💋‍👨🏿': 11, + '👩🏿‍❤️‍💋‍👩🏻': 11, + '👩🏿‍❤️‍💋‍👩🏽': 11, + '👩🏿‍❤️‍💋‍👩🏿': 11, + '👩🏿‍🌾': 6, + '👩🏿‍🍳': 6, + '👩🏿‍🍼': 6, + '👩🏿‍🎓': 6, + '👩🏿‍🎤': 6, + '👩🏿‍🎨': 6, + '👩🏿‍🏫': 6, + '👩🏿‍🏭': 6, + '👩🏿‍🐰‍👩🏻': 10, + '👩🏿‍🐰‍👩🏼': 10, + '👩🏿‍🐰‍👩🏽': 10, + '👩🏿‍🐰‍👩🏾': 10, + '👩🏿‍💻': 6, + '👩🏿‍💼': 6, + '👩🏿‍🔧': 6, + '👩🏿‍🔬': 6, + '👩🏿‍🚀': 6, + '👩🏿‍🚒': 6, + '👩🏿‍🤝‍👨🏻': 10, + '👩🏿‍🤝‍👨🏽': 10, + '👩🏿‍🤝‍👩🏻': 10, + '👩🏿‍🤝‍👩🏼': 10, + '👩🏿‍🤝‍👩🏽': 10, + '👩🏿‍🤝‍👩🏾': 10, + '👩🏿‍🦯': 6, + '👩🏿‍🦯‍➡️': 7, + '👩🏿‍🦰': 6, + '👩🏿‍🦱': 6, + '👩🏿‍🦲': 6, + '👩🏿‍🦳': 6, + '👩🏿‍🦼': 6, + '👩🏿‍🦼‍➡️': 7, + '👩🏿‍🦽': 6, + '👩🏿‍🦽‍➡️': 7, + '👩🏿‍🫯‍👩🏻': 10, + '👩🏿‍🫯‍👩🏼': 10, + '👩🏿‍🫯‍👩🏽': 10, + '👩🏿‍🫯‍👩🏾': 10, + '👮‍♀️': 3, + '👮‍♂️': 3, + '👮🏻‍♀️': 5, + '👮🏻‍♂️': 5, + '👮🏼‍♀️': 5, + '👮🏼‍♂️': 5, + '👮🏽‍♀️': 5, + '👮🏽‍♂️': 5, + '👮🏾‍♀️': 5, + '👮🏾‍♂️': 5, + '👮🏿‍♀️': 5, + '👮🏿‍♂️': 5, + '👯‍♀️': 3, + '👯‍♂️': 3, + '👯🏻‍♀️': 5, + '👯🏻‍♂️': 5, + '👯🏼‍♀️': 5, + '👯🏼‍♂️': 5, + '👯🏽‍♀️': 5, + '👯🏽‍♂️': 5, + '👯🏾‍♀️': 5, + '👯🏾‍♂️': 5, + '👯🏿‍♀️': 5, + '👯🏿‍♂️': 5, + '👰‍♀️': 3, + '👰‍♂️': 3, + '👰🏻‍♀️': 5, + '👰🏻‍♂️': 5, + '👰🏼‍♀️': 5, + '👰🏼‍♂️': 5, + '👰🏽‍♀️': 5, + '👰🏽‍♂️': 5, + '👰🏾‍♀️': 5, + '👰🏾‍♂️': 5, + '👰🏿‍♀️': 5, + '👰🏿‍♂️': 5, + '👱‍♀️': 3, + '👱‍♂️': 3, + '👱🏻‍♀️': 5, + '👱🏻‍♂️': 5, + '👱🏼‍♀️': 5, + '👱🏼‍♂️': 5, + '👱🏽‍♀️': 5, + '👱🏽‍♂️': 5, + '👱🏾‍♀️': 5, + '👱🏾‍♂️': 5, + '👱🏿‍♀️': 5, + '👱🏿‍♂️': 5, + '👳‍♀️': 3, + '👳‍♂️': 3, + '👳🏻‍♀️': 5, + '👳🏻‍♂️': 5, + '👳🏼‍♀️': 5, + '👳🏼‍♂️': 5, + '👳🏽‍♀️': 5, + '👳🏽‍♂️': 5, + '👳🏾‍♀️': 5, + '👳🏾‍♂️': 5, + '👳🏿‍♀️': 5, + '👳🏿‍♂️': 5, + '👷‍♀️': 3, + '👷‍♂️': 3, + '👷🏻‍♀️': 5, + '👷🏻‍♂️': 5, + '👷🏼‍♀️': 5, + '👷🏼‍♂️': 5, + '👷🏽‍♀️': 5, + '👷🏽‍♂️': 5, + '👷🏾‍♀️': 5, + '👷🏾‍♂️': 5, + '👷🏿‍♀️': 5, + '👷🏿‍♂️': 5, + '💁‍♀️': 3, + '💁‍♂️': 3, + '💁🏻‍♀️': 5, + '💁🏻‍♂️': 5, + '💁🏼‍♀️': 5, + '💁🏼‍♂️': 5, + '💁🏽‍♀️': 5, + '💁🏽‍♂️': 5, + '💁🏾‍♀️': 5, + '💁🏾‍♂️': 5, + '💁🏿‍♀️': 5, + '💁🏿‍♂️': 5, + '💂‍♀️': 3, + '💂‍♂️': 3, + '💂🏻‍♀️': 5, + '💂🏻‍♂️': 5, + '💂🏼‍♀️': 5, + '💂🏼‍♂️': 5, + '💂🏽‍♀️': 5, + '💂🏽‍♂️': 5, + '💂🏾‍♀️': 5, + '💂🏾‍♂️': 5, + '💂🏿‍♀️': 5, + '💂🏿‍♂️': 5, + '💆‍♀️': 3, + '💆‍♂️': 3, + '💆🏻‍♀️': 5, + '💆🏻‍♂️': 5, + '💆🏼‍♀️': 5, + '💆🏼‍♂️': 5, + '💆🏽‍♀️': 5, + '💆🏽‍♂️': 5, + '💆🏾‍♀️': 5, + '💆🏾‍♂️': 5, + '💆🏿‍♀️': 5, + '💆🏿‍♂️': 5, + '💇‍♀️': 3, + '💇‍♂️': 3, + '💇🏻‍♀️': 5, + '💇🏻‍♂️': 5, + '💇🏼‍♀️': 5, + '💇🏼‍♂️': 5, + '💇🏽‍♀️': 5, + '💇🏽‍♂️': 5, + '💇🏾‍♀️': 5, + '💇🏾‍♂️': 5, + '💇🏿‍♀️': 5, + '💇🏿‍♂️': 5, + '🕵🏻‍♀️': 4, + '🕵🏻‍♂️': 4, + '🕵🏼‍♀️': 4, + '🕵🏼‍♂️': 4, + '🕵🏽‍♀️': 4, + '🕵🏽‍♂️': 4, + '🕵🏾‍♀️': 4, + '🕵🏾‍♂️': 4, + '🕵🏿‍♀️': 4, + '🕵🏿‍♂️': 4, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 3, + '🙂‍↔️': 3, + '🙂‍↕️': 3, + '🙅‍♀️': 3, + '🙅‍♂️': 3, + '🙅🏻‍♀️': 5, + '🙅🏻‍♂️': 5, + '🙅🏼‍♀️': 5, + '🙅🏼‍♂️': 5, + '🙅🏽‍♀️': 5, + '🙅🏽‍♂️': 5, + '🙅🏾‍♀️': 5, + '🙅🏾‍♂️': 5, + '🙅🏿‍♀️': 5, + '🙅🏿‍♂️': 5, + '🙆‍♀️': 3, + '🙆‍♂️': 3, + '🙆🏻‍♀️': 5, + '🙆🏻‍♂️': 5, + '🙆🏼‍♀️': 5, + '🙆🏼‍♂️': 5, + '🙆🏽‍♀️': 5, + '🙆🏽‍♂️': 5, + '🙆🏾‍♀️': 5, + '🙆🏾‍♂️': 5, + '🙆🏿‍♀️': 5, + '🙆🏿‍♂️': 5, + '🙇‍♀️': 3, + '🙇‍♂️': 3, + '🙇🏻‍♀️': 5, + '🙇🏻‍♂️': 5, + '🙇🏼‍♀️': 5, + '🙇🏼‍♂️': 5, + '🙇🏽‍♀️': 5, + '🙇🏽‍♂️': 5, + '🙇🏾‍♀️': 5, + '🙇🏾‍♂️': 5, + '🙇🏿‍♀️': 5, + '🙇🏿‍♂️': 5, + '🙋‍♀️': 3, + '🙋‍♂️': 3, + '🙋🏻‍♀️': 5, + '🙋🏻‍♂️': 5, + '🙋🏼‍♀️': 5, + '🙋🏼‍♂️': 5, + '🙋🏽‍♀️': 5, + '🙋🏽‍♂️': 5, + '🙋🏾‍♀️': 5, + '🙋🏾‍♂️': 5, + '🙋🏿‍♀️': 5, + '🙋🏿‍♂️': 5, + '🙍‍♀️': 3, + '🙍‍♂️': 3, + '🙍🏻‍♀️': 5, + '🙍🏻‍♂️': 5, + '🙍🏼‍♀️': 5, + '🙍🏼‍♂️': 5, + '🙍🏽‍♀️': 5, + '🙍🏽‍♂️': 5, + '🙍🏾‍♀️': 5, + '🙍🏾‍♂️': 5, + '🙍🏿‍♀️': 5, + '🙍🏿‍♂️': 5, + '🙎‍♀️': 3, + '🙎‍♂️': 3, + '🙎🏻‍♀️': 5, + '🙎🏻‍♂️': 5, + '🙎🏼‍♀️': 5, + '🙎🏼‍♂️': 5, + '🙎🏽‍♀️': 5, + '🙎🏽‍♂️': 5, + '🙎🏾‍♀️': 5, + '🙎🏾‍♂️': 5, + '🙎🏿‍♀️': 5, + '🙎🏿‍♂️': 5, + '🚣‍♀️': 3, + '🚣‍♂️': 3, + '🚣🏻‍♀️': 5, + '🚣🏻‍♂️': 5, + '🚣🏼‍♀️': 5, + '🚣🏼‍♂️': 5, + '🚣🏽‍♀️': 5, + '🚣🏽‍♂️': 5, + '🚣🏾‍♀️': 5, + '🚣🏾‍♂️': 5, + '🚣🏿‍♀️': 5, + '🚣🏿‍♂️': 5, + '🚴‍♀️': 3, + '🚴‍♂️': 3, + '🚴🏻‍♀️': 5, + '🚴🏻‍♂️': 5, + '🚴🏼‍♀️': 5, + '🚴🏼‍♂️': 5, + '🚴🏽‍♀️': 5, + '🚴🏽‍♂️': 5, + '🚴🏾‍♀️': 5, + '🚴🏾‍♂️': 5, + '🚴🏿‍♀️': 5, + '🚴🏿‍♂️': 5, + '🚵‍♀️': 3, + '🚵‍♂️': 3, + '🚵🏻‍♀️': 5, + '🚵🏻‍♂️': 5, + '🚵🏼‍♀️': 5, + '🚵🏼‍♂️': 5, + '🚵🏽‍♀️': 5, + '🚵🏽‍♂️': 5, + '🚵🏾‍♀️': 5, + '🚵🏾‍♂️': 5, + '🚵🏿‍♀️': 5, + '🚵🏿‍♂️': 5, + '🚶‍♀️': 3, + '🚶‍♀️‍➡️': 4, + '🚶‍♂️': 3, + '🚶‍♂️‍➡️': 4, + '🚶‍➡️': 3, + '🚶🏻‍♀️': 5, + '🚶🏻‍♀️‍➡️': 6, + '🚶🏻‍♂️': 5, + '🚶🏻‍♂️‍➡️': 6, + '🚶🏻‍➡️': 5, + '🚶🏼‍♀️': 5, + '🚶🏼‍♀️‍➡️': 6, + '🚶🏼‍♂️': 5, + '🚶🏼‍♂️‍➡️': 6, + '🚶🏼‍➡️': 5, + '🚶🏽‍♀️': 5, + '🚶🏽‍♀️‍➡️': 6, + '🚶🏽‍♂️': 5, + '🚶🏽‍♂️‍➡️': 6, + '🚶🏽‍➡️': 5, + '🚶🏾‍♀️': 5, + '🚶🏾‍♀️‍➡️': 6, + '🚶🏾‍♂️': 5, + '🚶🏾‍♂️‍➡️': 6, + '🚶🏾‍➡️': 5, + '🚶🏿‍♀️': 5, + '🚶🏿‍♀️‍➡️': 6, + '🚶🏿‍♂️': 5, + '🚶🏿‍♂️‍➡️': 6, + '🚶🏿‍➡️': 5, + '🤦‍♀️': 3, + '🤦‍♂️': 3, + '🤦🏻‍♀️': 5, + '🤦🏻‍♂️': 5, + '🤦🏼‍♀️': 5, + '🤦🏼‍♂️': 5, + '🤦🏽‍♀️': 5, + '🤦🏽‍♂️': 5, + '🤦🏾‍♀️': 5, + '🤦🏾‍♂️': 5, + '🤦🏿‍♀️': 5, + '🤦🏿‍♂️': 5, + '🤵‍♀️': 3, + '🤵‍♂️': 3, + '🤵🏻‍♀️': 5, + '🤵🏻‍♂️': 5, + '🤵🏼‍♀️': 5, + '🤵🏼‍♂️': 5, + '🤵🏽‍♀️': 5, + '🤵🏽‍♂️': 5, + '🤵🏾‍♀️': 5, + '🤵🏾‍♂️': 5, + '🤵🏿‍♀️': 5, + '🤵🏿‍♂️': 5, + '🤷‍♀️': 3, + '🤷‍♂️': 3, + '🤷🏻‍♀️': 5, + '🤷🏻‍♂️': 5, + '🤷🏼‍♀️': 5, + '🤷🏼‍♂️': 5, + '🤷🏽‍♀️': 5, + '🤷🏽‍♂️': 5, + '🤷🏾‍♀️': 5, + '🤷🏾‍♂️': 5, + '🤷🏿‍♀️': 5, + '🤷🏿‍♂️': 5, + '🤸‍♀️': 3, + '🤸‍♂️': 3, + '🤸🏻‍♀️': 5, + '🤸🏻‍♂️': 5, + '🤸🏼‍♀️': 5, + '🤸🏼‍♂️': 5, + '🤸🏽‍♀️': 5, + '🤸🏽‍♂️': 5, + '🤸🏾‍♀️': 5, + '🤸🏾‍♂️': 5, + '🤸🏿‍♀️': 5, + '🤸🏿‍♂️': 5, + '🤹‍♀️': 3, + '🤹‍♂️': 3, + '🤹🏻‍♀️': 5, + '🤹🏻‍♂️': 5, + '🤹🏼‍♀️': 5, + '🤹🏼‍♂️': 5, + '🤹🏽‍♀️': 5, + '🤹🏽‍♂️': 5, + '🤹🏾‍♀️': 5, + '🤹🏾‍♂️': 5, + '🤹🏿‍♀️': 5, + '🤹🏿‍♂️': 5, + '🤼‍♀️': 3, + '🤼‍♂️': 3, + '🤼🏻‍♀️': 5, + '🤼🏻‍♂️': 5, + '🤼🏼‍♀️': 5, + '🤼🏼‍♂️': 5, + '🤼🏽‍♀️': 5, + '🤼🏽‍♂️': 5, + '🤼🏾‍♀️': 5, + '🤼🏾‍♂️': 5, + '🤼🏿‍♀️': 5, + '🤼🏿‍♂️': 5, + '🤽‍♀️': 3, + '🤽‍♂️': 3, + '🤽🏻‍♀️': 5, + '🤽🏻‍♂️': 5, + '🤽🏼‍♀️': 5, + '🤽🏼‍♂️': 5, + '🤽🏽‍♀️': 5, + '🤽🏽‍♂️': 5, + '🤽🏾‍♀️': 5, + '🤽🏾‍♂️': 5, + '🤽🏿‍♀️': 5, + '🤽🏿‍♂️': 5, + '🤾‍♀️': 3, + '🤾‍♂️': 3, + '🤾🏻‍♀️': 5, + '🤾🏻‍♂️': 5, + '🤾🏼‍♀️': 5, + '🤾🏼‍♂️': 5, + '🤾🏽‍♀️': 5, + '🤾🏽‍♂️': 5, + '🤾🏾‍♀️': 5, + '🤾🏾‍♂️': 5, + '🤾🏿‍♀️': 5, + '🤾🏿‍♂️': 5, + '🦸‍♀️': 3, + '🦸‍♂️': 3, + '🦸🏻‍♀️': 5, + '🦸🏻‍♂️': 5, + '🦸🏼‍♀️': 5, + '🦸🏼‍♂️': 5, + '🦸🏽‍♀️': 5, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_4cdf59ce.py b/libs/wcwidth/table_grapheme_overrides/_known_4cdf59ce.py new file mode 100644 index 000000000..33e718983 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_4cdf59ce.py @@ -0,0 +1,42 @@ +""" +Grapheme overrides for _known_4cdf59ce. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: ghostty +""" +# pylint: skip-file +GRAPHEMES = { + 'ꦏ꧀': 2, + 'ꦠ꧀': 2, + 'ꦢ꧀': 2, + 'ꦤ꧀': 2, + 'ꦥ꦳꧀': 2, + 'ꦥ꧀': 2, + 'ꦧ꧀': 2, + 'ꦩ꧀': 2, + 'ꦭ꧀': 2, + 'ꦱ꧀': 2, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑍍': 2, + '𑌣𑍍': 2, + '𑌤𑍍': 2, + '𑌦𑍍': 2, + '𑌧𑍍': 2, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑍍': 2, + '𑌮𑍍': 2, + '𑌯𑍍': 2, + '𑌰𑍍': 2, + '𑌲𑍍': 2, + '𑌵𑍍': 2, + '𑌶𑍍': 2, + '𑌷𑍍': 2, + '𑌸𑍍': 2, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_50bf0759.py b/libs/wcwidth/table_grapheme_overrides/_known_50bf0759.py new file mode 100644 index 000000000..31bada5da --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_50bf0759.py @@ -0,0 +1,2571 @@ +""" +Grapheme overrides for _known_50bf0759. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: xterm.js +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 3, + '⛹🏻‍♀️': 4, + '⛹🏻‍♂️': 4, + '⛹🏼‍♀️': 4, + '⛹🏼‍♂️': 4, + '⛹🏽‍♀️': 4, + '⛹🏽‍♂️': 4, + '⛹🏾‍♀️': 4, + '⛹🏾‍♂️': 4, + '⛹🏿‍♀️': 4, + '⛹🏿‍♂️': 4, + '❤️‍🔥': 3, + '❤️‍🩹': 3, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 3, + '🏃‍♀️‍➡️': 4, + '🏃‍♂️': 3, + '🏃‍♂️‍➡️': 4, + '🏃‍➡️': 3, + '🏃🏻‍♀️': 5, + '🏃🏻‍♀️‍➡️': 6, + '🏃🏻‍♂️': 5, + '🏃🏻‍♂️‍➡️': 6, + '🏃🏻‍➡️': 5, + '🏃🏼‍♀️': 5, + '🏃🏼‍♀️‍➡️': 6, + '🏃🏼‍♂️': 5, + '🏃🏼‍♂️‍➡️': 6, + '🏃🏼‍➡️': 5, + '🏃🏽‍♀️': 5, + '🏃🏽‍♀️‍➡️': 6, + '🏃🏽‍♂️': 5, + '🏃🏽‍♂️‍➡️': 6, + '🏃🏽‍➡️': 5, + '🏃🏾‍♀️': 5, + '🏃🏾‍♀️‍➡️': 6, + '🏃🏾‍♂️': 5, + '🏃🏾‍♂️‍➡️': 6, + '🏃🏾‍➡️': 5, + '🏃🏿‍♀️': 5, + '🏃🏿‍♀️‍➡️': 6, + '🏃🏿‍♂️': 5, + '🏃🏿‍♂️‍➡️': 6, + '🏃🏿‍➡️': 5, + '🏄‍♀️': 3, + '🏄‍♂️': 3, + '🏄🏻‍♀️': 5, + '🏄🏻‍♂️': 5, + '🏄🏼‍♀️': 5, + '🏄🏼‍♂️': 5, + '🏄🏽‍♀️': 5, + '🏄🏽‍♂️': 5, + '🏄🏾‍♀️': 5, + '🏄🏾‍♂️': 5, + '🏄🏿‍♀️': 5, + '🏄🏿‍♂️': 5, + '🏊‍♀️': 3, + '🏊‍♂️': 3, + '🏊🏻‍♀️': 5, + '🏊🏻‍♂️': 5, + '🏊🏼‍♀️': 5, + '🏊🏼‍♂️': 5, + '🏊🏽‍♀️': 5, + '🏊🏽‍♂️': 5, + '🏊🏾‍♀️': 5, + '🏊🏾‍♂️': 5, + '🏊🏿‍♀️': 5, + '🏊🏿‍♂️': 5, + '🏋🏻‍♀️': 4, + '🏋🏻‍♂️': 4, + '🏋🏼‍♀️': 4, + '🏋🏼‍♂️': 4, + '🏋🏽‍♀️': 4, + '🏋🏽‍♂️': 4, + '🏋🏾‍♀️': 4, + '🏋🏾‍♂️': 4, + '🏋🏿‍♀️': 4, + '🏋🏿‍♂️': 4, + '🏌🏻‍♀️': 4, + '🏌🏻‍♂️': 4, + '🏌🏼‍♀️': 4, + '🏌🏼‍♂️': 4, + '🏌🏽‍♀️': 4, + '🏌🏽‍♂️': 4, + '🏌🏾‍♀️': 4, + '🏌🏾‍♂️': 4, + '🏌🏿‍♀️': 4, + '🏌🏿‍♂️': 4, + '🏳️‍🌈': 3, + '🏴‍☠️': 3, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 3, + '👨‍⚕️': 3, + '👨‍⚖️': 3, + '👨‍✈️': 3, + '👨‍❤️‍👨': 5, + '👨‍❤️‍💋‍👨': 7, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 5, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 5, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 5, + '👨🏻‍⚕️': 5, + '👨🏻‍⚖️': 5, + '👨🏻‍✈️': 5, + '👨🏻‍❤️‍👨🏻': 9, + '👨🏻‍❤️‍👨🏽': 9, + '👨🏻‍❤️‍👨🏿': 9, + '👨🏻‍❤️‍💋‍👨🏻': 11, + '👨🏻‍❤️‍💋‍👨🏽': 11, + '👨🏻‍❤️‍💋‍👨🏾': 11, + '👨🏻‍❤️‍💋‍👨🏿': 11, + '👨🏻‍🌾': 6, + '👨🏻‍🍳': 6, + '👨🏻‍🍼': 6, + '👨🏻‍🎓': 6, + '👨🏻‍🎤': 6, + '👨🏻‍🎨': 6, + '👨🏻‍🏫': 6, + '👨🏻‍🏭': 6, + '👨🏻‍🐰‍👨🏼': 10, + '👨🏻‍🐰‍👨🏽': 10, + '👨🏻‍🐰‍👨🏾': 10, + '👨🏻‍🐰‍👨🏿': 10, + '👨🏻‍💻': 6, + '👨🏻‍💼': 6, + '👨🏻‍🔧': 6, + '👨🏻‍🔬': 6, + '👨🏻‍🚀': 6, + '👨🏻‍🚒': 6, + '👨🏻‍🤝‍👨🏼': 10, + '👨🏻‍🤝‍👨🏽': 10, + '👨🏻‍🤝‍👨🏾': 10, + '👨🏻‍🤝‍👨🏿': 10, + '👨🏻‍🦯': 6, + '👨🏻‍🦯‍➡️': 7, + '👨🏻‍🦰': 6, + '👨🏻‍🦱': 6, + '👨🏻‍🦲': 6, + '👨🏻‍🦳': 6, + '👨🏻‍🦼': 6, + '👨🏻‍🦼‍➡️': 7, + '👨🏻‍🦽': 6, + '👨🏻‍🦽‍➡️': 7, + '👨🏻‍🫯‍👨🏼': 9, + '👨🏻‍🫯‍👨🏽': 9, + '👨🏻‍🫯‍👨🏾': 9, + '👨🏻‍🫯‍👨🏿': 9, + '👨🏼‍⚕️': 5, + '👨🏼‍⚖️': 5, + '👨🏼‍✈️': 5, + '👨🏼‍❤️‍👨🏼': 9, + '👨🏼‍❤️‍💋‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏿': 11, + '👨🏼‍🌾': 6, + '👨🏼‍🍳': 6, + '👨🏼‍🍼': 6, + '👨🏼‍🎓': 6, + '👨🏼‍🎤': 6, + '👨🏼‍🎨': 6, + '👨🏼‍🏫': 6, + '👨🏼‍🏭': 6, + '👨🏼‍🐰‍👨🏻': 10, + '👨🏼‍🐰‍👨🏽': 10, + '👨🏼‍🐰‍👨🏿': 10, + '👨🏼‍💻': 6, + '👨🏼‍💼': 6, + '👨🏼‍🔧': 6, + '👨🏼‍🔬': 6, + '👨🏼‍🚀': 6, + '👨🏼‍🚒': 6, + '👨🏼‍🤝‍👨🏻': 10, + '👨🏼‍🤝‍👨🏽': 10, + '👨🏼‍🤝‍👨🏿': 10, + '👨🏼‍🦯': 6, + '👨🏼‍🦯‍➡️': 7, + '👨🏼‍🦰': 6, + '👨🏼‍🦱': 6, + '👨🏼‍🦲': 6, + '👨🏼‍🦳': 6, + '👨🏼‍🦼': 6, + '👨🏼‍🦽': 6, + '👨🏼‍🦽‍➡️': 7, + '👨🏼‍🫯‍👨🏻': 9, + '👨🏼‍🫯‍👨🏽': 9, + '👨🏼‍🫯‍👨🏾': 9, + '👨🏼‍🫯‍👨🏿': 9, + '👨🏽‍⚕️': 5, + '👨🏽‍⚖️': 5, + '👨🏽‍✈️': 5, + '👨🏽‍❤️‍👨🏻': 9, + '👨🏽‍❤️‍👨🏽': 9, + '👨🏽‍❤️‍👨🏿': 9, + '👨🏽‍❤️‍💋‍👨🏻': 11, + '👨🏽‍❤️‍💋‍👨🏽': 11, + '👨🏽‍❤️‍💋‍👨🏿': 11, + '👨🏽‍🌾': 6, + '👨🏽‍🍳': 6, + '👨🏽‍🍼': 6, + '👨🏽‍🎓': 6, + '👨🏽‍🎤': 6, + '👨🏽‍🎨': 6, + '👨🏽‍🏫': 6, + '👨🏽‍🏭': 6, + '👨🏽‍🐰‍👨🏻': 10, + '👨🏽‍🐰‍👨🏼': 10, + '👨🏽‍🐰‍👨🏾': 10, + '👨🏽‍🐰‍👨🏿': 10, + '👨🏽‍💻': 6, + '👨🏽‍💼': 6, + '👨🏽‍🔧': 6, + '👨🏽‍🔬': 6, + '👨🏽‍🚀': 6, + '👨🏽‍🚒': 6, + '👨🏽‍🤝‍👨🏻': 10, + '👨🏽‍🤝‍👨🏼': 10, + '👨🏽‍🤝‍👨🏾': 10, + '👨🏽‍🤝‍👨🏿': 10, + '👨🏽‍🦯': 6, + '👨🏽‍🦯‍➡️': 7, + '👨🏽‍🦰': 6, + '👨🏽‍🦱': 6, + '👨🏽‍🦲': 6, + '👨🏽‍🦳': 6, + '👨🏽‍🦼': 6, + '👨🏽‍🦼‍➡️': 7, + '👨🏽‍🦽': 6, + '👨🏽‍🦽‍➡️': 7, + '👨🏽‍🫯‍👨🏻': 9, + '👨🏽‍🫯‍👨🏼': 9, + '👨🏽‍🫯‍👨🏾': 9, + '👨🏽‍🫯‍👨🏿': 9, + '👨🏾‍⚕️': 5, + '👨🏾‍⚖️': 5, + '👨🏾‍✈️': 5, + '👨🏾‍❤️‍👨🏾': 9, + '👨🏾‍❤️‍💋‍👨🏻': 11, + '👨🏾‍❤️‍💋‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏿': 11, + '👨🏾‍🌾': 6, + '👨🏾‍🍳': 6, + '👨🏾‍🍼': 6, + '👨🏾‍🎓': 6, + '👨🏾‍🎤': 6, + '👨🏾‍🎨': 6, + '👨🏾‍🏫': 6, + '👨🏾‍🏭': 6, + '👨🏾‍🐰‍👨🏻': 10, + '👨🏾‍🐰‍👨🏽': 10, + '👨🏾‍🐰‍👨🏿': 10, + '👨🏾‍💻': 6, + '👨🏾‍💼': 6, + '👨🏾‍🔧': 6, + '👨🏾‍🔬': 6, + '👨🏾‍🚀': 6, + '👨🏾‍🚒': 6, + '👨🏾‍🤝‍👨🏻': 10, + '👨🏾‍🤝‍👨🏽': 10, + '👨🏾‍🤝‍👨🏿': 10, + '👨🏾‍🦯': 6, + '👨🏾‍🦯‍➡️': 7, + '👨🏾‍🦰': 6, + '👨🏾‍🦱': 6, + '👨🏾‍🦲': 6, + '👨🏾‍🦳': 6, + '👨🏾‍🦼': 6, + '👨🏾‍🦽': 6, + '👨🏾‍🦽‍➡️': 7, + '👨🏾‍🫯‍👨🏻': 9, + '👨🏾‍🫯‍👨🏼': 9, + '👨🏾‍🫯‍👨🏽': 9, + '👨🏾‍🫯‍👨🏿': 9, + '👨🏿‍⚕️': 5, + '👨🏿‍⚖️': 5, + '👨🏿‍✈️': 5, + '👨🏿‍❤️‍👨🏻': 9, + '👨🏿‍❤️‍👨🏽': 9, + '👨🏿‍❤️‍👨🏿': 9, + '👨🏿‍❤️‍💋‍👨🏻': 11, + '👨🏿‍❤️‍💋‍👨🏼': 11, + '👨🏿‍❤️‍💋‍👨🏽': 11, + '👨🏿‍❤️‍💋‍👨🏾': 11, + '👨🏿‍❤️‍💋‍👨🏿': 11, + '👨🏿‍🌾': 6, + '👨🏿‍🍳': 6, + '👨🏿‍🍼': 6, + '👨🏿‍🎓': 6, + '👨🏿‍🎤': 6, + '👨🏿‍🎨': 6, + '👨🏿‍🏫': 6, + '👨🏿‍🏭': 6, + '👨🏿‍🐰‍👨🏻': 10, + '👨🏿‍🐰‍👨🏼': 10, + '👨🏿‍🐰‍👨🏽': 10, + '👨🏿‍🐰‍👨🏾': 10, + '👨🏿‍💻': 6, + '👨🏿‍💼': 6, + '👨🏿‍🔧': 6, + '👨🏿‍🔬': 6, + '👨🏿‍🚀': 6, + '👨🏿‍🚒': 6, + '👨🏿‍🤝‍👨🏻': 10, + '👨🏿‍🤝‍👨🏼': 10, + '👨🏿‍🤝‍👨🏽': 10, + '👨🏿‍🤝‍👨🏾': 10, + '👨🏿‍🦯': 6, + '👨🏿‍🦯‍➡️': 7, + '👨🏿‍🦰': 6, + '👨🏿‍🦱': 6, + '👨🏿‍🦲': 6, + '👨🏿‍🦳': 6, + '👨🏿‍🦼': 6, + '👨🏿‍🦼‍➡️': 7, + '👨🏿‍🦽': 6, + '👨🏿‍🦽‍➡️': 7, + '👨🏿‍🫯‍👨🏻': 9, + '👨🏿‍🫯‍👨🏼': 9, + '👨🏿‍🫯‍👨🏽': 9, + '👨🏿‍🫯‍👨🏾': 9, + '👩‍⚕️': 3, + '👩‍⚖️': 3, + '👩‍✈️': 3, + '👩‍❤️‍👨': 5, + '👩‍❤️‍👩': 5, + '👩‍❤️‍💋‍👨': 7, + '👩‍❤️‍💋‍👩': 7, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 5, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 5, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 5, + '👩🏻‍⚕️': 5, + '👩🏻‍⚖️': 5, + '👩🏻‍✈️': 5, + '👩🏻‍❤️‍👨🏻': 9, + '👩🏻‍❤️‍👨🏿': 9, + '👩🏻‍❤️‍👩🏻': 9, + '👩🏻‍❤️‍💋‍👨🏻': 11, + '👩🏻‍❤️‍💋‍👨🏽': 11, + '👩🏻‍❤️‍💋‍👨🏿': 11, + '👩🏻‍❤️‍💋‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👩🏽': 11, + '👩🏻‍❤️‍💋‍👩🏿': 11, + '👩🏻‍🌾': 6, + '👩🏻‍🍳': 6, + '👩🏻‍🍼': 6, + '👩🏻‍🎓': 6, + '👩🏻‍🎤': 6, + '👩🏻‍🎨': 6, + '👩🏻‍🏫': 6, + '👩🏻‍🏭': 6, + '👩🏻‍🐰‍👩🏼': 10, + '👩🏻‍🐰‍👩🏽': 10, + '👩🏻‍🐰‍👩🏾': 10, + '👩🏻‍🐰‍👩🏿': 10, + '👩🏻‍💻': 6, + '👩🏻‍💼': 6, + '👩🏻‍🔧': 6, + '👩🏻‍🔬': 6, + '👩🏻‍🚀': 6, + '👩🏻‍🚒': 6, + '👩🏻‍🤝‍👨🏽': 10, + '👩🏻‍🤝‍👨🏿': 10, + '👩🏻‍🤝‍👩🏼': 10, + '👩🏻‍🤝‍👩🏽': 10, + '👩🏻‍🤝‍👩🏾': 10, + '👩🏻‍🤝‍👩🏿': 10, + '👩🏻‍🦯': 6, + '👩🏻‍🦯‍➡️': 7, + '👩🏻‍🦰': 6, + '👩🏻‍🦱': 6, + '👩🏻‍🦲': 6, + '👩🏻‍🦳': 6, + '👩🏻‍🦼': 6, + '👩🏻‍🦼‍➡️': 7, + '👩🏻‍🦽': 6, + '👩🏻‍🦽‍➡️': 7, + '👩🏻‍🫯‍👩🏼': 9, + '👩🏻‍🫯‍👩🏽': 9, + '👩🏻‍🫯‍👩🏾': 9, + '👩🏻‍🫯‍👩🏿': 9, + '👩🏼‍⚕️': 5, + '👩🏼‍⚖️': 5, + '👩🏼‍✈️': 5, + '👩🏼‍❤️‍👨🏼': 9, + '👩🏼‍❤️‍👩🏼': 9, + '👩🏼‍❤️‍💋‍👨🏼': 11, + '👩🏼‍❤️‍💋‍👩🏼': 11, + '👩🏼‍🌾': 6, + '👩🏼‍🍳': 6, + '👩🏼‍🍼': 6, + '👩🏼‍🎓': 6, + '👩🏼‍🎤': 6, + '👩🏼‍🎨': 6, + '👩🏼‍🏫': 6, + '👩🏼‍🏭': 6, + '👩🏼‍🐰‍👩🏻': 10, + '👩🏼‍🐰‍👩🏿': 10, + '👩🏼‍💻': 6, + '👩🏼‍💼': 6, + '👩🏼‍🔧': 6, + '👩🏼‍🔬': 6, + '👩🏼‍🚀': 6, + '👩🏼‍🚒': 6, + '👩🏼‍🤝‍👩🏻': 10, + '👩🏼‍🤝‍👩🏽': 10, + '👩🏼‍🤝‍👩🏿': 10, + '👩🏼‍🦯': 6, + '👩🏼‍🦯‍➡️': 7, + '👩🏼‍🦰': 6, + '👩🏼‍🦱': 6, + '👩🏼‍🦲': 6, + '👩🏼‍🦳': 6, + '👩🏼‍🦼': 6, + '👩🏼‍🦽': 6, + '👩🏼‍🫯‍👩🏻': 9, + '👩🏼‍🫯‍👩🏽': 9, + '👩🏼‍🫯‍👩🏾': 9, + '👩🏼‍🫯‍👩🏿': 9, + '👩🏽‍⚕️': 5, + '👩🏽‍⚖️': 5, + '👩🏽‍✈️': 5, + '👩🏽‍❤️‍👨🏽': 9, + '👩🏽‍❤️‍👩🏽': 9, + '👩🏽‍❤️‍💋‍👨🏻': 11, + '👩🏽‍❤️‍💋‍👨🏽': 11, + '👩🏽‍❤️‍💋‍👨🏿': 11, + '👩🏽‍❤️‍💋‍👩🏻': 11, + '👩🏽‍❤️‍💋‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👩🏿': 11, + '👩🏽‍🌾': 6, + '👩🏽‍🍳': 6, + '👩🏽‍🍼': 6, + '👩🏽‍🎓': 6, + '👩🏽‍🎤': 6, + '👩🏽‍🎨': 6, + '👩🏽‍🏫': 6, + '👩🏽‍🏭': 6, + '👩🏽‍🐰‍👩🏻': 10, + '👩🏽‍🐰‍👩🏾': 10, + '👩🏽‍🐰‍👩🏿': 10, + '👩🏽‍💻': 6, + '👩🏽‍💼': 6, + '👩🏽‍🔧': 6, + '👩🏽‍🔬': 6, + '👩🏽‍🚀': 6, + '👩🏽‍🚒': 6, + '👩🏽‍🤝‍👨🏻': 10, + '👩🏽‍🤝‍👨🏿': 10, + '👩🏽‍🤝‍👩🏻': 10, + '👩🏽‍🤝‍👩🏼': 10, + '👩🏽‍🤝‍👩🏾': 10, + '👩🏽‍🤝‍👩🏿': 10, + '👩🏽‍🦯': 6, + '👩🏽‍🦯‍➡️': 7, + '👩🏽‍🦰': 6, + '👩🏽‍🦱': 6, + '👩🏽‍🦲': 6, + '👩🏽‍🦳': 6, + '👩🏽‍🦼': 6, + '👩🏽‍🦼‍➡️': 7, + '👩🏽‍🦽': 6, + '👩🏽‍🦽‍➡️': 7, + '👩🏽‍🫯‍👩🏻': 9, + '👩🏽‍🫯‍👩🏼': 9, + '👩🏽‍🫯‍👩🏾': 9, + '👩🏽‍🫯‍👩🏿': 9, + '👩🏾‍⚕️': 5, + '👩🏾‍⚖️': 5, + '👩🏾‍✈️': 5, + '👩🏾‍❤️‍👨🏾': 9, + '👩🏾‍❤️‍👩🏾': 9, + '👩🏾‍❤️‍💋‍👨🏾': 11, + '👩🏾‍❤️‍💋‍👩🏾': 11, + '👩🏾‍🌾': 6, + '👩🏾‍🍳': 6, + '👩🏾‍🍼': 6, + '👩🏾‍🎓': 6, + '👩🏾‍🎤': 6, + '👩🏾‍🎨': 6, + '👩🏾‍🏫': 6, + '👩🏾‍🏭': 6, + '👩🏾‍🐰‍👩🏻': 10, + '👩🏾‍🐰‍👩🏽': 10, + '👩🏾‍🐰‍👩🏿': 10, + '👩🏾‍💻': 6, + '👩🏾‍💼': 6, + '👩🏾‍🔧': 6, + '👩🏾‍🔬': 6, + '👩🏾‍🚀': 6, + '👩🏾‍🚒': 6, + '👩🏾‍🤝‍👩🏻': 10, + '👩🏾‍🤝‍👩🏽': 10, + '👩🏾‍🤝‍👩🏿': 10, + '👩🏾‍🦯': 6, + '👩🏾‍🦯‍➡️': 7, + '👩🏾‍🦰': 6, + '👩🏾‍🦱': 6, + '👩🏾‍🦲': 6, + '👩🏾‍🦳': 6, + '👩🏾‍🦼': 6, + '👩🏾‍🦽': 6, + '👩🏾‍🦽‍➡️': 7, + '👩🏾‍🫯‍👩🏻': 9, + '👩🏾‍🫯‍👩🏼': 9, + '👩🏾‍🫯‍👩🏽': 9, + '👩🏾‍🫯‍👩🏿': 9, + '👩🏿‍⚕️': 5, + '👩🏿‍⚖️': 5, + '👩🏿‍✈️': 5, + '👩🏿‍❤️‍👨🏻': 9, + '👩🏿‍❤️‍👨🏿': 9, + '👩🏿‍❤️‍👩🏿': 9, + '👩🏿‍❤️‍💋‍👨🏻': 11, + '👩🏿‍❤️‍💋‍👨🏽': 11, + '👩🏿‍❤️‍💋‍👨🏿': 11, + '👩🏿‍❤️‍💋‍👩🏻': 11, + '👩🏿‍❤️‍💋‍👩🏽': 11, + '👩🏿‍❤️‍💋‍👩🏿': 11, + '👩🏿‍🌾': 6, + '👩🏿‍🍳': 6, + '👩🏿‍🍼': 6, + '👩🏿‍🎓': 6, + '👩🏿‍🎤': 6, + '👩🏿‍🎨': 6, + '👩🏿‍🏫': 6, + '👩🏿‍🏭': 6, + '👩🏿‍🐰‍👩🏻': 10, + '👩🏿‍🐰‍👩🏼': 10, + '👩🏿‍🐰‍👩🏽': 10, + '👩🏿‍🐰‍👩🏾': 10, + '👩🏿‍💻': 6, + '👩🏿‍💼': 6, + '👩🏿‍🔧': 6, + '👩🏿‍🔬': 6, + '👩🏿‍🚀': 6, + '👩🏿‍🚒': 6, + '👩🏿‍🤝‍👨🏻': 10, + '👩🏿‍🤝‍👨🏽': 10, + '👩🏿‍🤝‍👩🏻': 10, + '👩🏿‍🤝‍👩🏼': 10, + '👩🏿‍🤝‍👩🏽': 10, + '👩🏿‍🤝‍👩🏾': 10, + '👩🏿‍🦯': 6, + '👩🏿‍🦯‍➡️': 7, + '👩🏿‍🦰': 6, + '👩🏿‍🦱': 6, + '👩🏿‍🦲': 6, + '👩🏿‍🦳': 6, + '👩🏿‍🦼': 6, + '👩🏿‍🦼‍➡️': 7, + '👩🏿‍🦽': 6, + '👩🏿‍🦽‍➡️': 7, + '👩🏿‍🫯‍👩🏻': 9, + '👩🏿‍🫯‍👩🏼': 9, + '👩🏿‍🫯‍👩🏽': 9, + '👩🏿‍🫯‍👩🏾': 9, + '👮‍♀️': 3, + '👮‍♂️': 3, + '👮🏻‍♀️': 5, + '👮🏻‍♂️': 5, + '👮🏼‍♀️': 5, + '👮🏼‍♂️': 5, + '👮🏽‍♀️': 5, + '👮🏽‍♂️': 5, + '👮🏾‍♀️': 5, + '👮🏾‍♂️': 5, + '👮🏿‍♀️': 5, + '👮🏿‍♂️': 5, + '👯‍♀️': 3, + '👯‍♂️': 3, + '👯🏻‍♀️': 5, + '👯🏻‍♂️': 5, + '👯🏼‍♀️': 5, + '👯🏼‍♂️': 5, + '👯🏽‍♀️': 5, + '👯🏽‍♂️': 5, + '👯🏾‍♀️': 5, + '👯🏾‍♂️': 5, + '👯🏿‍♀️': 5, + '👯🏿‍♂️': 5, + '👰‍♀️': 3, + '👰‍♂️': 3, + '👰🏻‍♀️': 5, + '👰🏻‍♂️': 5, + '👰🏼‍♀️': 5, + '👰🏼‍♂️': 5, + '👰🏽‍♀️': 5, + '👰🏽‍♂️': 5, + '👰🏾‍♀️': 5, + '👰🏾‍♂️': 5, + '👰🏿‍♀️': 5, + '👰🏿‍♂️': 5, + '👱‍♀️': 3, + '👱‍♂️': 3, + '👱🏻‍♀️': 5, + '👱🏻‍♂️': 5, + '👱🏼‍♀️': 5, + '👱🏼‍♂️': 5, + '👱🏽‍♀️': 5, + '👱🏽‍♂️': 5, + '👱🏾‍♀️': 5, + '👱🏾‍♂️': 5, + '👱🏿‍♀️': 5, + '👱🏿‍♂️': 5, + '👳‍♀️': 3, + '👳‍♂️': 3, + '👳🏻‍♀️': 5, + '👳🏻‍♂️': 5, + '👳🏼‍♀️': 5, + '👳🏼‍♂️': 5, + '👳🏽‍♀️': 5, + '👳🏽‍♂️': 5, + '👳🏾‍♀️': 5, + '👳🏾‍♂️': 5, + '👳🏿‍♀️': 5, + '👳🏿‍♂️': 5, + '👷‍♀️': 3, + '👷‍♂️': 3, + '👷🏻‍♀️': 5, + '👷🏻‍♂️': 5, + '👷🏼‍♀️': 5, + '👷🏼‍♂️': 5, + '👷🏽‍♀️': 5, + '👷🏽‍♂️': 5, + '👷🏾‍♀️': 5, + '👷🏾‍♂️': 5, + '👷🏿‍♀️': 5, + '👷🏿‍♂️': 5, + '💁‍♀️': 3, + '💁‍♂️': 3, + '💁🏻‍♀️': 5, + '💁🏻‍♂️': 5, + '💁🏼‍♀️': 5, + '💁🏼‍♂️': 5, + '💁🏽‍♀️': 5, + '💁🏽‍♂️': 5, + '💁🏾‍♀️': 5, + '💁🏾‍♂️': 5, + '💁🏿‍♀️': 5, + '💁🏿‍♂️': 5, + '💂‍♀️': 3, + '💂‍♂️': 3, + '💂🏻‍♀️': 5, + '💂🏻‍♂️': 5, + '💂🏼‍♀️': 5, + '💂🏼‍♂️': 5, + '💂🏽‍♀️': 5, + '💂🏽‍♂️': 5, + '💂🏾‍♀️': 5, + '💂🏾‍♂️': 5, + '💂🏿‍♀️': 5, + '💂🏿‍♂️': 5, + '💆‍♀️': 3, + '💆‍♂️': 3, + '💆🏻‍♀️': 5, + '💆🏻‍♂️': 5, + '💆🏼‍♀️': 5, + '💆🏼‍♂️': 5, + '💆🏽‍♀️': 5, + '💆🏽‍♂️': 5, + '💆🏾‍♀️': 5, + '💆🏾‍♂️': 5, + '💆🏿‍♀️': 5, + '💆🏿‍♂️': 5, + '💇‍♀️': 3, + '💇‍♂️': 3, + '💇🏻‍♀️': 5, + '💇🏻‍♂️': 5, + '💇🏼‍♀️': 5, + '💇🏼‍♂️': 5, + '💇🏽‍♀️': 5, + '💇🏽‍♂️': 5, + '💇🏾‍♀️': 5, + '💇🏾‍♂️': 5, + '💇🏿‍♀️': 5, + '💇🏿‍♂️': 5, + '🕵🏻‍♀️': 4, + '🕵🏻‍♂️': 4, + '🕵🏼‍♀️': 4, + '🕵🏼‍♂️': 4, + '🕵🏽‍♀️': 4, + '🕵🏽‍♂️': 4, + '🕵🏾‍♀️': 4, + '🕵🏾‍♂️': 4, + '🕵🏿‍♀️': 4, + '🕵🏿‍♂️': 4, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 3, + '🙂‍↔️': 3, + '🙂‍↕️': 3, + '🙅‍♀️': 3, + '🙅‍♂️': 3, + '🙅🏻‍♀️': 5, + '🙅🏻‍♂️': 5, + '🙅🏼‍♀️': 5, + '🙅🏼‍♂️': 5, + '🙅🏽‍♀️': 5, + '🙅🏽‍♂️': 5, + '🙅🏾‍♀️': 5, + '🙅🏾‍♂️': 5, + '🙅🏿‍♀️': 5, + '🙅🏿‍♂️': 5, + '🙆‍♀️': 3, + '🙆‍♂️': 3, + '🙆🏻‍♀️': 5, + '🙆🏻‍♂️': 5, + '🙆🏼‍♀️': 5, + '🙆🏼‍♂️': 5, + '🙆🏽‍♀️': 5, + '🙆🏽‍♂️': 5, + '🙆🏾‍♀️': 5, + '🙆🏾‍♂️': 5, + '🙆🏿‍♀️': 5, + '🙆🏿‍♂️': 5, + '🙇‍♀️': 3, + '🙇‍♂️': 3, + '🙇🏻‍♀️': 5, + '🙇🏻‍♂️': 5, + '🙇🏼‍♀️': 5, + '🙇🏼‍♂️': 5, + '🙇🏽‍♀️': 5, + '🙇🏽‍♂️': 5, + '🙇🏾‍♀️': 5, + '🙇🏾‍♂️': 5, + '🙇🏿‍♀️': 5, + '🙇🏿‍♂️': 5, + '🙋‍♀️': 3, + '🙋‍♂️': 3, + '🙋🏻‍♀️': 5, + '🙋🏻‍♂️': 5, + '🙋🏼‍♀️': 5, + '🙋🏼‍♂️': 5, + '🙋🏽‍♀️': 5, + '🙋🏽‍♂️': 5, + '🙋🏾‍♀️': 5, + '🙋🏾‍♂️': 5, + '🙋🏿‍♀️': 5, + '🙋🏿‍♂️': 5, + '🙍‍♀️': 3, + '🙍‍♂️': 3, + '🙍🏻‍♀️': 5, + '🙍🏻‍♂️': 5, + '🙍🏼‍♀️': 5, + '🙍🏼‍♂️': 5, + '🙍🏽‍♀️': 5, + '🙍🏽‍♂️': 5, + '🙍🏾‍♀️': 5, + '🙍🏾‍♂️': 5, + '🙍🏿‍♀️': 5, + '🙍🏿‍♂️': 5, + '🙎‍♀️': 3, + '🙎‍♂️': 3, + '🙎🏻‍♀️': 5, + '🙎🏻‍♂️': 5, + '🙎🏼‍♀️': 5, + '🙎🏼‍♂️': 5, + '🙎🏽‍♀️': 5, + '🙎🏽‍♂️': 5, + '🙎🏾‍♀️': 5, + '🙎🏾‍♂️': 5, + '🙎🏿‍♀️': 5, + '🙎🏿‍♂️': 5, + '🚣‍♀️': 3, + '🚣‍♂️': 3, + '🚣🏻‍♀️': 5, + '🚣🏻‍♂️': 5, + '🚣🏼‍♀️': 5, + '🚣🏼‍♂️': 5, + '🚣🏽‍♀️': 5, + '🚣🏽‍♂️': 5, + '🚣🏾‍♀️': 5, + '🚣🏾‍♂️': 5, + '🚣🏿‍♀️': 5, + '🚣🏿‍♂️': 5, + '🚴‍♀️': 3, + '🚴‍♂️': 3, + '🚴🏻‍♀️': 5, + '🚴🏻‍♂️': 5, + '🚴🏼‍♀️': 5, + '🚴🏼‍♂️': 5, + '🚴🏽‍♀️': 5, + '🚴🏽‍♂️': 5, + '🚴🏾‍♀️': 5, + '🚴🏾‍♂️': 5, + '🚴🏿‍♀️': 5, + '🚴🏿‍♂️': 5, + '🚵‍♀️': 3, + '🚵‍♂️': 3, + '🚵🏻‍♀️': 5, + '🚵🏻‍♂️': 5, + '🚵🏼‍♀️': 5, + '🚵🏼‍♂️': 5, + '🚵🏽‍♀️': 5, + '🚵🏽‍♂️': 5, + '🚵🏾‍♀️': 5, + '🚵🏾‍♂️': 5, + '🚵🏿‍♀️': 5, + '🚵🏿‍♂️': 5, + '🚶‍♀️': 3, + '🚶‍♀️‍➡️': 4, + '🚶‍♂️': 3, + '🚶‍♂️‍➡️': 4, + '🚶‍➡️': 3, + '🚶🏻‍♀️': 5, + '🚶🏻‍♀️‍➡️': 6, + '🚶🏻‍♂️': 5, + '🚶🏻‍♂️‍➡️': 6, + '🚶🏻‍➡️': 5, + '🚶🏼‍♀️': 5, + '🚶🏼‍♀️‍➡️': 6, + '🚶🏼‍♂️': 5, + '🚶🏼‍♂️‍➡️': 6, + '🚶🏼‍➡️': 5, + '🚶🏽‍♀️': 5, + '🚶🏽‍♀️‍➡️': 6, + '🚶🏽‍♂️': 5, + '🚶🏽‍♂️‍➡️': 6, + '🚶🏽‍➡️': 5, + '🚶🏾‍♀️': 5, + '🚶🏾‍♀️‍➡️': 6, + '🚶🏾‍♂️': 5, + '🚶🏾‍♂️‍➡️': 6, + '🚶🏾‍➡️': 5, + '🚶🏿‍♀️': 5, + '🚶🏿‍♀️‍➡️': 6, + '🚶🏿‍♂️': 5, + '🚶🏿‍♂️‍➡️': 6, + '🚶🏿‍➡️': 5, + '🤦‍♀️': 3, + '🤦‍♂️': 3, + '🤦🏻‍♀️': 5, + '🤦🏻‍♂️': 5, + '🤦🏼‍♀️': 5, + '🤦🏼‍♂️': 5, + '🤦🏽‍♀️': 5, + '🤦🏽‍♂️': 5, + '🤦🏾‍♀️': 5, + '🤦🏾‍♂️': 5, + '🤦🏿‍♀️': 5, + '🤦🏿‍♂️': 5, + '🤵‍♀️': 3, + '🤵‍♂️': 3, + '🤵🏻‍♀️': 5, + '🤵🏻‍♂️': 5, + '🤵🏼‍♀️': 5, + '🤵🏼‍♂️': 5, + '🤵🏽‍♀️': 5, + '🤵🏽‍♂️': 5, + '🤵🏾‍♀️': 5, + '🤵🏾‍♂️': 5, + '🤵🏿‍♀️': 5, + '🤵🏿‍♂️': 5, + '🤷‍♀️': 3, + '🤷‍♂️': 3, + '🤷🏻‍♀️': 5, + '🤷🏻‍♂️': 5, + '🤷🏼‍♀️': 5, + '🤷🏼‍♂️': 5, + '🤷🏽‍♀️': 5, + '🤷🏽‍♂️': 5, + '🤷🏾‍♀️': 5, + '🤷🏾‍♂️': 5, + '🤷🏿‍♀️': 5, + '🤷🏿‍♂️': 5, + '🤸‍♀️': 3, + '🤸‍♂️': 3, + '🤸🏻‍♀️': 5, + '🤸🏻‍♂️': 5, + '🤸🏼‍♀️': 5, + '🤸🏼‍♂️': 5, + '🤸🏽‍♀️': 5, + '🤸🏽‍♂️': 5, + '🤸🏾‍♀️': 5, + '🤸🏾‍♂️': 5, + '🤸🏿‍♀️': 5, + '🤸🏿‍♂️': 5, + '🤹‍♀️': 3, + '🤹‍♂️': 3, + '🤹🏻‍♀️': 5, + '🤹🏻‍♂️': 5, + '🤹🏼‍♀️': 5, + '🤹🏼‍♂️': 5, + '🤹🏽‍♀️': 5, + '🤹🏽‍♂️': 5, + '🤹🏾‍♀️': 5, + '🤹🏾‍♂️': 5, + '🤹🏿‍♀️': 5, + '🤹🏿‍♂️': 5, + '🤼‍♀️': 3, + '🤼‍♂️': 3, + '🤼🏻‍♀️': 5, + '🤼🏻‍♂️': 5, + '🤼🏼‍♀️': 5, + '🤼🏼‍♂️': 5, + '🤼🏽‍♀️': 5, + '🤼🏽‍♂️': 5, + '🤼🏾‍♀️': 5, + '🤼🏾‍♂️': 5, + '🤼🏿‍♀️': 5, + '🤼🏿‍♂️': 5, + '🤽‍♀️': 3, + '🤽‍♂️': 3, + '🤽🏻‍♀️': 5, + '🤽🏻‍♂️': 5, + '🤽🏼‍♀️': 5, + '🤽🏼‍♂️': 5, + '🤽🏽‍♀️': 5, + '🤽🏽‍♂️': 5, + '🤽🏾‍♀️': 5, + '🤽🏾‍♂️': 5, + '🤽🏿‍♀️': 5, + '🤽🏿‍♂️': 5, + '🤾‍♀️': 3, + '🤾‍♂️': 3, + '🤾🏻‍♀️': 5, + '🤾🏻‍♂️': 5, + '🤾🏼‍♀️': 5, + '🤾🏼‍♂️': 5, + '🤾🏽‍♀️': 5, + '🤾🏽‍♂️': 5, + '🤾🏾‍♀️': 5, + '🤾🏾‍♂️': 5, + '🤾🏿‍♀️': 5, + '🤾🏿‍♂️': 5, + '🦸‍♀️': 3, + '🦸‍♂️': 3, + '🦸🏻‍♀️': 5, + '🦸🏻‍♂️': 5, + '🦸🏼‍♀️': 5, + '🦸🏼‍♂️': 5, + '🦸🏽‍♀️': 5, + '🦸🏽‍♂️': 5, + '🦸🏾‍♀️': 5, + '🦸🏾‍♂️': 5, + '🦸🏿‍♀️': 5, + '🦸🏿‍♂️': 5, + '🦹‍♀️': 3, + '🦹‍♂️': 3, + '🦹🏻‍♀️': 5, + '🦹🏻‍♂️': 5, + '🦹🏼‍♀️': 5, + '🦹🏼‍♂️': 5, + '🦹🏽‍♀️': 5, + '🦹🏽‍♂️': 5, + '🦹🏾‍♀️': 5, + '🦹🏾‍♂️': 5, + '🦹🏿‍♀️': 5, + '🦹🏿‍♂️': 5, + '🧍‍♀️': 3, + '🧍‍♂️': 3, + '🧍🏻‍♀️': 5, + '🧍🏻‍♂️': 5, + '🧍🏼‍♀️': 5, + '🧍🏼‍♂️': 5, + '🧍🏽‍♀️': 5, + '🧍🏽‍♂️': 5, + '🧍🏾‍♀️': 5, + '🧍🏾‍♂️': 5, + '🧍🏿‍♀️': 5, + '🧍🏿‍♂️': 5, + '🧎‍♀️': 3, + '🧎‍♀️‍➡️': 4, + '🧎‍♂️': 3, + '🧎‍♂️‍➡️': 4, + '🧎‍➡️': 3, + '🧎🏻‍♀️': 5, + '🧎🏻‍♀️‍➡️': 6, + '🧎🏻‍♂️': 5, + '🧎🏻‍♂️‍➡️': 6, + '🧎🏻‍➡️': 5, + '🧎🏼‍♀️': 5, + '🧎🏼‍♀️‍➡️': 6, + '🧎🏼‍♂️': 5, + '🧎🏼‍♂️‍➡️': 6, + '🧎🏼‍➡️': 5, + '🧎🏽‍♀️': 5, + '🧎🏽‍♀️‍➡️': 6, + '🧎🏽‍♂️': 5, + '🧎🏽‍♂️‍➡️': 6, + '🧎🏽‍➡️': 5, + '🧎🏾‍♀️': 5, + '🧎🏾‍♀️‍➡️': 6, + '🧎🏾‍♂️': 5, + '🧎🏾‍♂️‍➡️': 6, + '🧎🏾‍➡️': 5, + '🧎🏿‍♀️': 5, + '🧎🏿‍♀️‍➡️': 6, + '🧎🏿‍♂️': 5, + '🧎🏿‍♂️‍➡️': 6, + '🧎🏿‍➡️': 5, + '🧏‍♀️': 3, + '🧏‍♂️': 3, + '🧏🏻‍♀️': 5, + '🧏🏻‍♂️': 5, + '🧏🏼‍♀️': 5, + '🧏🏼‍♂️': 5, + '🧏🏽‍♀️': 5, + '🧏🏽‍♂️': 5, + '🧏🏾‍♀️': 5, + '🧏🏾‍♂️': 5, + '🧏🏿‍♀️': 5, + '🧏🏿‍♂️': 5, + '🧑‍⚕️': 3, + '🧑‍⚖️': 3, + '🧑‍✈️': 3, + '🧑‍🌾': 4, + '🧑‍🍳': 4, + '🧑‍🍼': 4, + '🧑‍🎄': 4, + '🧑‍🎓': 4, + '🧑‍🎤': 4, + '🧑‍🎨': 4, + '🧑‍🏫': 4, + '🧑‍🏭': 4, + '🧑‍💻': 4, + '🧑‍💼': 4, + '🧑‍🔧': 4, + '🧑‍🔬': 4, + '🧑‍🚀': 4, + '🧑‍🚒': 4, + '🧑‍🤝‍🧑': 6, + '🧑‍🦯': 4, + '🧑‍🦯‍➡️': 5, + '🧑‍🦰': 4, + '🧑‍🦱': 4, + '🧑‍🦲': 4, + '🧑‍🦳': 4, + '🧑‍🦼': 4, + '🧑‍🦼‍➡️': 5, + '🧑‍🦽': 4, + '🧑‍🦽‍➡️': 5, + '🧑‍🧑‍🧒': 6, + '🧑‍🧑‍🧒‍🧒': 8, + '🧑‍🧒': 4, + '🧑‍🧒‍🧒': 6, + '🧑‍🩰': 4, + '🧑🏻‍⚕️': 5, + '🧑🏻‍⚖️': 5, + '🧑🏻‍✈️': 5, + '🧑🏻‍❤️‍💋‍🧑🏿': 11, + '🧑🏻‍🌾': 6, + '🧑🏻‍🍳': 6, + '🧑🏻‍🍼': 6, + '🧑🏻‍🎄': 6, + '🧑🏻‍🎓': 6, + '🧑🏻‍🎤': 6, + '🧑🏻‍🎨': 6, + '🧑🏻‍🏫': 6, + '🧑🏻‍🏭': 6, + '🧑🏻‍🐰‍🧑🏽': 10, + '🧑🏻‍🐰‍🧑🏾': 10, + '🧑🏻‍🐰‍🧑🏿': 10, + '🧑🏻‍💻': 6, + '🧑🏻‍💼': 6, + '🧑🏻‍🔧': 6, + '🧑🏻‍🔬': 6, + '🧑🏻‍🚀': 6, + '🧑🏻‍🚒': 6, + '🧑🏻‍🤝‍🧑🏻': 10, + '🧑🏻‍🤝‍🧑🏼': 10, + '🧑🏻‍🤝‍🧑🏽': 10, + '🧑🏻‍🤝‍🧑🏾': 10, + '🧑🏻‍🤝‍🧑🏿': 10, + '🧑🏻‍🦯': 6, + '🧑🏻‍🦯‍➡️': 7, + '🧑🏻‍🦰': 6, + '🧑🏻‍🦱': 6, + '🧑🏻‍🦲': 6, + '🧑🏻‍🦳': 6, + '🧑🏻‍🦼': 6, + '🧑🏻‍🦼‍➡️': 7, + '🧑🏻‍🦽': 6, + '🧑🏻‍🦽‍➡️': 7, + '🧑🏻‍🩰': 6, + '🧑🏻‍🫯‍🧑🏼': 9, + '🧑🏻‍🫯‍🧑🏽': 9, + '🧑🏻‍🫯‍🧑🏾': 9, + '🧑🏻‍🫯‍🧑🏿': 9, + '🧑🏼‍⚕️': 5, + '🧑🏼‍⚖️': 5, + '🧑🏼‍✈️': 5, + '🧑🏼‍🌾': 6, + '🧑🏼‍🍳': 6, + '🧑🏼‍🍼': 6, + '🧑🏼‍🎄': 6, + '🧑🏼‍🎓': 6, + '🧑🏼‍🎤': 6, + '🧑🏼‍🎨': 6, + '🧑🏼‍🏫': 6, + '🧑🏼‍🏭': 6, + '🧑🏼‍🐰‍🧑🏿': 10, + '🧑🏼‍💻': 6, + '🧑🏼‍💼': 6, + '🧑🏼‍🔧': 6, + '🧑🏼‍🔬': 6, + '🧑🏼‍🚀': 6, + '🧑🏼‍🚒': 6, + '🧑🏼‍🤝‍🧑🏻': 10, + '🧑🏼‍🤝‍🧑🏼': 10, + '🧑🏼‍🤝‍🧑🏽': 10, + '🧑🏼‍🤝‍🧑🏿': 10, + '🧑🏼‍🦯': 6, + '🧑🏼‍🦯‍➡️': 7, + '🧑🏼‍🦰': 6, + '🧑🏼‍🦱': 6, + '🧑🏼‍🦲': 6, + '🧑🏼‍🦳': 6, + '🧑🏼‍🦼': 6, + '🧑🏼‍🦽': 6, + '🧑🏼‍🩰': 6, + '🧑🏼‍🫯‍🧑🏻': 9, + '🧑🏼‍🫯‍🧑🏽': 9, + '🧑🏼‍🫯‍🧑🏿': 9, + '🧑🏽‍⚕️': 5, + '🧑🏽‍⚖️': 5, + '🧑🏽‍✈️': 5, + '🧑🏽‍❤️‍💋‍🧑🏿': 11, + '🧑🏽‍🌾': 6, + '🧑🏽‍🍳': 6, + '🧑🏽‍🍼': 6, + '🧑🏽‍🎄': 6, + '🧑🏽‍🎓': 6, + '🧑🏽‍🎤': 6, + '🧑🏽‍🎨': 6, + '🧑🏽‍🏫': 6, + '🧑🏽‍🏭': 6, + '🧑🏽‍🐰‍🧑🏻': 10, + '🧑🏽‍🐰‍🧑🏿': 10, + '🧑🏽‍💻': 6, + '🧑🏽‍💼': 6, + '🧑🏽‍🔧': 6, + '🧑🏽‍🔬': 6, + '🧑🏽‍🚀': 6, + '🧑🏽‍🚒': 6, + '🧑🏽‍🤝‍🧑🏻': 10, + '🧑🏽‍🤝‍🧑🏼': 10, + '🧑🏽‍🤝‍🧑🏽': 10, + '🧑🏽‍🤝‍🧑🏾': 10, + '🧑🏽‍🤝‍🧑🏿': 10, + '🧑🏽‍🦯': 6, + '🧑🏽‍🦯‍➡️': 7, + '🧑🏽‍🦰': 6, + '🧑🏽‍🦱': 6, + '🧑🏽‍🦲': 6, + '🧑🏽‍🦳': 6, + '🧑🏽‍🦼': 6, + '🧑🏽‍🦼‍➡️': 7, + '🧑🏽‍🦽': 6, + '🧑🏽‍🦽‍➡️': 7, + '🧑🏽‍🩰': 6, + '🧑🏽‍🫯‍🧑🏻': 9, + '🧑🏽‍🫯‍🧑🏼': 9, + '🧑🏽‍🫯‍🧑🏾': 9, + '🧑🏽‍🫯‍🧑🏿': 9, + '🧑🏾‍⚕️': 5, + '🧑🏾‍⚖️': 5, + '🧑🏾‍✈️': 5, + '🧑🏾‍🌾': 6, + '🧑🏾‍🍳': 6, + '🧑🏾‍🍼': 6, + '🧑🏾‍🎄': 6, + '🧑🏾‍🎓': 6, + '🧑🏾‍🎤': 6, + '🧑🏾‍🎨': 6, + '🧑🏾‍🏫': 6, + '🧑🏾‍🏭': 6, + '🧑🏾‍🐰‍🧑🏻': 10, + '🧑🏾‍🐰‍🧑🏿': 10, + '🧑🏾‍💻': 6, + '🧑🏾‍💼': 6, + '🧑🏾‍🔧': 6, + '🧑🏾‍🔬': 6, + '🧑🏾‍🚀': 6, + '🧑🏾‍🚒': 6, + '🧑🏾‍🤝‍🧑🏻': 10, + '🧑🏾‍🤝‍🧑🏽': 10, + '🧑🏾‍🤝‍🧑🏾': 10, + '🧑🏾‍🤝‍🧑🏿': 10, + '🧑🏾‍🦯': 6, + '🧑🏾‍🦯‍➡️': 7, + '🧑🏾‍🦰': 6, + '🧑🏾‍🦱': 6, + '🧑🏾‍🦲': 6, + '🧑🏾‍🦳': 6, + '🧑🏾‍🦼': 6, + '🧑🏾‍🦽': 6, + '🧑🏾‍🩰': 6, + '🧑🏾‍🫯‍🧑🏻': 9, + '🧑🏾‍🫯‍🧑🏽': 9, + '🧑🏾‍🫯‍🧑🏿': 9, + '🧑🏿‍⚕️': 5, + '🧑🏿‍⚖️': 5, + '🧑🏿‍✈️': 5, + '🧑🏿‍❤️‍💋‍🧑🏻': 11, + '🧑🏿‍❤️‍💋‍🧑🏽': 11, + '🧑🏿‍🌾': 6, + '🧑🏿‍🍳': 6, + '🧑🏿‍🍼': 6, + '🧑🏿‍🎄': 6, + '🧑🏿‍🎓': 6, + '🧑🏿‍🎤': 6, + '🧑🏿‍🎨': 6, + '🧑🏿‍🏫': 6, + '🧑🏿‍🏭': 6, + '🧑🏿‍🐰‍🧑🏻': 10, + '🧑🏿‍🐰‍🧑🏼': 10, + '🧑🏿‍🐰‍🧑🏽': 10, + '🧑🏿‍🐰‍🧑🏾': 10, + '🧑🏿‍💻': 6, + '🧑🏿‍💼': 6, + '🧑🏿‍🔧': 6, + '🧑🏿‍🔬': 6, + '🧑🏿‍🚀': 6, + '🧑🏿‍🚒': 6, + '🧑🏿‍🤝‍🧑🏻': 10, + '🧑🏿‍🤝‍🧑🏼': 10, + '🧑🏿‍🤝‍🧑🏽': 10, + '🧑🏿‍🤝‍🧑🏾': 10, + '🧑🏿‍🤝‍🧑🏿': 10, + '🧑🏿‍🦯': 6, + '🧑🏿‍🦯‍➡️': 7, + '🧑🏿‍🦰': 6, + '🧑🏿‍🦱': 6, + '🧑🏿‍🦲': 6, + '🧑🏿‍🦳': 6, + '🧑🏿‍🦼': 6, + '🧑🏿‍🦼‍➡️': 7, + '🧑🏿‍🦽': 6, + '🧑🏿‍🦽‍➡️': 7, + '🧑🏿‍🩰': 6, + '🧑🏿‍🫯‍🧑🏻': 9, + '🧑🏿‍🫯‍🧑🏼': 9, + '🧑🏿‍🫯‍🧑🏽': 9, + '🧑🏿‍🫯‍🧑🏾': 9, + '🧔‍♀️': 3, + '🧔‍♂️': 3, + '🧔🏻‍♀️': 5, + '🧔🏻‍♂️': 5, + '🧔🏼‍♀️': 5, + '🧔🏼‍♂️': 5, + '🧔🏽‍♀️': 5, + '🧔🏽‍♂️': 5, + '🧔🏾‍♀️': 5, + '🧔🏾‍♂️': 5, + '🧔🏿‍♀️': 5, + '🧔🏿‍♂️': 5, + '🧖‍♀️': 3, + '🧖‍♂️': 3, + '🧖🏻‍♀️': 5, + '🧖🏻‍♂️': 5, + '🧖🏼‍♀️': 5, + '🧖🏼‍♂️': 5, + '🧖🏽‍♀️': 5, + '🧖🏽‍♂️': 5, + '🧖🏾‍♀️': 5, + '🧖🏾‍♂️': 5, + '🧖🏿‍♀️': 5, + '🧖🏿‍♂️': 5, + '🧗‍♀️': 3, + '🧗‍♂️': 3, + '🧗🏻‍♀️': 5, + '🧗🏻‍♂️': 5, + '🧗🏼‍♀️': 5, + '🧗🏼‍♂️': 5, + '🧗🏽‍♀️': 5, + '🧗🏽‍♂️': 5, + '🧗🏾‍♀️': 5, + '🧗🏾‍♂️': 5, + '🧗🏿‍♀️': 5, + '🧗🏿‍♂️': 5, + '🧘‍♀️': 3, + '🧘‍♂️': 3, + '🧘🏻‍♀️': 5, + '🧘🏻‍♂️': 5, + '🧘🏼‍♀️': 5, + '🧘🏼‍♂️': 5, + '🧘🏽‍♀️': 5, + '🧘🏽‍♂️': 5, + '🧘🏾‍♀️': 5, + '🧘🏾‍♂️': 5, + '🧘🏿‍♀️': 5, + '🧘🏿‍♂️': 5, + '🧙‍♀️': 3, + '🧙‍♂️': 3, + '🧙🏻‍♀️': 5, + '🧙🏻‍♂️': 5, + '🧙🏼‍♀️': 5, + '🧙🏼‍♂️': 5, + '🧙🏽‍♀️': 5, + '🧙🏽‍♂️': 5, + '🧙🏾‍♀️': 5, + '🧙🏾‍♂️': 5, + '🧙🏿‍♀️': 5, + '🧙🏿‍♂️': 5, + '🧚‍♀️': 3, + '🧚‍♂️': 3, + '🧚🏻‍♀️': 5, + '🧚🏻‍♂️': 5, + '🧚🏼‍♀️': 5, + '🧚🏼‍♂️': 5, + '🧚🏽‍♀️': 5, + '🧚🏽‍♂️': 5, + '🧚🏾‍♀️': 5, + '🧚🏾‍♂️': 5, + '🧚🏿‍♀️': 5, + '🧚🏿‍♂️': 5, + '🧛‍♀️': 3, + '🧛‍♂️': 3, + '🧛🏻‍♀️': 5, + '🧛🏻‍♂️': 5, + '🧛🏼‍♀️': 5, + '🧛🏼‍♂️': 5, + '🧛🏽‍♀️': 5, + '🧛🏽‍♂️': 5, + '🧛🏾‍♀️': 5, + '🧛🏾‍♂️': 5, + '🧛🏿‍♀️': 5, + '🧛🏿‍♂️': 5, + '🧜‍♀️': 3, + '🧜‍♂️': 3, + '🧜🏻‍♀️': 5, + '🧜🏻‍♂️': 5, + '🧜🏼‍♀️': 5, + '🧜🏼‍♂️': 5, + '🧜🏽‍♀️': 5, + '🧜🏽‍♂️': 5, + '🧜🏾‍♀️': 5, + '🧜🏾‍♂️': 5, + '🧜🏿‍♀️': 5, + '🧜🏿‍♂️': 5, + '🧝‍♀️': 3, + '🧝‍♂️': 3, + '🧝🏻‍♀️': 5, + '🧝🏻‍♂️': 5, + '🧝🏼‍♀️': 5, + '🧝🏼‍♂️': 5, + '🧝🏽‍♀️': 5, + '🧝🏽‍♂️': 5, + '🧝🏾‍♀️': 5, + '🧝🏾‍♂️': 5, + '🧝🏿‍♀️': 5, + '🧝🏿‍♂️': 5, + '🧞‍♀️': 3, + '🧞‍♂️': 3, + '🧟‍♀️': 3, + '🧟‍♂️': 3, + '🫱🏻‍🫲🏼': 6, + '🫱🏻‍🫲🏽': 6, + '🫱🏻‍🫲🏾': 6, + '🫱🏻‍🫲🏿': 6, + '🫱🏼‍🫲🏻': 6, + '🫱🏼‍🫲🏽': 6, + '🫱🏼‍🫲🏾': 6, + '🫱🏼‍🫲🏿': 6, + '🫱🏽‍🫲🏻': 6, + '🫱🏽‍🫲🏼': 6, + '🫱🏽‍🫲🏾': 6, + '🫱🏽‍🫲🏿': 6, + '🫱🏾‍🫲🏻': 6, + '🫱🏾‍🫲🏼': 6, + '🫱🏾‍🫲🏽': 6, + '🫱🏾‍🫲🏿': 6, + '🫱🏿‍🫲🏻': 6, + '🫱🏿‍🫲🏼': 6, + '🫱🏿‍🫲🏽': 6, + '🫱🏿‍🫲🏾': 6, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_529fbb4a.py b/libs/wcwidth/table_grapheme_overrides/_known_529fbb4a.py new file mode 100644 index 000000000..d629b104c --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_529fbb4a.py @@ -0,0 +1,1799 @@ +""" +Grapheme overrides for _known_529fbb4a. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: foot +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'का': 1, + 'कां': 1, + 'कि': 1, + 'किं': 1, + 'की': 1, + 'केा': 1, + 'को': 1, + 'कौ': 1, + 'खा': 1, + 'खि': 1, + 'खी': 1, + 'खो': 1, + 'ग़ा': 1, + 'गा': 1, + 'गाँ': 1, + 'गां': 1, + 'गि': 1, + 'गी': 1, + 'गो': 1, + 'गों': 1, + 'गौ': 1, + 'घा': 1, + 'घि': 1, + 'घी': 1, + 'घेि': 1, + 'घो': 1, + 'चा': 1, + 'चां': 1, + 'चि': 1, + 'ची': 1, + 'चो': 1, + 'चौ': 1, + 'छि': 1, + 'छुी': 1, + 'छो': 1, + 'जा': 1, + 'जि': 1, + 'जिं': 1, + 'जी': 1, + 'जो': 1, + 'झा': 1, + 'टा': 1, + 'टां': 1, + 'टि': 1, + 'टी': 1, + 'टों': 1, + 'ठा': 1, + 'ठी': 1, + 'ठैः': 1, + 'ड़ा': 1, + 'डा': 1, + 'डि': 1, + 'डी': 1, + 'डो': 1, + 'ढ़ा': 1, + 'ढ़ि': 1, + 'ढ़ी': 1, + 'ढ़ो': 1, + 'ढा': 1, + 'ढाँ': 1, + 'ढि': 1, + 'णा': 1, + 'णां': 1, + 'णि': 1, + 'णी': 1, + 'णो': 1, + 'णों': 1, + 'तः': 1, + 'ता': 1, + 'तां': 1, + 'ताः': 1, + 'ति': 1, + 'तिं': 1, + 'ती': 1, + 'तो': 1, + 'तों': 1, + 'तौ': 1, + 'था': 1, + 'थि': 1, + 'दः': 1, + 'दा': 1, + 'दां': 1, + 'दि': 1, + 'दी': 1, + 'दो': 1, + 'दौ': 1, + 'धा': 1, + 'धां': 1, + 'धाः': 1, + 'धि': 1, + 'धी': 1, + 'धेः': 1, + 'धो': 1, + 'धों': 1, + 'नः': 1, + 'ना': 1, + 'नां': 1, + 'नाः': 1, + 'ना‌': 1, + 'नि': 1, + 'निँ': 1, + 'निः': 1, + 'नी': 1, + 'नैः': 1, + 'नो': 1, + 'नों': 1, + 'नौ': 1, + 'पा': 1, + 'पाँ': 1, + 'पां': 1, + 'पि': 1, + 'पी': 1, + 'पो': 1, + 'पों': 1, + 'फा': 1, + 'फाँ': 1, + 'फौ': 1, + 'बा': 1, + 'बाँ': 1, + 'बि': 1, + 'बी': 1, + 'बो': 1, + 'भा': 1, + 'भि': 1, + 'भिः': 1, + 'भी': 1, + 'भो': 1, + 'भौ': 1, + 'मा': 1, + 'मां': 1, + 'मि': 1, + 'मी': 1, + 'मो': 1, + 'मों': 1, + 'मौ': 1, + 'यः': 1, + 'या': 1, + 'यां': 1, + 'याः': 1, + 'यि': 1, + 'यी': 1, + 'यैः': 1, + 'यो': 1, + 'यों': 1, + 'रः': 1, + 'रा': 1, + 'रां': 1, + 'राः': 1, + 'रि': 1, + 'रिं': 1, + 'री': 1, + 'रो': 1, + 'रों': 1, + 'रौ': 1, + 'ला': 1, + 'लां': 1, + 'लाः': 1, + 'लि': 1, + 'लिं': 1, + 'ली': 1, + 'लीं': 1, + 'लो': 1, + 'लों': 1, + 'लौ': 1, + 'ळा': 1, + 'ळी': 1, + 'वः': 1, + 'वा': 1, + 'वां': 1, + 'वाः': 1, + 'वि': 1, + 'वी': 1, + 'वो': 1, + 'वों': 1, + 'शः': 1, + 'शा': 1, + 'शां': 1, + 'शाः': 1, + 'शि': 1, + 'शी': 1, + 'शो': 1, + 'शों': 1, + 'षा': 1, + 'षां': 1, + 'षाः': 1, + 'षि': 1, + 'षी': 1, + 'षों': 1, + 'सः': 1, + 'सा': 1, + 'साँ': 1, + 'सां': 1, + 'सि': 1, + 'सी': 1, + 'सो': 1, + 'सौ': 1, + 'हा': 1, + 'हाँ': 1, + 'हां': 1, + 'हि': 1, + 'हिं': 1, + 'ही': 1, + 'हीं': 1, + 'हैः': 1, + 'हो': 1, + 'हों': 1, + 'हौ': 1, + 'क़ा': 1, + 'क़ि': 1, + 'ज़ा': 1, + 'ड़ा': 1, + 'ढ़ा': 1, + 'फ़ा': 1, + 'অং': 1, + 'কা': 1, + 'কা‌': 1, + 'কি': 1, + 'কিং': 1, + 'কে': 1, + 'কে‌': 1, + 'কো': 1, + 'খা': 1, + 'খি': 1, + 'খে': 1, + 'গি': 1, + 'গে': 1, + 'গো': 1, + 'ঘা': 1, + 'ঘে': 1, + 'ঘো': 1, + 'ঘো': 1, + 'চা': 1, + 'চি': 1, + 'চে': 1, + 'চো': 1, + 'চৌ': 1, + 'ছা': 1, + 'ছি': 1, + 'ছে': 1, + 'জা': 1, + 'জি': 1, + 'জী': 1, + 'জে': 1, + 'জে‌': 1, + 'ঝো': 1, + 'টা': 1, + 'টি': 1, + 'ঠি': 1, + 'ড়া': 1, + 'ণা': 1, + 'ণি': 1, + 'ণে': 1, + 'তঃ': 1, + 'তা': 1, + 'তাঁ': 1, + 'তা‌': 1, + 'তি': 1, + 'তি‌': 1, + 'তী': 1, + 'তে': 1, + 'তে‌': 1, + 'থা': 1, + 'থি': 1, + 'থে': 1, + 'দা': 1, + 'দি': 1, + 'দে': 1, + 'দে‌': 1, + 'দৈ': 1, + 'দো': 1, + 'ধা': 1, + 'ধি': 1, + 'ধী': 1, + 'ধে': 1, + 'না': 1, + 'না‌': 1, + 'নি': 1, + 'নী': 1, + 'নে': 1, + 'নৈ': 1, + 'নো': 1, + 'পা': 1, + 'পা‌': 1, + 'পি': 1, + 'পী': 1, + 'পে': 1, + 'পো': 1, + 'ফে': 1, + 'ফৌ': 1, + 'বং': 1, + 'বা': 1, + 'বা‌': 1, + 'বি': 1, + 'বী': 1, + 'বে': 1, + 'বৈ': 1, + 'বো': 1, + 'ভা': 1, + 'ভি': 1, + 'ভে': 1, + 'ভো': 1, + 'ভৌ': 1, + 'মা': 1, + 'মা‌': 1, + 'মি': 1, + 'মে': 1, + 'মৌ': 1, + 'য়া': 1, + 'য়ি': 1, + 'য়ে': 1, + 'য়ো': 1, + 'যা': 1, + 'যে': 1, + 'যো': 1, + 'রা': 1, + 'রাং': 1, + 'রি': 1, + 'রী': 1, + 'রে': 1, + 'রো': 1, + 'লা': 1, + 'লি': 1, + 'লী': 1, + 'লে': 1, + 'লে‌': 1, + 'শং': 1, + 'শা': 1, + 'শি': 1, + 'শী': 1, + 'শৃং': 1, + 'শে': 1, + 'শৈ': 1, + 'ষা': 1, + 'ষি': 1, + 'ষী': 1, + 'ষে': 1, + 'সং': 1, + 'সা': 1, + 'সাং': 1, + 'সি': 1, + 'সী': 1, + 'সে': 1, + 'সে‌': 1, + 'হা': 1, + 'হি': 1, + 'হে': 1, + 'হো': 1, + 'য়ে': 1, + 'ਕਾ': 1, + 'ਕਾਂ': 1, + 'ਕਿ': 1, + 'ਕਿ੍': 1, + 'ਕਿੱ': 1, + 'ਕੀ': 1, + 'ਖਾ': 1, + 'ਖਾਂ': 1, + 'ਖਿ': 1, + 'ਖਿ਼': 1, + 'ਖੀ': 1, + 'ਗਾ': 1, + 'ਗਿ': 1, + 'ਗਿ੍': 1, + 'ਗੀ': 1, + 'ਚਾ': 1, + 'ਚਿ': 1, + 'ਚਿੱ': 1, + 'ਚੀ': 1, + 'ਜਾ': 1, + 'ਜਾਂ': 1, + 'ਜਿ': 1, + 'ਜਿ਼': 1, + 'ਜਿ਼ੰ': 1, + 'ਜੀ': 1, + 'ਝਿ': 1, + 'ਝੀ': 1, + 'ਟਾ': 1, + 'ਟਿ': 1, + 'ਟਿ੍': 1, + 'ਟੀ': 1, + 'ਠੀ': 1, + 'ਡਾ': 1, + 'ਣਾ': 1, + 'ਣਾਂ': 1, + 'ਣੀ': 1, + 'ਤਾ': 1, + 'ਤਾਂ': 1, + 'ਤਿ': 1, + 'ਤੀ': 1, + 'ਦਾ': 1, + 'ਦਾਂ': 1, + 'ਦਿ': 1, + 'ਦਿੱ': 1, + 'ਦੀ': 1, + 'ਧਾ': 1, + 'ਧਾਂ': 1, + 'ਧਿ': 1, + 'ਧੀ': 1, + 'ਨਾ': 1, + 'ਨਾਂ': 1, + 'ਨਿ': 1, + 'ਨੀ': 1, + 'ਨੀਂ': 1, + 'ਪਾ': 1, + 'ਪਿ': 1, + 'ਪੀ': 1, + 'ਫਿ': 1, + 'ਬਾ': 1, + 'ਬਿ': 1, + 'ਬੀ': 1, + 'ਭਾ': 1, + 'ਭਿ': 1, + 'ਮਾ': 1, + 'ਮਾਂ': 1, + 'ਮਿ': 1, + 'ਮੀ': 1, + 'ਯਾ': 1, + 'ਰਾ': 1, + 'ਰਾਂ': 1, + 'ਰਿ': 1, + 'ਰੀ': 1, + 'ਲਾ': 1, + 'ਲਾਂ': 1, + 'ਲਿ': 1, + 'ਲਿੰ': 1, + 'ਲੀ': 1, + 'ਵਾ': 1, + 'ਵਾਂ': 1, + 'ਵਿ': 1, + 'ਵਿੱ': 1, + 'ਵੀ': 1, + 'ਸ਼ਾ': 1, + 'ਸ਼ਾਂ': 1, + 'ਸ਼ਿ': 1, + 'ਸ਼ੀ': 1, + 'ਸਾ': 1, + 'ਸਾਂ': 1, + 'ਸਿ': 1, + 'ਸਿੱ': 1, + 'ਸੀ': 1, + 'ਹਾ': 1, + 'ਹਾਂ': 1, + 'ਹਿ': 1, + 'ਹਿੰ': 1, + 'ਹਿੱ': 1, + 'ਹੀ': 1, + 'ਹੀਂ': 1, + 'ਜ਼ਾ': 1, + 'ਜ਼ੀ': 1, + 'ੜਾ': 1, + 'ੜੀਂ': 1, + 'ਫ਼ਾ': 1, + 'કઃ': 1, + 'કા': 1, + 'કાં': 1, + 'કિ': 1, + 'કી': 1, + 'કો': 1, + 'ખા': 1, + 'ખી': 1, + 'ખો': 1, + 'ગા': 1, + 'ગિ': 1, + 'ગી': 1, + 'ગો': 1, + 'ઘા': 1, + 'ઘો': 1, + 'ચા': 1, + 'ચિ': 1, + 'ચી': 1, + 'છા': 1, + 'છો': 1, + 'જા': 1, + 'જાં': 1, + 'જિ': 1, + 'જી': 1, + 'જો': 1, + 'ટઃ': 1, + 'ડા': 1, + 'ડાં': 1, + 'ડી': 1, + 'ઢા': 1, + 'ઢિ': 1, + 'ણા': 1, + 'ણી': 1, + 'તઃ': 1, + 'તા': 1, + 'તાં': 1, + 'તિ': 1, + 'તી': 1, + 'તો': 1, + 'થા': 1, + 'થી': 1, + 'દા': 1, + 'દી': 1, + 'દો': 1, + 'ધા': 1, + 'ધાં': 1, + 'ધિ': 1, + 'ધી': 1, + 'ધો': 1, + 'નઃ': 1, + 'ના': 1, + 'નાં': 1, + 'નિ': 1, + 'ની': 1, + 'નો': 1, + 'પા': 1, + 'પિ': 1, + 'પી': 1, + 'પો': 1, + 'ફા': 1, + 'ફો': 1, + 'બા': 1, + 'બાં': 1, + 'બી': 1, + 'ભા': 1, + 'ભાં': 1, + 'ભિ': 1, + 'ભો': 1, + 'ભૌ': 1, + 'મઃ': 1, + 'મા': 1, + 'માં': 1, + 'મિ': 1, + 'મી': 1, + 'મો': 1, + 'યા': 1, + 'યિ': 1, + 'યી': 1, + 'યો': 1, + 'રા': 1, + 'રિ': 1, + 'રી': 1, + 'રો': 1, + 'લા': 1, + 'લાં': 1, + 'લિ': 1, + 'લિં': 1, + 'લી': 1, + 'લો': 1, + 'ળા': 1, + 'ળી': 1, + 'વા': 1, + 'વાં': 1, + 'વિ': 1, + 'વી': 1, + 'વો': 1, + 'શા': 1, + 'શાં': 1, + 'શિ': 1, + 'શી': 1, + 'શો': 1, + 'ષા': 1, + 'ષી': 1, + 'ષો': 1, + 'સા': 1, + 'સાં': 1, + 'સિ': 1, + 'સી': 1, + 'સો': 1, + 'હા': 1, + 'હિ': 1, + 'હી': 1, + 'હો': 1, + 'கா': 1, + 'கி': 1, + 'கு': 1, + 'கூ': 1, + 'கெ': 1, + 'கே': 1, + 'கை': 1, + 'கொ': 1, + 'கோ': 1, + 'சா': 1, + 'சி': 1, + 'சு': 1, + 'சூ': 1, + 'செ': 1, + 'சே': 1, + 'சை': 1, + 'சொ': 1, + 'டா': 1, + 'டி': 1, + 'டு': 1, + 'டெ': 1, + 'டை': 1, + 'ணா': 1, + 'ணி': 1, + 'ணு': 1, + 'ணை': 1, + 'தா': 1, + 'தி': 1, + 'து': 1, + 'தூ': 1, + 'தெ': 1, + 'தே': 1, + 'தை': 1, + 'தொ': 1, + 'தோ': 1, + 'நா': 1, + 'நி': 1, + 'நு': 1, + 'நெ': 1, + 'நே': 1, + 'நோ': 1, + 'னா': 1, + 'னி': 1, + 'னு': 1, + 'னே': 1, + 'னை': 1, + 'னொ': 1, + 'பா': 1, + 'பி': 1, + 'பு': 1, + 'பெ': 1, + 'பே': 1, + 'பை': 1, + 'பொ': 1, + 'போ': 1, + 'மா': 1, + 'மி': 1, + 'மு': 1, + 'மூ': 1, + 'மெ': 1, + 'மே': 1, + 'மை': 1, + 'மொ': 1, + 'மோ': 1, + 'யா': 1, + 'யி': 1, + 'யு': 1, + 'யே': 1, + 'யை': 1, + 'யோ': 1, + 'ரா': 1, + 'ரி': 1, + 'ரு': 1, + 'ரெ': 1, + 'ரை': 1, + 'றா': 1, + 'றி': 1, + 'று': 1, + 'றே': 1, + 'றை': 1, + 'றொ': 1, + 'றோ': 1, + 'லா': 1, + 'லி': 1, + 'லு': 1, + 'லெ': 1, + 'லே': 1, + 'லை': 1, + 'லோ': 1, + 'ளா': 1, + 'ளி': 1, + 'ளு': 1, + 'ளெ': 1, + 'ளை': 1, + 'ழி': 1, + 'ழு': 1, + 'ழை': 1, + 'வா': 1, + 'வி': 1, + 'வு': 1, + 'வூ': 1, + 'வெ': 1, + 'வே': 1, + 'வை': 1, + 'வொ': 1, + 'வோ': 1, + 'అం': 1, + 'ఆం': 1, + 'ఒం': 1, + 'కం': 1, + 'కాం': 1, + 'కు': 1, + 'కుం': 1, + 'కూ': 1, + 'కృ': 1, + 'కెం': 1, + 'గిం': 1, + 'గు': 1, + 'గూ': 1, + 'గృ': 1, + 'చు': 1, + 'చెం': 1, + 'టం': 1, + 'టిం': 1, + 'టు': 1, + 'టుం': 1, + 'డు': 1, + 'ణిం': 1, + 'తం': 1, + 'తః': 1, + 'తు': 1, + 'తృ': 1, + 'దం': 1, + 'దిం': 1, + 'దు': 1, + 'దుం': 1, + 'దృ': 1, + 'ధిం': 1, + 'ధు': 1, + 'నం': 1, + 'నః': 1, + 'నాం': 1, + 'నిం': 1, + 'ను': 1, + 'నుం': 1, + 'నొం': 1, + 'పం': 1, + 'పిం': 1, + 'పు': 1, + 'పూ': 1, + 'పెం': 1, + 'పొం': 1, + 'బం': 1, + 'భం': 1, + 'భు': 1, + 'మిం': 1, + 'ము': 1, + 'మూ': 1, + 'యం': 1, + 'యః': 1, + 'యిం': 1, + 'యు': 1, + 'యుం': 1, + 'రం': 1, + 'రిం': 1, + 'రు': 1, + 'రూ': 1, + 'ఱు': 1, + 'లం': 1, + 'లిం': 1, + 'లు': 1, + 'విం': 1, + 'వు': 1, + 'వృ': 1, + 'శాం': 1, + 'శిం': 1, + 'శు': 1, + 'షిం': 1, + 'షు': 1, + 'సం': 1, + 'సాం': 1, + 'సు': 1, + 'సూ': 1, + 'హిం': 1, + 'ಅಂ': 1, + 'ಇಂ': 1, + 'ಉಂ': 1, + 'ಎಂ': 1, + 'ಒಂ': 1, + 'ಕಾ': 1, + 'ಕಾಂ': 1, + 'ಕಿಂ': 1, + 'ಕೀ': 1, + 'ಕು': 1, + 'ಕುಂ': 1, + 'ಕೂ': 1, + 'ಕೃ': 1, + 'ಕೆಂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಕೊಂ': 1, + 'ಖಾಂ': 1, + 'ಗಾ': 1, + 'ಗಾಂ': 1, + 'ಗೀ': 1, + 'ಗು': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೊಂ': 1, + 'ಗೋ': 1, + 'ಚಾ': 1, + 'ಚೀ': 1, + 'ಚು': 1, + 'ಛಾ': 1, + 'ಜಾ': 1, + 'ಜೀ': 1, + 'ಟು': 1, + 'ಟುಂ': 1, + 'ಟೇ': 1, + 'ಠಾ': 1, + 'ಡಾ': 1, + 'ಡು': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣಂ': 1, + 'ಣಾ': 1, + 'ಣು': 1, + 'ಣೈ': 1, + 'ತಂ': 1, + 'ತಃ': 1, + 'ತಾ': 1, + 'ತೀ': 1, + 'ತು': 1, + 'ತೃ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ಥಾ': 1, + 'ದಂ': 1, + 'ದಾ': 1, + 'ದಿಂ': 1, + 'ದು': 1, + 'ದೂ': 1, + 'ದೃ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧಾ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಃ': 1, + 'ನಾ': 1, + 'ನಾಂ': 1, + 'ನಿಂ': 1, + 'ನು': 1, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪಂ': 1, + 'ಪಾ': 1, + 'ಪು': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬಂ': 1, + 'ಬಾ': 1, + 'ಬು': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭಾ': 1, + 'ಭೀ': 1, + 'ಭು': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮಾ': 1, + 'ಮೀ': 1, + 'ಮು': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯಂ': 1, + 'ಯಾ': 1, + 'ಯಿಂ': 1, + 'ಯು': 1, + 'ಯುಂ': 1, + 'ಯೂ': 1, + 'ಯೆಂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಂ': 1, + 'ರಾ': 1, + 'ರಾಂ': 1, + 'ರಿಂ': 1, + 'ರೀ': 1, + 'ರು': 1, + 'ರೂ': 1, + 'ರೆಂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲಂ': 1, + 'ಲಾ': 1, + 'ಲೀ': 1, + 'ಲು': 1, + 'ಲೂ': 1, + 'ಲೆಂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳಾ': 1, + 'ಳಿಂ': 1, + 'ಳು': 1, + 'ಳೂ': 1, + 'ವಾ': 1, + 'ವೀ': 1, + 'ವು': 1, + 'ವುಂ': 1, + 'ವೂ': 1, + 'ವೃ': 1, + 'ವೆಂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾ': 1, + 'ಶಾಂ': 1, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷಾ': 1, + 'ಷೇ': 1, + 'ಸಂ': 1, + 'ಸಾ': 1, + 'ಸಾಂ': 1, + 'ಸು': 1, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹಾ': 1, + 'ಹು': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೊಂ': 1, + 'ಹೋ': 1, + 'അം': 1, + 'കാ': 1, + 'കി': 1, + 'കെ': 1, + 'കേ': 1, + 'കൈ': 1, + 'കൊ': 1, + 'കോ': 1, + 'ഗം': 1, + 'ഗി': 1, + 'ഗീ': 1, + 'ഘി': 1, + 'ചാ': 1, + 'ചി': 1, + 'ചെ': 1, + 'ചേ': 1, + 'ചോ': 1, + 'ജാ': 1, + 'ജി': 1, + 'ജീ': 1, + 'ടാ': 1, + 'ടി': 1, + 'ടും': 1, + 'ടെ': 1, + 'ടേ': 1, + 'ഠി': 1, + 'ണം': 1, + 'ണാ': 1, + 'ണി': 1, + 'ണെ': 1, + 'തം': 1, + 'താ': 1, + 'തി': 1, + 'തീ': 1, + 'തും': 1, + 'തെ': 1, + 'തേ': 1, + 'തൊ': 1, + 'തോ': 1, + 'ദം': 1, + 'ദാ': 1, + 'ദി': 1, + 'ദേ': 1, + 'ധാ': 1, + 'ധി': 1, + 'നം': 1, + 'നാ': 1, + 'നി': 1, + 'നീ': 1, + 'നും': 1, + 'നെ': 1, + 'നേ': 1, + 'പാ': 1, + 'പി': 1, + 'പീ': 1, + 'പെ': 1, + 'പൈ': 1, + 'പൊ': 1, + 'പോ': 1, + 'പൌ': 1, + 'ബം': 1, + 'ബാ': 1, + 'ബോ': 1, + 'ഭാ': 1, + 'ഭി': 1, + 'ഭേ': 1, + 'ഭൌ': 1, + 'മം': 1, + 'മാ': 1, + 'മി': 1, + 'മെ': 1, + 'മേ': 1, + 'മൊ': 1, + 'മോ': 1, + 'മൌ': 1, + 'യം': 1, + 'യാ': 1, + 'യി': 1, + 'യും': 1, + 'യെ': 1, + 'യേ': 1, + 'യോ': 1, + 'രം': 1, + 'രാ': 1, + 'രി': 1, + 'രീ': 1, + 'രും': 1, + 'രെ': 1, + 'രേ': 1, + 'രോ': 1, + 'റം': 1, + 'റി': 1, + 'റെ': 1, + 'ലം': 1, + 'ലാ': 1, + 'ലി': 1, + 'ലും': 1, + 'ലെ': 1, + 'ലേ': 1, + 'ലോ': 1, + 'ളം': 1, + 'ളാ': 1, + 'ളി': 1, + 'ളും': 1, + 'ളെ': 1, + 'ളേ': 1, + 'ളോ': 1, + 'ഴി': 1, + 'വാ': 1, + 'വി': 1, + 'വും': 1, + 'വെ': 1, + 'വേ': 1, + 'വൈ': 1, + 'വോ': 1, + 'ശാ': 1, + 'ശി': 1, + 'ശീ': 1, + 'ശേ': 1, + 'ഷം': 1, + 'ഷി': 1, + 'സം': 1, + 'സാ': 1, + 'സാം': 1, + 'സി': 1, + 'സൌ': 1, + 'ഹം': 1, + 'ഹാ': 1, + 'ഹി': 1, + 'ഹിം': 1, + 'කා': 1, + 'කැ': 1, + 'කෘ': 1, + 'කෙ': 1, + 'කො': 1, + 'කෝ': 1, + 'කෲ': 1, + 'ගා': 1, + 'ගැ': 1, + 'ගෙ': 1, + 'ගේ': 1, + 'චා': 1, + 'චැ': 1, + 'චෝ': 1, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ටේ': 1, + 'ඨා': 1, + 'ඩං': 1, + 'ඩා': 1, + 'තා': 1, + 'තැ': 1, + 'තෘ': 1, + 'තෙ': 1, + 'තේ': 1, + 'තො': 1, + 'තෝ': 1, + 'ථා': 1, + 'දා': 1, + 'දැ': 1, + 'දිං': 1, + 'දෙ': 1, + 'දේ': 1, + 'දෝ': 1, + 'ධා': 1, + 'නා': 1, + 'නැ': 1, + 'නෑ': 1, + 'නෙ': 1, + 'නේ': 1, + 'නො': 1, + 'ඳා': 1, + 'පා': 1, + 'පැ': 1, + 'පෑ': 1, + 'පෙ': 1, + 'පේ': 1, + 'පො': 1, + 'පෞ': 1, + 'බා': 1, + 'බැ': 1, + 'බෙ': 1, + 'බේ': 1, + 'බෝ': 1, + 'භා': 1, + 'භෞ': 1, + 'මා': 1, + 'මැ': 1, + 'මෙ': 1, + 'මේ': 1, + 'යා': 1, + 'යෙ': 1, + 'යේ': 1, + 'යො': 1, + 'යෝ': 1, + 'රා': 1, + 'රැ': 1, + 'රෙ': 1, + 'රේ': 1, + 'රෝ': 1, + 'ලං': 1, + 'ලා': 1, + 'ලැ': 1, + 'ලෙ': 1, + 'ලේ': 1, + 'ලෝ': 1, + 'වං': 1, + 'වා': 1, + 'වැ': 1, + 'වෘ': 1, + 'වෙ': 1, + 'වේ': 1, + 'වෝ': 1, + 'ශේ': 1, + 'ෂා': 1, + 'සං': 1, + 'සා': 1, + 'සැ': 1, + 'සෑ': 1, + 'සෙ': 1, + 'සේ': 1, + 'සෞ': 1, + 'හා': 1, + 'හැ': 1, + 'හිං': 1, + 'හෘ': 1, + 'හෙ': 1, + 'හො': 1, + 'හෝ': 1, + 'ළැ': 1, + 'ကေ': 1, + 'ကေု': 1, + 'က်ျ': 1, + 'ကျ': 1, + 'ကျို': 1, + 'ကျူ': 1, + 'ကျေ': 1, + 'ကျွ': 1, + 'ကြ': 1, + 'ကြို': 1, + 'ကြီ': 1, + 'ကြု': 1, + 'ကြေ': 1, + 'ကြံ': 1, + 'ကြွေ': 1, + 'ကၠေ': 1, + 'ကၠေံ': 1, + 'ခေ': 1, + 'ချ': 1, + 'ချိ': 1, + 'ချို': 1, + 'ချို့': 1, + 'ချီ': 1, + 'ချု': 1, + 'ချေ': 1, + 'ချဳ': 1, + 'ခြ': 1, + 'ခြုံ': 1, + 'ခြေ': 1, + 'ဂျို': 1, + 'ဂၠေ': 1, + 'ငြ': 1, + 'ငြိ': 1, + 'ငွေ': 1, + 'ငႄ': 1, + 'စေ': 1, + 'စှ်ေ': 1, + 'စၞေ': 1, + 'စၠေ': 1, + 'ဆေ': 1, + 'ဇြ': 1, + 'ဇြေ': 1, + 'ဇြဳ': 1, + 'ဇၞေ': 1, + 'ညေ': 1, + 'တေ': 1, + 'တေံ': 1, + 'တြ': 1, + 'တြုံ': 1, + 'တွေ': 1, + 'တှ်ေ': 1, + 'တၟေ': 1, + 'ထေ': 1, + 'ထွေ': 1, + 'ဒေ': 1, + 'ဒေံ': 1, + 'ဒြ': 1, + 'နေ': 1, + 'နေ့': 1, + 'နှေ': 1, + 'ပေ': 1, + 'ပေဲ': 1, + 'ပျ': 1, + 'ပျေ': 1, + 'ပျဲ': 1, + 'ပြ': 1, + 'ပြီ': 1, + 'ပြု': 1, + 'ပြေ': 1, + 'ပြဳ': 1, + 'ပြံ': 1, + 'ဖေ': 1, + 'ဖျ': 1, + 'ဖျု': 1, + 'ဖျေံ': 1, + 'ဖြ': 1, + 'ဖြို': 1, + 'ဗျဵု': 1, + 'ဗြေ': 1, + 'ဗြဴ': 1, + 'ဗၠေ': 1, + 'ဘေ': 1, + 'မေ': 1, + 'မျ': 1, + 'မျို': 1, + 'မျီု': 1, + 'မျှ': 1, + 'မြ': 1, + 'မြေ': 1, + 'မြှ': 1, + 'မွေ': 1, + 'မွေ့': 1, + 'မှေ': 1, + 'မၠေ': 1, + 'မႄ': 1, + 'ယေ': 1, + 'ယျ': 1, + 'ယျဵု': 1, + 'ယြဴ': 1, + 'ရေ': 1, + 'ရေံ': 1, + 'ရွေ': 1, + 'ရွှေ့': 1, + 'ရှေ': 1, + 'ရှေ့': 1, + 'ရှ်ေ': 1, + 'လေ': 1, + 'လျ': 1, + 'လျို': 1, + 'လျေ': 1, + 'လျှ': 1, + 'လျှို့': 1, + 'လျှေ': 1, + 'လၟေ': 1, + 'လႄ': 1, + 'ဝေ': 1, + 'သေ': 1, + 'သြ': 1, + 'သြို': 1, + 'သှ်ေ': 1, + 'သၞေ': 1, + 'ဟေ': 1, + 'အေ': 1, + 'ၜေ': 1, + 'ၵေ': 1, + 'ၵႄ': 1, + 'ၶေ': 1, + 'ၼေ': 1, + 'ၼႄ': 1, + 'ၽႄ': 1, + 'ႁေ': 1, + 'ႁႄ': 1, + 'កា': 1, + 'កើ': 1, + 'កេ': 1, + 'កោ': 1, + 'ខា': 1, + 'ខែ': 1, + 'គា': 1, + 'គាំ': 1, + 'គេ': 1, + 'គោ': 1, + 'ឃា': 1, + 'ឃើ': 1, + 'ឃោ': 1, + 'ឃៅ': 1, + 'ងា': 1, + 'ចា': 1, + 'ចាំ': 1, + 'ចេ': 1, + 'ចែ': 1, + 'ចោ': 1, + 'ឆាំ': 1, + 'ជា': 1, + 'ជៀ': 1, + 'ឈា': 1, + 'ញុះ': 1, + 'ដា': 1, + 'ដើ': 1, + 'ដែ': 1, + 'ដៃ': 1, + 'ដោ': 1, + 'ដៅ': 1, + 'ឋា': 1, + 'ណា': 1, + 'ណើ': 1, + 'ណែ': 1, + 'ណោ': 1, + 'ណោះ': 1, + 'ណៈ': 1, + 'តា': 1, + 'តេ': 1, + 'តែ': 1, + 'ថា': 1, + 'ថែ': 1, + 'ទា': 1, + 'ទាំ': 1, + 'ទៀ': 1, + 'ទេ': 1, + 'ទៃ': 1, + 'ទោ': 1, + 'ទោះ': 1, + 'ទៅ': 1, + 'ធា': 1, + 'នា': 1, + 'នាំ': 1, + 'នឿ': 1, + 'នេះ': 1, + 'នៃ': 1, + 'នោះ': 1, + 'នៅ': 1, + 'នៈ': 1, + 'បា': 1, + 'បើ': 1, + 'បៀ': 1, + 'បែ': 1, + 'បោ': 1, + 'បោះ': 1, + 'បះ': 1, + 'ប៉ះ': 1, + 'ពា': 1, + 'ពើ': 1, + 'ពេ': 1, + 'ពោះ': 1, + 'ភា': 1, + 'ភេ': 1, + 'មា': 1, + 'មើ': 1, + 'មេ': 1, + 'មែ': 1, + 'ម៉ា': 1, + 'ម៉ោ': 1, + 'យា': 1, + 'យោ': 1, + 'យៈ': 1, + 'យ៉ា': 1, + 'រា': 1, + 'រាំ': 1, + 'រើ': 1, + 'រឿ': 1, + 'រៀ': 1, + 'រះ': 1, + 'រៈ': 1, + 'លា': 1, + 'លុះ': 1, + 'លើ': 1, + 'លេ': 1, + 'លោ': 1, + 'លៈ': 1, + 'វា': 1, + 'វេ': 1, + 'វៈ': 1, + 'សា': 1, + 'សើ': 1, + 'សេ': 1, + 'សៈ': 1, + 'ហា': 1, + 'ហើ': 1, + 'ហេ': 1, + 'ហែ': 1, + 'ហោ': 1, + 'ឡា': 1, + 'ឡើ': 1, + 'អា': 1, + 'អើ': 1, + 'អៀ': 1, + 'ᨠᩮ᩠': 1, + 'ᨠᩮᩢ᩵': 1, + 'ᨠᩮ᩵': 1, + 'ᨠᩯ᩠': 1, + 'ᨠᩯ᩵': 1, + 'ᨡᩮ᩠': 1, + 'ᨡᩮᩢ': 1, + 'ᨡᩮᩢᩢ': 1, + 'ᨡᩯ᩠': 1, + 'ᨡᩱᩢ': 1, + 'ᨣᩕ': 1, + 'ᨣᩕ᩠': 1, + 'ᨣᩕᩮ᩠': 1, + 'ᨣᩭ': 1, + 'ᨣᩮᩢᩢ': 1, + 'ᨣᩯ᩠': 1, + 'ᨤᩮᩢ': 1, + 'ᨤᩱ᩵': 1, + 'ᨧᩮ': 1, + 'ᨧᩮ᩠': 1, + 'ᨧᩮᩢᩢ': 1, + 'ᨧᩱ': 1, + 'ᨧᩱ᩠': 1, + 'ᨩᩭ᩵': 1, + 'ᨩᩮ᩠': 1, + 'ᨩᩱᩢ': 1, + 'ᨩ᩵ᩭ': 1, + 'ᨪᩮᩢ': 1, + 'ᨯᩪᩯ': 1, + 'ᨯᩯ᩠': 1, + 'ᨯᩰ᩠': 1, + 'ᨯᩱ': 1, + 'ᨯᩱᩢ': 1, + 'ᨲᩕ': 1, + 'ᨲᩕᩦ': 1, + 'ᨲᩕ᩼': 1, + 'ᨲᩮ᩠': 1, + 'ᨲᩯ᩠': 1, + 'ᨲᩯ᩵': 1, + 'ᨲᩱ': 1, + 'ᨲᩱ᩠': 1, + 'ᨲᩱᩢ': 1, + 'ᨳᩮ᩠': 1, + 'ᨳᩮᩢᩢ': 1, + 'ᨳᩯ᩠': 1, + 'ᨴᩮ᩠': 1, + 'ᨴᩮᩢᩢ': 1, + 'ᨴᩮᩢ᩵': 1, + 'ᨴᩯ᩠': 1, + 'ᨴᩯᩢ': 1, + 'ᨴᩰ᩠': 1, + 'ᨶᩮ': 1, + 'ᨶᩯ᩠': 1, + 'ᨶᩱ': 1, + 'ᨷᩕ': 1, + 'ᨷᩮ᩠': 1, + 'ᨸᩮ᩠': 1, + 'ᨸᩮᩢ᩵': 1, + 'ᨹᩪᩢᩱ': 1, + 'ᨹᩮᩨ᩠': 1, + 'ᨻᩕ᩠': 1, + 'ᨻᩮ᩠': 1, + 'ᨻᩱ': 1, + 'ᨽᩱ᩠': 1, + 'ᨾᩮ᩠': 1, + 'ᨾᩯ᩵': 1, + 'ᨾᩱ': 1, + 'ᨿᩮ᩠': 1, + 'ᨿᩰ': 1, + 'ᩁᩢᩭ': 1, + 'ᩁᩮ᩠': 1, + 'ᩁᩮᩨ᩠': 1, + 'ᩁᩯ᩠': 1, + 'ᩃᩮ᩠': 1, + 'ᩃᩯ': 1, + 'ᩃᩰ᩠': 1, + 'ᩅᩮ': 1, + 'ᩅᩮ᩠': 1, + 'ᩅᩱᩢ': 1, + 'ᩈᩮ': 1, + 'ᩈᩮ᩠': 1, + 'ᩈᩯ᩠': 1, + 'ᩈᩱ': 1, + 'ᩈᩱ᩠': 1, + 'ᩉᩕᩨ': 1, + 'ᩉᩮᩖᩨ': 1, + 'ᩉᩮ᩠': 1, + 'ᩉᩯ᩠': 1, + 'ᩉᩯ᩵': 1, + 'ᩉᩱ᩠': 1, + 'ꦏꦃ': 1, + 'ꦏꦺ': 1, + 'ꦏꦺꦴ': 1, + 'ꦏꦿ': 1, + 'ꦏꦿꦁ': 1, + 'ꦏꦿꦂ': 1, + 'ꦒꦸꦃ': 1, + 'ꦒꦺꦴ': 1, + 'ꦒꦿ': 1, + 'ꦒꦿꦺ': 1, + 'ꦔꦃ': 1, + 'ꦔꦺ': 1, + 'ꦔꦺꦴ': 1, + 'ꦔꦿ': 1, + 'ꦕꦃ': 1, + 'ꦕꦺꦴ': 1, + 'ꦗꦃ': 1, + 'ꦗꦺ': 1, + 'ꦗꦿꦺꦴ': 1, + 'ꦝꦃ': 1, + 'ꦝꦺ': 1, + 'ꦝꦺꦴ': 1, + 'ꦝꦺꦴꦂ': 1, + 'ꦠꦸꦃ': 1, + 'ꦠꦺ': 1, + 'ꦠꦺꦃ': 1, + 'ꦠꦺꦴ': 1, + 'ꦠꦾ': 1, + 'ꦠꦿ': 1, + 'ꦠꦿꦶ': 1, + 'ꦢꦃ': 1, + 'ꦢꦺ': 1, + 'ꦢꦺꦴ': 1, + 'ꦢꦾ': 1, + 'ꦢꦿ': 1, + 'ꦤꦺ': 1, + 'ꦤꦺꦃ': 1, + 'ꦤꦺꦴ': 1, + 'ꦥꦃ': 1, + 'ꦥꦺ': 1, + 'ꦥꦺꦴ': 1, + 'ꦥꦿ': 1, + 'ꦥꦿꦃ': 1, + 'ꦥꦿꦶ': 1, + 'ꦥꦿꦺꦴ': 1, + 'ꦧꦺ': 1, + 'ꦧꦺꦃ': 1, + 'ꦧꦺꦴ': 1, + 'ꦧꦿ': 1, + 'ꦩꦃ': 1, + 'ꦩꦺ': 1, + 'ꦩꦺꦃ': 1, + 'ꦩꦺꦴ': 1, + 'ꦩꦿ': 1, + 'ꦩꦿꦃ': 1, + 'ꦩꦿꦶꦃ': 1, + 'ꦪꦃ': 1, + 'ꦪꦸꦃ': 1, + 'ꦪꦺꦴ': 1, + 'ꦫꦶꦃ': 1, + 'ꦫꦺ': 1, + 'ꦫꦺꦴ': 1, + 'ꦫꦾ': 1, + 'ꦭꦃ': 1, + 'ꦭꦶꦃ': 1, + 'ꦭꦺ': 1, + 'ꦭꦺꦃ': 1, + 'ꦭꦺꦴ': 1, + 'ꦭꦺꦴꦁ': 1, + 'ꦮꦃ': 1, + 'ꦮꦶꦃ': 1, + 'ꦮꦸꦃ': 1, + 'ꦮꦺ': 1, + 'ꦮꦺꦴ': 1, + 'ꦮꦺꦴꦁ': 1, + 'ꦮꦺꦴꦃ': 1, + 'ꦮꦿꦸꦃ': 1, + 'ꦱꦶꦃ': 1, + 'ꦱꦺ': 1, + 'ꦱꦺꦴ': 1, + 'ꦱꦾ': 1, + 'ꦱꦿ': 1, + 'ꦱꦿꦃ': 1, + 'ꦲꦺ': 1, + 'ꦲꦺꦴ': 1, + '𑄃𑄬': 1, + '𑄃𑄬𑄭': 1, + '𑄇𑄬': 1, + '𑄈𑄬': 1, + '𑄉𑄬': 1, + '𑄉𑄬𑄭': 1, + '𑄊𑄬': 1, + '𑄌𑄬': 1, + '𑄎𑄬': 1, + '𑄎𑄬𑄭': 1, + '𑄏𑄬': 1, + '𑄑𑄬': 1, + '𑄖𑄬': 1, + '𑄗𑄬': 1, + '𑄘𑄬': 1, + '𑄙𑄬': 1, + '𑄚𑄬': 1, + '𑄚𑄬𑄭': 1, + '𑄛𑄬': 1, + '𑄜𑄬': 1, + '𑄝𑄬': 1, + '𑄞𑄬': 1, + '𑄞𑄬𑄭': 1, + '𑄟𑄬': 1, + '𑄠𑄬': 1, + '𑄠𑄬𑄭': 1, + '𑄡𑄬': 1, + '𑄡𑄬𑄭': 1, + '𑄢𑄬': 1, + '𑄢𑄬𑄭': 1, + '𑄣𑄬': 1, + '𑄣𑄬𑄭': 1, + '𑄥𑄬': 1, + '𑄦𑄬': 1, + '𑌅𑌂': 1, + '𑌕𑌂': 1, + '𑌕𑌾': 1, + '𑌕𑌾𑌂': 1, + '𑌕𑌾𑌃': 1, + '𑌕𑌿': 1, + '𑌕𑍂': 1, + '𑌕𑍃': 1, + '𑌕𑍇': 1, + '𑌕𑍋': 1, + '𑌗𑌾': 1, + '𑌗𑌿': 1, + '𑌗𑍁': 1, + '𑌗𑍃': 1, + '𑌗𑍇': 1, + '𑌗𑍌': 1, + '𑌘𑌾': 1, + '𑌘𑍇': 1, + '𑌘𑍋': 1, + '𑌚𑌾': 1, + '𑌚𑌿': 1, + '𑌚𑍇': 1, + '𑌚𑍈': 1, + '𑌛𑌾': 1, + '𑌛𑍁': 1, + '𑌛𑍇': 1, + '𑌜𑌾': 1, + '𑌜𑌿': 1, + '𑌜𑍇': 1, + '𑌞𑌾': 1, + '𑌟𑌃': 1, + '𑌟𑌾': 1, + '𑌟𑌾𑌂': 1, + '𑌟𑌾𑌃': 1, + '𑌠𑌾': 1, + '𑌠𑍇': 1, + '𑌠𑍈𑌃': 1, + '𑌡𑌂': 1, + '𑌡𑌃': 1, + '𑌡𑌿': 1, + '𑌣𑌂': 1, + '𑌣𑌾': 1, + '𑌣𑌾𑌂': 1, + '𑌣𑌿': 1, + '𑌣𑍁': 1, + '𑌣𑍇': 1, + '𑌤𑌂': 1, + '𑌤𑌃': 1, + '𑌤𑌾': 1, + '𑌤𑌾𑌂': 1, + '𑌤𑌾𑌃': 1, + '𑌤𑌿': 1, + '𑌤𑌿𑌂': 1, + '𑌤𑌿𑌃': 1, + '𑌤𑍁': 1, + '𑌤𑍁𑌂': 1, + '𑌤𑍃': 1, + '𑌤𑍇': 1, + '𑌤𑍇𑌃': 1, + '𑌤𑍈': 1, + '𑌤𑍋': 1, + '𑌤𑍌': 1, + '𑌥𑌂': 1, + '𑌥𑌾': 1, + '𑌥𑌾𑌂': 1, + '𑌥𑌿': 1, + '𑌦𑌂': 1, + '𑌦𑌃': 1, + '𑌦𑌾': 1, + '𑌦𑌿': 1, + '𑌦𑍁': 1, + '𑌦𑍃': 1, + '𑌦𑍇': 1, + '𑌦𑍋': 1, + '𑌧𑌃': 1, + '𑌧𑌾': 1, + '𑌧𑌾𑌃': 1, + '𑌧𑌿': 1, + '𑌧𑌿𑌂': 1, + '𑌧𑍁': 1, + '𑌧𑍃': 1, + '𑌧𑍇': 1, + '𑌧𑍇𑌃': 1, + '𑌧𑍈𑌃': 1, + '𑌨𑌂': 1, + '𑌨𑌃': 1, + '𑌨𑌾': 1, + '𑌨𑌾𑌂': 1, + '𑌨𑌾𑌃': 1, + '𑌨𑌿': 1, + '𑌨𑌿𑌃': 1, + '𑌨𑍁': 1, + '𑌨𑍂': 1, + '𑌨𑍇': 1, + '𑌨𑍈': 1, + '𑌨𑍈𑌃': 1, + '𑌨𑍋': 1, + '𑌪𑌂': 1, + '𑌪𑌾': 1, + '𑌪𑌿': 1, + '𑌪𑍁': 1, + '𑌪𑍂': 1, + '𑌪𑍇': 1, + '𑌪𑍋': 1, + '𑌬𑌾': 1, + '𑌬𑍃': 1, + '𑌭𑌾': 1, + '𑌭𑌿': 1, + '𑌭𑌿𑌃': 1, + '𑌭𑍁': 1, + '𑌭𑍂': 1, + '𑌭𑍇': 1, + '𑌮𑌂': 1, + '𑌮𑌾': 1, + '𑌮𑌿': 1, + '𑌮𑌿𑌂': 1, + '𑌮𑍁': 1, + '𑌮𑍂': 1, + '𑌮𑍇': 1, + '𑌮𑍈': 1, + '𑌮𑍋': 1, + '𑌮𑍌': 1, + '𑌯𑌂': 1, + '𑌯𑌃': 1, + '𑌯𑌾': 1, + '𑌯𑌾𑌂': 1, + '𑌯𑌾𑌃': 1, + '𑌯𑌿': 1, + '𑌯𑍁': 1, + '𑌯𑍁𑌃': 1, + '𑌯𑍇': 1, + '𑌯𑍈': 1, + '𑌯𑍈𑌃': 1, + '𑌯𑍋': 1, + '𑌯𑍋𑌃': 1, + '𑌰𑌂': 1, + '𑌰𑌃': 1, + '𑌰𑌾': 1, + '𑌰𑌾𑌃': 1, + '𑌰𑌿': 1, + '𑌰𑌿𑌂': 1, + '𑌰𑍁': 1, + '𑌰𑍂': 1, + '𑌰𑍇': 1, + '𑌰𑍋': 1, + '𑌰𑍌': 1, + '𑌲𑌂': 1, + '𑌲𑌾': 1, + '𑌲𑌾𑌃': 1, + '𑌲𑌿': 1, + '𑌲𑌿𑌂': 1, + '𑌲𑍇': 1, + '𑌲𑍈': 1, + '𑌲𑍋': 1, + '𑌵𑌂': 1, + '𑌵𑌃': 1, + '𑌵𑌾': 1, + '𑌵𑌾𑌂': 1, + '𑌵𑌾𑌃': 1, + '𑌵𑌿': 1, + '𑌵𑍃': 1, + '𑌵𑍇': 1, + '𑌵𑍈': 1, + '𑌵𑍋': 1, + '𑌶𑌂': 1, + '𑌶𑌃': 1, + '𑌶𑌾': 1, + '𑌶𑌾𑌃': 1, + '𑌶𑌿': 1, + '𑌶𑍁': 1, + '𑌶𑍂': 1, + '𑌶𑍇': 1, + '𑌶𑍋': 1, + '𑌷𑌂': 1, + '𑌷𑌾': 1, + '𑌷𑌾𑌂': 1, + '𑌷𑌾𑌃': 1, + '𑌷𑌿': 1, + '𑌷𑍁': 1, + '𑌷𑍇': 1, + '𑌷𑍈': 1, + '𑌷𑍋': 1, + '𑌸𑌂': 1, + '𑌸𑌃': 1, + '𑌸𑌾': 1, + '𑌸𑌾𑌂': 1, + '𑌸𑌿': 1, + '𑌸𑍁': 1, + '𑌸𑍂': 1, + '𑌸𑍇': 1, + '𑌸𑍋': 1, + '𑌸𑍌': 1, + '𑌹𑌾': 1, + '𑌹𑌾𑌃': 1, + '𑌹𑌿': 1, + '𑌹𑍇': 1, + '𑌹𑍋': 1, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_5bfac390.py b/libs/wcwidth/table_grapheme_overrides/_known_5bfac390.py new file mode 100644 index 000000000..ecc4f35b0 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_5bfac390.py @@ -0,0 +1,2571 @@ +""" +Grapheme overrides for _known_5bfac390. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: pterm, vte +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 3, + '⛹🏻‍♀️': 4, + '⛹🏻‍♂️': 4, + '⛹🏼‍♀️': 4, + '⛹🏼‍♂️': 4, + '⛹🏽‍♀️': 4, + '⛹🏽‍♂️': 4, + '⛹🏾‍♀️': 4, + '⛹🏾‍♂️': 4, + '⛹🏿‍♀️': 4, + '⛹🏿‍♂️': 4, + '❤️‍🔥': 3, + '❤️‍🩹': 3, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 3, + '🏃‍♀️‍➡️': 4, + '🏃‍♂️': 3, + '🏃‍♂️‍➡️': 4, + '🏃‍➡️': 3, + '🏃🏻‍♀️': 5, + '🏃🏻‍♀️‍➡️': 6, + '🏃🏻‍♂️': 5, + '🏃🏻‍♂️‍➡️': 6, + '🏃🏻‍➡️': 5, + '🏃🏼‍♀️': 5, + '🏃🏼‍♀️‍➡️': 6, + '🏃🏼‍♂️': 5, + '🏃🏼‍♂️‍➡️': 6, + '🏃🏼‍➡️': 5, + '🏃🏽‍♀️': 5, + '🏃🏽‍♀️‍➡️': 6, + '🏃🏽‍♂️': 5, + '🏃🏽‍♂️‍➡️': 6, + '🏃🏽‍➡️': 5, + '🏃🏾‍♀️': 5, + '🏃🏾‍♀️‍➡️': 6, + '🏃🏾‍♂️': 5, + '🏃🏾‍♂️‍➡️': 6, + '🏃🏾‍➡️': 5, + '🏃🏿‍♀️': 5, + '🏃🏿‍♀️‍➡️': 6, + '🏃🏿‍♂️': 5, + '🏃🏿‍♂️‍➡️': 6, + '🏃🏿‍➡️': 5, + '🏄‍♀️': 3, + '🏄‍♂️': 3, + '🏄🏻‍♀️': 5, + '🏄🏻‍♂️': 5, + '🏄🏼‍♀️': 5, + '🏄🏼‍♂️': 5, + '🏄🏽‍♀️': 5, + '🏄🏽‍♂️': 5, + '🏄🏾‍♀️': 5, + '🏄🏾‍♂️': 5, + '🏄🏿‍♀️': 5, + '🏄🏿‍♂️': 5, + '🏊‍♀️': 3, + '🏊‍♂️': 3, + '🏊🏻‍♀️': 5, + '🏊🏻‍♂️': 5, + '🏊🏼‍♀️': 5, + '🏊🏼‍♂️': 5, + '🏊🏽‍♀️': 5, + '🏊🏽‍♂️': 5, + '🏊🏾‍♀️': 5, + '🏊🏾‍♂️': 5, + '🏊🏿‍♀️': 5, + '🏊🏿‍♂️': 5, + '🏋🏻‍♀️': 4, + '🏋🏻‍♂️': 4, + '🏋🏼‍♀️': 4, + '🏋🏼‍♂️': 4, + '🏋🏽‍♀️': 4, + '🏋🏽‍♂️': 4, + '🏋🏾‍♀️': 4, + '🏋🏾‍♂️': 4, + '🏋🏿‍♀️': 4, + '🏋🏿‍♂️': 4, + '🏌🏻‍♀️': 4, + '🏌🏻‍♂️': 4, + '🏌🏼‍♀️': 4, + '🏌🏼‍♂️': 4, + '🏌🏽‍♀️': 4, + '🏌🏽‍♂️': 4, + '🏌🏾‍♀️': 4, + '🏌🏾‍♂️': 4, + '🏌🏿‍♀️': 4, + '🏌🏿‍♂️': 4, + '🏳️‍🌈': 3, + '🏴‍☠️': 3, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 3, + '👨‍⚕️': 3, + '👨‍⚖️': 3, + '👨‍✈️': 3, + '👨‍❤️‍👨': 5, + '👨‍❤️‍💋‍👨': 7, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 5, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 5, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 5, + '👨🏻‍⚕️': 5, + '👨🏻‍⚖️': 5, + '👨🏻‍✈️': 5, + '👨🏻‍❤️‍👨🏻': 9, + '👨🏻‍❤️‍👨🏽': 9, + '👨🏻‍❤️‍👨🏿': 9, + '👨🏻‍❤️‍💋‍👨🏻': 11, + '👨🏻‍❤️‍💋‍👨🏽': 11, + '👨🏻‍❤️‍💋‍👨🏾': 11, + '👨🏻‍❤️‍💋‍👨🏿': 11, + '👨🏻‍🌾': 6, + '👨🏻‍🍳': 6, + '👨🏻‍🍼': 6, + '👨🏻‍🎓': 6, + '👨🏻‍🎤': 6, + '👨🏻‍🎨': 6, + '👨🏻‍🏫': 6, + '👨🏻‍🏭': 6, + '👨🏻‍🐰‍👨🏼': 10, + '👨🏻‍🐰‍👨🏽': 10, + '👨🏻‍🐰‍👨🏾': 10, + '👨🏻‍🐰‍👨🏿': 10, + '👨🏻‍💻': 6, + '👨🏻‍💼': 6, + '👨🏻‍🔧': 6, + '👨🏻‍🔬': 6, + '👨🏻‍🚀': 6, + '👨🏻‍🚒': 6, + '👨🏻‍🤝‍👨🏼': 10, + '👨🏻‍🤝‍👨🏽': 10, + '👨🏻‍🤝‍👨🏾': 10, + '👨🏻‍🤝‍👨🏿': 10, + '👨🏻‍🦯': 6, + '👨🏻‍🦯‍➡️': 7, + '👨🏻‍🦰': 6, + '👨🏻‍🦱': 6, + '👨🏻‍🦲': 6, + '👨🏻‍🦳': 6, + '👨🏻‍🦼': 6, + '👨🏻‍🦼‍➡️': 7, + '👨🏻‍🦽': 6, + '👨🏻‍🦽‍➡️': 7, + '👨🏻‍🫯‍👨🏼': 9, + '👨🏻‍🫯‍👨🏽': 9, + '👨🏻‍🫯‍👨🏾': 9, + '👨🏻‍🫯‍👨🏿': 9, + '👨🏼‍⚕️': 5, + '👨🏼‍⚖️': 5, + '👨🏼‍✈️': 5, + '👨🏼‍❤️‍👨🏼': 9, + '👨🏼‍❤️‍💋‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏿': 11, + '👨🏼‍🌾': 6, + '👨🏼‍🍳': 6, + '👨🏼‍🍼': 6, + '👨🏼‍🎓': 6, + '👨🏼‍🎤': 6, + '👨🏼‍🎨': 6, + '👨🏼‍🏫': 6, + '👨🏼‍🏭': 6, + '👨🏼‍🐰‍👨🏻': 10, + '👨🏼‍🐰‍👨🏽': 10, + '👨🏼‍🐰‍👨🏿': 10, + '👨🏼‍💻': 6, + '👨🏼‍💼': 6, + '👨🏼‍🔧': 6, + '👨🏼‍🔬': 6, + '👨🏼‍🚀': 6, + '👨🏼‍🚒': 6, + '👨🏼‍🤝‍👨🏻': 10, + '👨🏼‍🤝‍👨🏽': 10, + '👨🏼‍🤝‍👨🏿': 10, + '👨🏼‍🦯': 6, + '👨🏼‍🦯‍➡️': 7, + '👨🏼‍🦰': 6, + '👨🏼‍🦱': 6, + '👨🏼‍🦲': 6, + '👨🏼‍🦳': 6, + '👨🏼‍🦼': 6, + '👨🏼‍🦽': 6, + '👨🏼‍🦽‍➡️': 7, + '👨🏼‍🫯‍👨🏻': 9, + '👨🏼‍🫯‍👨🏽': 9, + '👨🏼‍🫯‍👨🏾': 9, + '👨🏼‍🫯‍👨🏿': 9, + '👨🏽‍⚕️': 5, + '👨🏽‍⚖️': 5, + '👨🏽‍✈️': 5, + '👨🏽‍❤️‍👨🏻': 9, + '👨🏽‍❤️‍👨🏽': 9, + '👨🏽‍❤️‍👨🏿': 9, + '👨🏽‍❤️‍💋‍👨🏻': 11, + '👨🏽‍❤️‍💋‍👨🏽': 11, + '👨🏽‍❤️‍💋‍👨🏿': 11, + '👨🏽‍🌾': 6, + '👨🏽‍🍳': 6, + '👨🏽‍🍼': 6, + '👨🏽‍🎓': 6, + '👨🏽‍🎤': 6, + '👨🏽‍🎨': 6, + '👨🏽‍🏫': 6, + '👨🏽‍🏭': 6, + '👨🏽‍🐰‍👨🏻': 10, + '👨🏽‍🐰‍👨🏼': 10, + '👨🏽‍🐰‍👨🏾': 10, + '👨🏽‍🐰‍👨🏿': 10, + '👨🏽‍💻': 6, + '👨🏽‍💼': 6, + '👨🏽‍🔧': 6, + '👨🏽‍🔬': 6, + '👨🏽‍🚀': 6, + '👨🏽‍🚒': 6, + '👨🏽‍🤝‍👨🏻': 10, + '👨🏽‍🤝‍👨🏼': 10, + '👨🏽‍🤝‍👨🏾': 10, + '👨🏽‍🤝‍👨🏿': 10, + '👨🏽‍🦯': 6, + '👨🏽‍🦯‍➡️': 7, + '👨🏽‍🦰': 6, + '👨🏽‍🦱': 6, + '👨🏽‍🦲': 6, + '👨🏽‍🦳': 6, + '👨🏽‍🦼': 6, + '👨🏽‍🦼‍➡️': 7, + '👨🏽‍🦽': 6, + '👨🏽‍🦽‍➡️': 7, + '👨🏽‍🫯‍👨🏻': 9, + '👨🏽‍🫯‍👨🏼': 9, + '👨🏽‍🫯‍👨🏾': 9, + '👨🏽‍🫯‍👨🏿': 9, + '👨🏾‍⚕️': 5, + '👨🏾‍⚖️': 5, + '👨🏾‍✈️': 5, + '👨🏾‍❤️‍👨🏾': 9, + '👨🏾‍❤️‍💋‍👨🏻': 11, + '👨🏾‍❤️‍💋‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏿': 11, + '👨🏾‍🌾': 6, + '👨🏾‍🍳': 6, + '👨🏾‍🍼': 6, + '👨🏾‍🎓': 6, + '👨🏾‍🎤': 6, + '👨🏾‍🎨': 6, + '👨🏾‍🏫': 6, + '👨🏾‍🏭': 6, + '👨🏾‍🐰‍👨🏻': 10, + '👨🏾‍🐰‍👨🏽': 10, + '👨🏾‍🐰‍👨🏿': 10, + '👨🏾‍💻': 6, + '👨🏾‍💼': 6, + '👨🏾‍🔧': 6, + '👨🏾‍🔬': 6, + '👨🏾‍🚀': 6, + '👨🏾‍🚒': 6, + '👨🏾‍🤝‍👨🏻': 10, + '👨🏾‍🤝‍👨🏽': 10, + '👨🏾‍🤝‍👨🏿': 10, + '👨🏾‍🦯': 6, + '👨🏾‍🦯‍➡️': 7, + '👨🏾‍🦰': 6, + '👨🏾‍🦱': 6, + '👨🏾‍🦲': 6, + '👨🏾‍🦳': 6, + '👨🏾‍🦼': 6, + '👨🏾‍🦽': 6, + '👨🏾‍🦽‍➡️': 7, + '👨🏾‍🫯‍👨🏻': 9, + '👨🏾‍🫯‍👨🏼': 9, + '👨🏾‍🫯‍👨🏽': 9, + '👨🏾‍🫯‍👨🏿': 9, + '👨🏿‍⚕️': 5, + '👨🏿‍⚖️': 5, + '👨🏿‍✈️': 5, + '👨🏿‍❤️‍👨🏻': 9, + '👨🏿‍❤️‍👨🏽': 9, + '👨🏿‍❤️‍👨🏿': 9, + '👨🏿‍❤️‍💋‍👨🏻': 11, + '👨🏿‍❤️‍💋‍👨🏼': 11, + '👨🏿‍❤️‍💋‍👨🏽': 11, + '👨🏿‍❤️‍💋‍👨🏾': 11, + '👨🏿‍❤️‍💋‍👨🏿': 11, + '👨🏿‍🌾': 6, + '👨🏿‍🍳': 6, + '👨🏿‍🍼': 6, + '👨🏿‍🎓': 6, + '👨🏿‍🎤': 6, + '👨🏿‍🎨': 6, + '👨🏿‍🏫': 6, + '👨🏿‍🏭': 6, + '👨🏿‍🐰‍👨🏻': 10, + '👨🏿‍🐰‍👨🏼': 10, + '👨🏿‍🐰‍👨🏽': 10, + '👨🏿‍🐰‍👨🏾': 10, + '👨🏿‍💻': 6, + '👨🏿‍💼': 6, + '👨🏿‍🔧': 6, + '👨🏿‍🔬': 6, + '👨🏿‍🚀': 6, + '👨🏿‍🚒': 6, + '👨🏿‍🤝‍👨🏻': 10, + '👨🏿‍🤝‍👨🏼': 10, + '👨🏿‍🤝‍👨🏽': 10, + '👨🏿‍🤝‍👨🏾': 10, + '👨🏿‍🦯': 6, + '👨🏿‍🦯‍➡️': 7, + '👨🏿‍🦰': 6, + '👨🏿‍🦱': 6, + '👨🏿‍🦲': 6, + '👨🏿‍🦳': 6, + '👨🏿‍🦼': 6, + '👨🏿‍🦼‍➡️': 7, + '👨🏿‍🦽': 6, + '👨🏿‍🦽‍➡️': 7, + '👨🏿‍🫯‍👨🏻': 9, + '👨🏿‍🫯‍👨🏼': 9, + '👨🏿‍🫯‍👨🏽': 9, + '👨🏿‍🫯‍👨🏾': 9, + '👩‍⚕️': 3, + '👩‍⚖️': 3, + '👩‍✈️': 3, + '👩‍❤️‍👨': 5, + '👩‍❤️‍👩': 5, + '👩‍❤️‍💋‍👨': 7, + '👩‍❤️‍💋‍👩': 7, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 5, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 5, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 5, + '👩🏻‍⚕️': 5, + '👩🏻‍⚖️': 5, + '👩🏻‍✈️': 5, + '👩🏻‍❤️‍👨🏻': 9, + '👩🏻‍❤️‍👨🏿': 9, + '👩🏻‍❤️‍👩🏻': 9, + '👩🏻‍❤️‍💋‍👨🏻': 11, + '👩🏻‍❤️‍💋‍👨🏽': 11, + '👩🏻‍❤️‍💋‍👨🏿': 11, + '👩🏻‍❤️‍💋‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👩🏽': 11, + '👩🏻‍❤️‍💋‍👩🏿': 11, + '👩🏻‍🌾': 6, + '👩🏻‍🍳': 6, + '👩🏻‍🍼': 6, + '👩🏻‍🎓': 6, + '👩🏻‍🎤': 6, + '👩🏻‍🎨': 6, + '👩🏻‍🏫': 6, + '👩🏻‍🏭': 6, + '👩🏻‍🐰‍👩🏼': 10, + '👩🏻‍🐰‍👩🏽': 10, + '👩🏻‍🐰‍👩🏾': 10, + '👩🏻‍🐰‍👩🏿': 10, + '👩🏻‍💻': 6, + '👩🏻‍💼': 6, + '👩🏻‍🔧': 6, + '👩🏻‍🔬': 6, + '👩🏻‍🚀': 6, + '👩🏻‍🚒': 6, + '👩🏻‍🤝‍👨🏽': 10, + '👩🏻‍🤝‍👨🏿': 10, + '👩🏻‍🤝‍👩🏼': 10, + '👩🏻‍🤝‍👩🏽': 10, + '👩🏻‍🤝‍👩🏾': 10, + '👩🏻‍🤝‍👩🏿': 10, + '👩🏻‍🦯': 6, + '👩🏻‍🦯‍➡️': 7, + '👩🏻‍🦰': 6, + '👩🏻‍🦱': 6, + '👩🏻‍🦲': 6, + '👩🏻‍🦳': 6, + '👩🏻‍🦼': 6, + '👩🏻‍🦼‍➡️': 7, + '👩🏻‍🦽': 6, + '👩🏻‍🦽‍➡️': 7, + '👩🏻‍🫯‍👩🏼': 9, + '👩🏻‍🫯‍👩🏽': 9, + '👩🏻‍🫯‍👩🏾': 9, + '👩🏻‍🫯‍👩🏿': 9, + '👩🏼‍⚕️': 5, + '👩🏼‍⚖️': 5, + '👩🏼‍✈️': 5, + '👩🏼‍❤️‍👨🏼': 9, + '👩🏼‍❤️‍👩🏼': 9, + '👩🏼‍❤️‍💋‍👨🏼': 11, + '👩🏼‍❤️‍💋‍👩🏼': 11, + '👩🏼‍🌾': 6, + '👩🏼‍🍳': 6, + '👩🏼‍🍼': 6, + '👩🏼‍🎓': 6, + '👩🏼‍🎤': 6, + '👩🏼‍🎨': 6, + '👩🏼‍🏫': 6, + '👩🏼‍🏭': 6, + '👩🏼‍🐰‍👩🏻': 10, + '👩🏼‍🐰‍👩🏿': 10, + '👩🏼‍💻': 6, + '👩🏼‍💼': 6, + '👩🏼‍🔧': 6, + '👩🏼‍🔬': 6, + '👩🏼‍🚀': 6, + '👩🏼‍🚒': 6, + '👩🏼‍🤝‍👩🏻': 10, + '👩🏼‍🤝‍👩🏽': 10, + '👩🏼‍🤝‍👩🏿': 10, + '👩🏼‍🦯': 6, + '👩🏼‍🦯‍➡️': 7, + '👩🏼‍🦰': 6, + '👩🏼‍🦱': 6, + '👩🏼‍🦲': 6, + '👩🏼‍🦳': 6, + '👩🏼‍🦼': 6, + '👩🏼‍🦽': 6, + '👩🏼‍🫯‍👩🏻': 9, + '👩🏼‍🫯‍👩🏽': 9, + '👩🏼‍🫯‍👩🏾': 9, + '👩🏼‍🫯‍👩🏿': 9, + '👩🏽‍⚕️': 5, + '👩🏽‍⚖️': 5, + '👩🏽‍✈️': 5, + '👩🏽‍❤️‍👨🏽': 9, + '👩🏽‍❤️‍👩🏽': 9, + '👩🏽‍❤️‍💋‍👨🏻': 11, + '👩🏽‍❤️‍💋‍👨🏽': 11, + '👩🏽‍❤️‍💋‍👨🏿': 11, + '👩🏽‍❤️‍💋‍👩🏻': 11, + '👩🏽‍❤️‍💋‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👩🏿': 11, + '👩🏽‍🌾': 6, + '👩🏽‍🍳': 6, + '👩🏽‍🍼': 6, + '👩🏽‍🎓': 6, + '👩🏽‍🎤': 6, + '👩🏽‍🎨': 6, + '👩🏽‍🏫': 6, + '👩🏽‍🏭': 6, + '👩🏽‍🐰‍👩🏻': 10, + '👩🏽‍🐰‍👩🏾': 10, + '👩🏽‍🐰‍👩🏿': 10, + '👩🏽‍💻': 6, + '👩🏽‍💼': 6, + '👩🏽‍🔧': 6, + '👩🏽‍🔬': 6, + '👩🏽‍🚀': 6, + '👩🏽‍🚒': 6, + '👩🏽‍🤝‍👨🏻': 10, + '👩🏽‍🤝‍👨🏿': 10, + '👩🏽‍🤝‍👩🏻': 10, + '👩🏽‍🤝‍👩🏼': 10, + '👩🏽‍🤝‍👩🏾': 10, + '👩🏽‍🤝‍👩🏿': 10, + '👩🏽‍🦯': 6, + '👩🏽‍🦯‍➡️': 7, + '👩🏽‍🦰': 6, + '👩🏽‍🦱': 6, + '👩🏽‍🦲': 6, + '👩🏽‍🦳': 6, + '👩🏽‍🦼': 6, + '👩🏽‍🦼‍➡️': 7, + '👩🏽‍🦽': 6, + '👩🏽‍🦽‍➡️': 7, + '👩🏽‍🫯‍👩🏻': 9, + '👩🏽‍🫯‍👩🏼': 9, + '👩🏽‍🫯‍👩🏾': 9, + '👩🏽‍🫯‍👩🏿': 9, + '👩🏾‍⚕️': 5, + '👩🏾‍⚖️': 5, + '👩🏾‍✈️': 5, + '👩🏾‍❤️‍👨🏾': 9, + '👩🏾‍❤️‍👩🏾': 9, + '👩🏾‍❤️‍💋‍👨🏾': 11, + '👩🏾‍❤️‍💋‍👩🏾': 11, + '👩🏾‍🌾': 6, + '👩🏾‍🍳': 6, + '👩🏾‍🍼': 6, + '👩🏾‍🎓': 6, + '👩🏾‍🎤': 6, + '👩🏾‍🎨': 6, + '👩🏾‍🏫': 6, + '👩🏾‍🏭': 6, + '👩🏾‍🐰‍👩🏻': 10, + '👩🏾‍🐰‍👩🏽': 10, + '👩🏾‍🐰‍👩🏿': 10, + '👩🏾‍💻': 6, + '👩🏾‍💼': 6, + '👩🏾‍🔧': 6, + '👩🏾‍🔬': 6, + '👩🏾‍🚀': 6, + '👩🏾‍🚒': 6, + '👩🏾‍🤝‍👩🏻': 10, + '👩🏾‍🤝‍👩🏽': 10, + '👩🏾‍🤝‍👩🏿': 10, + '👩🏾‍🦯': 6, + '👩🏾‍🦯‍➡️': 7, + '👩🏾‍🦰': 6, + '👩🏾‍🦱': 6, + '👩🏾‍🦲': 6, + '👩🏾‍🦳': 6, + '👩🏾‍🦼': 6, + '👩🏾‍🦽': 6, + '👩🏾‍🦽‍➡️': 7, + '👩🏾‍🫯‍👩🏻': 9, + '👩🏾‍🫯‍👩🏼': 9, + '👩🏾‍🫯‍👩🏽': 9, + '👩🏾‍🫯‍👩🏿': 9, + '👩🏿‍⚕️': 5, + '👩🏿‍⚖️': 5, + '👩🏿‍✈️': 5, + '👩🏿‍❤️‍👨🏻': 9, + '👩🏿‍❤️‍👨🏿': 9, + '👩🏿‍❤️‍👩🏿': 9, + '👩🏿‍❤️‍💋‍👨🏻': 11, + '👩🏿‍❤️‍💋‍👨🏽': 11, + '👩🏿‍❤️‍💋‍👨🏿': 11, + '👩🏿‍❤️‍💋‍👩🏻': 11, + '👩🏿‍❤️‍💋‍👩🏽': 11, + '👩🏿‍❤️‍💋‍👩🏿': 11, + '👩🏿‍🌾': 6, + '👩🏿‍🍳': 6, + '👩🏿‍🍼': 6, + '👩🏿‍🎓': 6, + '👩🏿‍🎤': 6, + '👩🏿‍🎨': 6, + '👩🏿‍🏫': 6, + '👩🏿‍🏭': 6, + '👩🏿‍🐰‍👩🏻': 10, + '👩🏿‍🐰‍👩🏼': 10, + '👩🏿‍🐰‍👩🏽': 10, + '👩🏿‍🐰‍👩🏾': 10, + '👩🏿‍💻': 6, + '👩🏿‍💼': 6, + '👩🏿‍🔧': 6, + '👩🏿‍🔬': 6, + '👩🏿‍🚀': 6, + '👩🏿‍🚒': 6, + '👩🏿‍🤝‍👨🏻': 10, + '👩🏿‍🤝‍👨🏽': 10, + '👩🏿‍🤝‍👩🏻': 10, + '👩🏿‍🤝‍👩🏼': 10, + '👩🏿‍🤝‍👩🏽': 10, + '👩🏿‍🤝‍👩🏾': 10, + '👩🏿‍🦯': 6, + '👩🏿‍🦯‍➡️': 7, + '👩🏿‍🦰': 6, + '👩🏿‍🦱': 6, + '👩🏿‍🦲': 6, + '👩🏿‍🦳': 6, + '👩🏿‍🦼': 6, + '👩🏿‍🦼‍➡️': 7, + '👩🏿‍🦽': 6, + '👩🏿‍🦽‍➡️': 7, + '👩🏿‍🫯‍👩🏻': 9, + '👩🏿‍🫯‍👩🏼': 9, + '👩🏿‍🫯‍👩🏽': 9, + '👩🏿‍🫯‍👩🏾': 9, + '👮‍♀️': 3, + '👮‍♂️': 3, + '👮🏻‍♀️': 5, + '👮🏻‍♂️': 5, + '👮🏼‍♀️': 5, + '👮🏼‍♂️': 5, + '👮🏽‍♀️': 5, + '👮🏽‍♂️': 5, + '👮🏾‍♀️': 5, + '👮🏾‍♂️': 5, + '👮🏿‍♀️': 5, + '👮🏿‍♂️': 5, + '👯‍♀️': 3, + '👯‍♂️': 3, + '👯🏻‍♀️': 5, + '👯🏻‍♂️': 5, + '👯🏼‍♀️': 5, + '👯🏼‍♂️': 5, + '👯🏽‍♀️': 5, + '👯🏽‍♂️': 5, + '👯🏾‍♀️': 5, + '👯🏾‍♂️': 5, + '👯🏿‍♀️': 5, + '👯🏿‍♂️': 5, + '👰‍♀️': 3, + '👰‍♂️': 3, + '👰🏻‍♀️': 5, + '👰🏻‍♂️': 5, + '👰🏼‍♀️': 5, + '👰🏼‍♂️': 5, + '👰🏽‍♀️': 5, + '👰🏽‍♂️': 5, + '👰🏾‍♀️': 5, + '👰🏾‍♂️': 5, + '👰🏿‍♀️': 5, + '👰🏿‍♂️': 5, + '👱‍♀️': 3, + '👱‍♂️': 3, + '👱🏻‍♀️': 5, + '👱🏻‍♂️': 5, + '👱🏼‍♀️': 5, + '👱🏼‍♂️': 5, + '👱🏽‍♀️': 5, + '👱🏽‍♂️': 5, + '👱🏾‍♀️': 5, + '👱🏾‍♂️': 5, + '👱🏿‍♀️': 5, + '👱🏿‍♂️': 5, + '👳‍♀️': 3, + '👳‍♂️': 3, + '👳🏻‍♀️': 5, + '👳🏻‍♂️': 5, + '👳🏼‍♀️': 5, + '👳🏼‍♂️': 5, + '👳🏽‍♀️': 5, + '👳🏽‍♂️': 5, + '👳🏾‍♀️': 5, + '👳🏾‍♂️': 5, + '👳🏿‍♀️': 5, + '👳🏿‍♂️': 5, + '👷‍♀️': 3, + '👷‍♂️': 3, + '👷🏻‍♀️': 5, + '👷🏻‍♂️': 5, + '👷🏼‍♀️': 5, + '👷🏼‍♂️': 5, + '👷🏽‍♀️': 5, + '👷🏽‍♂️': 5, + '👷🏾‍♀️': 5, + '👷🏾‍♂️': 5, + '👷🏿‍♀️': 5, + '👷🏿‍♂️': 5, + '💁‍♀️': 3, + '💁‍♂️': 3, + '💁🏻‍♀️': 5, + '💁🏻‍♂️': 5, + '💁🏼‍♀️': 5, + '💁🏼‍♂️': 5, + '💁🏽‍♀️': 5, + '💁🏽‍♂️': 5, + '💁🏾‍♀️': 5, + '💁🏾‍♂️': 5, + '💁🏿‍♀️': 5, + '💁🏿‍♂️': 5, + '💂‍♀️': 3, + '💂‍♂️': 3, + '💂🏻‍♀️': 5, + '💂🏻‍♂️': 5, + '💂🏼‍♀️': 5, + '💂🏼‍♂️': 5, + '💂🏽‍♀️': 5, + '💂🏽‍♂️': 5, + '💂🏾‍♀️': 5, + '💂🏾‍♂️': 5, + '💂🏿‍♀️': 5, + '💂🏿‍♂️': 5, + '💆‍♀️': 3, + '💆‍♂️': 3, + '💆🏻‍♀️': 5, + '💆🏻‍♂️': 5, + '💆🏼‍♀️': 5, + '💆🏼‍♂️': 5, + '💆🏽‍♀️': 5, + '💆🏽‍♂️': 5, + '💆🏾‍♀️': 5, + '💆🏾‍♂️': 5, + '💆🏿‍♀️': 5, + '💆🏿‍♂️': 5, + '💇‍♀️': 3, + '💇‍♂️': 3, + '💇🏻‍♀️': 5, + '💇🏻‍♂️': 5, + '💇🏼‍♀️': 5, + '💇🏼‍♂️': 5, + '💇🏽‍♀️': 5, + '💇🏽‍♂️': 5, + '💇🏾‍♀️': 5, + '💇🏾‍♂️': 5, + '💇🏿‍♀️': 5, + '💇🏿‍♂️': 5, + '🕵🏻‍♀️': 4, + '🕵🏻‍♂️': 4, + '🕵🏼‍♀️': 4, + '🕵🏼‍♂️': 4, + '🕵🏽‍♀️': 4, + '🕵🏽‍♂️': 4, + '🕵🏾‍♀️': 4, + '🕵🏾‍♂️': 4, + '🕵🏿‍♀️': 4, + '🕵🏿‍♂️': 4, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 3, + '🙂‍↔️': 3, + '🙂‍↕️': 3, + '🙅‍♀️': 3, + '🙅‍♂️': 3, + '🙅🏻‍♀️': 5, + '🙅🏻‍♂️': 5, + '🙅🏼‍♀️': 5, + '🙅🏼‍♂️': 5, + '🙅🏽‍♀️': 5, + '🙅🏽‍♂️': 5, + '🙅🏾‍♀️': 5, + '🙅🏾‍♂️': 5, + '🙅🏿‍♀️': 5, + '🙅🏿‍♂️': 5, + '🙆‍♀️': 3, + '🙆‍♂️': 3, + '🙆🏻‍♀️': 5, + '🙆🏻‍♂️': 5, + '🙆🏼‍♀️': 5, + '🙆🏼‍♂️': 5, + '🙆🏽‍♀️': 5, + '🙆🏽‍♂️': 5, + '🙆🏾‍♀️': 5, + '🙆🏾‍♂️': 5, + '🙆🏿‍♀️': 5, + '🙆🏿‍♂️': 5, + '🙇‍♀️': 3, + '🙇‍♂️': 3, + '🙇🏻‍♀️': 5, + '🙇🏻‍♂️': 5, + '🙇🏼‍♀️': 5, + '🙇🏼‍♂️': 5, + '🙇🏽‍♀️': 5, + '🙇🏽‍♂️': 5, + '🙇🏾‍♀️': 5, + '🙇🏾‍♂️': 5, + '🙇🏿‍♀️': 5, + '🙇🏿‍♂️': 5, + '🙋‍♀️': 3, + '🙋‍♂️': 3, + '🙋🏻‍♀️': 5, + '🙋🏻‍♂️': 5, + '🙋🏼‍♀️': 5, + '🙋🏼‍♂️': 5, + '🙋🏽‍♀️': 5, + '🙋🏽‍♂️': 5, + '🙋🏾‍♀️': 5, + '🙋🏾‍♂️': 5, + '🙋🏿‍♀️': 5, + '🙋🏿‍♂️': 5, + '🙍‍♀️': 3, + '🙍‍♂️': 3, + '🙍🏻‍♀️': 5, + '🙍🏻‍♂️': 5, + '🙍🏼‍♀️': 5, + '🙍🏼‍♂️': 5, + '🙍🏽‍♀️': 5, + '🙍🏽‍♂️': 5, + '🙍🏾‍♀️': 5, + '🙍🏾‍♂️': 5, + '🙍🏿‍♀️': 5, + '🙍🏿‍♂️': 5, + '🙎‍♀️': 3, + '🙎‍♂️': 3, + '🙎🏻‍♀️': 5, + '🙎🏻‍♂️': 5, + '🙎🏼‍♀️': 5, + '🙎🏼‍♂️': 5, + '🙎🏽‍♀️': 5, + '🙎🏽‍♂️': 5, + '🙎🏾‍♀️': 5, + '🙎🏾‍♂️': 5, + '🙎🏿‍♀️': 5, + '🙎🏿‍♂️': 5, + '🚣‍♀️': 3, + '🚣‍♂️': 3, + '🚣🏻‍♀️': 5, + '🚣🏻‍♂️': 5, + '🚣🏼‍♀️': 5, + '🚣🏼‍♂️': 5, + '🚣🏽‍♀️': 5, + '🚣🏽‍♂️': 5, + '🚣🏾‍♀️': 5, + '🚣🏾‍♂️': 5, + '🚣🏿‍♀️': 5, + '🚣🏿‍♂️': 5, + '🚴‍♀️': 3, + '🚴‍♂️': 3, + '🚴🏻‍♀️': 5, + '🚴🏻‍♂️': 5, + '🚴🏼‍♀️': 5, + '🚴🏼‍♂️': 5, + '🚴🏽‍♀️': 5, + '🚴🏽‍♂️': 5, + '🚴🏾‍♀️': 5, + '🚴🏾‍♂️': 5, + '🚴🏿‍♀️': 5, + '🚴🏿‍♂️': 5, + '🚵‍♀️': 3, + '🚵‍♂️': 3, + '🚵🏻‍♀️': 5, + '🚵🏻‍♂️': 5, + '🚵🏼‍♀️': 5, + '🚵🏼‍♂️': 5, + '🚵🏽‍♀️': 5, + '🚵🏽‍♂️': 5, + '🚵🏾‍♀️': 5, + '🚵🏾‍♂️': 5, + '🚵🏿‍♀️': 5, + '🚵🏿‍♂️': 5, + '🚶‍♀️': 3, + '🚶‍♀️‍➡️': 4, + '🚶‍♂️': 3, + '🚶‍♂️‍➡️': 4, + '🚶‍➡️': 3, + '🚶🏻‍♀️': 5, + '🚶🏻‍♀️‍➡️': 6, + '🚶🏻‍♂️': 5, + '🚶🏻‍♂️‍➡️': 6, + '🚶🏻‍➡️': 5, + '🚶🏼‍♀️': 5, + '🚶🏼‍♀️‍➡️': 6, + '🚶🏼‍♂️': 5, + '🚶🏼‍♂️‍➡️': 6, + '🚶🏼‍➡️': 5, + '🚶🏽‍♀️': 5, + '🚶🏽‍♀️‍➡️': 6, + '🚶🏽‍♂️': 5, + '🚶🏽‍♂️‍➡️': 6, + '🚶🏽‍➡️': 5, + '🚶🏾‍♀️': 5, + '🚶🏾‍♀️‍➡️': 6, + '🚶🏾‍♂️': 5, + '🚶🏾‍♂️‍➡️': 6, + '🚶🏾‍➡️': 5, + '🚶🏿‍♀️': 5, + '🚶🏿‍♀️‍➡️': 6, + '🚶🏿‍♂️': 5, + '🚶🏿‍♂️‍➡️': 6, + '🚶🏿‍➡️': 5, + '🤦‍♀️': 3, + '🤦‍♂️': 3, + '🤦🏻‍♀️': 5, + '🤦🏻‍♂️': 5, + '🤦🏼‍♀️': 5, + '🤦🏼‍♂️': 5, + '🤦🏽‍♀️': 5, + '🤦🏽‍♂️': 5, + '🤦🏾‍♀️': 5, + '🤦🏾‍♂️': 5, + '🤦🏿‍♀️': 5, + '🤦🏿‍♂️': 5, + '🤵‍♀️': 3, + '🤵‍♂️': 3, + '🤵🏻‍♀️': 5, + '🤵🏻‍♂️': 5, + '🤵🏼‍♀️': 5, + '🤵🏼‍♂️': 5, + '🤵🏽‍♀️': 5, + '🤵🏽‍♂️': 5, + '🤵🏾‍♀️': 5, + '🤵🏾‍♂️': 5, + '🤵🏿‍♀️': 5, + '🤵🏿‍♂️': 5, + '🤷‍♀️': 3, + '🤷‍♂️': 3, + '🤷🏻‍♀️': 5, + '🤷🏻‍♂️': 5, + '🤷🏼‍♀️': 5, + '🤷🏼‍♂️': 5, + '🤷🏽‍♀️': 5, + '🤷🏽‍♂️': 5, + '🤷🏾‍♀️': 5, + '🤷🏾‍♂️': 5, + '🤷🏿‍♀️': 5, + '🤷🏿‍♂️': 5, + '🤸‍♀️': 3, + '🤸‍♂️': 3, + '🤸🏻‍♀️': 5, + '🤸🏻‍♂️': 5, + '🤸🏼‍♀️': 5, + '🤸🏼‍♂️': 5, + '🤸🏽‍♀️': 5, + '🤸🏽‍♂️': 5, + '🤸🏾‍♀️': 5, + '🤸🏾‍♂️': 5, + '🤸🏿‍♀️': 5, + '🤸🏿‍♂️': 5, + '🤹‍♀️': 3, + '🤹‍♂️': 3, + '🤹🏻‍♀️': 5, + '🤹🏻‍♂️': 5, + '🤹🏼‍♀️': 5, + '🤹🏼‍♂️': 5, + '🤹🏽‍♀️': 5, + '🤹🏽‍♂️': 5, + '🤹🏾‍♀️': 5, + '🤹🏾‍♂️': 5, + '🤹🏿‍♀️': 5, + '🤹🏿‍♂️': 5, + '🤼‍♀️': 3, + '🤼‍♂️': 3, + '🤼🏻‍♀️': 5, + '🤼🏻‍♂️': 5, + '🤼🏼‍♀️': 5, + '🤼🏼‍♂️': 5, + '🤼🏽‍♀️': 5, + '🤼🏽‍♂️': 5, + '🤼🏾‍♀️': 5, + '🤼🏾‍♂️': 5, + '🤼🏿‍♀️': 5, + '🤼🏿‍♂️': 5, + '🤽‍♀️': 3, + '🤽‍♂️': 3, + '🤽🏻‍♀️': 5, + '🤽🏻‍♂️': 5, + '🤽🏼‍♀️': 5, + '🤽🏼‍♂️': 5, + '🤽🏽‍♀️': 5, + '🤽🏽‍♂️': 5, + '🤽🏾‍♀️': 5, + '🤽🏾‍♂️': 5, + '🤽🏿‍♀️': 5, + '🤽🏿‍♂️': 5, + '🤾‍♀️': 3, + '🤾‍♂️': 3, + '🤾🏻‍♀️': 5, + '🤾🏻‍♂️': 5, + '🤾🏼‍♀️': 5, + '🤾🏼‍♂️': 5, + '🤾🏽‍♀️': 5, + '🤾🏽‍♂️': 5, + '🤾🏾‍♀️': 5, + '🤾🏾‍♂️': 5, + '🤾🏿‍♀️': 5, + '🤾🏿‍♂️': 5, + '🦸‍♀️': 3, + '🦸‍♂️': 3, + '🦸🏻‍♀️': 5, + '🦸🏻‍♂️': 5, + '🦸🏼‍♀️': 5, + '🦸🏼‍♂️': 5, + '🦸🏽‍♀️': 5, + '🦸🏽‍♂️': 5, + '🦸🏾‍♀️': 5, + '🦸🏾‍♂️': 5, + '🦸🏿‍♀️': 5, + '🦸🏿‍♂️': 5, + '🦹‍♀️': 3, + '🦹‍♂️': 3, + '🦹🏻‍♀️': 5, + '🦹🏻‍♂️': 5, + '🦹🏼‍♀️': 5, + '🦹🏼‍♂️': 5, + '🦹🏽‍♀️': 5, + '🦹🏽‍♂️': 5, + '🦹🏾‍♀️': 5, + '🦹🏾‍♂️': 5, + '🦹🏿‍♀️': 5, + '🦹🏿‍♂️': 5, + '🧍‍♀️': 3, + '🧍‍♂️': 3, + '🧍🏻‍♀️': 5, + '🧍🏻‍♂️': 5, + '🧍🏼‍♀️': 5, + '🧍🏼‍♂️': 5, + '🧍🏽‍♀️': 5, + '🧍🏽‍♂️': 5, + '🧍🏾‍♀️': 5, + '🧍🏾‍♂️': 5, + '🧍🏿‍♀️': 5, + '🧍🏿‍♂️': 5, + '🧎‍♀️': 3, + '🧎‍♀️‍➡️': 4, + '🧎‍♂️': 3, + '🧎‍♂️‍➡️': 4, + '🧎‍➡️': 3, + '🧎🏻‍♀️': 5, + '🧎🏻‍♀️‍➡️': 6, + '🧎🏻‍♂️': 5, + '🧎🏻‍♂️‍➡️': 6, + '🧎🏻‍➡️': 5, + '🧎🏼‍♀️': 5, + '🧎🏼‍♀️‍➡️': 6, + '🧎🏼‍♂️': 5, + '🧎🏼‍♂️‍➡️': 6, + '🧎🏼‍➡️': 5, + '🧎🏽‍♀️': 5, + '🧎🏽‍♀️‍➡️': 6, + '🧎🏽‍♂️': 5, + '🧎🏽‍♂️‍➡️': 6, + '🧎🏽‍➡️': 5, + '🧎🏾‍♀️': 5, + '🧎🏾‍♀️‍➡️': 6, + '🧎🏾‍♂️': 5, + '🧎🏾‍♂️‍➡️': 6, + '🧎🏾‍➡️': 5, + '🧎🏿‍♀️': 5, + '🧎🏿‍♀️‍➡️': 6, + '🧎🏿‍♂️': 5, + '🧎🏿‍♂️‍➡️': 6, + '🧎🏿‍➡️': 5, + '🧏‍♀️': 3, + '🧏‍♂️': 3, + '🧏🏻‍♀️': 5, + '🧏🏻‍♂️': 5, + '🧏🏼‍♀️': 5, + '🧏🏼‍♂️': 5, + '🧏🏽‍♀️': 5, + '🧏🏽‍♂️': 5, + '🧏🏾‍♀️': 5, + '🧏🏾‍♂️': 5, + '🧏🏿‍♀️': 5, + '🧏🏿‍♂️': 5, + '🧑‍⚕️': 3, + '🧑‍⚖️': 3, + '🧑‍✈️': 3, + '🧑‍🌾': 4, + '🧑‍🍳': 4, + '🧑‍🍼': 4, + '🧑‍🎄': 4, + '🧑‍🎓': 4, + '🧑‍🎤': 4, + '🧑‍🎨': 4, + '🧑‍🏫': 4, + '🧑‍🏭': 4, + '🧑‍💻': 4, + '🧑‍💼': 4, + '🧑‍🔧': 4, + '🧑‍🔬': 4, + '🧑‍🚀': 4, + '🧑‍🚒': 4, + '🧑‍🤝‍🧑': 6, + '🧑‍🦯': 4, + '🧑‍🦯‍➡️': 5, + '🧑‍🦰': 4, + '🧑‍🦱': 4, + '🧑‍🦲': 4, + '🧑‍🦳': 4, + '🧑‍🦼': 4, + '🧑‍🦼‍➡️': 5, + '🧑‍🦽': 4, + '🧑‍🦽‍➡️': 5, + '🧑‍🧑‍🧒': 6, + '🧑‍🧑‍🧒‍🧒': 8, + '🧑‍🧒': 4, + '🧑‍🧒‍🧒': 6, + '🧑‍🩰': 4, + '🧑🏻‍⚕️': 5, + '🧑🏻‍⚖️': 5, + '🧑🏻‍✈️': 5, + '🧑🏻‍❤️‍💋‍🧑🏿': 11, + '🧑🏻‍🌾': 6, + '🧑🏻‍🍳': 6, + '🧑🏻‍🍼': 6, + '🧑🏻‍🎄': 6, + '🧑🏻‍🎓': 6, + '🧑🏻‍🎤': 6, + '🧑🏻‍🎨': 6, + '🧑🏻‍🏫': 6, + '🧑🏻‍🏭': 6, + '🧑🏻‍🐰‍🧑🏽': 10, + '🧑🏻‍🐰‍🧑🏾': 10, + '🧑🏻‍🐰‍🧑🏿': 10, + '🧑🏻‍💻': 6, + '🧑🏻‍💼': 6, + '🧑🏻‍🔧': 6, + '🧑🏻‍🔬': 6, + '🧑🏻‍🚀': 6, + '🧑🏻‍🚒': 6, + '🧑🏻‍🤝‍🧑🏻': 10, + '🧑🏻‍🤝‍🧑🏼': 10, + '🧑🏻‍🤝‍🧑🏽': 10, + '🧑🏻‍🤝‍🧑🏾': 10, + '🧑🏻‍🤝‍🧑🏿': 10, + '🧑🏻‍🦯': 6, + '🧑🏻‍🦯‍➡️': 7, + '🧑🏻‍🦰': 6, + '🧑🏻‍🦱': 6, + '🧑🏻‍🦲': 6, + '🧑🏻‍🦳': 6, + '🧑🏻‍🦼': 6, + '🧑🏻‍🦼‍➡️': 7, + '🧑🏻‍🦽': 6, + '🧑🏻‍🦽‍➡️': 7, + '🧑🏻‍🩰': 6, + '🧑🏻‍🫯‍🧑🏼': 9, + '🧑🏻‍🫯‍🧑🏽': 9, + '🧑🏻‍🫯‍🧑🏾': 9, + '🧑🏻‍🫯‍🧑🏿': 9, + '🧑🏼‍⚕️': 5, + '🧑🏼‍⚖️': 5, + '🧑🏼‍✈️': 5, + '🧑🏼‍🌾': 6, + '🧑🏼‍🍳': 6, + '🧑🏼‍🍼': 6, + '🧑🏼‍🎄': 6, + '🧑🏼‍🎓': 6, + '🧑🏼‍🎤': 6, + '🧑🏼‍🎨': 6, + '🧑🏼‍🏫': 6, + '🧑🏼‍🏭': 6, + '🧑🏼‍🐰‍🧑🏿': 10, + '🧑🏼‍💻': 6, + '🧑🏼‍💼': 6, + '🧑🏼‍🔧': 6, + '🧑🏼‍🔬': 6, + '🧑🏼‍🚀': 6, + '🧑🏼‍🚒': 6, + '🧑🏼‍🤝‍🧑🏻': 10, + '🧑🏼‍🤝‍🧑🏼': 10, + '🧑🏼‍🤝‍🧑🏽': 10, + '🧑🏼‍🤝‍🧑🏿': 10, + '🧑🏼‍🦯': 6, + '🧑🏼‍🦯‍➡️': 7, + '🧑🏼‍🦰': 6, + '🧑🏼‍🦱': 6, + '🧑🏼‍🦲': 6, + '🧑🏼‍🦳': 6, + '🧑🏼‍🦼': 6, + '🧑🏼‍🦽': 6, + '🧑🏼‍🩰': 6, + '🧑🏼‍🫯‍🧑🏻': 9, + '🧑🏼‍🫯‍🧑🏽': 9, + '🧑🏼‍🫯‍🧑🏿': 9, + '🧑🏽‍⚕️': 5, + '🧑🏽‍⚖️': 5, + '🧑🏽‍✈️': 5, + '🧑🏽‍❤️‍💋‍🧑🏿': 11, + '🧑🏽‍🌾': 6, + '🧑🏽‍🍳': 6, + '🧑🏽‍🍼': 6, + '🧑🏽‍🎄': 6, + '🧑🏽‍🎓': 6, + '🧑🏽‍🎤': 6, + '🧑🏽‍🎨': 6, + '🧑🏽‍🏫': 6, + '🧑🏽‍🏭': 6, + '🧑🏽‍🐰‍🧑🏻': 10, + '🧑🏽‍🐰‍🧑🏿': 10, + '🧑🏽‍💻': 6, + '🧑🏽‍💼': 6, + '🧑🏽‍🔧': 6, + '🧑🏽‍🔬': 6, + '🧑🏽‍🚀': 6, + '🧑🏽‍🚒': 6, + '🧑🏽‍🤝‍🧑🏻': 10, + '🧑🏽‍🤝‍🧑🏼': 10, + '🧑🏽‍🤝‍🧑🏽': 10, + '🧑🏽‍🤝‍🧑🏾': 10, + '🧑🏽‍🤝‍🧑🏿': 10, + '🧑🏽‍🦯': 6, + '🧑🏽‍🦯‍➡️': 7, + '🧑🏽‍🦰': 6, + '🧑🏽‍🦱': 6, + '🧑🏽‍🦲': 6, + '🧑🏽‍🦳': 6, + '🧑🏽‍🦼': 6, + '🧑🏽‍🦼‍➡️': 7, + '🧑🏽‍🦽': 6, + '🧑🏽‍🦽‍➡️': 7, + '🧑🏽‍🩰': 6, + '🧑🏽‍🫯‍🧑🏻': 9, + '🧑🏽‍🫯‍🧑🏼': 9, + '🧑🏽‍🫯‍🧑🏾': 9, + '🧑🏽‍🫯‍🧑🏿': 9, + '🧑🏾‍⚕️': 5, + '🧑🏾‍⚖️': 5, + '🧑🏾‍✈️': 5, + '🧑🏾‍🌾': 6, + '🧑🏾‍🍳': 6, + '🧑🏾‍🍼': 6, + '🧑🏾‍🎄': 6, + '🧑🏾‍🎓': 6, + '🧑🏾‍🎤': 6, + '🧑🏾‍🎨': 6, + '🧑🏾‍🏫': 6, + '🧑🏾‍🏭': 6, + '🧑🏾‍🐰‍🧑🏻': 10, + '🧑🏾‍🐰‍🧑🏿': 10, + '🧑🏾‍💻': 6, + '🧑🏾‍💼': 6, + '🧑🏾‍🔧': 6, + '🧑🏾‍🔬': 6, + '🧑🏾‍🚀': 6, + '🧑🏾‍🚒': 6, + '🧑🏾‍🤝‍🧑🏻': 10, + '🧑🏾‍🤝‍🧑🏽': 10, + '🧑🏾‍🤝‍🧑🏾': 10, + '🧑🏾‍🤝‍🧑🏿': 10, + '🧑🏾‍🦯': 6, + '🧑🏾‍🦯‍➡️': 7, + '🧑🏾‍🦰': 6, + '🧑🏾‍🦱': 6, + '🧑🏾‍🦲': 6, + '🧑🏾‍🦳': 6, + '🧑🏾‍🦼': 6, + '🧑🏾‍🦽': 6, + '🧑🏾‍🩰': 6, + '🧑🏾‍🫯‍🧑🏻': 9, + '🧑🏾‍🫯‍🧑🏽': 9, + '🧑🏾‍🫯‍🧑🏿': 9, + '🧑🏿‍⚕️': 5, + '🧑🏿‍⚖️': 5, + '🧑🏿‍✈️': 5, + '🧑🏿‍❤️‍💋‍🧑🏻': 11, + '🧑🏿‍❤️‍💋‍🧑🏽': 11, + '🧑🏿‍🌾': 6, + '🧑🏿‍🍳': 6, + '🧑🏿‍🍼': 6, + '🧑🏿‍🎄': 6, + '🧑🏿‍🎓': 6, + '🧑🏿‍🎤': 6, + '🧑🏿‍🎨': 6, + '🧑🏿‍🏫': 6, + '🧑🏿‍🏭': 6, + '🧑🏿‍🐰‍🧑🏻': 10, + '🧑🏿‍🐰‍🧑🏼': 10, + '🧑🏿‍🐰‍🧑🏽': 10, + '🧑🏿‍🐰‍🧑🏾': 10, + '🧑🏿‍💻': 6, + '🧑🏿‍💼': 6, + '🧑🏿‍🔧': 6, + '🧑🏿‍🔬': 6, + '🧑🏿‍🚀': 6, + '🧑🏿‍🚒': 6, + '🧑🏿‍🤝‍🧑🏻': 10, + '🧑🏿‍🤝‍🧑🏼': 10, + '🧑🏿‍🤝‍🧑🏽': 10, + '🧑🏿‍🤝‍🧑🏾': 10, + '🧑🏿‍🤝‍🧑🏿': 10, + '🧑🏿‍🦯': 6, + '🧑🏿‍🦯‍➡️': 7, + '🧑🏿‍🦰': 6, + '🧑🏿‍🦱': 6, + '🧑🏿‍🦲': 6, + '🧑🏿‍🦳': 6, + '🧑🏿‍🦼': 6, + '🧑🏿‍🦼‍➡️': 7, + '🧑🏿‍🦽': 6, + '🧑🏿‍🦽‍➡️': 7, + '🧑🏿‍🩰': 6, + '🧑🏿‍🫯‍🧑🏻': 9, + '🧑🏿‍🫯‍🧑🏼': 9, + '🧑🏿‍🫯‍🧑🏽': 9, + '🧑🏿‍🫯‍🧑🏾': 9, + '🧔‍♀️': 3, + '🧔‍♂️': 3, + '🧔🏻‍♀️': 5, + '🧔🏻‍♂️': 5, + '🧔🏼‍♀️': 5, + '🧔🏼‍♂️': 5, + '🧔🏽‍♀️': 5, + '🧔🏽‍♂️': 5, + '🧔🏾‍♀️': 5, + '🧔🏾‍♂️': 5, + '🧔🏿‍♀️': 5, + '🧔🏿‍♂️': 5, + '🧖‍♀️': 3, + '🧖‍♂️': 3, + '🧖🏻‍♀️': 5, + '🧖🏻‍♂️': 5, + '🧖🏼‍♀️': 5, + '🧖🏼‍♂️': 5, + '🧖🏽‍♀️': 5, + '🧖🏽‍♂️': 5, + '🧖🏾‍♀️': 5, + '🧖🏾‍♂️': 5, + '🧖🏿‍♀️': 5, + '🧖🏿‍♂️': 5, + '🧗‍♀️': 3, + '🧗‍♂️': 3, + '🧗🏻‍♀️': 5, + '🧗🏻‍♂️': 5, + '🧗🏼‍♀️': 5, + '🧗🏼‍♂️': 5, + '🧗🏽‍♀️': 5, + '🧗🏽‍♂️': 5, + '🧗🏾‍♀️': 5, + '🧗🏾‍♂️': 5, + '🧗🏿‍♀️': 5, + '🧗🏿‍♂️': 5, + '🧘‍♀️': 3, + '🧘‍♂️': 3, + '🧘🏻‍♀️': 5, + '🧘🏻‍♂️': 5, + '🧘🏼‍♀️': 5, + '🧘🏼‍♂️': 5, + '🧘🏽‍♀️': 5, + '🧘🏽‍♂️': 5, + '🧘🏾‍♀️': 5, + '🧘🏾‍♂️': 5, + '🧘🏿‍♀️': 5, + '🧘🏿‍♂️': 5, + '🧙‍♀️': 3, + '🧙‍♂️': 3, + '🧙🏻‍♀️': 5, + '🧙🏻‍♂️': 5, + '🧙🏼‍♀️': 5, + '🧙🏼‍♂️': 5, + '🧙🏽‍♀️': 5, + '🧙🏽‍♂️': 5, + '🧙🏾‍♀️': 5, + '🧙🏾‍♂️': 5, + '🧙🏿‍♀️': 5, + '🧙🏿‍♂️': 5, + '🧚‍♀️': 3, + '🧚‍♂️': 3, + '🧚🏻‍♀️': 5, + '🧚🏻‍♂️': 5, + '🧚🏼‍♀️': 5, + '🧚🏼‍♂️': 5, + '🧚🏽‍♀️': 5, + '🧚🏽‍♂️': 5, + '🧚🏾‍♀️': 5, + '🧚🏾‍♂️': 5, + '🧚🏿‍♀️': 5, + '🧚🏿‍♂️': 5, + '🧛‍♀️': 3, + '🧛‍♂️': 3, + '🧛🏻‍♀️': 5, + '🧛🏻‍♂️': 5, + '🧛🏼‍♀️': 5, + '🧛🏼‍♂️': 5, + '🧛🏽‍♀️': 5, + '🧛🏽‍♂️': 5, + '🧛🏾‍♀️': 5, + '🧛🏾‍♂️': 5, + '🧛🏿‍♀️': 5, + '🧛🏿‍♂️': 5, + '🧜‍♀️': 3, + '🧜‍♂️': 3, + '🧜🏻‍♀️': 5, + '🧜🏻‍♂️': 5, + '🧜🏼‍♀️': 5, + '🧜🏼‍♂️': 5, + '🧜🏽‍♀️': 5, + '🧜🏽‍♂️': 5, + '🧜🏾‍♀️': 5, + '🧜🏾‍♂️': 5, + '🧜🏿‍♀️': 5, + '🧜🏿‍♂️': 5, + '🧝‍♀️': 3, + '🧝‍♂️': 3, + '🧝🏻‍♀️': 5, + '🧝🏻‍♂️': 5, + '🧝🏼‍♀️': 5, + '🧝🏼‍♂️': 5, + '🧝🏽‍♀️': 5, + '🧝🏽‍♂️': 5, + '🧝🏾‍♀️': 5, + '🧝🏾‍♂️': 5, + '🧝🏿‍♀️': 5, + '🧝🏿‍♂️': 5, + '🧞‍♀️': 3, + '🧞‍♂️': 3, + '🧟‍♀️': 3, + '🧟‍♂️': 3, + '🫱🏻‍🫲🏼': 8, + '🫱🏻‍🫲🏽': 8, + '🫱🏻‍🫲🏾': 8, + '🫱🏻‍🫲🏿': 8, + '🫱🏼‍🫲🏻': 8, + '🫱🏼‍🫲🏽': 8, + '🫱🏼‍🫲🏾': 8, + '🫱🏼‍🫲🏿': 8, + '🫱🏽‍🫲🏻': 8, + '🫱🏽‍🫲🏼': 8, + '🫱🏽‍🫲🏾': 8, + '🫱🏽‍🫲🏿': 8, + '🫱🏾‍🫲🏻': 8, + '🫱🏾‍🫲🏼': 8, + '🫱🏾‍🫲🏽': 8, + '🫱🏾‍🫲🏿': 8, + '🫱🏿‍🫲🏻': 8, + '🫱🏿‍🫲🏼': 8, + '🫱🏿‍🫲🏽': 8, + '🫱🏿‍🫲🏾': 8, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_813fee16.py b/libs/wcwidth/table_grapheme_overrides/_known_813fee16.py new file mode 100644 index 000000000..3bf260da1 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_813fee16.py @@ -0,0 +1,2570 @@ +""" +Grapheme overrides for _known_813fee16. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: st, urxvt, xterm +""" +# pylint: skip-file +GRAPHEMES = { + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 3, + '⛹🏻‍♀️': 4, + '⛹🏻‍♂️': 4, + '⛹🏼‍♀️': 4, + '⛹🏼‍♂️': 4, + '⛹🏽‍♀️': 4, + '⛹🏽‍♂️': 4, + '⛹🏾‍♀️': 4, + '⛹🏾‍♂️': 4, + '⛹🏿‍♀️': 4, + '⛹🏿‍♂️': 4, + '❤️‍🔥': 3, + '❤️‍🩹': 3, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 3, + '🏃‍♀️‍➡️': 4, + '🏃‍♂️': 3, + '🏃‍♂️‍➡️': 4, + '🏃‍➡️': 3, + '🏃🏻‍♀️': 5, + '🏃🏻‍♀️‍➡️': 6, + '🏃🏻‍♂️': 5, + '🏃🏻‍♂️‍➡️': 6, + '🏃🏻‍➡️': 5, + '🏃🏼‍♀️': 5, + '🏃🏼‍♀️‍➡️': 6, + '🏃🏼‍♂️': 5, + '🏃🏼‍♂️‍➡️': 6, + '🏃🏼‍➡️': 5, + '🏃🏽‍♀️': 5, + '🏃🏽‍♀️‍➡️': 6, + '🏃🏽‍♂️': 5, + '🏃🏽‍♂️‍➡️': 6, + '🏃🏽‍➡️': 5, + '🏃🏾‍♀️': 5, + '🏃🏾‍♀️‍➡️': 6, + '🏃🏾‍♂️': 5, + '🏃🏾‍♂️‍➡️': 6, + '🏃🏾‍➡️': 5, + '🏃🏿‍♀️': 5, + '🏃🏿‍♀️‍➡️': 6, + '🏃🏿‍♂️': 5, + '🏃🏿‍♂️‍➡️': 6, + '🏃🏿‍➡️': 5, + '🏄‍♀️': 3, + '🏄‍♂️': 3, + '🏄🏻‍♀️': 5, + '🏄🏻‍♂️': 5, + '🏄🏼‍♀️': 5, + '🏄🏼‍♂️': 5, + '🏄🏽‍♀️': 5, + '🏄🏽‍♂️': 5, + '🏄🏾‍♀️': 5, + '🏄🏾‍♂️': 5, + '🏄🏿‍♀️': 5, + '🏄🏿‍♂️': 5, + '🏊‍♀️': 3, + '🏊‍♂️': 3, + '🏊🏻‍♀️': 5, + '🏊🏻‍♂️': 5, + '🏊🏼‍♀️': 5, + '🏊🏼‍♂️': 5, + '🏊🏽‍♀️': 5, + '🏊🏽‍♂️': 5, + '🏊🏾‍♀️': 5, + '🏊🏾‍♂️': 5, + '🏊🏿‍♀️': 5, + '🏊🏿‍♂️': 5, + '🏋🏻‍♀️': 4, + '🏋🏻‍♂️': 4, + '🏋🏼‍♀️': 4, + '🏋🏼‍♂️': 4, + '🏋🏽‍♀️': 4, + '🏋🏽‍♂️': 4, + '🏋🏾‍♀️': 4, + '🏋🏾‍♂️': 4, + '🏋🏿‍♀️': 4, + '🏋🏿‍♂️': 4, + '🏌🏻‍♀️': 4, + '🏌🏻‍♂️': 4, + '🏌🏼‍♀️': 4, + '🏌🏼‍♂️': 4, + '🏌🏽‍♀️': 4, + '🏌🏽‍♂️': 4, + '🏌🏾‍♀️': 4, + '🏌🏾‍♂️': 4, + '🏌🏿‍♀️': 4, + '🏌🏿‍♂️': 4, + '🏳️‍🌈': 3, + '🏴‍☠️': 3, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 3, + '👨‍⚕️': 3, + '👨‍⚖️': 3, + '👨‍✈️': 3, + '👨‍❤️‍👨': 5, + '👨‍❤️‍💋‍👨': 7, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 5, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 5, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 5, + '👨🏻‍⚕️': 5, + '👨🏻‍⚖️': 5, + '👨🏻‍✈️': 5, + '👨🏻‍❤️‍👨🏻': 9, + '👨🏻‍❤️‍👨🏽': 9, + '👨🏻‍❤️‍👨🏿': 9, + '👨🏻‍❤️‍💋‍👨🏻': 11, + '👨🏻‍❤️‍💋‍👨🏽': 11, + '👨🏻‍❤️‍💋‍👨🏾': 11, + '👨🏻‍❤️‍💋‍👨🏿': 11, + '👨🏻‍🌾': 6, + '👨🏻‍🍳': 6, + '👨🏻‍🍼': 6, + '👨🏻‍🎓': 6, + '👨🏻‍🎤': 6, + '👨🏻‍🎨': 6, + '👨🏻‍🏫': 6, + '👨🏻‍🏭': 6, + '👨🏻‍🐰‍👨🏼': 10, + '👨🏻‍🐰‍👨🏽': 10, + '👨🏻‍🐰‍👨🏾': 10, + '👨🏻‍🐰‍👨🏿': 10, + '👨🏻‍💻': 6, + '👨🏻‍💼': 6, + '👨🏻‍🔧': 6, + '👨🏻‍🔬': 6, + '👨🏻‍🚀': 6, + '👨🏻‍🚒': 6, + '👨🏻‍🤝‍👨🏼': 10, + '👨🏻‍🤝‍👨🏽': 10, + '👨🏻‍🤝‍👨🏾': 10, + '👨🏻‍🤝‍👨🏿': 10, + '👨🏻‍🦯': 6, + '👨🏻‍🦯‍➡️': 7, + '👨🏻‍🦰': 6, + '👨🏻‍🦱': 6, + '👨🏻‍🦲': 6, + '👨🏻‍🦳': 6, + '👨🏻‍🦼': 6, + '👨🏻‍🦼‍➡️': 7, + '👨🏻‍🦽': 6, + '👨🏻‍🦽‍➡️': 7, + '👨🏻‍🫯‍👨🏼': 10, + '👨🏻‍🫯‍👨🏽': 10, + '👨🏻‍🫯‍👨🏾': 10, + '👨🏻‍🫯‍👨🏿': 10, + '👨🏼‍⚕️': 5, + '👨🏼‍⚖️': 5, + '👨🏼‍✈️': 5, + '👨🏼‍❤️‍👨🏼': 9, + '👨🏼‍❤️‍💋‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏿': 11, + '👨🏼‍🌾': 6, + '👨🏼‍🍳': 6, + '👨🏼‍🍼': 6, + '👨🏼‍🎓': 6, + '👨🏼‍🎤': 6, + '👨🏼‍🎨': 6, + '👨🏼‍🏫': 6, + '👨🏼‍🏭': 6, + '👨🏼‍🐰‍👨🏻': 10, + '👨🏼‍🐰‍👨🏽': 10, + '👨🏼‍🐰‍👨🏿': 10, + '👨🏼‍💻': 6, + '👨🏼‍💼': 6, + '👨🏼‍🔧': 6, + '👨🏼‍🔬': 6, + '👨🏼‍🚀': 6, + '👨🏼‍🚒': 6, + '👨🏼‍🤝‍👨🏻': 10, + '👨🏼‍🤝‍👨🏽': 10, + '👨🏼‍🤝‍👨🏿': 10, + '👨🏼‍🦯': 6, + '👨🏼‍🦯‍➡️': 7, + '👨🏼‍🦰': 6, + '👨🏼‍🦱': 6, + '👨🏼‍🦲': 6, + '👨🏼‍🦳': 6, + '👨🏼‍🦼': 6, + '👨🏼‍🦽': 6, + '👨🏼‍🦽‍➡️': 7, + '👨🏼‍🫯‍👨🏻': 10, + '👨🏼‍🫯‍👨🏽': 10, + '👨🏼‍🫯‍👨🏾': 10, + '👨🏼‍🫯‍👨🏿': 10, + '👨🏽‍⚕️': 5, + '👨🏽‍⚖️': 5, + '👨🏽‍✈️': 5, + '👨🏽‍❤️‍👨🏻': 9, + '👨🏽‍❤️‍👨🏽': 9, + '👨🏽‍❤️‍👨🏿': 9, + '👨🏽‍❤️‍💋‍👨🏻': 11, + '👨🏽‍❤️‍💋‍👨🏽': 11, + '👨🏽‍❤️‍💋‍👨🏿': 11, + '👨🏽‍🌾': 6, + '👨🏽‍🍳': 6, + '👨🏽‍🍼': 6, + '👨🏽‍🎓': 6, + '👨🏽‍🎤': 6, + '👨🏽‍🎨': 6, + '👨🏽‍🏫': 6, + '👨🏽‍🏭': 6, + '👨🏽‍🐰‍👨🏻': 10, + '👨🏽‍🐰‍👨🏼': 10, + '👨🏽‍🐰‍👨🏾': 10, + '👨🏽‍🐰‍👨🏿': 10, + '👨🏽‍💻': 6, + '👨🏽‍💼': 6, + '👨🏽‍🔧': 6, + '👨🏽‍🔬': 6, + '👨🏽‍🚀': 6, + '👨🏽‍🚒': 6, + '👨🏽‍🤝‍👨🏻': 10, + '👨🏽‍🤝‍👨🏼': 10, + '👨🏽‍🤝‍👨🏾': 10, + '👨🏽‍🤝‍👨🏿': 10, + '👨🏽‍🦯': 6, + '👨🏽‍🦯‍➡️': 7, + '👨🏽‍🦰': 6, + '👨🏽‍🦱': 6, + '👨🏽‍🦲': 6, + '👨🏽‍🦳': 6, + '👨🏽‍🦼': 6, + '👨🏽‍🦼‍➡️': 7, + '👨🏽‍🦽': 6, + '👨🏽‍🦽‍➡️': 7, + '👨🏽‍🫯‍👨🏻': 10, + '👨🏽‍🫯‍👨🏼': 10, + '👨🏽‍🫯‍👨🏾': 10, + '👨🏽‍🫯‍👨🏿': 10, + '👨🏾‍⚕️': 5, + '👨🏾‍⚖️': 5, + '👨🏾‍✈️': 5, + '👨🏾‍❤️‍👨🏾': 9, + '👨🏾‍❤️‍💋‍👨🏻': 11, + '👨🏾‍❤️‍💋‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏿': 11, + '👨🏾‍🌾': 6, + '👨🏾‍🍳': 6, + '👨🏾‍🍼': 6, + '👨🏾‍🎓': 6, + '👨🏾‍🎤': 6, + '👨🏾‍🎨': 6, + '👨🏾‍🏫': 6, + '👨🏾‍🏭': 6, + '👨🏾‍🐰‍👨🏻': 10, + '👨🏾‍🐰‍👨🏽': 10, + '👨🏾‍🐰‍👨🏿': 10, + '👨🏾‍💻': 6, + '👨🏾‍💼': 6, + '👨🏾‍🔧': 6, + '👨🏾‍🔬': 6, + '👨🏾‍🚀': 6, + '👨🏾‍🚒': 6, + '👨🏾‍🤝‍👨🏻': 10, + '👨🏾‍🤝‍👨🏽': 10, + '👨🏾‍🤝‍👨🏿': 10, + '👨🏾‍🦯': 6, + '👨🏾‍🦯‍➡️': 7, + '👨🏾‍🦰': 6, + '👨🏾‍🦱': 6, + '👨🏾‍🦲': 6, + '👨🏾‍🦳': 6, + '👨🏾‍🦼': 6, + '👨🏾‍🦽': 6, + '👨🏾‍🦽‍➡️': 7, + '👨🏾‍🫯‍👨🏻': 10, + '👨🏾‍🫯‍👨🏼': 10, + '👨🏾‍🫯‍👨🏽': 10, + '👨🏾‍🫯‍👨🏿': 10, + '👨🏿‍⚕️': 5, + '👨🏿‍⚖️': 5, + '👨🏿‍✈️': 5, + '👨🏿‍❤️‍👨🏻': 9, + '👨🏿‍❤️‍👨🏽': 9, + '👨🏿‍❤️‍👨🏿': 9, + '👨🏿‍❤️‍💋‍👨🏻': 11, + '👨🏿‍❤️‍💋‍👨🏼': 11, + '👨🏿‍❤️‍💋‍👨🏽': 11, + '👨🏿‍❤️‍💋‍👨🏾': 11, + '👨🏿‍❤️‍💋‍👨🏿': 11, + '👨🏿‍🌾': 6, + '👨🏿‍🍳': 6, + '👨🏿‍🍼': 6, + '👨🏿‍🎓': 6, + '👨🏿‍🎤': 6, + '👨🏿‍🎨': 6, + '👨🏿‍🏫': 6, + '👨🏿‍🏭': 6, + '👨🏿‍🐰‍👨🏻': 10, + '👨🏿‍🐰‍👨🏼': 10, + '👨🏿‍🐰‍👨🏽': 10, + '👨🏿‍🐰‍👨🏾': 10, + '👨🏿‍💻': 6, + '👨🏿‍💼': 6, + '👨🏿‍🔧': 6, + '👨🏿‍🔬': 6, + '👨🏿‍🚀': 6, + '👨🏿‍🚒': 6, + '👨🏿‍🤝‍👨🏻': 10, + '👨🏿‍🤝‍👨🏼': 10, + '👨🏿‍🤝‍👨🏽': 10, + '👨🏿‍🤝‍👨🏾': 10, + '👨🏿‍🦯': 6, + '👨🏿‍🦯‍➡️': 7, + '👨🏿‍🦰': 6, + '👨🏿‍🦱': 6, + '👨🏿‍🦲': 6, + '👨🏿‍🦳': 6, + '👨🏿‍🦼': 6, + '👨🏿‍🦼‍➡️': 7, + '👨🏿‍🦽': 6, + '👨🏿‍🦽‍➡️': 7, + '👨🏿‍🫯‍👨🏻': 10, + '👨🏿‍🫯‍👨🏼': 10, + '👨🏿‍🫯‍👨🏽': 10, + '👨🏿‍🫯‍👨🏾': 10, + '👩‍⚕️': 3, + '👩‍⚖️': 3, + '👩‍✈️': 3, + '👩‍❤️‍👨': 5, + '👩‍❤️‍👩': 5, + '👩‍❤️‍💋‍👨': 7, + '👩‍❤️‍💋‍👩': 7, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 5, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 5, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 5, + '👩🏻‍⚕️': 5, + '👩🏻‍⚖️': 5, + '👩🏻‍✈️': 5, + '👩🏻‍❤️‍👨🏻': 9, + '👩🏻‍❤️‍👨🏿': 9, + '👩🏻‍❤️‍👩🏻': 9, + '👩🏻‍❤️‍💋‍👨🏻': 11, + '👩🏻‍❤️‍💋‍👨🏽': 11, + '👩🏻‍❤️‍💋‍👨🏿': 11, + '👩🏻‍❤️‍💋‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👩🏽': 11, + '👩🏻‍❤️‍💋‍👩🏿': 11, + '👩🏻‍🌾': 6, + '👩🏻‍🍳': 6, + '👩🏻‍🍼': 6, + '👩🏻‍🎓': 6, + '👩🏻‍🎤': 6, + '👩🏻‍🎨': 6, + '👩🏻‍🏫': 6, + '👩🏻‍🏭': 6, + '👩🏻‍🐰‍👩🏼': 10, + '👩🏻‍🐰‍👩🏽': 10, + '👩🏻‍🐰‍👩🏾': 10, + '👩🏻‍🐰‍👩🏿': 10, + '👩🏻‍💻': 6, + '👩🏻‍💼': 6, + '👩🏻‍🔧': 6, + '👩🏻‍🔬': 6, + '👩🏻‍🚀': 6, + '👩🏻‍🚒': 6, + '👩🏻‍🤝‍👨🏽': 10, + '👩🏻‍🤝‍👨🏿': 10, + '👩🏻‍🤝‍👩🏼': 10, + '👩🏻‍🤝‍👩🏽': 10, + '👩🏻‍🤝‍👩🏾': 10, + '👩🏻‍🤝‍👩🏿': 10, + '👩🏻‍🦯': 6, + '👩🏻‍🦯‍➡️': 7, + '👩🏻‍🦰': 6, + '👩🏻‍🦱': 6, + '👩🏻‍🦲': 6, + '👩🏻‍🦳': 6, + '👩🏻‍🦼': 6, + '👩🏻‍🦼‍➡️': 7, + '👩🏻‍🦽': 6, + '👩🏻‍🦽‍➡️': 7, + '👩🏻‍🫯‍👩🏼': 10, + '👩🏻‍🫯‍👩🏽': 10, + '👩🏻‍🫯‍👩🏾': 10, + '👩🏻‍🫯‍👩🏿': 10, + '👩🏼‍⚕️': 5, + '👩🏼‍⚖️': 5, + '👩🏼‍✈️': 5, + '👩🏼‍❤️‍👨🏼': 9, + '👩🏼‍❤️‍👩🏼': 9, + '👩🏼‍❤️‍💋‍👨🏼': 11, + '👩🏼‍❤️‍💋‍👩🏼': 11, + '👩🏼‍🌾': 6, + '👩🏼‍🍳': 6, + '👩🏼‍🍼': 6, + '👩🏼‍🎓': 6, + '👩🏼‍🎤': 6, + '👩🏼‍🎨': 6, + '👩🏼‍🏫': 6, + '👩🏼‍🏭': 6, + '👩🏼‍🐰‍👩🏻': 10, + '👩🏼‍🐰‍👩🏿': 10, + '👩🏼‍💻': 6, + '👩🏼‍💼': 6, + '👩🏼‍🔧': 6, + '👩🏼‍🔬': 6, + '👩🏼‍🚀': 6, + '👩🏼‍🚒': 6, + '👩🏼‍🤝‍👩🏻': 10, + '👩🏼‍🤝‍👩🏽': 10, + '👩🏼‍🤝‍👩🏿': 10, + '👩🏼‍🦯': 6, + '👩🏼‍🦯‍➡️': 7, + '👩🏼‍🦰': 6, + '👩🏼‍🦱': 6, + '👩🏼‍🦲': 6, + '👩🏼‍🦳': 6, + '👩🏼‍🦼': 6, + '👩🏼‍🦽': 6, + '👩🏼‍🫯‍👩🏻': 10, + '👩🏼‍🫯‍👩🏽': 10, + '👩🏼‍🫯‍👩🏾': 10, + '👩🏼‍🫯‍👩🏿': 10, + '👩🏽‍⚕️': 5, + '👩🏽‍⚖️': 5, + '👩🏽‍✈️': 5, + '👩🏽‍❤️‍👨🏽': 9, + '👩🏽‍❤️‍👩🏽': 9, + '👩🏽‍❤️‍💋‍👨🏻': 11, + '👩🏽‍❤️‍💋‍👨🏽': 11, + '👩🏽‍❤️‍💋‍👨🏿': 11, + '👩🏽‍❤️‍💋‍👩🏻': 11, + '👩🏽‍❤️‍💋‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👩🏿': 11, + '👩🏽‍🌾': 6, + '👩🏽‍🍳': 6, + '👩🏽‍🍼': 6, + '👩🏽‍🎓': 6, + '👩🏽‍🎤': 6, + '👩🏽‍🎨': 6, + '👩🏽‍🏫': 6, + '👩🏽‍🏭': 6, + '👩🏽‍🐰‍👩🏻': 10, + '👩🏽‍🐰‍👩🏾': 10, + '👩🏽‍🐰‍👩🏿': 10, + '👩🏽‍💻': 6, + '👩🏽‍💼': 6, + '👩🏽‍🔧': 6, + '👩🏽‍🔬': 6, + '👩🏽‍🚀': 6, + '👩🏽‍🚒': 6, + '👩🏽‍🤝‍👨🏻': 10, + '👩🏽‍🤝‍👨🏿': 10, + '👩🏽‍🤝‍👩🏻': 10, + '👩🏽‍🤝‍👩🏼': 10, + '👩🏽‍🤝‍👩🏾': 10, + '👩🏽‍🤝‍👩🏿': 10, + '👩🏽‍🦯': 6, + '👩🏽‍🦯‍➡️': 7, + '👩🏽‍🦰': 6, + '👩🏽‍🦱': 6, + '👩🏽‍🦲': 6, + '👩🏽‍🦳': 6, + '👩🏽‍🦼': 6, + '👩🏽‍🦼‍➡️': 7, + '👩🏽‍🦽': 6, + '👩🏽‍🦽‍➡️': 7, + '👩🏽‍🫯‍👩🏻': 10, + '👩🏽‍🫯‍👩🏼': 10, + '👩🏽‍🫯‍👩🏾': 10, + '👩🏽‍🫯‍👩🏿': 10, + '👩🏾‍⚕️': 5, + '👩🏾‍⚖️': 5, + '👩🏾‍✈️': 5, + '👩🏾‍❤️‍👨🏾': 9, + '👩🏾‍❤️‍👩🏾': 9, + '👩🏾‍❤️‍💋‍👨🏾': 11, + '👩🏾‍❤️‍💋‍👩🏾': 11, + '👩🏾‍🌾': 6, + '👩🏾‍🍳': 6, + '👩🏾‍🍼': 6, + '👩🏾‍🎓': 6, + '👩🏾‍🎤': 6, + '👩🏾‍🎨': 6, + '👩🏾‍🏫': 6, + '👩🏾‍🏭': 6, + '👩🏾‍🐰‍👩🏻': 10, + '👩🏾‍🐰‍👩🏽': 10, + '👩🏾‍🐰‍👩🏿': 10, + '👩🏾‍💻': 6, + '👩🏾‍💼': 6, + '👩🏾‍🔧': 6, + '👩🏾‍🔬': 6, + '👩🏾‍🚀': 6, + '👩🏾‍🚒': 6, + '👩🏾‍🤝‍👩🏻': 10, + '👩🏾‍🤝‍👩🏽': 10, + '👩🏾‍🤝‍👩🏿': 10, + '👩🏾‍🦯': 6, + '👩🏾‍🦯‍➡️': 7, + '👩🏾‍🦰': 6, + '👩🏾‍🦱': 6, + '👩🏾‍🦲': 6, + '👩🏾‍🦳': 6, + '👩🏾‍🦼': 6, + '👩🏾‍🦽': 6, + '👩🏾‍🦽‍➡️': 7, + '👩🏾‍🫯‍👩🏻': 10, + '👩🏾‍🫯‍👩🏼': 10, + '👩🏾‍🫯‍👩🏽': 10, + '👩🏾‍🫯‍👩🏿': 10, + '👩🏿‍⚕️': 5, + '👩🏿‍⚖️': 5, + '👩🏿‍✈️': 5, + '👩🏿‍❤️‍👨🏻': 9, + '👩🏿‍❤️‍👨🏿': 9, + '👩🏿‍❤️‍👩🏿': 9, + '👩🏿‍❤️‍💋‍👨🏻': 11, + '👩🏿‍❤️‍💋‍👨🏽': 11, + '👩🏿‍❤️‍💋‍👨🏿': 11, + '👩🏿‍❤️‍💋‍👩🏻': 11, + '👩🏿‍❤️‍💋‍👩🏽': 11, + '👩🏿‍❤️‍💋‍👩🏿': 11, + '👩🏿‍🌾': 6, + '👩🏿‍🍳': 6, + '👩🏿‍🍼': 6, + '👩🏿‍🎓': 6, + '👩🏿‍🎤': 6, + '👩🏿‍🎨': 6, + '👩🏿‍🏫': 6, + '👩🏿‍🏭': 6, + '👩🏿‍🐰‍👩🏻': 10, + '👩🏿‍🐰‍👩🏼': 10, + '👩🏿‍🐰‍👩🏽': 10, + '👩🏿‍🐰‍👩🏾': 10, + '👩🏿‍💻': 6, + '👩🏿‍💼': 6, + '👩🏿‍🔧': 6, + '👩🏿‍🔬': 6, + '👩🏿‍🚀': 6, + '👩🏿‍🚒': 6, + '👩🏿‍🤝‍👨🏻': 10, + '👩🏿‍🤝‍👨🏽': 10, + '👩🏿‍🤝‍👩🏻': 10, + '👩🏿‍🤝‍👩🏼': 10, + '👩🏿‍🤝‍👩🏽': 10, + '👩🏿‍🤝‍👩🏾': 10, + '👩🏿‍🦯': 6, + '👩🏿‍🦯‍➡️': 7, + '👩🏿‍🦰': 6, + '👩🏿‍🦱': 6, + '👩🏿‍🦲': 6, + '👩🏿‍🦳': 6, + '👩🏿‍🦼': 6, + '👩🏿‍🦼‍➡️': 7, + '👩🏿‍🦽': 6, + '👩🏿‍🦽‍➡️': 7, + '👩🏿‍🫯‍👩🏻': 10, + '👩🏿‍🫯‍👩🏼': 10, + '👩🏿‍🫯‍👩🏽': 10, + '👩🏿‍🫯‍👩🏾': 10, + '👮‍♀️': 3, + '👮‍♂️': 3, + '👮🏻‍♀️': 5, + '👮🏻‍♂️': 5, + '👮🏼‍♀️': 5, + '👮🏼‍♂️': 5, + '👮🏽‍♀️': 5, + '👮🏽‍♂️': 5, + '👮🏾‍♀️': 5, + '👮🏾‍♂️': 5, + '👮🏿‍♀️': 5, + '👮🏿‍♂️': 5, + '👯‍♀️': 3, + '👯‍♂️': 3, + '👯🏻‍♀️': 5, + '👯🏻‍♂️': 5, + '👯🏼‍♀️': 5, + '👯🏼‍♂️': 5, + '👯🏽‍♀️': 5, + '👯🏽‍♂️': 5, + '👯🏾‍♀️': 5, + '👯🏾‍♂️': 5, + '👯🏿‍♀️': 5, + '👯🏿‍♂️': 5, + '👰‍♀️': 3, + '👰‍♂️': 3, + '👰🏻‍♀️': 5, + '👰🏻‍♂️': 5, + '👰🏼‍♀️': 5, + '👰🏼‍♂️': 5, + '👰🏽‍♀️': 5, + '👰🏽‍♂️': 5, + '👰🏾‍♀️': 5, + '👰🏾‍♂️': 5, + '👰🏿‍♀️': 5, + '👰🏿‍♂️': 5, + '👱‍♀️': 3, + '👱‍♂️': 3, + '👱🏻‍♀️': 5, + '👱🏻‍♂️': 5, + '👱🏼‍♀️': 5, + '👱🏼‍♂️': 5, + '👱🏽‍♀️': 5, + '👱🏽‍♂️': 5, + '👱🏾‍♀️': 5, + '👱🏾‍♂️': 5, + '👱🏿‍♀️': 5, + '👱🏿‍♂️': 5, + '👳‍♀️': 3, + '👳‍♂️': 3, + '👳🏻‍♀️': 5, + '👳🏻‍♂️': 5, + '👳🏼‍♀️': 5, + '👳🏼‍♂️': 5, + '👳🏽‍♀️': 5, + '👳🏽‍♂️': 5, + '👳🏾‍♀️': 5, + '👳🏾‍♂️': 5, + '👳🏿‍♀️': 5, + '👳🏿‍♂️': 5, + '👷‍♀️': 3, + '👷‍♂️': 3, + '👷🏻‍♀️': 5, + '👷🏻‍♂️': 5, + '👷🏼‍♀️': 5, + '👷🏼‍♂️': 5, + '👷🏽‍♀️': 5, + '👷🏽‍♂️': 5, + '👷🏾‍♀️': 5, + '👷🏾‍♂️': 5, + '👷🏿‍♀️': 5, + '👷🏿‍♂️': 5, + '💁‍♀️': 3, + '💁‍♂️': 3, + '💁🏻‍♀️': 5, + '💁🏻‍♂️': 5, + '💁🏼‍♀️': 5, + '💁🏼‍♂️': 5, + '💁🏽‍♀️': 5, + '💁🏽‍♂️': 5, + '💁🏾‍♀️': 5, + '💁🏾‍♂️': 5, + '💁🏿‍♀️': 5, + '💁🏿‍♂️': 5, + '💂‍♀️': 3, + '💂‍♂️': 3, + '💂🏻‍♀️': 5, + '💂🏻‍♂️': 5, + '💂🏼‍♀️': 5, + '💂🏼‍♂️': 5, + '💂🏽‍♀️': 5, + '💂🏽‍♂️': 5, + '💂🏾‍♀️': 5, + '💂🏾‍♂️': 5, + '💂🏿‍♀️': 5, + '💂🏿‍♂️': 5, + '💆‍♀️': 3, + '💆‍♂️': 3, + '💆🏻‍♀️': 5, + '💆🏻‍♂️': 5, + '💆🏼‍♀️': 5, + '💆🏼‍♂️': 5, + '💆🏽‍♀️': 5, + '💆🏽‍♂️': 5, + '💆🏾‍♀️': 5, + '💆🏾‍♂️': 5, + '💆🏿‍♀️': 5, + '💆🏿‍♂️': 5, + '💇‍♀️': 3, + '💇‍♂️': 3, + '💇🏻‍♀️': 5, + '💇🏻‍♂️': 5, + '💇🏼‍♀️': 5, + '💇🏼‍♂️': 5, + '💇🏽‍♀️': 5, + '💇🏽‍♂️': 5, + '💇🏾‍♀️': 5, + '💇🏾‍♂️': 5, + '💇🏿‍♀️': 5, + '💇🏿‍♂️': 5, + '🕵🏻‍♀️': 4, + '🕵🏻‍♂️': 4, + '🕵🏼‍♀️': 4, + '🕵🏼‍♂️': 4, + '🕵🏽‍♀️': 4, + '🕵🏽‍♂️': 4, + '🕵🏾‍♀️': 4, + '🕵🏾‍♂️': 4, + '🕵🏿‍♀️': 4, + '🕵🏿‍♂️': 4, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 3, + '🙂‍↔️': 3, + '🙂‍↕️': 3, + '🙅‍♀️': 3, + '🙅‍♂️': 3, + '🙅🏻‍♀️': 5, + '🙅🏻‍♂️': 5, + '🙅🏼‍♀️': 5, + '🙅🏼‍♂️': 5, + '🙅🏽‍♀️': 5, + '🙅🏽‍♂️': 5, + '🙅🏾‍♀️': 5, + '🙅🏾‍♂️': 5, + '🙅🏿‍♀️': 5, + '🙅🏿‍♂️': 5, + '🙆‍♀️': 3, + '🙆‍♂️': 3, + '🙆🏻‍♀️': 5, + '🙆🏻‍♂️': 5, + '🙆🏼‍♀️': 5, + '🙆🏼‍♂️': 5, + '🙆🏽‍♀️': 5, + '🙆🏽‍♂️': 5, + '🙆🏾‍♀️': 5, + '🙆🏾‍♂️': 5, + '🙆🏿‍♀️': 5, + '🙆🏿‍♂️': 5, + '🙇‍♀️': 3, + '🙇‍♂️': 3, + '🙇🏻‍♀️': 5, + '🙇🏻‍♂️': 5, + '🙇🏼‍♀️': 5, + '🙇🏼‍♂️': 5, + '🙇🏽‍♀️': 5, + '🙇🏽‍♂️': 5, + '🙇🏾‍♀️': 5, + '🙇🏾‍♂️': 5, + '🙇🏿‍♀️': 5, + '🙇🏿‍♂️': 5, + '🙋‍♀️': 3, + '🙋‍♂️': 3, + '🙋🏻‍♀️': 5, + '🙋🏻‍♂️': 5, + '🙋🏼‍♀️': 5, + '🙋🏼‍♂️': 5, + '🙋🏽‍♀️': 5, + '🙋🏽‍♂️': 5, + '🙋🏾‍♀️': 5, + '🙋🏾‍♂️': 5, + '🙋🏿‍♀️': 5, + '🙋🏿‍♂️': 5, + '🙍‍♀️': 3, + '🙍‍♂️': 3, + '🙍🏻‍♀️': 5, + '🙍🏻‍♂️': 5, + '🙍🏼‍♀️': 5, + '🙍🏼‍♂️': 5, + '🙍🏽‍♀️': 5, + '🙍🏽‍♂️': 5, + '🙍🏾‍♀️': 5, + '🙍🏾‍♂️': 5, + '🙍🏿‍♀️': 5, + '🙍🏿‍♂️': 5, + '🙎‍♀️': 3, + '🙎‍♂️': 3, + '🙎🏻‍♀️': 5, + '🙎🏻‍♂️': 5, + '🙎🏼‍♀️': 5, + '🙎🏼‍♂️': 5, + '🙎🏽‍♀️': 5, + '🙎🏽‍♂️': 5, + '🙎🏾‍♀️': 5, + '🙎🏾‍♂️': 5, + '🙎🏿‍♀️': 5, + '🙎🏿‍♂️': 5, + '🚣‍♀️': 3, + '🚣‍♂️': 3, + '🚣🏻‍♀️': 5, + '🚣🏻‍♂️': 5, + '🚣🏼‍♀️': 5, + '🚣🏼‍♂️': 5, + '🚣🏽‍♀️': 5, + '🚣🏽‍♂️': 5, + '🚣🏾‍♀️': 5, + '🚣🏾‍♂️': 5, + '🚣🏿‍♀️': 5, + '🚣🏿‍♂️': 5, + '🚴‍♀️': 3, + '🚴‍♂️': 3, + '🚴🏻‍♀️': 5, + '🚴🏻‍♂️': 5, + '🚴🏼‍♀️': 5, + '🚴🏼‍♂️': 5, + '🚴🏽‍♀️': 5, + '🚴🏽‍♂️': 5, + '🚴🏾‍♀️': 5, + '🚴🏾‍♂️': 5, + '🚴🏿‍♀️': 5, + '🚴🏿‍♂️': 5, + '🚵‍♀️': 3, + '🚵‍♂️': 3, + '🚵🏻‍♀️': 5, + '🚵🏻‍♂️': 5, + '🚵🏼‍♀️': 5, + '🚵🏼‍♂️': 5, + '🚵🏽‍♀️': 5, + '🚵🏽‍♂️': 5, + '🚵🏾‍♀️': 5, + '🚵🏾‍♂️': 5, + '🚵🏿‍♀️': 5, + '🚵🏿‍♂️': 5, + '🚶‍♀️': 3, + '🚶‍♀️‍➡️': 4, + '🚶‍♂️': 3, + '🚶‍♂️‍➡️': 4, + '🚶‍➡️': 3, + '🚶🏻‍♀️': 5, + '🚶🏻‍♀️‍➡️': 6, + '🚶🏻‍♂️': 5, + '🚶🏻‍♂️‍➡️': 6, + '🚶🏻‍➡️': 5, + '🚶🏼‍♀️': 5, + '🚶🏼‍♀️‍➡️': 6, + '🚶🏼‍♂️': 5, + '🚶🏼‍♂️‍➡️': 6, + '🚶🏼‍➡️': 5, + '🚶🏽‍♀️': 5, + '🚶🏽‍♀️‍➡️': 6, + '🚶🏽‍♂️': 5, + '🚶🏽‍♂️‍➡️': 6, + '🚶🏽‍➡️': 5, + '🚶🏾‍♀️': 5, + '🚶🏾‍♀️‍➡️': 6, + '🚶🏾‍♂️': 5, + '🚶🏾‍♂️‍➡️': 6, + '🚶🏾‍➡️': 5, + '🚶🏿‍♀️': 5, + '🚶🏿‍♀️‍➡️': 6, + '🚶🏿‍♂️': 5, + '🚶🏿‍♂️‍➡️': 6, + '🚶🏿‍➡️': 5, + '🤦‍♀️': 3, + '🤦‍♂️': 3, + '🤦🏻‍♀️': 5, + '🤦🏻‍♂️': 5, + '🤦🏼‍♀️': 5, + '🤦🏼‍♂️': 5, + '🤦🏽‍♀️': 5, + '🤦🏽‍♂️': 5, + '🤦🏾‍♀️': 5, + '🤦🏾‍♂️': 5, + '🤦🏿‍♀️': 5, + '🤦🏿‍♂️': 5, + '🤵‍♀️': 3, + '🤵‍♂️': 3, + '🤵🏻‍♀️': 5, + '🤵🏻‍♂️': 5, + '🤵🏼‍♀️': 5, + '🤵🏼‍♂️': 5, + '🤵🏽‍♀️': 5, + '🤵🏽‍♂️': 5, + '🤵🏾‍♀️': 5, + '🤵🏾‍♂️': 5, + '🤵🏿‍♀️': 5, + '🤵🏿‍♂️': 5, + '🤷‍♀️': 3, + '🤷‍♂️': 3, + '🤷🏻‍♀️': 5, + '🤷🏻‍♂️': 5, + '🤷🏼‍♀️': 5, + '🤷🏼‍♂️': 5, + '🤷🏽‍♀️': 5, + '🤷🏽‍♂️': 5, + '🤷🏾‍♀️': 5, + '🤷🏾‍♂️': 5, + '🤷🏿‍♀️': 5, + '🤷🏿‍♂️': 5, + '🤸‍♀️': 3, + '🤸‍♂️': 3, + '🤸🏻‍♀️': 5, + '🤸🏻‍♂️': 5, + '🤸🏼‍♀️': 5, + '🤸🏼‍♂️': 5, + '🤸🏽‍♀️': 5, + '🤸🏽‍♂️': 5, + '🤸🏾‍♀️': 5, + '🤸🏾‍♂️': 5, + '🤸🏿‍♀️': 5, + '🤸🏿‍♂️': 5, + '🤹‍♀️': 3, + '🤹‍♂️': 3, + '🤹🏻‍♀️': 5, + '🤹🏻‍♂️': 5, + '🤹🏼‍♀️': 5, + '🤹🏼‍♂️': 5, + '🤹🏽‍♀️': 5, + '🤹🏽‍♂️': 5, + '🤹🏾‍♀️': 5, + '🤹🏾‍♂️': 5, + '🤹🏿‍♀️': 5, + '🤹🏿‍♂️': 5, + '🤼‍♀️': 3, + '🤼‍♂️': 3, + '🤼🏻‍♀️': 5, + '🤼🏻‍♂️': 5, + '🤼🏼‍♀️': 5, + '🤼🏼‍♂️': 5, + '🤼🏽‍♀️': 5, + '🤼🏽‍♂️': 5, + '🤼🏾‍♀️': 5, + '🤼🏾‍♂️': 5, + '🤼🏿‍♀️': 5, + '🤼🏿‍♂️': 5, + '🤽‍♀️': 3, + '🤽‍♂️': 3, + '🤽🏻‍♀️': 5, + '🤽🏻‍♂️': 5, + '🤽🏼‍♀️': 5, + '🤽🏼‍♂️': 5, + '🤽🏽‍♀️': 5, + '🤽🏽‍♂️': 5, + '🤽🏾‍♀️': 5, + '🤽🏾‍♂️': 5, + '🤽🏿‍♀️': 5, + '🤽🏿‍♂️': 5, + '🤾‍♀️': 3, + '🤾‍♂️': 3, + '🤾🏻‍♀️': 5, + '🤾🏻‍♂️': 5, + '🤾🏼‍♀️': 5, + '🤾🏼‍♂️': 5, + '🤾🏽‍♀️': 5, + '🤾🏽‍♂️': 5, + '🤾🏾‍♀️': 5, + '🤾🏾‍♂️': 5, + '🤾🏿‍♀️': 5, + '🤾🏿‍♂️': 5, + '🦸‍♀️': 3, + '🦸‍♂️': 3, + '🦸🏻‍♀️': 5, + '🦸🏻‍♂️': 5, + '🦸🏼‍♀️': 5, + '🦸🏼‍♂️': 5, + '🦸🏽‍♀️': 5, + '🦸🏽‍♂️': 5, + '🦸🏾‍♀️': 5, + '🦸🏾‍♂️': 5, + '🦸🏿‍♀️': 5, + '🦸🏿‍♂️': 5, + '🦹‍♀️': 3, + '🦹‍♂️': 3, + '🦹🏻‍♀️': 5, + '🦹🏻‍♂️': 5, + '🦹🏼‍♀️': 5, + '🦹🏼‍♂️': 5, + '🦹🏽‍♀️': 5, + '🦹🏽‍♂️': 5, + '🦹🏾‍♀️': 5, + '🦹🏾‍♂️': 5, + '🦹🏿‍♀️': 5, + '🦹🏿‍♂️': 5, + '🧍‍♀️': 3, + '🧍‍♂️': 3, + '🧍🏻‍♀️': 5, + '🧍🏻‍♂️': 5, + '🧍🏼‍♀️': 5, + '🧍🏼‍♂️': 5, + '🧍🏽‍♀️': 5, + '🧍🏽‍♂️': 5, + '🧍🏾‍♀️': 5, + '🧍🏾‍♂️': 5, + '🧍🏿‍♀️': 5, + '🧍🏿‍♂️': 5, + '🧎‍♀️': 3, + '🧎‍♀️‍➡️': 4, + '🧎‍♂️': 3, + '🧎‍♂️‍➡️': 4, + '🧎‍➡️': 3, + '🧎🏻‍♀️': 5, + '🧎🏻‍♀️‍➡️': 6, + '🧎🏻‍♂️': 5, + '🧎🏻‍♂️‍➡️': 6, + '🧎🏻‍➡️': 5, + '🧎🏼‍♀️': 5, + '🧎🏼‍♀️‍➡️': 6, + '🧎🏼‍♂️': 5, + '🧎🏼‍♂️‍➡️': 6, + '🧎🏼‍➡️': 5, + '🧎🏽‍♀️': 5, + '🧎🏽‍♀️‍➡️': 6, + '🧎🏽‍♂️': 5, + '🧎🏽‍♂️‍➡️': 6, + '🧎🏽‍➡️': 5, + '🧎🏾‍♀️': 5, + '🧎🏾‍♀️‍➡️': 6, + '🧎🏾‍♂️': 5, + '🧎🏾‍♂️‍➡️': 6, + '🧎🏾‍➡️': 5, + '🧎🏿‍♀️': 5, + '🧎🏿‍♀️‍➡️': 6, + '🧎🏿‍♂️': 5, + '🧎🏿‍♂️‍➡️': 6, + '🧎🏿‍➡️': 5, + '🧏‍♀️': 3, + '🧏‍♂️': 3, + '🧏🏻‍♀️': 5, + '🧏🏻‍♂️': 5, + '🧏🏼‍♀️': 5, + '🧏🏼‍♂️': 5, + '🧏🏽‍♀️': 5, + '🧏🏽‍♂️': 5, + '🧏🏾‍♀️': 5, + '🧏🏾‍♂️': 5, + '🧏🏿‍♀️': 5, + '🧏🏿‍♂️': 5, + '🧑‍⚕️': 3, + '🧑‍⚖️': 3, + '🧑‍✈️': 3, + '🧑‍🌾': 4, + '🧑‍🍳': 4, + '🧑‍🍼': 4, + '🧑‍🎄': 4, + '🧑‍🎓': 4, + '🧑‍🎤': 4, + '🧑‍🎨': 4, + '🧑‍🏫': 4, + '🧑‍🏭': 4, + '🧑‍💻': 4, + '🧑‍💼': 4, + '🧑‍🔧': 4, + '🧑‍🔬': 4, + '🧑‍🚀': 4, + '🧑‍🚒': 4, + '🧑‍🤝‍🧑': 6, + '🧑‍🦯': 4, + '🧑‍🦯‍➡️': 5, + '🧑‍🦰': 4, + '🧑‍🦱': 4, + '🧑‍🦲': 4, + '🧑‍🦳': 4, + '🧑‍🦼': 4, + '🧑‍🦼‍➡️': 5, + '🧑‍🦽': 4, + '🧑‍🦽‍➡️': 5, + '🧑‍🧑‍🧒': 6, + '🧑‍🧑‍🧒‍🧒': 8, + '🧑‍🧒': 4, + '🧑‍🧒‍🧒': 6, + '🧑‍🩰': 4, + '🧑🏻‍⚕️': 5, + '🧑🏻‍⚖️': 5, + '🧑🏻‍✈️': 5, + '🧑🏻‍❤️‍💋‍🧑🏿': 11, + '🧑🏻‍🌾': 6, + '🧑🏻‍🍳': 6, + '🧑🏻‍🍼': 6, + '🧑🏻‍🎄': 6, + '🧑🏻‍🎓': 6, + '🧑🏻‍🎤': 6, + '🧑🏻‍🎨': 6, + '🧑🏻‍🏫': 6, + '🧑🏻‍🏭': 6, + '🧑🏻‍🐰‍🧑🏽': 10, + '🧑🏻‍🐰‍🧑🏾': 10, + '🧑🏻‍🐰‍🧑🏿': 10, + '🧑🏻‍💻': 6, + '🧑🏻‍💼': 6, + '🧑🏻‍🔧': 6, + '🧑🏻‍🔬': 6, + '🧑🏻‍🚀': 6, + '🧑🏻‍🚒': 6, + '🧑🏻‍🤝‍🧑🏻': 10, + '🧑🏻‍🤝‍🧑🏼': 10, + '🧑🏻‍🤝‍🧑🏽': 10, + '🧑🏻‍🤝‍🧑🏾': 10, + '🧑🏻‍🤝‍🧑🏿': 10, + '🧑🏻‍🦯': 6, + '🧑🏻‍🦯‍➡️': 7, + '🧑🏻‍🦰': 6, + '🧑🏻‍🦱': 6, + '🧑🏻‍🦲': 6, + '🧑🏻‍🦳': 6, + '🧑🏻‍🦼': 6, + '🧑🏻‍🦼‍➡️': 7, + '🧑🏻‍🦽': 6, + '🧑🏻‍🦽‍➡️': 7, + '🧑🏻‍🩰': 6, + '🧑🏻‍🫯‍🧑🏼': 10, + '🧑🏻‍🫯‍🧑🏽': 10, + '🧑🏻‍🫯‍🧑🏾': 10, + '🧑🏻‍🫯‍🧑🏿': 10, + '🧑🏼‍⚕️': 5, + '🧑🏼‍⚖️': 5, + '🧑🏼‍✈️': 5, + '🧑🏼‍🌾': 6, + '🧑🏼‍🍳': 6, + '🧑🏼‍🍼': 6, + '🧑🏼‍🎄': 6, + '🧑🏼‍🎓': 6, + '🧑🏼‍🎤': 6, + '🧑🏼‍🎨': 6, + '🧑🏼‍🏫': 6, + '🧑🏼‍🏭': 6, + '🧑🏼‍🐰‍🧑🏿': 10, + '🧑🏼‍💻': 6, + '🧑🏼‍💼': 6, + '🧑🏼‍🔧': 6, + '🧑🏼‍🔬': 6, + '🧑🏼‍🚀': 6, + '🧑🏼‍🚒': 6, + '🧑🏼‍🤝‍🧑🏻': 10, + '🧑🏼‍🤝‍🧑🏼': 10, + '🧑🏼‍🤝‍🧑🏽': 10, + '🧑🏼‍🤝‍🧑🏿': 10, + '🧑🏼‍🦯': 6, + '🧑🏼‍🦯‍➡️': 7, + '🧑🏼‍🦰': 6, + '🧑🏼‍🦱': 6, + '🧑🏼‍🦲': 6, + '🧑🏼‍🦳': 6, + '🧑🏼‍🦼': 6, + '🧑🏼‍🦽': 6, + '🧑🏼‍🩰': 6, + '🧑🏼‍🫯‍🧑🏻': 10, + '🧑🏼‍🫯‍🧑🏽': 10, + '🧑🏼‍🫯‍🧑🏿': 10, + '🧑🏽‍⚕️': 5, + '🧑🏽‍⚖️': 5, + '🧑🏽‍✈️': 5, + '🧑🏽‍❤️‍💋‍🧑🏿': 11, + '🧑🏽‍🌾': 6, + '🧑🏽‍🍳': 6, + '🧑🏽‍🍼': 6, + '🧑🏽‍🎄': 6, + '🧑🏽‍🎓': 6, + '🧑🏽‍🎤': 6, + '🧑🏽‍🎨': 6, + '🧑🏽‍🏫': 6, + '🧑🏽‍🏭': 6, + '🧑🏽‍🐰‍🧑🏻': 10, + '🧑🏽‍🐰‍🧑🏿': 10, + '🧑🏽‍💻': 6, + '🧑🏽‍💼': 6, + '🧑🏽‍🔧': 6, + '🧑🏽‍🔬': 6, + '🧑🏽‍🚀': 6, + '🧑🏽‍🚒': 6, + '🧑🏽‍🤝‍🧑🏻': 10, + '🧑🏽‍🤝‍🧑🏼': 10, + '🧑🏽‍🤝‍🧑🏽': 10, + '🧑🏽‍🤝‍🧑🏾': 10, + '🧑🏽‍🤝‍🧑🏿': 10, + '🧑🏽‍🦯': 6, + '🧑🏽‍🦯‍➡️': 7, + '🧑🏽‍🦰': 6, + '🧑🏽‍🦱': 6, + '🧑🏽‍🦲': 6, + '🧑🏽‍🦳': 6, + '🧑🏽‍🦼': 6, + '🧑🏽‍🦼‍➡️': 7, + '🧑🏽‍🦽': 6, + '🧑🏽‍🦽‍➡️': 7, + '🧑🏽‍🩰': 6, + '🧑🏽‍🫯‍🧑🏻': 10, + '🧑🏽‍🫯‍🧑🏼': 10, + '🧑🏽‍🫯‍🧑🏾': 10, + '🧑🏽‍🫯‍🧑🏿': 10, + '🧑🏾‍⚕️': 5, + '🧑🏾‍⚖️': 5, + '🧑🏾‍✈️': 5, + '🧑🏾‍🌾': 6, + '🧑🏾‍🍳': 6, + '🧑🏾‍🍼': 6, + '🧑🏾‍🎄': 6, + '🧑🏾‍🎓': 6, + '🧑🏾‍🎤': 6, + '🧑🏾‍🎨': 6, + '🧑🏾‍🏫': 6, + '🧑🏾‍🏭': 6, + '🧑🏾‍🐰‍🧑🏻': 10, + '🧑🏾‍🐰‍🧑🏿': 10, + '🧑🏾‍💻': 6, + '🧑🏾‍💼': 6, + '🧑🏾‍🔧': 6, + '🧑🏾‍🔬': 6, + '🧑🏾‍🚀': 6, + '🧑🏾‍🚒': 6, + '🧑🏾‍🤝‍🧑🏻': 10, + '🧑🏾‍🤝‍🧑🏽': 10, + '🧑🏾‍🤝‍🧑🏾': 10, + '🧑🏾‍🤝‍🧑🏿': 10, + '🧑🏾‍🦯': 6, + '🧑🏾‍🦯‍➡️': 7, + '🧑🏾‍🦰': 6, + '🧑🏾‍🦱': 6, + '🧑🏾‍🦲': 6, + '🧑🏾‍🦳': 6, + '🧑🏾‍🦼': 6, + '🧑🏾‍🦽': 6, + '🧑🏾‍🩰': 6, + '🧑🏾‍🫯‍🧑🏻': 10, + '🧑🏾‍🫯‍🧑🏽': 10, + '🧑🏾‍🫯‍🧑🏿': 10, + '🧑🏿‍⚕️': 5, + '🧑🏿‍⚖️': 5, + '🧑🏿‍✈️': 5, + '🧑🏿‍❤️‍💋‍🧑🏻': 11, + '🧑🏿‍❤️‍💋‍🧑🏽': 11, + '🧑🏿‍🌾': 6, + '🧑🏿‍🍳': 6, + '🧑🏿‍🍼': 6, + '🧑🏿‍🎄': 6, + '🧑🏿‍🎓': 6, + '🧑🏿‍🎤': 6, + '🧑🏿‍🎨': 6, + '🧑🏿‍🏫': 6, + '🧑🏿‍🏭': 6, + '🧑🏿‍🐰‍🧑🏻': 10, + '🧑🏿‍🐰‍🧑🏼': 10, + '🧑🏿‍🐰‍🧑🏽': 10, + '🧑🏿‍🐰‍🧑🏾': 10, + '🧑🏿‍💻': 6, + '🧑🏿‍💼': 6, + '🧑🏿‍🔧': 6, + '🧑🏿‍🔬': 6, + '🧑🏿‍🚀': 6, + '🧑🏿‍🚒': 6, + '🧑🏿‍🤝‍🧑🏻': 10, + '🧑🏿‍🤝‍🧑🏼': 10, + '🧑🏿‍🤝‍🧑🏽': 10, + '🧑🏿‍🤝‍🧑🏾': 10, + '🧑🏿‍🤝‍🧑🏿': 10, + '🧑🏿‍🦯': 6, + '🧑🏿‍🦯‍➡️': 7, + '🧑🏿‍🦰': 6, + '🧑🏿‍🦱': 6, + '🧑🏿‍🦲': 6, + '🧑🏿‍🦳': 6, + '🧑🏿‍🦼': 6, + '🧑🏿‍🦼‍➡️': 7, + '🧑🏿‍🦽': 6, + '🧑🏿‍🦽‍➡️': 7, + '🧑🏿‍🩰': 6, + '🧑🏿‍🫯‍🧑🏻': 10, + '🧑🏿‍🫯‍🧑🏼': 10, + '🧑🏿‍🫯‍🧑🏽': 10, + '🧑🏿‍🫯‍🧑🏾': 10, + '🧔‍♀️': 3, + '🧔‍♂️': 3, + '🧔🏻‍♀️': 5, + '🧔🏻‍♂️': 5, + '🧔🏼‍♀️': 5, + '🧔🏼‍♂️': 5, + '🧔🏽‍♀️': 5, + '🧔🏽‍♂️': 5, + '🧔🏾‍♀️': 5, + '🧔🏾‍♂️': 5, + '🧔🏿‍♀️': 5, + '🧔🏿‍♂️': 5, + '🧖‍♀️': 3, + '🧖‍♂️': 3, + '🧖🏻‍♀️': 5, + '🧖🏻‍♂️': 5, + '🧖🏼‍♀️': 5, + '🧖🏼‍♂️': 5, + '🧖🏽‍♀️': 5, + '🧖🏽‍♂️': 5, + '🧖🏾‍♀️': 5, + '🧖🏾‍♂️': 5, + '🧖🏿‍♀️': 5, + '🧖🏿‍♂️': 5, + '🧗‍♀️': 3, + '🧗‍♂️': 3, + '🧗🏻‍♀️': 5, + '🧗🏻‍♂️': 5, + '🧗🏼‍♀️': 5, + '🧗🏼‍♂️': 5, + '🧗🏽‍♀️': 5, + '🧗🏽‍♂️': 5, + '🧗🏾‍♀️': 5, + '🧗🏾‍♂️': 5, + '🧗🏿‍♀️': 5, + '🧗🏿‍♂️': 5, + '🧘‍♀️': 3, + '🧘‍♂️': 3, + '🧘🏻‍♀️': 5, + '🧘🏻‍♂️': 5, + '🧘🏼‍♀️': 5, + '🧘🏼‍♂️': 5, + '🧘🏽‍♀️': 5, + '🧘🏽‍♂️': 5, + '🧘🏾‍♀️': 5, + '🧘🏾‍♂️': 5, + '🧘🏿‍♀️': 5, + '🧘🏿‍♂️': 5, + '🧙‍♀️': 3, + '🧙‍♂️': 3, + '🧙🏻‍♀️': 5, + '🧙🏻‍♂️': 5, + '🧙🏼‍♀️': 5, + '🧙🏼‍♂️': 5, + '🧙🏽‍♀️': 5, + '🧙🏽‍♂️': 5, + '🧙🏾‍♀️': 5, + '🧙🏾‍♂️': 5, + '🧙🏿‍♀️': 5, + '🧙🏿‍♂️': 5, + '🧚‍♀️': 3, + '🧚‍♂️': 3, + '🧚🏻‍♀️': 5, + '🧚🏻‍♂️': 5, + '🧚🏼‍♀️': 5, + '🧚🏼‍♂️': 5, + '🧚🏽‍♀️': 5, + '🧚🏽‍♂️': 5, + '🧚🏾‍♀️': 5, + '🧚🏾‍♂️': 5, + '🧚🏿‍♀️': 5, + '🧚🏿‍♂️': 5, + '🧛‍♀️': 3, + '🧛‍♂️': 3, + '🧛🏻‍♀️': 5, + '🧛🏻‍♂️': 5, + '🧛🏼‍♀️': 5, + '🧛🏼‍♂️': 5, + '🧛🏽‍♀️': 5, + '🧛🏽‍♂️': 5, + '🧛🏾‍♀️': 5, + '🧛🏾‍♂️': 5, + '🧛🏿‍♀️': 5, + '🧛🏿‍♂️': 5, + '🧜‍♀️': 3, + '🧜‍♂️': 3, + '🧜🏻‍♀️': 5, + '🧜🏻‍♂️': 5, + '🧜🏼‍♀️': 5, + '🧜🏼‍♂️': 5, + '🧜🏽‍♀️': 5, + '🧜🏽‍♂️': 5, + '🧜🏾‍♀️': 5, + '🧜🏾‍♂️': 5, + '🧜🏿‍♀️': 5, + '🧜🏿‍♂️': 5, + '🧝‍♀️': 3, + '🧝‍♂️': 3, + '🧝🏻‍♀️': 5, + '🧝🏻‍♂️': 5, + '🧝🏼‍♀️': 5, + '🧝🏼‍♂️': 5, + '🧝🏽‍♀️': 5, + '🧝🏽‍♂️': 5, + '🧝🏾‍♀️': 5, + '🧝🏾‍♂️': 5, + '🧝🏿‍♀️': 5, + '🧝🏿‍♂️': 5, + '🧞‍♀️': 3, + '🧞‍♂️': 3, + '🧟‍♀️': 3, + '🧟‍♂️': 3, + '🫱🏻‍🫲🏼': 8, + '🫱🏻‍🫲🏽': 8, + '🫱🏻‍🫲🏾': 8, + '🫱🏻‍🫲🏿': 8, + '🫱🏼‍🫲🏻': 8, + '🫱🏼‍🫲🏽': 8, + '🫱🏼‍🫲🏾': 8, + '🫱🏼‍🫲🏿': 8, + '🫱🏽‍🫲🏻': 8, + '🫱🏽‍🫲🏼': 8, + '🫱🏽‍🫲🏾': 8, + '🫱🏽‍🫲🏿': 8, + '🫱🏾‍🫲🏻': 8, + '🫱🏾‍🫲🏼': 8, + '🫱🏾‍🫲🏽': 8, + '🫱🏾‍🫲🏿': 8, + '🫱🏿‍🫲🏻': 8, + '🫱🏿‍🫲🏼': 8, + '🫱🏿‍🫲🏽': 8, + '🫱🏿‍🫲🏾': 8, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_8589765c.py b/libs/wcwidth/table_grapheme_overrides/_known_8589765c.py new file mode 100644 index 000000000..22d9dd93c --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_8589765c.py @@ -0,0 +1,1310 @@ +""" +Grapheme overrides for _known_8589765c. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: iterm2 +""" +# pylint: skip-file +GRAPHEMES = { + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ख्‍ने': 1, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 1, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍প': 1, + 'ত্‍পী': 1, + 'ত্‍সা': 1, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍കൂ': 1, + 'ന്‍പി': 1, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 1, + 'ര്‍ക്കും': 1, + 'ര്‍ക്ക്‌': 1, + 'ര്‍ഗ്ഗം': 1, + 'ര്‍ച്ച': 1, + 'ര്‍ണ്ണ': 1, + 'ര്‍ത്ത': 1, + 'ര്‍ത്തി': 1, + 'ര്‍ത്തു': 1, + 'ര്‍ത്തേ': 1, + 'ര്‍ദേ': 1, + 'ര്‍ദ്ദ': 1, + 'ര്‍പ്പെ': 1, + 'ര്‍ബ': 1, + 'ര്‍ഭ': 1, + 'ര്‍മ്മി': 1, + 'ര്‍രാ': 1, + 'ര്‍വ്വ': 1, + 'ര്‍ഹ': 1, + 'ര്‍ഹി': 1, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍കി': 1, + 'ല്‍കു': 1, + 'ല്‍കേ': 1, + 'ല്‍ത': 1, + 'ല്‍നി': 1, + 'ല്‍പ്പ': 1, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 1, + 'ള്‍ക്ക': 1, + 'ള്‍ക്കി': 1, + 'ള്‍ക്കു': 1, + 'ള്‍ക്കും': 1, + 'ള്‍ക്കെ': 1, + 'ള്‍ക്ക്‌': 1, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛹🏻‍♀️': 3, + '⛹🏻‍♂️': 3, + '⛹🏼‍♀️': 3, + '⛹🏼‍♂️': 3, + '⛹🏽‍♀️': 3, + '⛹🏽‍♂️': 3, + '⛹🏾‍♀️': 3, + '⛹🏾‍♂️': 3, + '⛹🏿‍♀️': 3, + '⛹🏿‍♂️': 3, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄃𑄬': 1, + '𑄃𑄬𑄭': 1, + '𑄇𑄬': 1, + '𑄈𑄬': 1, + '𑄉𑄬': 1, + '𑄉𑄬𑄭': 1, + '𑄊𑄬': 1, + '𑄌𑄬': 1, + '𑄎𑄬': 1, + '𑄎𑄬𑄭': 1, + '𑄏𑄬': 1, + '𑄑𑄬': 1, + '𑄖𑄬': 1, + '𑄗𑄬': 1, + '𑄘𑄬': 1, + '𑄙𑄬': 1, + '𑄚𑄬': 1, + '𑄚𑄬𑄭': 1, + '𑄛𑄬': 1, + '𑄜𑄬': 1, + '𑄝𑄬': 1, + '𑄞𑄬': 1, + '𑄞𑄬𑄭': 1, + '𑄟𑄬': 1, + '𑄠𑄬': 1, + '𑄠𑄬𑄭': 1, + '𑄡𑄬': 1, + '𑄡𑄬𑄭': 1, + '𑄢𑄬': 1, + '𑄢𑄬𑄭': 1, + '𑄣𑄬': 1, + '𑄣𑄬𑄭': 1, + '𑄥𑄬': 1, + '𑄦𑄬': 1, + '𑌅𑌂': 1, + '𑌕𑌂': 1, + '𑌕𑌾': 1, + '𑌕𑌾𑌂': 1, + '𑌕𑌾𑌃': 1, + '𑌕𑌿': 1, + '𑌕𑍂': 1, + '𑌕𑍃': 1, + '𑌕𑍇': 1, + '𑌕𑍋': 1, + '𑌗𑌾': 1, + '𑌗𑌿': 1, + '𑌗𑍁': 1, + '𑌗𑍃': 1, + '𑌗𑍇': 1, + '𑌗𑍌': 1, + '𑌘𑌾': 1, + '𑌘𑍇': 1, + '𑌘𑍋': 1, + '𑌚𑌾': 1, + '𑌚𑌿': 1, + '𑌚𑍇': 1, + '𑌚𑍈': 1, + '𑌛𑌾': 1, + '𑌛𑍁': 1, + '𑌛𑍇': 1, + '𑌜𑌾': 1, + '𑌜𑌿': 1, + '𑌜𑍇': 1, + '𑌞𑌾': 1, + '𑌟𑌃': 1, + '𑌟𑌾': 1, + '𑌟𑌾𑌂': 1, + '𑌟𑌾𑌃': 1, + '𑌠𑌾': 1, + '𑌠𑍇': 1, + '𑌠𑍈𑌃': 1, + '𑌡𑌂': 1, + '𑌡𑌃': 1, + '𑌡𑌿': 1, + '𑌣𑌂': 1, + '𑌣𑌾': 1, + '𑌣𑌾𑌂': 1, + '𑌣𑌿': 1, + '𑌣𑍁': 1, + '𑌣𑍇': 1, + '𑌤𑌂': 1, + '𑌤𑌃': 1, + '𑌤𑌾': 1, + '𑌤𑌾𑌂': 1, + '𑌤𑌾𑌃': 1, + '𑌤𑌿': 1, + '𑌤𑌿𑌂': 1, + '𑌤𑌿𑌃': 1, + '𑌤𑍁': 1, + '𑌤𑍁𑌂': 1, + '𑌤𑍃': 1, + '𑌤𑍇': 1, + '𑌤𑍇𑌃': 1, + '𑌤𑍈': 1, + '𑌤𑍋': 1, + '𑌤𑍌': 1, + '𑌥𑌂': 1, + '𑌥𑌾': 1, + '𑌥𑌾𑌂': 1, + '𑌥𑌿': 1, + '𑌦𑌂': 1, + '𑌦𑌃': 1, + '𑌦𑌾': 1, + '𑌦𑌿': 1, + '𑌦𑍁': 1, + '𑌦𑍃': 1, + '𑌦𑍇': 1, + '𑌦𑍋': 1, + '𑌧𑌃': 1, + '𑌧𑌾': 1, + '𑌧𑌾𑌃': 1, + '𑌧𑌿': 1, + '𑌧𑌿𑌂': 1, + '𑌧𑍁': 1, + '𑌧𑍃': 1, + '𑌧𑍇': 1, + '𑌧𑍇𑌃': 1, + '𑌧𑍈𑌃': 1, + '𑌨𑌂': 1, + '𑌨𑌃': 1, + '𑌨𑌾': 1, + '𑌨𑌾𑌂': 1, + '𑌨𑌾𑌃': 1, + '𑌨𑌿': 1, + '𑌨𑌿𑌃': 1, + '𑌨𑍁': 1, + '𑌨𑍂': 1, + '𑌨𑍇': 1, + '𑌨𑍈': 1, + '𑌨𑍈𑌃': 1, + '𑌨𑍋': 1, + '𑌪𑌂': 1, + '𑌪𑌾': 1, + '𑌪𑌿': 1, + '𑌪𑍁': 1, + '𑌪𑍂': 1, + '𑌪𑍇': 1, + '𑌪𑍋': 1, + '𑌬𑌾': 1, + '𑌬𑍃': 1, + '𑌭𑌾': 1, + '𑌭𑌿': 1, + '𑌭𑌿𑌃': 1, + '𑌭𑍁': 1, + '𑌭𑍂': 1, + '𑌭𑍇': 1, + '𑌮𑌂': 1, + '𑌮𑌾': 1, + '𑌮𑌿': 1, + '𑌮𑌿𑌂': 1, + '𑌮𑍁': 1, + '𑌮𑍂': 1, + '𑌮𑍇': 1, + '𑌮𑍈': 1, + '𑌮𑍋': 1, + '𑌮𑍌': 1, + '𑌯𑌂': 1, + '𑌯𑌃': 1, + '𑌯𑌾': 1, + '𑌯𑌾𑌂': 1, + '𑌯𑌾𑌃': 1, + '𑌯𑌿': 1, + '𑌯𑍁': 1, + '𑌯𑍁𑌃': 1, + '𑌯𑍇': 1, + '𑌯𑍈': 1, + '𑌯𑍈𑌃': 1, + '𑌯𑍋': 1, + '𑌯𑍋𑌃': 1, + '𑌰𑌂': 1, + '𑌰𑌃': 1, + '𑌰𑌾': 1, + '𑌰𑌾𑌃': 1, + '𑌰𑌿': 1, + '𑌰𑌿𑌂': 1, + '𑌰𑍁': 1, + '𑌰𑍂': 1, + '𑌰𑍇': 1, + '𑌰𑍋': 1, + '𑌰𑍌': 1, + '𑌲𑌂': 1, + '𑌲𑌾': 1, + '𑌲𑌾𑌃': 1, + '𑌲𑌿': 1, + '𑌲𑌿𑌂': 1, + '𑌲𑍇': 1, + '𑌲𑍈': 1, + '𑌲𑍋': 1, + '𑌵𑌂': 1, + '𑌵𑌃': 1, + '𑌵𑌾': 1, + '𑌵𑌾𑌂': 1, + '𑌵𑌾𑌃': 1, + '𑌵𑌿': 1, + '𑌵𑍃': 1, + '𑌵𑍇': 1, + '𑌵𑍈': 1, + '𑌵𑍋': 1, + '𑌶𑌂': 1, + '𑌶𑌃': 1, + '𑌶𑌾': 1, + '𑌶𑌾𑌃': 1, + '𑌶𑌿': 1, + '𑌶𑍁': 1, + '𑌶𑍂': 1, + '𑌶𑍇': 1, + '𑌶𑍋': 1, + '𑌷𑌂': 1, + '𑌷𑌾': 1, + '𑌷𑌾𑌂': 1, + '𑌷𑌾𑌃': 1, + '𑌷𑌿': 1, + '𑌷𑍁': 1, + '𑌷𑍇': 1, + '𑌷𑍈': 1, + '𑌷𑍋': 1, + '𑌸𑌂': 1, + '𑌸𑌃': 1, + '𑌸𑌾': 1, + '𑌸𑌾𑌂': 1, + '𑌸𑌿': 1, + '𑌸𑍁': 1, + '𑌸𑍂': 1, + '𑌸𑍇': 1, + '𑌸𑍋': 1, + '𑌸𑍌': 1, + '𑌹𑌾': 1, + '𑌹𑌾𑌃': 1, + '𑌹𑌿': 1, + '𑌹𑍇': 1, + '𑌹𑍋': 1, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_8f94b404.py b/libs/wcwidth/table_grapheme_overrides/_known_8f94b404.py new file mode 100644 index 000000000..9d97dde34 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_8f94b404.py @@ -0,0 +1,2581 @@ +""" +Grapheme overrides for _known_8f94b404. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: rio +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 4, + '⛹️‍♀️': 4, + '⛹️‍♂️': 4, + '⛹🏻‍♀️': 5, + '⛹🏻‍♂️': 5, + '⛹🏼‍♀️': 5, + '⛹🏼‍♂️': 5, + '⛹🏽‍♀️': 5, + '⛹🏽‍♂️': 5, + '⛹🏾‍♀️': 5, + '⛹🏾‍♂️': 5, + '⛹🏿‍♀️': 5, + '⛹🏿‍♂️': 5, + '❤️‍🔥': 4, + '❤️‍🩹': 4, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 4, + '🏃‍♀️‍➡️': 6, + '🏃‍♂️': 4, + '🏃‍♂️‍➡️': 6, + '🏃‍➡️': 4, + '🏃🏻‍♀️': 6, + '🏃🏻‍♀️‍➡️': 8, + '🏃🏻‍♂️': 6, + '🏃🏻‍♂️‍➡️': 8, + '🏃🏻‍➡️': 6, + '🏃🏼‍♀️': 6, + '🏃🏼‍♀️‍➡️': 8, + '🏃🏼‍♂️': 6, + '🏃🏼‍♂️‍➡️': 8, + '🏃🏼‍➡️': 6, + '🏃🏽‍♀️': 6, + '🏃🏽‍♀️‍➡️': 8, + '🏃🏽‍♂️': 6, + '🏃🏽‍♂️‍➡️': 8, + '🏃🏽‍➡️': 6, + '🏃🏾‍♀️': 6, + '🏃🏾‍♀️‍➡️': 8, + '🏃🏾‍♂️': 6, + '🏃🏾‍♂️‍➡️': 8, + '🏃🏾‍➡️': 6, + '🏃🏿‍♀️': 6, + '🏃🏿‍♀️‍➡️': 8, + '🏃🏿‍♂️': 6, + '🏃🏿‍♂️‍➡️': 8, + '🏃🏿‍➡️': 6, + '🏄‍♀️': 4, + '🏄‍♂️': 4, + '🏄🏻‍♀️': 6, + '🏄🏻‍♂️': 6, + '🏄🏼‍♀️': 6, + '🏄🏼‍♂️': 6, + '🏄🏽‍♀️': 6, + '🏄🏽‍♂️': 6, + '🏄🏾‍♀️': 6, + '🏄🏾‍♂️': 6, + '🏄🏿‍♀️': 6, + '🏄🏿‍♂️': 6, + '🏊‍♀️': 4, + '🏊‍♂️': 4, + '🏊🏻‍♀️': 6, + '🏊🏻‍♂️': 6, + '🏊🏼‍♀️': 6, + '🏊🏼‍♂️': 6, + '🏊🏽‍♀️': 6, + '🏊🏽‍♂️': 6, + '🏊🏾‍♀️': 6, + '🏊🏾‍♂️': 6, + '🏊🏿‍♀️': 6, + '🏊🏿‍♂️': 6, + '🏋️‍♀️': 4, + '🏋️‍♂️': 4, + '🏋🏻‍♀️': 5, + '🏋🏻‍♂️': 5, + '🏋🏼‍♀️': 5, + '🏋🏼‍♂️': 5, + '🏋🏽‍♀️': 5, + '🏋🏽‍♂️': 5, + '🏋🏾‍♀️': 5, + '🏋🏾‍♂️': 5, + '🏋🏿‍♀️': 5, + '🏋🏿‍♂️': 5, + '🏌️‍♀️': 4, + '🏌️‍♂️': 4, + '🏌🏻‍♀️': 5, + '🏌🏻‍♂️': 5, + '🏌🏼‍♀️': 5, + '🏌🏼‍♂️': 5, + '🏌🏽‍♀️': 5, + '🏌🏽‍♂️': 5, + '🏌🏾‍♀️': 5, + '🏌🏾‍♂️': 5, + '🏌🏿‍♀️': 5, + '🏌🏿‍♂️': 5, + '🏳️‍⚧️': 4, + '🏳️‍🌈': 4, + '🏴‍☠️': 4, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 4, + '👁️‍🗨️': 4, + '👨‍⚕️': 4, + '👨‍⚖️': 4, + '👨‍✈️': 4, + '👨‍❤️‍👨': 6, + '👨‍❤️‍💋‍👨': 8, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 6, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 6, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 6, + '👨🏻‍⚕️': 6, + '👨🏻‍⚖️': 6, + '👨🏻‍✈️': 6, + '👨🏻‍❤️‍👨🏻': 10, + '👨🏻‍❤️‍👨🏽': 10, + '👨🏻‍❤️‍👨🏿': 10, + '👨🏻‍❤️‍💋‍👨🏻': 12, + '👨🏻‍❤️‍💋‍👨🏽': 12, + '👨🏻‍❤️‍💋‍👨🏾': 12, + '👨🏻‍❤️‍💋‍👨🏿': 12, + '👨🏻‍🌾': 6, + '👨🏻‍🍳': 6, + '👨🏻‍🍼': 6, + '👨🏻‍🎓': 6, + '👨🏻‍🎤': 6, + '👨🏻‍🎨': 6, + '👨🏻‍🏫': 6, + '👨🏻‍🏭': 6, + '👨🏻‍🐰‍👨🏼': 10, + '👨🏻‍🐰‍👨🏽': 10, + '👨🏻‍🐰‍👨🏾': 10, + '👨🏻‍🐰‍👨🏿': 10, + '👨🏻‍💻': 6, + '👨🏻‍💼': 6, + '👨🏻‍🔧': 6, + '👨🏻‍🔬': 6, + '👨🏻‍🚀': 6, + '👨🏻‍🚒': 6, + '👨🏻‍🤝‍👨🏼': 10, + '👨🏻‍🤝‍👨🏽': 10, + '👨🏻‍🤝‍👨🏾': 10, + '👨🏻‍🤝‍👨🏿': 10, + '👨🏻‍🦯': 6, + '👨🏻‍🦯‍➡️': 8, + '👨🏻‍🦰': 6, + '👨🏻‍🦱': 6, + '👨🏻‍🦲': 6, + '👨🏻‍🦳': 6, + '👨🏻‍🦼': 6, + '👨🏻‍🦼‍➡️': 8, + '👨🏻‍🦽': 6, + '👨🏻‍🦽‍➡️': 8, + '👨🏻‍🫯‍👨🏼': 9, + '👨🏻‍🫯‍👨🏽': 9, + '👨🏻‍🫯‍👨🏾': 9, + '👨🏻‍🫯‍👨🏿': 9, + '👨🏼‍⚕️': 6, + '👨🏼‍⚖️': 6, + '👨🏼‍✈️': 6, + '👨🏼‍❤️‍👨🏼': 10, + '👨🏼‍❤️‍💋‍👨🏼': 12, + '👨🏼‍❤️‍💋‍👨🏿': 12, + '👨🏼‍🌾': 6, + '👨🏼‍🍳': 6, + '👨🏼‍🍼': 6, + '👨🏼‍🎓': 6, + '👨🏼‍🎤': 6, + '👨🏼‍🎨': 6, + '👨🏼‍🏫': 6, + '👨🏼‍🏭': 6, + '👨🏼‍🐰‍👨🏻': 10, + '👨🏼‍🐰‍👨🏽': 10, + '👨🏼‍🐰‍👨🏿': 10, + '👨🏼‍💻': 6, + '👨🏼‍💼': 6, + '👨🏼‍🔧': 6, + '👨🏼‍🔬': 6, + '👨🏼‍🚀': 6, + '👨🏼‍🚒': 6, + '👨🏼‍🤝‍👨🏻': 10, + '👨🏼‍🤝‍👨🏽': 10, + '👨🏼‍🤝‍👨🏿': 10, + '👨🏼‍🦯': 6, + '👨🏼‍🦯‍➡️': 8, + '👨🏼‍🦰': 6, + '👨🏼‍🦱': 6, + '👨🏼‍🦲': 6, + '👨🏼‍🦳': 6, + '👨🏼‍🦼': 6, + '👨🏼‍🦽': 6, + '👨🏼‍🦽‍➡️': 8, + '👨🏼‍🫯‍👨🏻': 9, + '👨🏼‍🫯‍👨🏽': 9, + '👨🏼‍🫯‍👨🏾': 9, + '👨🏼‍🫯‍👨🏿': 9, + '👨🏽‍⚕️': 6, + '👨🏽‍⚖️': 6, + '👨🏽‍✈️': 6, + '👨🏽‍❤️‍👨🏻': 10, + '👨🏽‍❤️‍👨🏽': 10, + '👨🏽‍❤️‍👨🏿': 10, + '👨🏽‍❤️‍💋‍👨🏻': 12, + '👨🏽‍❤️‍💋‍👨🏽': 12, + '👨🏽‍❤️‍💋‍👨🏿': 12, + '👨🏽‍🌾': 6, + '👨🏽‍🍳': 6, + '👨🏽‍🍼': 6, + '👨🏽‍🎓': 6, + '👨🏽‍🎤': 6, + '👨🏽‍🎨': 6, + '👨🏽‍🏫': 6, + '👨🏽‍🏭': 6, + '👨🏽‍🐰‍👨🏻': 10, + '👨🏽‍🐰‍👨🏼': 10, + '👨🏽‍🐰‍👨🏾': 10, + '👨🏽‍🐰‍👨🏿': 10, + '👨🏽‍💻': 6, + '👨🏽‍💼': 6, + '👨🏽‍🔧': 6, + '👨🏽‍🔬': 6, + '👨🏽‍🚀': 6, + '👨🏽‍🚒': 6, + '👨🏽‍🤝‍👨🏻': 10, + '👨🏽‍🤝‍👨🏼': 10, + '👨🏽‍🤝‍👨🏾': 10, + '👨🏽‍🤝‍👨🏿': 10, + '👨🏽‍🦯': 6, + '👨🏽‍🦯‍➡️': 8, + '👨🏽‍🦰': 6, + '👨🏽‍🦱': 6, + '👨🏽‍🦲': 6, + '👨🏽‍🦳': 6, + '👨🏽‍🦼': 6, + '👨🏽‍🦼‍➡️': 8, + '👨🏽‍🦽': 6, + '👨🏽‍🦽‍➡️': 8, + '👨🏽‍🫯‍👨🏻': 9, + '👨🏽‍🫯‍👨🏼': 9, + '👨🏽‍🫯‍👨🏾': 9, + '👨🏽‍🫯‍👨🏿': 9, + '👨🏾‍⚕️': 6, + '👨🏾‍⚖️': 6, + '👨🏾‍✈️': 6, + '👨🏾‍❤️‍👨🏾': 10, + '👨🏾‍❤️‍💋‍👨🏻': 12, + '👨🏾‍❤️‍💋‍👨🏾': 12, + '👨🏾‍❤️‍💋‍👨🏿': 12, + '👨🏾‍🌾': 6, + '👨🏾‍🍳': 6, + '👨🏾‍🍼': 6, + '👨🏾‍🎓': 6, + '👨🏾‍🎤': 6, + '👨🏾‍🎨': 6, + '👨🏾‍🏫': 6, + '👨🏾‍🏭': 6, + '👨🏾‍🐰‍👨🏻': 10, + '👨🏾‍🐰‍👨🏽': 10, + '👨🏾‍🐰‍👨🏿': 10, + '👨🏾‍💻': 6, + '👨🏾‍💼': 6, + '👨🏾‍🔧': 6, + '👨🏾‍🔬': 6, + '👨🏾‍🚀': 6, + '👨🏾‍🚒': 6, + '👨🏾‍🤝‍👨🏻': 10, + '👨🏾‍🤝‍👨🏽': 10, + '👨🏾‍🤝‍👨🏿': 10, + '👨🏾‍🦯': 6, + '👨🏾‍🦯‍➡️': 8, + '👨🏾‍🦰': 6, + '👨🏾‍🦱': 6, + '👨🏾‍🦲': 6, + '👨🏾‍🦳': 6, + '👨🏾‍🦼': 6, + '👨🏾‍🦽': 6, + '👨🏾‍🦽‍➡️': 8, + '👨🏾‍🫯‍👨🏻': 9, + '👨🏾‍🫯‍👨🏼': 9, + '👨🏾‍🫯‍👨🏽': 9, + '👨🏾‍🫯‍👨🏿': 9, + '👨🏿‍⚕️': 6, + '👨🏿‍⚖️': 6, + '👨🏿‍✈️': 6, + '👨🏿‍❤️‍👨🏻': 10, + '👨🏿‍❤️‍👨🏽': 10, + '👨🏿‍❤️‍👨🏿': 10, + '👨🏿‍❤️‍💋‍👨🏻': 12, + '👨🏿‍❤️‍💋‍👨🏼': 12, + '👨🏿‍❤️‍💋‍👨🏽': 12, + '👨🏿‍❤️‍💋‍👨🏾': 12, + '👨🏿‍❤️‍💋‍👨🏿': 12, + '👨🏿‍🌾': 6, + '👨🏿‍🍳': 6, + '👨🏿‍🍼': 6, + '👨🏿‍🎓': 6, + '👨🏿‍🎤': 6, + '👨🏿‍🎨': 6, + '👨🏿‍🏫': 6, + '👨🏿‍🏭': 6, + '👨🏿‍🐰‍👨🏻': 10, + '👨🏿‍🐰‍👨🏼': 10, + '👨🏿‍🐰‍👨🏽': 10, + '👨🏿‍🐰‍👨🏾': 10, + '👨🏿‍💻': 6, + '👨🏿‍💼': 6, + '👨🏿‍🔧': 6, + '👨🏿‍🔬': 6, + '👨🏿‍🚀': 6, + '👨🏿‍🚒': 6, + '👨🏿‍🤝‍👨🏻': 10, + '👨🏿‍🤝‍👨🏼': 10, + '👨🏿‍🤝‍👨🏽': 10, + '👨🏿‍🤝‍👨🏾': 10, + '👨🏿‍🦯': 6, + '👨🏿‍🦯‍➡️': 8, + '👨🏿‍🦰': 6, + '👨🏿‍🦱': 6, + '👨🏿‍🦲': 6, + '👨🏿‍🦳': 6, + '👨🏿‍🦼': 6, + '👨🏿‍🦼‍➡️': 8, + '👨🏿‍🦽': 6, + '👨🏿‍🦽‍➡️': 8, + '👨🏿‍🫯‍👨🏻': 9, + '👨🏿‍🫯‍👨🏼': 9, + '👨🏿‍🫯‍👨🏽': 9, + '👨🏿‍🫯‍👨🏾': 9, + '👩‍⚕️': 4, + '👩‍⚖️': 4, + '👩‍✈️': 4, + '👩‍❤️‍👨': 6, + '👩‍❤️‍👩': 6, + '👩‍❤️‍💋‍👨': 8, + '👩‍❤️‍💋‍👩': 8, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 6, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 6, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 6, + '👩🏻‍⚕️': 6, + '👩🏻‍⚖️': 6, + '👩🏻‍✈️': 6, + '👩🏻‍❤️‍👨🏻': 10, + '👩🏻‍❤️‍👨🏿': 10, + '👩🏻‍❤️‍👩🏻': 10, + '👩🏻‍❤️‍💋‍👨🏻': 12, + '👩🏻‍❤️‍💋‍👨🏽': 12, + '👩🏻‍❤️‍💋‍👨🏿': 12, + '👩🏻‍❤️‍💋‍👩🏻': 12, + '👩🏻‍❤️‍💋‍👩🏽': 12, + '👩🏻‍❤️‍💋‍👩🏿': 12, + '👩🏻‍🌾': 6, + '👩🏻‍🍳': 6, + '👩🏻‍🍼': 6, + '👩🏻‍🎓': 6, + '👩🏻‍🎤': 6, + '👩🏻‍🎨': 6, + '👩🏻‍🏫': 6, + '👩🏻‍🏭': 6, + '👩🏻‍🐰‍👩🏼': 10, + '👩🏻‍🐰‍👩🏽': 10, + '👩🏻‍🐰‍👩🏾': 10, + '👩🏻‍🐰‍👩🏿': 10, + '👩🏻‍💻': 6, + '👩🏻‍💼': 6, + '👩🏻‍🔧': 6, + '👩🏻‍🔬': 6, + '👩🏻‍🚀': 6, + '👩🏻‍🚒': 6, + '👩🏻‍🤝‍👨🏽': 10, + '👩🏻‍🤝‍👨🏿': 10, + '👩🏻‍🤝‍👩🏼': 10, + '👩🏻‍🤝‍👩🏽': 10, + '👩🏻‍🤝‍👩🏾': 10, + '👩🏻‍🤝‍👩🏿': 10, + '👩🏻‍🦯': 6, + '👩🏻‍🦯‍➡️': 8, + '👩🏻‍🦰': 6, + '👩🏻‍🦱': 6, + '👩🏻‍🦲': 6, + '👩🏻‍🦳': 6, + '👩🏻‍🦼': 6, + '👩🏻‍🦼‍➡️': 8, + '👩🏻‍🦽': 6, + '👩🏻‍🦽‍➡️': 8, + '👩🏻‍🫯‍👩🏼': 9, + '👩🏻‍🫯‍👩🏽': 9, + '👩🏻‍🫯‍👩🏾': 9, + '👩🏻‍🫯‍👩🏿': 9, + '👩🏼‍⚕️': 6, + '👩🏼‍⚖️': 6, + '👩🏼‍✈️': 6, + '👩🏼‍❤️‍👨🏼': 10, + '👩🏼‍❤️‍👩🏼': 10, + '👩🏼‍❤️‍💋‍👨🏼': 12, + '👩🏼‍❤️‍💋‍👩🏼': 12, + '👩🏼‍🌾': 6, + '👩🏼‍🍳': 6, + '👩🏼‍🍼': 6, + '👩🏼‍🎓': 6, + '👩🏼‍🎤': 6, + '👩🏼‍🎨': 6, + '👩🏼‍🏫': 6, + '👩🏼‍🏭': 6, + '👩🏼‍🐰‍👩🏻': 10, + '👩🏼‍🐰‍👩🏿': 10, + '👩🏼‍💻': 6, + '👩🏼‍💼': 6, + '👩🏼‍🔧': 6, + '👩🏼‍🔬': 6, + '👩🏼‍🚀': 6, + '👩🏼‍🚒': 6, + '👩🏼‍🤝‍👩🏻': 10, + '👩🏼‍🤝‍👩🏽': 10, + '👩🏼‍🤝‍👩🏿': 10, + '👩🏼‍🦯': 6, + '👩🏼‍🦯‍➡️': 8, + '👩🏼‍🦰': 6, + '👩🏼‍🦱': 6, + '👩🏼‍🦲': 6, + '👩🏼‍🦳': 6, + '👩🏼‍🦼': 6, + '👩🏼‍🦽': 6, + '👩🏼‍🫯‍👩🏻': 9, + '👩🏼‍🫯‍👩🏽': 9, + '👩🏼‍🫯‍👩🏾': 9, + '👩🏼‍🫯‍👩🏿': 9, + '👩🏽‍⚕️': 6, + '👩🏽‍⚖️': 6, + '👩🏽‍✈️': 6, + '👩🏽‍❤️‍👨🏽': 10, + '👩🏽‍❤️‍👩🏽': 10, + '👩🏽‍❤️‍💋‍👨🏻': 12, + '👩🏽‍❤️‍💋‍👨🏽': 12, + '👩🏽‍❤️‍💋‍👨🏿': 12, + '👩🏽‍❤️‍💋‍👩🏻': 12, + '👩🏽‍❤️‍💋‍👩🏽': 12, + '👩🏽‍❤️‍💋‍👩🏿': 12, + '👩🏽‍🌾': 6, + '👩🏽‍🍳': 6, + '👩🏽‍🍼': 6, + '👩🏽‍🎓': 6, + '👩🏽‍🎤': 6, + '👩🏽‍🎨': 6, + '👩🏽‍🏫': 6, + '👩🏽‍🏭': 6, + '👩🏽‍🐰‍👩🏻': 10, + '👩🏽‍🐰‍👩🏾': 10, + '👩🏽‍🐰‍👩🏿': 10, + '👩🏽‍💻': 6, + '👩🏽‍💼': 6, + '👩🏽‍🔧': 6, + '👩🏽‍🔬': 6, + '👩🏽‍🚀': 6, + '👩🏽‍🚒': 6, + '👩🏽‍🤝‍👨🏻': 10, + '👩🏽‍🤝‍👨🏿': 10, + '👩🏽‍🤝‍👩🏻': 10, + '👩🏽‍🤝‍👩🏼': 10, + '👩🏽‍🤝‍👩🏾': 10, + '👩🏽‍🤝‍👩🏿': 10, + '👩🏽‍🦯': 6, + '👩🏽‍🦯‍➡️': 8, + '👩🏽‍🦰': 6, + '👩🏽‍🦱': 6, + '👩🏽‍🦲': 6, + '👩🏽‍🦳': 6, + '👩🏽‍🦼': 6, + '👩🏽‍🦼‍➡️': 8, + '👩🏽‍🦽': 6, + '👩🏽‍🦽‍➡️': 8, + '👩🏽‍🫯‍👩🏻': 9, + '👩🏽‍🫯‍👩🏼': 9, + '👩🏽‍🫯‍👩🏾': 9, + '👩🏽‍🫯‍👩🏿': 9, + '👩🏾‍⚕️': 6, + '👩🏾‍⚖️': 6, + '👩🏾‍✈️': 6, + '👩🏾‍❤️‍👨🏾': 10, + '👩🏾‍❤️‍👩🏾': 10, + '👩🏾‍❤️‍💋‍👨🏾': 12, + '👩🏾‍❤️‍💋‍👩🏾': 12, + '👩🏾‍🌾': 6, + '👩🏾‍🍳': 6, + '👩🏾‍🍼': 6, + '👩🏾‍🎓': 6, + '👩🏾‍🎤': 6, + '👩🏾‍🎨': 6, + '👩🏾‍🏫': 6, + '👩🏾‍🏭': 6, + '👩🏾‍🐰‍👩🏻': 10, + '👩🏾‍🐰‍👩🏽': 10, + '👩🏾‍🐰‍👩🏿': 10, + '👩🏾‍💻': 6, + '👩🏾‍💼': 6, + '👩🏾‍🔧': 6, + '👩🏾‍🔬': 6, + '👩🏾‍🚀': 6, + '👩🏾‍🚒': 6, + '👩🏾‍🤝‍👩🏻': 10, + '👩🏾‍🤝‍👩🏽': 10, + '👩🏾‍🤝‍👩🏿': 10, + '👩🏾‍🦯': 6, + '👩🏾‍🦯‍➡️': 8, + '👩🏾‍🦰': 6, + '👩🏾‍🦱': 6, + '👩🏾‍🦲': 6, + '👩🏾‍🦳': 6, + '👩🏾‍🦼': 6, + '👩🏾‍🦽': 6, + '👩🏾‍🦽‍➡️': 8, + '👩🏾‍🫯‍👩🏻': 9, + '👩🏾‍🫯‍👩🏼': 9, + '👩🏾‍🫯‍👩🏽': 9, + '👩🏾‍🫯‍👩🏿': 9, + '👩🏿‍⚕️': 6, + '👩🏿‍⚖️': 6, + '👩🏿‍✈️': 6, + '👩🏿‍❤️‍👨🏻': 10, + '👩🏿‍❤️‍👨🏿': 10, + '👩🏿‍❤️‍👩🏿': 10, + '👩🏿‍❤️‍💋‍👨🏻': 12, + '👩🏿‍❤️‍💋‍👨🏽': 12, + '👩🏿‍❤️‍💋‍👨🏿': 12, + '👩🏿‍❤️‍💋‍👩🏻': 12, + '👩🏿‍❤️‍💋‍👩🏽': 12, + '👩🏿‍❤️‍💋‍👩🏿': 12, + '👩🏿‍🌾': 6, + '👩🏿‍🍳': 6, + '👩🏿‍🍼': 6, + '👩🏿‍🎓': 6, + '👩🏿‍🎤': 6, + '👩🏿‍🎨': 6, + '👩🏿‍🏫': 6, + '👩🏿‍🏭': 6, + '👩🏿‍🐰‍👩🏻': 10, + '👩🏿‍🐰‍👩🏼': 10, + '👩🏿‍🐰‍👩🏽': 10, + '👩🏿‍🐰‍👩🏾': 10, + '👩🏿‍💻': 6, + '👩🏿‍💼': 6, + '👩🏿‍🔧': 6, + '👩🏿‍🔬': 6, + '👩🏿‍🚀': 6, + '👩🏿‍🚒': 6, + '👩🏿‍🤝‍👨🏻': 10, + '👩🏿‍🤝‍👨🏽': 10, + '👩🏿‍🤝‍👩🏻': 10, + '👩🏿‍🤝‍👩🏼': 10, + '👩🏿‍🤝‍👩🏽': 10, + '👩🏿‍🤝‍👩🏾': 10, + '👩🏿‍🦯': 6, + '👩🏿‍🦯‍➡️': 8, + '👩🏿‍🦰': 6, + '👩🏿‍🦱': 6, + '👩🏿‍🦲': 6, + '👩🏿‍🦳': 6, + '👩🏿‍🦼': 6, + '👩🏿‍🦼‍➡️': 8, + '👩🏿‍🦽': 6, + '👩🏿‍🦽‍➡️': 8, + '👩🏿‍🫯‍👩🏻': 9, + '👩🏿‍🫯‍👩🏼': 9, + '👩🏿‍🫯‍👩🏽': 9, + '👩🏿‍🫯‍👩🏾': 9, + '👮‍♀️': 4, + '👮‍♂️': 4, + '👮🏻‍♀️': 6, + '👮🏻‍♂️': 6, + '👮🏼‍♀️': 6, + '👮🏼‍♂️': 6, + '👮🏽‍♀️': 6, + '👮🏽‍♂️': 6, + '👮🏾‍♀️': 6, + '👮🏾‍♂️': 6, + '👮🏿‍♀️': 6, + '👮🏿‍♂️': 6, + '👯‍♀️': 4, + '👯‍♂️': 4, + '👯🏻‍♀️': 6, + '👯🏻‍♂️': 6, + '👯🏼‍♀️': 6, + '👯🏼‍♂️': 6, + '👯🏽‍♀️': 6, + '👯🏽‍♂️': 6, + '👯🏾‍♀️': 6, + '👯🏾‍♂️': 6, + '👯🏿‍♀️': 6, + '👯🏿‍♂️': 6, + '👰‍♀️': 4, + '👰‍♂️': 4, + '👰🏻‍♀️': 6, + '👰🏻‍♂️': 6, + '👰🏼‍♀️': 6, + '👰🏼‍♂️': 6, + '👰🏽‍♀️': 6, + '👰🏽‍♂️': 6, + '👰🏾‍♀️': 6, + '👰🏾‍♂️': 6, + '👰🏿‍♀️': 6, + '👰🏿‍♂️': 6, + '👱‍♀️': 4, + '👱‍♂️': 4, + '👱🏻‍♀️': 6, + '👱🏻‍♂️': 6, + '👱🏼‍♀️': 6, + '👱🏼‍♂️': 6, + '👱🏽‍♀️': 6, + '👱🏽‍♂️': 6, + '👱🏾‍♀️': 6, + '👱🏾‍♂️': 6, + '👱🏿‍♀️': 6, + '👱🏿‍♂️': 6, + '👳‍♀️': 4, + '👳‍♂️': 4, + '👳🏻‍♀️': 6, + '👳🏻‍♂️': 6, + '👳🏼‍♀️': 6, + '👳🏼‍♂️': 6, + '👳🏽‍♀️': 6, + '👳🏽‍♂️': 6, + '👳🏾‍♀️': 6, + '👳🏾‍♂️': 6, + '👳🏿‍♀️': 6, + '👳🏿‍♂️': 6, + '👷‍♀️': 4, + '👷‍♂️': 4, + '👷🏻‍♀️': 6, + '👷🏻‍♂️': 6, + '👷🏼‍♀️': 6, + '👷🏼‍♂️': 6, + '👷🏽‍♀️': 6, + '👷🏽‍♂️': 6, + '👷🏾‍♀️': 6, + '👷🏾‍♂️': 6, + '👷🏿‍♀️': 6, + '👷🏿‍♂️': 6, + '💁‍♀️': 4, + '💁‍♂️': 4, + '💁🏻‍♀️': 6, + '💁🏻‍♂️': 6, + '💁🏼‍♀️': 6, + '💁🏼‍♂️': 6, + '💁🏽‍♀️': 6, + '💁🏽‍♂️': 6, + '💁🏾‍♀️': 6, + '💁🏾‍♂️': 6, + '💁🏿‍♀️': 6, + '💁🏿‍♂️': 6, + '💂‍♀️': 4, + '💂‍♂️': 4, + '💂🏻‍♀️': 6, + '💂🏻‍♂️': 6, + '💂🏼‍♀️': 6, + '💂🏼‍♂️': 6, + '💂🏽‍♀️': 6, + '💂🏽‍♂️': 6, + '💂🏾‍♀️': 6, + '💂🏾‍♂️': 6, + '💂🏿‍♀️': 6, + '💂🏿‍♂️': 6, + '💆‍♀️': 4, + '💆‍♂️': 4, + '💆🏻‍♀️': 6, + '💆🏻‍♂️': 6, + '💆🏼‍♀️': 6, + '💆🏼‍♂️': 6, + '💆🏽‍♀️': 6, + '💆🏽‍♂️': 6, + '💆🏾‍♀️': 6, + '💆🏾‍♂️': 6, + '💆🏿‍♀️': 6, + '💆🏿‍♂️': 6, + '💇‍♀️': 4, + '💇‍♂️': 4, + '💇🏻‍♀️': 6, + '💇🏻‍♂️': 6, + '💇🏼‍♀️': 6, + '💇🏼‍♂️': 6, + '💇🏽‍♀️': 6, + '💇🏽‍♂️': 6, + '💇🏾‍♀️': 6, + '💇🏾‍♂️': 6, + '💇🏿‍♀️': 6, + '💇🏿‍♂️': 6, + '🕵️‍♀️': 4, + '🕵️‍♂️': 4, + '🕵🏻‍♀️': 5, + '🕵🏻‍♂️': 5, + '🕵🏼‍♀️': 5, + '🕵🏼‍♂️': 5, + '🕵🏽‍♀️': 5, + '🕵🏽‍♂️': 5, + '🕵🏾‍♀️': 5, + '🕵🏾‍♂️': 5, + '🕵🏿‍♀️': 5, + '🕵🏿‍♂️': 5, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 4, + '🙂‍↔️': 4, + '🙂‍↕️': 4, + '🙅‍♀️': 4, + '🙅‍♂️': 4, + '🙅🏻‍♀️': 6, + '🙅🏻‍♂️': 6, + '🙅🏼‍♀️': 6, + '🙅🏼‍♂️': 6, + '🙅🏽‍♀️': 6, + '🙅🏽‍♂️': 6, + '🙅🏾‍♀️': 6, + '🙅🏾‍♂️': 6, + '🙅🏿‍♀️': 6, + '🙅🏿‍♂️': 6, + '🙆‍♀️': 4, + '🙆‍♂️': 4, + '🙆🏻‍♀️': 6, + '🙆🏻‍♂️': 6, + '🙆🏼‍♀️': 6, + '🙆🏼‍♂️': 6, + '🙆🏽‍♀️': 6, + '🙆🏽‍♂️': 6, + '🙆🏾‍♀️': 6, + '🙆🏾‍♂️': 6, + '🙆🏿‍♀️': 6, + '🙆🏿‍♂️': 6, + '🙇‍♀️': 4, + '🙇‍♂️': 4, + '🙇🏻‍♀️': 6, + '🙇🏻‍♂️': 6, + '🙇🏼‍♀️': 6, + '🙇🏼‍♂️': 6, + '🙇🏽‍♀️': 6, + '🙇🏽‍♂️': 6, + '🙇🏾‍♀️': 6, + '🙇🏾‍♂️': 6, + '🙇🏿‍♀️': 6, + '🙇🏿‍♂️': 6, + '🙋‍♀️': 4, + '🙋‍♂️': 4, + '🙋🏻‍♀️': 6, + '🙋🏻‍♂️': 6, + '🙋🏼‍♀️': 6, + '🙋🏼‍♂️': 6, + '🙋🏽‍♀️': 6, + '🙋🏽‍♂️': 6, + '🙋🏾‍♀️': 6, + '🙋🏾‍♂️': 6, + '🙋🏿‍♀️': 6, + '🙋🏿‍♂️': 6, + '🙍‍♀️': 4, + '🙍‍♂️': 4, + '🙍🏻‍♀️': 6, + '🙍🏻‍♂️': 6, + '🙍🏼‍♀️': 6, + '🙍🏼‍♂️': 6, + '🙍🏽‍♀️': 6, + '🙍🏽‍♂️': 6, + '🙍🏾‍♀️': 6, + '🙍🏾‍♂️': 6, + '🙍🏿‍♀️': 6, + '🙍🏿‍♂️': 6, + '🙎‍♀️': 4, + '🙎‍♂️': 4, + '🙎🏻‍♀️': 6, + '🙎🏻‍♂️': 6, + '🙎🏼‍♀️': 6, + '🙎🏼‍♂️': 6, + '🙎🏽‍♀️': 6, + '🙎🏽‍♂️': 6, + '🙎🏾‍♀️': 6, + '🙎🏾‍♂️': 6, + '🙎🏿‍♀️': 6, + '🙎🏿‍♂️': 6, + '🚣‍♀️': 4, + '🚣‍♂️': 4, + '🚣🏻‍♀️': 6, + '🚣🏻‍♂️': 6, + '🚣🏼‍♀️': 6, + '🚣🏼‍♂️': 6, + '🚣🏽‍♀️': 6, + '🚣🏽‍♂️': 6, + '🚣🏾‍♀️': 6, + '🚣🏾‍♂️': 6, + '🚣🏿‍♀️': 6, + '🚣🏿‍♂️': 6, + '🚴‍♀️': 4, + '🚴‍♂️': 4, + '🚴🏻‍♀️': 6, + '🚴🏻‍♂️': 6, + '🚴🏼‍♀️': 6, + '🚴🏼‍♂️': 6, + '🚴🏽‍♀️': 6, + '🚴🏽‍♂️': 6, + '🚴🏾‍♀️': 6, + '🚴🏾‍♂️': 6, + '🚴🏿‍♀️': 6, + '🚴🏿‍♂️': 6, + '🚵‍♀️': 4, + '🚵‍♂️': 4, + '🚵🏻‍♀️': 6, + '🚵🏻‍♂️': 6, + '🚵🏼‍♀️': 6, + '🚵🏼‍♂️': 6, + '🚵🏽‍♀️': 6, + '🚵🏽‍♂️': 6, + '🚵🏾‍♀️': 6, + '🚵🏾‍♂️': 6, + '🚵🏿‍♀️': 6, + '🚵🏿‍♂️': 6, + '🚶‍♀️': 4, + '🚶‍♀️‍➡️': 6, + '🚶‍♂️': 4, + '🚶‍♂️‍➡️': 6, + '🚶‍➡️': 4, + '🚶🏻‍♀️': 6, + '🚶🏻‍♀️‍➡️': 8, + '🚶🏻‍♂️': 6, + '🚶🏻‍♂️‍➡️': 8, + '🚶🏻‍➡️': 6, + '🚶🏼‍♀️': 6, + '🚶🏼‍♀️‍➡️': 8, + '🚶🏼‍♂️': 6, + '🚶🏼‍♂️‍➡️': 8, + '🚶🏼‍➡️': 6, + '🚶🏽‍♀️': 6, + '🚶🏽‍♀️‍➡️': 8, + '🚶🏽‍♂️': 6, + '🚶🏽‍♂️‍➡️': 8, + '🚶🏽‍➡️': 6, + '🚶🏾‍♀️': 6, + '🚶🏾‍♀️‍➡️': 8, + '🚶🏾‍♂️': 6, + '🚶🏾‍♂️‍➡️': 8, + '🚶🏾‍➡️': 6, + '🚶🏿‍♀️': 6, + '🚶🏿‍♀️‍➡️': 8, + '🚶🏿‍♂️': 6, + '🚶🏿‍♂️‍➡️': 8, + '🚶🏿‍➡️': 6, + '🤦‍♀️': 4, + '🤦‍♂️': 4, + '🤦🏻‍♀️': 6, + '🤦🏻‍♂️': 6, + '🤦🏼‍♀️': 6, + '🤦🏼‍♂️': 6, + '🤦🏽‍♀️': 6, + '🤦🏽‍♂️': 6, + '🤦🏾‍♀️': 6, + '🤦🏾‍♂️': 6, + '🤦🏿‍♀️': 6, + '🤦🏿‍♂️': 6, + '🤵‍♀️': 4, + '🤵‍♂️': 4, + '🤵🏻‍♀️': 6, + '🤵🏻‍♂️': 6, + '🤵🏼‍♀️': 6, + '🤵🏼‍♂️': 6, + '🤵🏽‍♀️': 6, + '🤵🏽‍♂️': 6, + '🤵🏾‍♀️': 6, + '🤵🏾‍♂️': 6, + '🤵🏿‍♀️': 6, + '🤵🏿‍♂️': 6, + '🤷‍♀️': 4, + '🤷‍♂️': 4, + '🤷🏻‍♀️': 6, + '🤷🏻‍♂️': 6, + '🤷🏼‍♀️': 6, + '🤷🏼‍♂️': 6, + '🤷🏽‍♀️': 6, + '🤷🏽‍♂️': 6, + '🤷🏾‍♀️': 6, + '🤷🏾‍♂️': 6, + '🤷🏿‍♀️': 6, + '🤷🏿‍♂️': 6, + '🤸‍♀️': 4, + '🤸‍♂️': 4, + '🤸🏻‍♀️': 6, + '🤸🏻‍♂️': 6, + '🤸🏼‍♀️': 6, + '🤸🏼‍♂️': 6, + '🤸🏽‍♀️': 6, + '🤸🏽‍♂️': 6, + '🤸🏾‍♀️': 6, + '🤸🏾‍♂️': 6, + '🤸🏿‍♀️': 6, + '🤸🏿‍♂️': 6, + '🤹‍♀️': 4, + '🤹‍♂️': 4, + '🤹🏻‍♀️': 6, + '🤹🏻‍♂️': 6, + '🤹🏼‍♀️': 6, + '🤹🏼‍♂️': 6, + '🤹🏽‍♀️': 6, + '🤹🏽‍♂️': 6, + '🤹🏾‍♀️': 6, + '🤹🏾‍♂️': 6, + '🤹🏿‍♀️': 6, + '🤹🏿‍♂️': 6, + '🤼‍♀️': 4, + '🤼‍♂️': 4, + '🤼🏻‍♀️': 6, + '🤼🏻‍♂️': 6, + '🤼🏼‍♀️': 6, + '🤼🏼‍♂️': 6, + '🤼🏽‍♀️': 6, + '🤼🏽‍♂️': 6, + '🤼🏾‍♀️': 6, + '🤼🏾‍♂️': 6, + '🤼🏿‍♀️': 6, + '🤼🏿‍♂️': 6, + '🤽‍♀️': 4, + '🤽‍♂️': 4, + '🤽🏻‍♀️': 6, + '🤽🏻‍♂️': 6, + '🤽🏼‍♀️': 6, + '🤽🏼‍♂️': 6, + '🤽🏽‍♀️': 6, + '🤽🏽‍♂️': 6, + '🤽🏾‍♀️': 6, + '🤽🏾‍♂️': 6, + '🤽🏿‍♀️': 6, + '🤽🏿‍♂️': 6, + '🤾‍♀️': 4, + '🤾‍♂️': 4, + '🤾🏻‍♀️': 6, + '🤾🏻‍♂️': 6, + '🤾🏼‍♀️': 6, + '🤾🏼‍♂️': 6, + '🤾🏽‍♀️': 6, + '🤾🏽‍♂️': 6, + '🤾🏾‍♀️': 6, + '🤾🏾‍♂️': 6, + '🤾🏿‍♀️': 6, + '🤾🏿‍♂️': 6, + '🦸‍♀️': 4, + '🦸‍♂️': 4, + '🦸🏻‍♀️': 6, + '🦸🏻‍♂️': 6, + '🦸🏼‍♀️': 6, + '🦸🏼‍♂️': 6, + '🦸🏽‍♀️': 6, + '🦸🏽‍♂️': 6, + '🦸🏾‍♀️': 6, + '🦸🏾‍♂️': 6, + '🦸🏿‍♀️': 6, + '🦸🏿‍♂️': 6, + '🦹‍♀️': 4, + '🦹‍♂️': 4, + '🦹🏻‍♀️': 6, + '🦹🏻‍♂️': 6, + '🦹🏼‍♀️': 6, + '🦹🏼‍♂️': 6, + '🦹🏽‍♀️': 6, + '🦹🏽‍♂️': 6, + '🦹🏾‍♀️': 6, + '🦹🏾‍♂️': 6, + '🦹🏿‍♀️': 6, + '🦹🏿‍♂️': 6, + '🧍‍♀️': 4, + '🧍‍♂️': 4, + '🧍🏻‍♀️': 6, + '🧍🏻‍♂️': 6, + '🧍🏼‍♀️': 6, + '🧍🏼‍♂️': 6, + '🧍🏽‍♀️': 6, + '🧍🏽‍♂️': 6, + '🧍🏾‍♀️': 6, + '🧍🏾‍♂️': 6, + '🧍🏿‍♀️': 6, + '🧍🏿‍♂️': 6, + '🧎‍♀️': 4, + '🧎‍♀️‍➡️': 6, + '🧎‍♂️': 4, + '🧎‍♂️‍➡️': 6, + '🧎‍➡️': 4, + '🧎🏻‍♀️': 6, + '🧎🏻‍♀️‍➡️': 8, + '🧎🏻‍♂️': 6, + '🧎🏻‍♂️‍➡️': 8, + '🧎🏻‍➡️': 6, + '🧎🏼‍♀️': 6, + '🧎🏼‍♀️‍➡️': 8, + '🧎🏼‍♂️': 6, + '🧎🏼‍♂️‍➡️': 8, + '🧎🏼‍➡️': 6, + '🧎🏽‍♀️': 6, + '🧎🏽‍♀️‍➡️': 8, + '🧎🏽‍♂️': 6, + '🧎🏽‍♂️‍➡️': 8, + '🧎🏽‍➡️': 6, + '🧎🏾‍♀️': 6, + '🧎🏾‍♀️‍➡️': 8, + '🧎🏾‍♂️': 6, + '🧎🏾‍♂️‍➡️': 8, + '🧎🏾‍➡️': 6, + '🧎🏿‍♀️': 6, + '🧎🏿‍♀️‍➡️': 8, + '🧎🏿‍♂️': 6, + '🧎🏿‍♂️‍➡️': 8, + '🧎🏿‍➡️': 6, + '🧏‍♀️': 4, + '🧏‍♂️': 4, + '🧏🏻‍♀️': 6, + '🧏🏻‍♂️': 6, + '🧏🏼‍♀️': 6, + '🧏🏼‍♂️': 6, + '🧏🏽‍♀️': 6, + '🧏🏽‍♂️': 6, + '🧏🏾‍♀️': 6, + '🧏🏾‍♂️': 6, + '🧏🏿‍♀️': 6, + '🧏🏿‍♂️': 6, + '🧑‍⚕️': 4, + '🧑‍⚖️': 4, + '🧑‍✈️': 4, + '🧑‍🌾': 4, + '🧑‍🍳': 4, + '🧑‍🍼': 4, + '🧑‍🎄': 4, + '🧑‍🎓': 4, + '🧑‍🎤': 4, + '🧑‍🎨': 4, + '🧑‍🏫': 4, + '🧑‍🏭': 4, + '🧑‍💻': 4, + '🧑‍💼': 4, + '🧑‍🔧': 4, + '🧑‍🔬': 4, + '🧑‍🚀': 4, + '🧑‍🚒': 4, + '🧑‍🤝‍🧑': 6, + '🧑‍🦯': 4, + '🧑‍🦯‍➡️': 6, + '🧑‍🦰': 4, + '🧑‍🦱': 4, + '🧑‍🦲': 4, + '🧑‍🦳': 4, + '🧑‍🦼': 4, + '🧑‍🦼‍➡️': 6, + '🧑‍🦽': 4, + '🧑‍🦽‍➡️': 6, + '🧑‍🧑‍🧒': 6, + '🧑‍🧑‍🧒‍🧒': 8, + '🧑‍🧒': 4, + '🧑‍🧒‍🧒': 6, + '🧑‍🩰': 4, + '🧑🏻‍⚕️': 6, + '🧑🏻‍⚖️': 6, + '🧑🏻‍✈️': 6, + '🧑🏻‍❤️‍💋‍🧑🏿': 12, + '🧑🏻‍🌾': 6, + '🧑🏻‍🍳': 6, + '🧑🏻‍🍼': 6, + '🧑🏻‍🎄': 6, + '🧑🏻‍🎓': 6, + '🧑🏻‍🎤': 6, + '🧑🏻‍🎨': 6, + '🧑🏻‍🏫': 6, + '🧑🏻‍🏭': 6, + '🧑🏻‍🐰‍🧑🏽': 10, + '🧑🏻‍🐰‍🧑🏾': 10, + '🧑🏻‍🐰‍🧑🏿': 10, + '🧑🏻‍💻': 6, + '🧑🏻‍💼': 6, + '🧑🏻‍🔧': 6, + '🧑🏻‍🔬': 6, + '🧑🏻‍🚀': 6, + '🧑🏻‍🚒': 6, + '🧑🏻‍🤝‍🧑🏻': 10, + '🧑🏻‍🤝‍🧑🏼': 10, + '🧑🏻‍🤝‍🧑🏽': 10, + '🧑🏻‍🤝‍🧑🏾': 10, + '🧑🏻‍🤝‍🧑🏿': 10, + '🧑🏻‍🦯': 6, + '🧑🏻‍🦯‍➡️': 8, + '🧑🏻‍🦰': 6, + '🧑🏻‍🦱': 6, + '🧑🏻‍🦲': 6, + '🧑🏻‍🦳': 6, + '🧑🏻‍🦼': 6, + '🧑🏻‍🦼‍➡️': 8, + '🧑🏻‍🦽': 6, + '🧑🏻‍🦽‍➡️': 8, + '🧑🏻‍🩰': 6, + '🧑🏻‍🫯‍🧑🏼': 9, + '🧑🏻‍🫯‍🧑🏽': 9, + '🧑🏻‍🫯‍🧑🏾': 9, + '🧑🏻‍🫯‍🧑🏿': 9, + '🧑🏼‍⚕️': 6, + '🧑🏼‍⚖️': 6, + '🧑🏼‍✈️': 6, + '🧑🏼‍🌾': 6, + '🧑🏼‍🍳': 6, + '🧑🏼‍🍼': 6, + '🧑🏼‍🎄': 6, + '🧑🏼‍🎓': 6, + '🧑🏼‍🎤': 6, + '🧑🏼‍🎨': 6, + '🧑🏼‍🏫': 6, + '🧑🏼‍🏭': 6, + '🧑🏼‍🐰‍🧑🏿': 10, + '🧑🏼‍💻': 6, + '🧑🏼‍💼': 6, + '🧑🏼‍🔧': 6, + '🧑🏼‍🔬': 6, + '🧑🏼‍🚀': 6, + '🧑🏼‍🚒': 6, + '🧑🏼‍🤝‍🧑🏻': 10, + '🧑🏼‍🤝‍🧑🏼': 10, + '🧑🏼‍🤝‍🧑🏽': 10, + '🧑🏼‍🤝‍🧑🏿': 10, + '🧑🏼‍🦯': 6, + '🧑🏼‍🦯‍➡️': 8, + '🧑🏼‍🦰': 6, + '🧑🏼‍🦱': 6, + '🧑🏼‍🦲': 6, + '🧑🏼‍🦳': 6, + '🧑🏼‍🦼': 6, + '🧑🏼‍🦽': 6, + '🧑🏼‍🩰': 6, + '🧑🏼‍🫯‍🧑🏻': 9, + '🧑🏼‍🫯‍🧑🏽': 9, + '🧑🏼‍🫯‍🧑🏿': 9, + '🧑🏽‍⚕️': 6, + '🧑🏽‍⚖️': 6, + '🧑🏽‍✈️': 6, + '🧑🏽‍❤️‍💋‍🧑🏿': 12, + '🧑🏽‍🌾': 6, + '🧑🏽‍🍳': 6, + '🧑🏽‍🍼': 6, + '🧑🏽‍🎄': 6, + '🧑🏽‍🎓': 6, + '🧑🏽‍🎤': 6, + '🧑🏽‍🎨': 6, + '🧑🏽‍🏫': 6, + '🧑🏽‍🏭': 6, + '🧑🏽‍🐰‍🧑🏻': 10, + '🧑🏽‍🐰‍🧑🏿': 10, + '🧑🏽‍💻': 6, + '🧑🏽‍💼': 6, + '🧑🏽‍🔧': 6, + '🧑🏽‍🔬': 6, + '🧑🏽‍🚀': 6, + '🧑🏽‍🚒': 6, + '🧑🏽‍🤝‍🧑🏻': 10, + '🧑🏽‍🤝‍🧑🏼': 10, + '🧑🏽‍🤝‍🧑🏽': 10, + '🧑🏽‍🤝‍🧑🏾': 10, + '🧑🏽‍🤝‍🧑🏿': 10, + '🧑🏽‍🦯': 6, + '🧑🏽‍🦯‍➡️': 8, + '🧑🏽‍🦰': 6, + '🧑🏽‍🦱': 6, + '🧑🏽‍🦲': 6, + '🧑🏽‍🦳': 6, + '🧑🏽‍🦼': 6, + '🧑🏽‍🦼‍➡️': 8, + '🧑🏽‍🦽': 6, + '🧑🏽‍🦽‍➡️': 8, + '🧑🏽‍🩰': 6, + '🧑🏽‍🫯‍🧑🏻': 9, + '🧑🏽‍🫯‍🧑🏼': 9, + '🧑🏽‍🫯‍🧑🏾': 9, + '🧑🏽‍🫯‍🧑🏿': 9, + '🧑🏾‍⚕️': 6, + '🧑🏾‍⚖️': 6, + '🧑🏾‍✈️': 6, + '🧑🏾‍🌾': 6, + '🧑🏾‍🍳': 6, + '🧑🏾‍🍼': 6, + '🧑🏾‍🎄': 6, + '🧑🏾‍🎓': 6, + '🧑🏾‍🎤': 6, + '🧑🏾‍🎨': 6, + '🧑🏾‍🏫': 6, + '🧑🏾‍🏭': 6, + '🧑🏾‍🐰‍🧑🏻': 10, + '🧑🏾‍🐰‍🧑🏿': 10, + '🧑🏾‍💻': 6, + '🧑🏾‍💼': 6, + '🧑🏾‍🔧': 6, + '🧑🏾‍🔬': 6, + '🧑🏾‍🚀': 6, + '🧑🏾‍🚒': 6, + '🧑🏾‍🤝‍🧑🏻': 10, + '🧑🏾‍🤝‍🧑🏽': 10, + '🧑🏾‍🤝‍🧑🏾': 10, + '🧑🏾‍🤝‍🧑🏿': 10, + '🧑🏾‍🦯': 6, + '🧑🏾‍🦯‍➡️': 8, + '🧑🏾‍🦰': 6, + '🧑🏾‍🦱': 6, + '🧑🏾‍🦲': 6, + '🧑🏾‍🦳': 6, + '🧑🏾‍🦼': 6, + '🧑🏾‍🦽': 6, + '🧑🏾‍🩰': 6, + '🧑🏾‍🫯‍🧑🏻': 9, + '🧑🏾‍🫯‍🧑🏽': 9, + '🧑🏾‍🫯‍🧑🏿': 9, + '🧑🏿‍⚕️': 6, + '🧑🏿‍⚖️': 6, + '🧑🏿‍✈️': 6, + '🧑🏿‍❤️‍💋‍🧑🏻': 12, + '🧑🏿‍❤️‍💋‍🧑🏽': 12, + '🧑🏿‍🌾': 6, + '🧑🏿‍🍳': 6, + '🧑🏿‍🍼': 6, + '🧑🏿‍🎄': 6, + '🧑🏿‍🎓': 6, + '🧑🏿‍🎤': 6, + '🧑🏿‍🎨': 6, + '🧑🏿‍🏫': 6, + '🧑🏿‍🏭': 6, + '🧑🏿‍🐰‍🧑🏻': 10, + '🧑🏿‍🐰‍🧑🏼': 10, + '🧑🏿‍🐰‍🧑🏽': 10, + '🧑🏿‍🐰‍🧑🏾': 10, + '🧑🏿‍💻': 6, + '🧑🏿‍💼': 6, + '🧑🏿‍🔧': 6, + '🧑🏿‍🔬': 6, + '🧑🏿‍🚀': 6, + '🧑🏿‍🚒': 6, + '🧑🏿‍🤝‍🧑🏻': 10, + '🧑🏿‍🤝‍🧑🏼': 10, + '🧑🏿‍🤝‍🧑🏽': 10, + '🧑🏿‍🤝‍🧑🏾': 10, + '🧑🏿‍🤝‍🧑🏿': 10, + '🧑🏿‍🦯': 6, + '🧑🏿‍🦯‍➡️': 8, + '🧑🏿‍🦰': 6, + '🧑🏿‍🦱': 6, + '🧑🏿‍🦲': 6, + '🧑🏿‍🦳': 6, + '🧑🏿‍🦼': 6, + '🧑🏿‍🦼‍➡️': 8, + '🧑🏿‍🦽': 6, + '🧑🏿‍🦽‍➡️': 8, + '🧑🏿‍🩰': 6, + '🧑🏿‍🫯‍🧑🏻': 9, + '🧑🏿‍🫯‍🧑🏼': 9, + '🧑🏿‍🫯‍🧑🏽': 9, + '🧑🏿‍🫯‍🧑🏾': 9, + '🧔‍♀️': 4, + '🧔‍♂️': 4, + '🧔🏻‍♀️': 6, + '🧔🏻‍♂️': 6, + '🧔🏼‍♀️': 6, + '🧔🏼‍♂️': 6, + '🧔🏽‍♀️': 6, + '🧔🏽‍♂️': 6, + '🧔🏾‍♀️': 6, + '🧔🏾‍♂️': 6, + '🧔🏿‍♀️': 6, + '🧔🏿‍♂️': 6, + '🧖‍♀️': 4, + '🧖‍♂️': 4, + '🧖🏻‍♀️': 6, + '🧖🏻‍♂️': 6, + '🧖🏼‍♀️': 6, + '🧖🏼‍♂️': 6, + '🧖🏽‍♀️': 6, + '🧖🏽‍♂️': 6, + '🧖🏾‍♀️': 6, + '🧖🏾‍♂️': 6, + '🧖🏿‍♀️': 6, + '🧖🏿‍♂️': 6, + '🧗‍♀️': 4, + '🧗‍♂️': 4, + '🧗🏻‍♀️': 6, + '🧗🏻‍♂️': 6, + '🧗🏼‍♀️': 6, + '🧗🏼‍♂️': 6, + '🧗🏽‍♀️': 6, + '🧗🏽‍♂️': 6, + '🧗🏾‍♀️': 6, + '🧗🏾‍♂️': 6, + '🧗🏿‍♀️': 6, + '🧗🏿‍♂️': 6, + '🧘‍♀️': 4, + '🧘‍♂️': 4, + '🧘🏻‍♀️': 6, + '🧘🏻‍♂️': 6, + '🧘🏼‍♀️': 6, + '🧘🏼‍♂️': 6, + '🧘🏽‍♀️': 6, + '🧘🏽‍♂️': 6, + '🧘🏾‍♀️': 6, + '🧘🏾‍♂️': 6, + '🧘🏿‍♀️': 6, + '🧘🏿‍♂️': 6, + '🧙‍♀️': 4, + '🧙‍♂️': 4, + '🧙🏻‍♀️': 6, + '🧙🏻‍♂️': 6, + '🧙🏼‍♀️': 6, + '🧙🏼‍♂️': 6, + '🧙🏽‍♀️': 6, + '🧙🏽‍♂️': 6, + '🧙🏾‍♀️': 6, + '🧙🏾‍♂️': 6, + '🧙🏿‍♀️': 6, + '🧙🏿‍♂️': 6, + '🧚‍♀️': 4, + '🧚‍♂️': 4, + '🧚🏻‍♀️': 6, + '🧚🏻‍♂️': 6, + '🧚🏼‍♀️': 6, + '🧚🏼‍♂️': 6, + '🧚🏽‍♀️': 6, + '🧚🏽‍♂️': 6, + '🧚🏾‍♀️': 6, + '🧚🏾‍♂️': 6, + '🧚🏿‍♀️': 6, + '🧚🏿‍♂️': 6, + '🧛‍♀️': 4, + '🧛‍♂️': 4, + '🧛🏻‍♀️': 6, + '🧛🏻‍♂️': 6, + '🧛🏼‍♀️': 6, + '🧛🏼‍♂️': 6, + '🧛🏽‍♀️': 6, + '🧛🏽‍♂️': 6, + '🧛🏾‍♀️': 6, + '🧛🏾‍♂️': 6, + '🧛🏿‍♀️': 6, + '🧛🏿‍♂️': 6, + '🧜‍♀️': 4, + '🧜‍♂️': 4, + '🧜🏻‍♀️': 6, + '🧜🏻‍♂️': 6, + '🧜🏼‍♀️': 6, + '🧜🏼‍♂️': 6, + '🧜🏽‍♀️': 6, + '🧜🏽‍♂️': 6, + '🧜🏾‍♀️': 6, + '🧜🏾‍♂️': 6, + '🧜🏿‍♀️': 6, + '🧜🏿‍♂️': 6, + '🧝‍♀️': 4, + '🧝‍♂️': 4, + '🧝🏻‍♀️': 6, + '🧝🏻‍♂️': 6, + '🧝🏼‍♀️': 6, + '🧝🏼‍♂️': 6, + '🧝🏽‍♀️': 6, + '🧝🏽‍♂️': 6, + '🧝🏾‍♀️': 6, + '🧝🏾‍♂️': 6, + '🧝🏿‍♀️': 6, + '🧝🏿‍♂️': 6, + '🧞‍♀️': 4, + '🧞‍♂️': 4, + '🧟‍♀️': 4, + '🧟‍♂️': 4, + '🫱🏻‍🫲🏼': 8, + '🫱🏻‍🫲🏽': 8, + '🫱🏻‍🫲🏾': 8, + '🫱🏻‍🫲🏿': 8, + '🫱🏼‍🫲🏻': 8, + '🫱🏼‍🫲🏽': 8, + '🫱🏼‍🫲🏾': 8, + '🫱🏼‍🫲🏿': 8, + '🫱🏽‍🫲🏻': 8, + '🫱🏽‍🫲🏼': 8, + '🫱🏽‍🫲🏾': 8, + '🫱🏽‍🫲🏿': 8, + '🫱🏾‍🫲🏻': 8, + '🫱🏾‍🫲🏼': 8, + '🫱🏾‍🫲🏽': 8, + '🫱🏾‍🫲🏿': 8, + '🫱🏿‍🫲🏻': 8, + '🫱🏿‍🫲🏼': 8, + '🫱🏿‍🫲🏽': 8, + '🫱🏿‍🫲🏾': 8, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_970dbe10.py b/libs/wcwidth/table_grapheme_overrides/_known_970dbe10.py new file mode 100644 index 000000000..996239aa3 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_970dbe10.py @@ -0,0 +1,2839 @@ +""" +Grapheme overrides for _known_970dbe10. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: bobcat +""" +# pylint: skip-file +GRAPHEMES = { + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'কিং': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 4, + '⛹️‍♀️': 4, + '⛹️‍♂️': 4, + '⛹🏻‍♀️': 3, + '⛹🏻‍♂️': 3, + '⛹🏼‍♀️': 3, + '⛹🏼‍♂️': 3, + '⛹🏽‍♀️': 3, + '⛹🏽‍♂️': 3, + '⛹🏾‍♀️': 3, + '⛹🏾‍♂️': 3, + '⛹🏿‍♀️': 3, + '⛹🏿‍♂️': 3, + '❤️‍🔥': 4, + '❤️‍🩹': 4, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '🇦🇨': 4, + '🇦🇩': 4, + '🇦🇪': 4, + '🇦🇫': 4, + '🇦🇬': 4, + '🇦🇮': 4, + '🇦🇱': 4, + '🇦🇲': 4, + '🇦🇴': 4, + '🇦🇶': 4, + '🇦🇷': 4, + '🇦🇸': 4, + '🇦🇹': 4, + '🇦🇺': 4, + '🇦🇼': 4, + '🇦🇽': 4, + '🇦🇿': 4, + '🇧🇦': 4, + '🇧🇧': 4, + '🇧🇩': 4, + '🇧🇪': 4, + '🇧🇫': 4, + '🇧🇬': 4, + '🇧🇭': 4, + '🇧🇮': 4, + '🇧🇯': 4, + '🇧🇱': 4, + '🇧🇲': 4, + '🇧🇳': 4, + '🇧🇴': 4, + '🇧🇶': 4, + '🇧🇷': 4, + '🇧🇸': 4, + '🇧🇹': 4, + '🇧🇻': 4, + '🇧🇼': 4, + '🇧🇾': 4, + '🇧🇿': 4, + '🇨🇦': 4, + '🇨🇨': 4, + '🇨🇩': 4, + '🇨🇫': 4, + '🇨🇬': 4, + '🇨🇭': 4, + '🇨🇮': 4, + '🇨🇰': 4, + '🇨🇱': 4, + '🇨🇲': 4, + '🇨🇳': 4, + '🇨🇴': 4, + '🇨🇵': 4, + '🇨🇶': 4, + '🇨🇷': 4, + '🇨🇺': 4, + '🇨🇻': 4, + '🇨🇼': 4, + '🇨🇽': 4, + '🇨🇾': 4, + '🇨🇿': 4, + '🇩🇪': 4, + '🇩🇬': 4, + '🇩🇯': 4, + '🇩🇰': 4, + '🇩🇲': 4, + '🇩🇴': 4, + '🇩🇿': 4, + '🇪🇦': 4, + '🇪🇨': 4, + '🇪🇪': 4, + '🇪🇬': 4, + '🇪🇭': 4, + '🇪🇷': 4, + '🇪🇸': 4, + '🇪🇹': 4, + '🇪🇺': 4, + '🇫🇮': 4, + '🇫🇯': 4, + '🇫🇰': 4, + '🇫🇲': 4, + '🇫🇴': 4, + '🇫🇷': 4, + '🇬🇦': 4, + '🇬🇧': 4, + '🇬🇩': 4, + '🇬🇪': 4, + '🇬🇫': 4, + '🇬🇬': 4, + '🇬🇭': 4, + '🇬🇮': 4, + '🇬🇱': 4, + '🇬🇲': 4, + '🇬🇳': 4, + '🇬🇵': 4, + '🇬🇶': 4, + '🇬🇷': 4, + '🇬🇸': 4, + '🇬🇹': 4, + '🇬🇺': 4, + '🇬🇼': 4, + '🇬🇾': 4, + '🇭🇰': 4, + '🇭🇲': 4, + '🇭🇳': 4, + '🇭🇷': 4, + '🇭🇹': 4, + '🇭🇺': 4, + '🇮🇨': 4, + '🇮🇩': 4, + '🇮🇪': 4, + '🇮🇱': 4, + '🇮🇲': 4, + '🇮🇳': 4, + '🇮🇴': 4, + '🇮🇶': 4, + '🇮🇷': 4, + '🇮🇸': 4, + '🇮🇹': 4, + '🇯🇪': 4, + '🇯🇲': 4, + '🇯🇴': 4, + '🇯🇵': 4, + '🇰🇪': 4, + '🇰🇬': 4, + '🇰🇭': 4, + '🇰🇮': 4, + '🇰🇲': 4, + '🇰🇳': 4, + '🇰🇵': 4, + '🇰🇷': 4, + '🇰🇼': 4, + '🇰🇾': 4, + '🇰🇿': 4, + '🇱🇦': 4, + '🇱🇧': 4, + '🇱🇨': 4, + '🇱🇮': 4, + '🇱🇰': 4, + '🇱🇷': 4, + '🇱🇸': 4, + '🇱🇹': 4, + '🇱🇺': 4, + '🇱🇻': 4, + '🇱🇾': 4, + '🇲🇦': 4, + '🇲🇨': 4, + '🇲🇩': 4, + '🇲🇪': 4, + '🇲🇫': 4, + '🇲🇬': 4, + '🇲🇭': 4, + '🇲🇰': 4, + '🇲🇱': 4, + '🇲🇲': 4, + '🇲🇳': 4, + '🇲🇴': 4, + '🇲🇵': 4, + '🇲🇶': 4, + '🇲🇷': 4, + '🇲🇸': 4, + '🇲🇹': 4, + '🇲🇺': 4, + '🇲🇻': 4, + '🇲🇼': 4, + '🇲🇽': 4, + '🇲🇾': 4, + '🇲🇿': 4, + '🇳🇦': 4, + '🇳🇨': 4, + '🇳🇪': 4, + '🇳🇫': 4, + '🇳🇬': 4, + '🇳🇮': 4, + '🇳🇱': 4, + '🇳🇴': 4, + '🇳🇵': 4, + '🇳🇷': 4, + '🇳🇺': 4, + '🇳🇿': 4, + '🇴🇲': 4, + '🇵🇦': 4, + '🇵🇪': 4, + '🇵🇫': 4, + '🇵🇬': 4, + '🇵🇭': 4, + '🇵🇰': 4, + '🇵🇱': 4, + '🇵🇲': 4, + '🇵🇳': 4, + '🇵🇷': 4, + '🇵🇸': 4, + '🇵🇹': 4, + '🇵🇼': 4, + '🇵🇾': 4, + '🇶🇦': 4, + '🇷🇪': 4, + '🇷🇴': 4, + '🇷🇸': 4, + '🇷🇺': 4, + '🇷🇼': 4, + '🇸🇦': 4, + '🇸🇧': 4, + '🇸🇨': 4, + '🇸🇩': 4, + '🇸🇪': 4, + '🇸🇬': 4, + '🇸🇭': 4, + '🇸🇮': 4, + '🇸🇯': 4, + '🇸🇰': 4, + '🇸🇱': 4, + '🇸🇲': 4, + '🇸🇳': 4, + '🇸🇴': 4, + '🇸🇷': 4, + '🇸🇸': 4, + '🇸🇹': 4, + '🇸🇻': 4, + '🇸🇽': 4, + '🇸🇾': 4, + '🇸🇿': 4, + '🇹🇦': 4, + '🇹🇨': 4, + '🇹🇩': 4, + '🇹🇫': 4, + '🇹🇬': 4, + '🇹🇭': 4, + '🇹🇯': 4, + '🇹🇰': 4, + '🇹🇱': 4, + '🇹🇲': 4, + '🇹🇳': 4, + '🇹🇴': 4, + '🇹🇷': 4, + '🇹🇹': 4, + '🇹🇻': 4, + '🇹🇼': 4, + '🇹🇿': 4, + '🇺🇦': 4, + '🇺🇬': 4, + '🇺🇲': 4, + '🇺🇳': 4, + '🇺🇸': 4, + '🇺🇾': 4, + '🇺🇿': 4, + '🇻🇦': 4, + '🇻🇨': 4, + '🇻🇪': 4, + '🇻🇬': 4, + '🇻🇮': 4, + '🇻🇳': 4, + '🇻🇺': 4, + '🇼🇫': 4, + '🇼🇸': 4, + '🇽🇰': 4, + '🇾🇪': 4, + '🇾🇹': 4, + '🇿🇦': 4, + '🇿🇲': 4, + '🇿🇼': 4, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 4, + '🏃‍♀️‍➡️': 6, + '🏃‍♂️': 4, + '🏃‍♂️‍➡️': 6, + '🏃‍➡️': 4, + '🏃🏻‍♀️': 4, + '🏃🏻‍♀️‍➡️': 6, + '🏃🏻‍♂️': 4, + '🏃🏻‍♂️‍➡️': 6, + '🏃🏻‍➡️': 4, + '🏃🏼‍♀️': 4, + '🏃🏼‍♀️‍➡️': 6, + '🏃🏼‍♂️': 4, + '🏃🏼‍♂️‍➡️': 6, + '🏃🏼‍➡️': 4, + '🏃🏽‍♀️': 4, + '🏃🏽‍♀️‍➡️': 6, + '🏃🏽‍♂️': 4, + '🏃🏽‍♂️‍➡️': 6, + '🏃🏽‍➡️': 4, + '🏃🏾‍♀️': 4, + '🏃🏾‍♀️‍➡️': 6, + '🏃🏾‍♂️': 4, + '🏃🏾‍♂️‍➡️': 6, + '🏃🏾‍➡️': 4, + '🏃🏿‍♀️': 4, + '🏃🏿‍♀️‍➡️': 6, + '🏃🏿‍♂️': 4, + '🏃🏿‍♂️‍➡️': 6, + '🏃🏿‍➡️': 4, + '🏄‍♀️': 4, + '🏄‍♂️': 4, + '🏄🏻‍♀️': 4, + '🏄🏻‍♂️': 4, + '🏄🏼‍♀️': 4, + '🏄🏼‍♂️': 4, + '🏄🏽‍♀️': 4, + '🏄🏽‍♂️': 4, + '🏄🏾‍♀️': 4, + '🏄🏾‍♂️': 4, + '🏄🏿‍♀️': 4, + '🏄🏿‍♂️': 4, + '🏊‍♀️': 4, + '🏊‍♂️': 4, + '🏊🏻‍♀️': 4, + '🏊🏻‍♂️': 4, + '🏊🏼‍♀️': 4, + '🏊🏼‍♂️': 4, + '🏊🏽‍♀️': 4, + '🏊🏽‍♂️': 4, + '🏊🏾‍♀️': 4, + '🏊🏾‍♂️': 4, + '🏊🏿‍♀️': 4, + '🏊🏿‍♂️': 4, + '🏋️‍♀️': 4, + '🏋️‍♂️': 4, + '🏋🏻‍♀️': 3, + '🏋🏻‍♂️': 3, + '🏋🏼‍♀️': 3, + '🏋🏼‍♂️': 3, + '🏋🏽‍♀️': 3, + '🏋🏽‍♂️': 3, + '🏋🏾‍♀️': 3, + '🏋🏾‍♂️': 3, + '🏋🏿‍♀️': 3, + '🏋🏿‍♂️': 3, + '🏌️‍♀️': 4, + '🏌️‍♂️': 4, + '🏌🏻‍♀️': 3, + '🏌🏻‍♂️': 3, + '🏌🏼‍♀️': 3, + '🏌🏼‍♂️': 3, + '🏌🏽‍♀️': 3, + '🏌🏽‍♂️': 3, + '🏌🏾‍♀️': 3, + '🏌🏾‍♂️': 3, + '🏌🏿‍♀️': 3, + '🏌🏿‍♂️': 3, + '🏳️‍⚧️': 4, + '🏳️‍🌈': 4, + '🏴‍☠️': 4, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 4, + '👁️‍🗨️': 4, + '👨‍⚕️': 4, + '👨‍⚖️': 4, + '👨‍✈️': 4, + '👨‍❤️‍👨': 6, + '👨‍❤️‍💋‍👨': 8, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 6, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 6, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 6, + '👨🏻‍⚕️': 4, + '👨🏻‍⚖️': 4, + '👨🏻‍✈️': 4, + '👨🏻‍❤️‍👨🏻': 6, + '👨🏻‍❤️‍👨🏽': 6, + '👨🏻‍❤️‍👨🏿': 6, + '👨🏻‍❤️‍💋‍👨🏻': 8, + '👨🏻‍❤️‍💋‍👨🏽': 8, + '👨🏻‍❤️‍💋‍👨🏾': 8, + '👨🏻‍❤️‍💋‍👨🏿': 8, + '👨🏻‍🌾': 4, + '👨🏻‍🍳': 4, + '👨🏻‍🍼': 4, + '👨🏻‍🎓': 4, + '👨🏻‍🎤': 4, + '👨🏻‍🎨': 4, + '👨🏻‍🏫': 4, + '👨🏻‍🏭': 4, + '👨🏻‍🐰‍👨🏼': 6, + '👨🏻‍🐰‍👨🏽': 6, + '👨🏻‍🐰‍👨🏾': 6, + '👨🏻‍🐰‍👨🏿': 6, + '👨🏻‍💻': 4, + '👨🏻‍💼': 4, + '👨🏻‍🔧': 4, + '👨🏻‍🔬': 4, + '👨🏻‍🚀': 4, + '👨🏻‍🚒': 4, + '👨🏻‍🤝‍👨🏼': 6, + '👨🏻‍🤝‍👨🏽': 6, + '👨🏻‍🤝‍👨🏾': 6, + '👨🏻‍🤝‍👨🏿': 6, + '👨🏻‍🦯': 4, + '👨🏻‍🦯‍➡️': 6, + '👨🏻‍🦰': 4, + '👨🏻‍🦱': 4, + '👨🏻‍🦲': 4, + '👨🏻‍🦳': 4, + '👨🏻‍🦼': 4, + '👨🏻‍🦼‍➡️': 6, + '👨🏻‍🦽': 4, + '👨🏻‍🦽‍➡️': 6, + '👨🏻‍🫯‍👨🏼': 6, + '👨🏻‍🫯‍👨🏽': 6, + '👨🏻‍🫯‍👨🏾': 6, + '👨🏻‍🫯‍👨🏿': 6, + '👨🏼‍⚕️': 4, + '👨🏼‍⚖️': 4, + '👨🏼‍✈️': 4, + '👨🏼‍❤️‍👨🏼': 6, + '👨🏼‍❤️‍💋‍👨🏼': 8, + '👨🏼‍❤️‍💋‍👨🏿': 8, + '👨🏼‍🌾': 4, + '👨🏼‍🍳': 4, + '👨🏼‍🍼': 4, + '👨🏼‍🎓': 4, + '👨🏼‍🎤': 4, + '👨🏼‍🎨': 4, + '👨🏼‍🏫': 4, + '👨🏼‍🏭': 4, + '👨🏼‍🐰‍👨🏻': 6, + '👨🏼‍🐰‍👨🏽': 6, + '👨🏼‍🐰‍👨🏿': 6, + '👨🏼‍💻': 4, + '👨🏼‍💼': 4, + '👨🏼‍🔧': 4, + '👨🏼‍🔬': 4, + '👨🏼‍🚀': 4, + '👨🏼‍🚒': 4, + '👨🏼‍🤝‍👨🏻': 6, + '👨🏼‍🤝‍👨🏽': 6, + '👨🏼‍🤝‍👨🏿': 6, + '👨🏼‍🦯': 4, + '👨🏼‍🦯‍➡️': 6, + '👨🏼‍🦰': 4, + '👨🏼‍🦱': 4, + '👨🏼‍🦲': 4, + '👨🏼‍🦳': 4, + '👨🏼‍🦼': 4, + '👨🏼‍🦽': 4, + '👨🏼‍🦽‍➡️': 6, + '👨🏼‍🫯‍👨🏻': 6, + '👨🏼‍🫯‍👨🏽': 6, + '👨🏼‍🫯‍👨🏾': 6, + '👨🏼‍🫯‍👨🏿': 6, + '👨🏽‍⚕️': 4, + '👨🏽‍⚖️': 4, + '👨🏽‍✈️': 4, + '👨🏽‍❤️‍👨🏻': 6, + '👨🏽‍❤️‍👨🏽': 6, + '👨🏽‍❤️‍👨🏿': 6, + '👨🏽‍❤️‍💋‍👨🏻': 8, + '👨🏽‍❤️‍💋‍👨🏽': 8, + '👨🏽‍❤️‍💋‍👨🏿': 8, + '👨🏽‍🌾': 4, + '👨🏽‍🍳': 4, + '👨🏽‍🍼': 4, + '👨🏽‍🎓': 4, + '👨🏽‍🎤': 4, + '👨🏽‍🎨': 4, + '👨🏽‍🏫': 4, + '👨🏽‍🏭': 4, + '👨🏽‍🐰‍👨🏻': 6, + '👨🏽‍🐰‍👨🏼': 6, + '👨🏽‍🐰‍👨🏾': 6, + '👨🏽‍🐰‍👨🏿': 6, + '👨🏽‍💻': 4, + '👨🏽‍💼': 4, + '👨🏽‍🔧': 4, + '👨🏽‍🔬': 4, + '👨🏽‍🚀': 4, + '👨🏽‍🚒': 4, + '👨🏽‍🤝‍👨🏻': 6, + '👨🏽‍🤝‍👨🏼': 6, + '👨🏽‍🤝‍👨🏾': 6, + '👨🏽‍🤝‍👨🏿': 6, + '👨🏽‍🦯': 4, + '👨🏽‍🦯‍➡️': 6, + '👨🏽‍🦰': 4, + '👨🏽‍🦱': 4, + '👨🏽‍🦲': 4, + '👨🏽‍🦳': 4, + '👨🏽‍🦼': 4, + '👨🏽‍🦼‍➡️': 6, + '👨🏽‍🦽': 4, + '👨🏽‍🦽‍➡️': 6, + '👨🏽‍🫯‍👨🏻': 6, + '👨🏽‍🫯‍👨🏼': 6, + '👨🏽‍🫯‍👨🏾': 6, + '👨🏽‍🫯‍👨🏿': 6, + '👨🏾‍⚕️': 4, + '👨🏾‍⚖️': 4, + '👨🏾‍✈️': 4, + '👨🏾‍❤️‍👨🏾': 6, + '👨🏾‍❤️‍💋‍👨🏻': 8, + '👨🏾‍❤️‍💋‍👨🏾': 8, + '👨🏾‍❤️‍💋‍👨🏿': 8, + '👨🏾‍🌾': 4, + '👨🏾‍🍳': 4, + '👨🏾‍🍼': 4, + '👨🏾‍🎓': 4, + '👨🏾‍🎤': 4, + '👨🏾‍🎨': 4, + '👨🏾‍🏫': 4, + '👨🏾‍🏭': 4, + '👨🏾‍🐰‍👨🏻': 6, + '👨🏾‍🐰‍👨🏽': 6, + '👨🏾‍🐰‍👨🏿': 6, + '👨🏾‍💻': 4, + '👨🏾‍💼': 4, + '👨🏾‍🔧': 4, + '👨🏾‍🔬': 4, + '👨🏾‍🚀': 4, + '👨🏾‍🚒': 4, + '👨🏾‍🤝‍👨🏻': 6, + '👨🏾‍🤝‍👨🏽': 6, + '👨🏾‍🤝‍👨🏿': 6, + '👨🏾‍🦯': 4, + '👨🏾‍🦯‍➡️': 6, + '👨🏾‍🦰': 4, + '👨🏾‍🦱': 4, + '👨🏾‍🦲': 4, + '👨🏾‍🦳': 4, + '👨🏾‍🦼': 4, + '👨🏾‍🦽': 4, + '👨🏾‍🦽‍➡️': 6, + '👨🏾‍🫯‍👨🏻': 6, + '👨🏾‍🫯‍👨🏼': 6, + '👨🏾‍🫯‍👨🏽': 6, + '👨🏾‍🫯‍👨🏿': 6, + '👨🏿‍⚕️': 4, + '👨🏿‍⚖️': 4, + '👨🏿‍✈️': 4, + '👨🏿‍❤️‍👨🏻': 6, + '👨🏿‍❤️‍👨🏽': 6, + '👨🏿‍❤️‍👨🏿': 6, + '👨🏿‍❤️‍💋‍👨🏻': 8, + '👨🏿‍❤️‍💋‍👨🏼': 8, + '👨🏿‍❤️‍💋‍👨🏽': 8, + '👨🏿‍❤️‍💋‍👨🏾': 8, + '👨🏿‍❤️‍💋‍👨🏿': 8, + '👨🏿‍🌾': 4, + '👨🏿‍🍳': 4, + '👨🏿‍🍼': 4, + '👨🏿‍🎓': 4, + '👨🏿‍🎤': 4, + '👨🏿‍🎨': 4, + '👨🏿‍🏫': 4, + '👨🏿‍🏭': 4, + '👨🏿‍🐰‍👨🏻': 6, + '👨🏿‍🐰‍👨🏼': 6, + '👨🏿‍🐰‍👨🏽': 6, + '👨🏿‍🐰‍👨🏾': 6, + '👨🏿‍💻': 4, + '👨🏿‍💼': 4, + '👨🏿‍🔧': 4, + '👨🏿‍🔬': 4, + '👨🏿‍🚀': 4, + '👨🏿‍🚒': 4, + '👨🏿‍🤝‍👨🏻': 6, + '👨🏿‍🤝‍👨🏼': 6, + '👨🏿‍🤝‍👨🏽': 6, + '👨🏿‍🤝‍👨🏾': 6, + '👨🏿‍🦯': 4, + '👨🏿‍🦯‍➡️': 6, + '👨🏿‍🦰': 4, + '👨🏿‍🦱': 4, + '👨🏿‍🦲': 4, + '👨🏿‍🦳': 4, + '👨🏿‍🦼': 4, + '👨🏿‍🦼‍➡️': 6, + '👨🏿‍🦽': 4, + '👨🏿‍🦽‍➡️': 6, + '👨🏿‍🫯‍👨🏻': 6, + '👨🏿‍🫯‍👨🏼': 6, + '👨🏿‍🫯‍👨🏽': 6, + '👨🏿‍🫯‍👨🏾': 6, + '👩‍⚕️': 4, + '👩‍⚖️': 4, + '👩‍✈️': 4, + '👩‍❤️‍👨': 6, + '👩‍❤️‍👩': 6, + '👩‍❤️‍💋‍👨': 8, + '👩‍❤️‍💋‍👩': 8, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 6, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 6, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 6, + '👩🏻‍⚕️': 4, + '👩🏻‍⚖️': 4, + '👩🏻‍✈️': 4, + '👩🏻‍❤️‍👨🏻': 6, + '👩🏻‍❤️‍👨🏿': 6, + '👩🏻‍❤️‍👩🏻': 6, + '👩🏻‍❤️‍💋‍👨🏻': 8, + '👩🏻‍❤️‍💋‍👨🏽': 8, + '👩🏻‍❤️‍💋‍👨🏿': 8, + '👩🏻‍❤️‍💋‍👩🏻': 8, + '👩🏻‍❤️‍💋‍👩🏽': 8, + '👩🏻‍❤️‍💋‍👩🏿': 8, + '👩🏻‍🌾': 4, + '👩🏻‍🍳': 4, + '👩🏻‍🍼': 4, + '👩🏻‍🎓': 4, + '👩🏻‍🎤': 4, + '👩🏻‍🎨': 4, + '👩🏻‍🏫': 4, + '👩🏻‍🏭': 4, + '👩🏻‍🐰‍👩🏼': 6, + '👩🏻‍🐰‍👩🏽': 6, + '👩🏻‍🐰‍👩🏾': 6, + '👩🏻‍🐰‍👩🏿': 6, + '👩🏻‍💻': 4, + '👩🏻‍💼': 4, + '👩🏻‍🔧': 4, + '👩🏻‍🔬': 4, + '👩🏻‍🚀': 4, + '👩🏻‍🚒': 4, + '👩🏻‍🤝‍👨🏽': 6, + '👩🏻‍🤝‍👨🏿': 6, + '👩🏻‍🤝‍👩🏼': 6, + '👩🏻‍🤝‍👩🏽': 6, + '👩🏻‍🤝‍👩🏾': 6, + '👩🏻‍🤝‍👩🏿': 6, + '👩🏻‍🦯': 4, + '👩🏻‍🦯‍➡️': 6, + '👩🏻‍🦰': 4, + '👩🏻‍🦱': 4, + '👩🏻‍🦲': 4, + '👩🏻‍🦳': 4, + '👩🏻‍🦼': 4, + '👩🏻‍🦼‍➡️': 6, + '👩🏻‍🦽': 4, + '👩🏻‍🦽‍➡️': 6, + '👩🏻‍🫯‍👩🏼': 6, + '👩🏻‍🫯‍👩🏽': 6, + '👩🏻‍🫯‍👩🏾': 6, + '👩🏻‍🫯‍👩🏿': 6, + '👩🏼‍⚕️': 4, + '👩🏼‍⚖️': 4, + '👩🏼‍✈️': 4, + '👩🏼‍❤️‍👨🏼': 6, + '👩🏼‍❤️‍👩🏼': 6, + '👩🏼‍❤️‍💋‍👨🏼': 8, + '👩🏼‍❤️‍💋‍👩🏼': 8, + '👩🏼‍🌾': 4, + '👩🏼‍🍳': 4, + '👩🏼‍🍼': 4, + '👩🏼‍🎓': 4, + '👩🏼‍🎤': 4, + '👩🏼‍🎨': 4, + '👩🏼‍🏫': 4, + '👩🏼‍🏭': 4, + '👩🏼‍🐰‍👩🏻': 6, + '👩🏼‍🐰‍👩🏿': 6, + '👩🏼‍💻': 4, + '👩🏼‍💼': 4, + '👩🏼‍🔧': 4, + '👩🏼‍🔬': 4, + '👩🏼‍🚀': 4, + '👩🏼‍🚒': 4, + '👩🏼‍🤝‍👩🏻': 6, + '👩🏼‍🤝‍👩🏽': 6, + '👩🏼‍🤝‍👩🏿': 6, + '👩🏼‍🦯': 4, + '👩🏼‍🦯‍➡️': 6, + '👩🏼‍🦰': 4, + '👩🏼‍🦱': 4, + '👩🏼‍🦲': 4, + '👩🏼‍🦳': 4, + '👩🏼‍🦼': 4, + '👩🏼‍🦽': 4, + '👩🏼‍🫯‍👩🏻': 6, + '👩🏼‍🫯‍👩🏽': 6, + '👩🏼‍🫯‍👩🏾': 6, + '👩🏼‍🫯‍👩🏿': 6, + '👩🏽‍⚕️': 4, + '👩🏽‍⚖️': 4, + '👩🏽‍✈️': 4, + '👩🏽‍❤️‍👨🏽': 6, + '👩🏽‍❤️‍👩🏽': 6, + '👩🏽‍❤️‍💋‍👨🏻': 8, + '👩🏽‍❤️‍💋‍👨🏽': 8, + '👩🏽‍❤️‍💋‍👨🏿': 8, + '👩🏽‍❤️‍💋‍👩🏻': 8, + '👩🏽‍❤️‍💋‍👩🏽': 8, + '👩🏽‍❤️‍💋‍👩🏿': 8, + '👩🏽‍🌾': 4, + '👩🏽‍🍳': 4, + '👩🏽‍🍼': 4, + '👩🏽‍🎓': 4, + '👩🏽‍🎤': 4, + '👩🏽‍🎨': 4, + '👩🏽‍🏫': 4, + '👩🏽‍🏭': 4, + '👩🏽‍🐰‍👩🏻': 6, + '👩🏽‍🐰‍👩🏾': 6, + '👩🏽‍🐰‍👩🏿': 6, + '👩🏽‍💻': 4, + '👩🏽‍💼': 4, + '👩🏽‍🔧': 4, + '👩🏽‍🔬': 4, + '👩🏽‍🚀': 4, + '👩🏽‍🚒': 4, + '👩🏽‍🤝‍👨🏻': 6, + '👩🏽‍🤝‍👨🏿': 6, + '👩🏽‍🤝‍👩🏻': 6, + '👩🏽‍🤝‍👩🏼': 6, + '👩🏽‍🤝‍👩🏾': 6, + '👩🏽‍🤝‍👩🏿': 6, + '👩🏽‍🦯': 4, + '👩🏽‍🦯‍➡️': 6, + '👩🏽‍🦰': 4, + '👩🏽‍🦱': 4, + '👩🏽‍🦲': 4, + '👩🏽‍🦳': 4, + '👩🏽‍🦼': 4, + '👩🏽‍🦼‍➡️': 6, + '👩🏽‍🦽': 4, + '👩🏽‍🦽‍➡️': 6, + '👩🏽‍🫯‍👩🏻': 6, + '👩🏽‍🫯‍👩🏼': 6, + '👩🏽‍🫯‍👩🏾': 6, + '👩🏽‍🫯‍👩🏿': 6, + '👩🏾‍⚕️': 4, + '👩🏾‍⚖️': 4, + '👩🏾‍✈️': 4, + '👩🏾‍❤️‍👨🏾': 6, + '👩🏾‍❤️‍👩🏾': 6, + '👩🏾‍❤️‍💋‍👨🏾': 8, + '👩🏾‍❤️‍💋‍👩🏾': 8, + '👩🏾‍🌾': 4, + '👩🏾‍🍳': 4, + '👩🏾‍🍼': 4, + '👩🏾‍🎓': 4, + '👩🏾‍🎤': 4, + '👩🏾‍🎨': 4, + '👩🏾‍🏫': 4, + '👩🏾‍🏭': 4, + '👩🏾‍🐰‍👩🏻': 6, + '👩🏾‍🐰‍👩🏽': 6, + '👩🏾‍🐰‍👩🏿': 6, + '👩🏾‍💻': 4, + '👩🏾‍💼': 4, + '👩🏾‍🔧': 4, + '👩🏾‍🔬': 4, + '👩🏾‍🚀': 4, + '👩🏾‍🚒': 4, + '👩🏾‍🤝‍👩🏻': 6, + '👩🏾‍🤝‍👩🏽': 6, + '👩🏾‍🤝‍👩🏿': 6, + '👩🏾‍🦯': 4, + '👩🏾‍🦯‍➡️': 6, + '👩🏾‍🦰': 4, + '👩🏾‍🦱': 4, + '👩🏾‍🦲': 4, + '👩🏾‍🦳': 4, + '👩🏾‍🦼': 4, + '👩🏾‍🦽': 4, + '👩🏾‍🦽‍➡️': 6, + '👩🏾‍🫯‍👩🏻': 6, + '👩🏾‍🫯‍👩🏼': 6, + '👩🏾‍🫯‍👩🏽': 6, + '👩🏾‍🫯‍👩🏿': 6, + '👩🏿‍⚕️': 4, + '👩🏿‍⚖️': 4, + '👩🏿‍✈️': 4, + '👩🏿‍❤️‍👨🏻': 6, + '👩🏿‍❤️‍👨🏿': 6, + '👩🏿‍❤️‍👩🏿': 6, + '👩🏿‍❤️‍💋‍👨🏻': 8, + '👩🏿‍❤️‍💋‍👨🏽': 8, + '👩🏿‍❤️‍💋‍👨🏿': 8, + '👩🏿‍❤️‍💋‍👩🏻': 8, + '👩🏿‍❤️‍💋‍👩🏽': 8, + '👩🏿‍❤️‍💋‍👩🏿': 8, + '👩🏿‍🌾': 4, + '👩🏿‍🍳': 4, + '👩🏿‍🍼': 4, + '👩🏿‍🎓': 4, + '👩🏿‍🎤': 4, + '👩🏿‍🎨': 4, + '👩🏿‍🏫': 4, + '👩🏿‍🏭': 4, + '👩🏿‍🐰‍👩🏻': 6, + '👩🏿‍🐰‍👩🏼': 6, + '👩🏿‍🐰‍👩🏽': 6, + '👩🏿‍🐰‍👩🏾': 6, + '👩🏿‍💻': 4, + '👩🏿‍💼': 4, + '👩🏿‍🔧': 4, + '👩🏿‍🔬': 4, + '👩🏿‍🚀': 4, + '👩🏿‍🚒': 4, + '👩🏿‍🤝‍👨🏻': 6, + '👩🏿‍🤝‍👨🏽': 6, + '👩🏿‍🤝‍👩🏻': 6, + '👩🏿‍🤝‍👩🏼': 6, + '👩🏿‍🤝‍👩🏽': 6, + '👩🏿‍🤝‍👩🏾': 6, + '👩🏿‍🦯': 4, + '👩🏿‍🦯‍➡️': 6, + '👩🏿‍🦰': 4, + '👩🏿‍🦱': 4, + '👩🏿‍🦲': 4, + '👩🏿‍🦳': 4, + '👩🏿‍🦼': 4, + '👩🏿‍🦼‍➡️': 6, + '👩🏿‍🦽': 4, + '👩🏿‍🦽‍➡️': 6, + '👩🏿‍🫯‍👩🏻': 6, + '👩🏿‍🫯‍👩🏼': 6, + '👩🏿‍🫯‍👩🏽': 6, + '👩🏿‍🫯‍👩🏾': 6, + '👮‍♀️': 4, + '👮‍♂️': 4, + '👮🏻‍♀️': 4, + '👮🏻‍♂️': 4, + '👮🏼‍♀️': 4, + '👮🏼‍♂️': 4, + '👮🏽‍♀️': 4, + '👮🏽‍♂️': 4, + '👮🏾‍♀️': 4, + '👮🏾‍♂️': 4, + '👮🏿‍♀️': 4, + '👮🏿‍♂️': 4, + '👯‍♀️': 4, + '👯‍♂️': 4, + '👯🏻‍♀️': 4, + '👯🏻‍♂️': 4, + '👯🏼‍♀️': 4, + '👯🏼‍♂️': 4, + '👯🏽‍♀️': 4, + '👯🏽‍♂️': 4, + '👯🏾‍♀️': 4, + '👯🏾‍♂️': 4, + '👯🏿‍♀️': 4, + '👯🏿‍♂️': 4, + '👰‍♀️': 4, + '👰‍♂️': 4, + '👰🏻‍♀️': 4, + '👰🏻‍♂️': 4, + '👰🏼‍♀️': 4, + '👰🏼‍♂️': 4, + '👰🏽‍♀️': 4, + '👰🏽‍♂️': 4, + '👰🏾‍♀️': 4, + '👰🏾‍♂️': 4, + '👰🏿‍♀️': 4, + '👰🏿‍♂️': 4, + '👱‍♀️': 4, + '👱‍♂️': 4, + '👱🏻‍♀️': 4, + '👱🏻‍♂️': 4, + '👱🏼‍♀️': 4, + '👱🏼‍♂️': 4, + '👱🏽‍♀️': 4, + '👱🏽‍♂️': 4, + '👱🏾‍♀️': 4, + '👱🏾‍♂️': 4, + '👱🏿‍♀️': 4, + '👱🏿‍♂️': 4, + '👳‍♀️': 4, + '👳‍♂️': 4, + '👳🏻‍♀️': 4, + '👳🏻‍♂️': 4, + '👳🏼‍♀️': 4, + '👳🏼‍♂️': 4, + '👳🏽‍♀️': 4, + '👳🏽‍♂️': 4, + '👳🏾‍♀️': 4, + '👳🏾‍♂️': 4, + '👳🏿‍♀️': 4, + '👳🏿‍♂️': 4, + '👷‍♀️': 4, + '👷‍♂️': 4, + '👷🏻‍♀️': 4, + '👷🏻‍♂️': 4, + '👷🏼‍♀️': 4, + '👷🏼‍♂️': 4, + '👷🏽‍♀️': 4, + '👷🏽‍♂️': 4, + '👷🏾‍♀️': 4, + '👷🏾‍♂️': 4, + '👷🏿‍♀️': 4, + '👷🏿‍♂️': 4, + '💁‍♀️': 4, + '💁‍♂️': 4, + '💁🏻‍♀️': 4, + '💁🏻‍♂️': 4, + '💁🏼‍♀️': 4, + '💁🏼‍♂️': 4, + '💁🏽‍♀️': 4, + '💁🏽‍♂️': 4, + '💁🏾‍♀️': 4, + '💁🏾‍♂️': 4, + '💁🏿‍♀️': 4, + '💁🏿‍♂️': 4, + '💂‍♀️': 4, + '💂‍♂️': 4, + '💂🏻‍♀️': 4, + '💂🏻‍♂️': 4, + '💂🏼‍♀️': 4, + '💂🏼‍♂️': 4, + '💂🏽‍♀️': 4, + '💂🏽‍♂️': 4, + '💂🏾‍♀️': 4, + '💂🏾‍♂️': 4, + '💂🏿‍♀️': 4, + '💂🏿‍♂️': 4, + '💆‍♀️': 4, + '💆‍♂️': 4, + '💆🏻‍♀️': 4, + '💆🏻‍♂️': 4, + '💆🏼‍♀️': 4, + '💆🏼‍♂️': 4, + '💆🏽‍♀️': 4, + '💆🏽‍♂️': 4, + '💆🏾‍♀️': 4, + '💆🏾‍♂️': 4, + '💆🏿‍♀️': 4, + '💆🏿‍♂️': 4, + '💇‍♀️': 4, + '💇‍♂️': 4, + '💇🏻‍♀️': 4, + '💇🏻‍♂️': 4, + '💇🏼‍♀️': 4, + '💇🏼‍♂️': 4, + '💇🏽‍♀️': 4, + '💇🏽‍♂️': 4, + '💇🏾‍♀️': 4, + '💇🏾‍♂️': 4, + '💇🏿‍♀️': 4, + '💇🏿‍♂️': 4, + '🕵️‍♀️': 4, + '🕵️‍♂️': 4, + '🕵🏻‍♀️': 3, + '🕵🏻‍♂️': 3, + '🕵🏼‍♀️': 3, + '🕵🏼‍♂️': 3, + '🕵🏽‍♀️': 3, + '🕵🏽‍♂️': 3, + '🕵🏾‍♀️': 3, + '🕵🏾‍♂️': 3, + '🕵🏿‍♀️': 3, + '🕵🏿‍♂️': 3, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 4, + '🙂‍↔️': 4, + '🙂‍↕️': 4, + '🙅‍♀️': 4, + '🙅‍♂️': 4, + '🙅🏻‍♀️': 4, + '🙅🏻‍♂️': 4, + '🙅🏼‍♀️': 4, + '🙅🏼‍♂️': 4, + '🙅🏽‍♀️': 4, + '🙅🏽‍♂️': 4, + '🙅🏾‍♀️': 4, + '🙅🏾‍♂️': 4, + '🙅🏿‍♀️': 4, + '🙅🏿‍♂️': 4, + '🙆‍♀️': 4, + '🙆‍♂️': 4, + '🙆🏻‍♀️': 4, + '🙆🏻‍♂️': 4, + '🙆🏼‍♀️': 4, + '🙆🏼‍♂️': 4, + '🙆🏽‍♀️': 4, + '🙆🏽‍♂️': 4, + '🙆🏾‍♀️': 4, + '🙆🏾‍♂️': 4, + '🙆🏿‍♀️': 4, + '🙆🏿‍♂️': 4, + '🙇‍♀️': 4, + '🙇‍♂️': 4, + '🙇🏻‍♀️': 4, + '🙇🏻‍♂️': 4, + '🙇🏼‍♀️': 4, + '🙇🏼‍♂️': 4, + '🙇🏽‍♀️': 4, + '🙇🏽‍♂️': 4, + '🙇🏾‍♀️': 4, + '🙇🏾‍♂️': 4, + '🙇🏿‍♀️': 4, + '🙇🏿‍♂️': 4, + '🙋‍♀️': 4, + '🙋‍♂️': 4, + '🙋🏻‍♀️': 4, + '🙋🏻‍♂️': 4, + '🙋🏼‍♀️': 4, + '🙋🏼‍♂️': 4, + '🙋🏽‍♀️': 4, + '🙋🏽‍♂️': 4, + '🙋🏾‍♀️': 4, + '🙋🏾‍♂️': 4, + '🙋🏿‍♀️': 4, + '🙋🏿‍♂️': 4, + '🙍‍♀️': 4, + '🙍‍♂️': 4, + '🙍🏻‍♀️': 4, + '🙍🏻‍♂️': 4, + '🙍🏼‍♀️': 4, + '🙍🏼‍♂️': 4, + '🙍🏽‍♀️': 4, + '🙍🏽‍♂️': 4, + '🙍🏾‍♀️': 4, + '🙍🏾‍♂️': 4, + '🙍🏿‍♀️': 4, + '🙍🏿‍♂️': 4, + '🙎‍♀️': 4, + '🙎‍♂️': 4, + '🙎🏻‍♀️': 4, + '🙎🏻‍♂️': 4, + '🙎🏼‍♀️': 4, + '🙎🏼‍♂️': 4, + '🙎🏽‍♀️': 4, + '🙎🏽‍♂️': 4, + '🙎🏾‍♀️': 4, + '🙎🏾‍♂️': 4, + '🙎🏿‍♀️': 4, + '🙎🏿‍♂️': 4, + '🚣‍♀️': 4, + '🚣‍♂️': 4, + '🚣🏻‍♀️': 4, + '🚣🏻‍♂️': 4, + '🚣🏼‍♀️': 4, + '🚣🏼‍♂️': 4, + '🚣🏽‍♀️': 4, + '🚣🏽‍♂️': 4, + '🚣🏾‍♀️': 4, + '🚣🏾‍♂️': 4, + '🚣🏿‍♀️': 4, + '🚣🏿‍♂️': 4, + '🚴‍♀️': 4, + '🚴‍♂️': 4, + '🚴🏻‍♀️': 4, + '🚴🏻‍♂️': 4, + '🚴🏼‍♀️': 4, + '🚴🏼‍♂️': 4, + '🚴🏽‍♀️': 4, + '🚴🏽‍♂️': 4, + '🚴🏾‍♀️': 4, + '🚴🏾‍♂️': 4, + '🚴🏿‍♀️': 4, + '🚴🏿‍♂️': 4, + '🚵‍♀️': 4, + '🚵‍♂️': 4, + '🚵🏻‍♀️': 4, + '🚵🏻‍♂️': 4, + '🚵🏼‍♀️': 4, + '🚵🏼‍♂️': 4, + '🚵🏽‍♀️': 4, + '🚵🏽‍♂️': 4, + '🚵🏾‍♀️': 4, + '🚵🏾‍♂️': 4, + '🚵🏿‍♀️': 4, + '🚵🏿‍♂️': 4, + '🚶‍♀️': 4, + '🚶‍♀️‍➡️': 6, + '🚶‍♂️': 4, + '🚶‍♂️‍➡️': 6, + '🚶‍➡️': 4, + '🚶🏻‍♀️': 4, + '🚶🏻‍♀️‍➡️': 6, + '🚶🏻‍♂️': 4, + '🚶🏻‍♂️‍➡️': 6, + '🚶🏻‍➡️': 4, + '🚶🏼‍♀️': 4, + '🚶🏼‍♀️‍➡️': 6, + '🚶🏼‍♂️': 4, + '🚶🏼‍♂️‍➡️': 6, + '🚶🏼‍➡️': 4, + '🚶🏽‍♀️': 4, + '🚶🏽‍♀️‍➡️': 6, + '🚶🏽‍♂️': 4, + '🚶🏽‍♂️‍➡️': 6, + '🚶🏽‍➡️': 4, + '🚶🏾‍♀️': 4, + '🚶🏾‍♀️‍➡️': 6, + '🚶🏾‍♂️': 4, + '🚶🏾‍♂️‍➡️': 6, + '🚶🏾‍➡️': 4, + '🚶🏿‍♀️': 4, + '🚶🏿‍♀️‍➡️': 6, + '🚶🏿‍♂️': 4, + '🚶🏿‍♂️‍➡️': 6, + '🚶🏿‍➡️': 4, + '🤦‍♀️': 4, + '🤦‍♂️': 4, + '🤦🏻‍♀️': 4, + '🤦🏻‍♂️': 4, + '🤦🏼‍♀️': 4, + '🤦🏼‍♂️': 4, + '🤦🏽‍♀️': 4, + '🤦🏽‍♂️': 4, + '🤦🏾‍♀️': 4, + '🤦🏾‍♂️': 4, + '🤦🏿‍♀️': 4, + '🤦🏿‍♂️': 4, + '🤵‍♀️': 4, + '🤵‍♂️': 4, + '🤵🏻‍♀️': 4, + '🤵🏻‍♂️': 4, + '🤵🏼‍♀️': 4, + '🤵🏼‍♂️': 4, + '🤵🏽‍♀️': 4, + '🤵🏽‍♂️': 4, + '🤵🏾‍♀️': 4, + '🤵🏾‍♂️': 4, + '🤵🏿‍♀️': 4, + '🤵🏿‍♂️': 4, + '🤷‍♀️': 4, + '🤷‍♂️': 4, + '🤷🏻‍♀️': 4, + '🤷🏻‍♂️': 4, + '🤷🏼‍♀️': 4, + '🤷🏼‍♂️': 4, + '🤷🏽‍♀️': 4, + '🤷🏽‍♂️': 4, + '🤷🏾‍♀️': 4, + '🤷🏾‍♂️': 4, + '🤷🏿‍♀️': 4, + '🤷🏿‍♂️': 4, + '🤸‍♀️': 4, + '🤸‍♂️': 4, + '🤸🏻‍♀️': 4, + '🤸🏻‍♂️': 4, + '🤸🏼‍♀️': 4, + '🤸🏼‍♂️': 4, + '🤸🏽‍♀️': 4, + '🤸🏽‍♂️': 4, + '🤸🏾‍♀️': 4, + '🤸🏾‍♂️': 4, + '🤸🏿‍♀️': 4, + '🤸🏿‍♂️': 4, + '🤹‍♀️': 4, + '🤹‍♂️': 4, + '🤹🏻‍♀️': 4, + '🤹🏻‍♂️': 4, + '🤹🏼‍♀️': 4, + '🤹🏼‍♂️': 4, + '🤹🏽‍♀️': 4, + '🤹🏽‍♂️': 4, + '🤹🏾‍♀️': 4, + '🤹🏾‍♂️': 4, + '🤹🏿‍♀️': 4, + '🤹🏿‍♂️': 4, + '🤼‍♀️': 4, + '🤼‍♂️': 4, + '🤼🏻‍♀️': 4, + '🤼🏻‍♂️': 4, + '🤼🏼‍♀️': 4, + '🤼🏼‍♂️': 4, + '🤼🏽‍♀️': 4, + '🤼🏽‍♂️': 4, + '🤼🏾‍♀️': 4, + '🤼🏾‍♂️': 4, + '🤼🏿‍♀️': 4, + '🤼🏿‍♂️': 4, + '🤽‍♀️': 4, + '🤽‍♂️': 4, + '🤽🏻‍♀️': 4, + '🤽🏻‍♂️': 4, + '🤽🏼‍♀️': 4, + '🤽🏼‍♂️': 4, + '🤽🏽‍♀️': 4, + '🤽🏽‍♂️': 4, + '🤽🏾‍♀️': 4, + '🤽🏾‍♂️': 4, + '🤽🏿‍♀️': 4, + '🤽🏿‍♂️': 4, + '🤾‍♀️': 4, + '🤾‍♂️': 4, + '🤾🏻‍♀️': 4, + '🤾🏻‍♂️': 4, + '🤾🏼‍♀️': 4, + '🤾🏼‍♂️': 4, + '🤾🏽‍♀️': 4, + '🤾🏽‍♂️': 4, + '🤾🏾‍♀️': 4, + '🤾🏾‍♂️': 4, + '🤾🏿‍♀️': 4, + '🤾🏿‍♂️': 4, + '🦸‍♀️': 4, + '🦸‍♂️': 4, + '🦸🏻‍♀️': 4, + '🦸🏻‍♂️': 4, + '🦸🏼‍♀️': 4, + '🦸🏼‍♂️': 4, + '🦸🏽‍♀️': 4, + '🦸🏽‍♂️': 4, + '🦸🏾‍♀️': 4, + '🦸🏾‍♂️': 4, + '🦸🏿‍♀️': 4, + '🦸🏿‍♂️': 4, + '🦹‍♀️': 4, + '🦹‍♂️': 4, + '🦹🏻‍♀️': 4, + '🦹🏻‍♂️': 4, + '🦹🏼‍♀️': 4, + '🦹🏼‍♂️': 4, + '🦹🏽‍♀️': 4, + '🦹🏽‍♂️': 4, + '🦹🏾‍♀️': 4, + '🦹🏾‍♂️': 4, + '🦹🏿‍♀️': 4, + '🦹🏿‍♂️': 4, + '🧍‍♀️': 4, + '🧍‍♂️': 4, + '🧍🏻‍♀️': 4, + '🧍🏻‍♂️': 4, + '🧍🏼‍♀️': 4, + '🧍🏼‍♂️': 4, + '🧍🏽‍♀️': 4, + '🧍🏽‍♂️': 4, + '🧍🏾‍♀️': 4, + '🧍🏾‍♂️': 4, + '🧍🏿‍♀️': 4, + '🧍🏿‍♂️': 4, + '🧎‍♀️': 4, + '🧎‍♀️‍➡️': 6, + '🧎‍♂️': 4, + '🧎‍♂️‍➡️': 6, + '🧎‍➡️': 4, + '🧎🏻‍♀️': 4, + '🧎🏻‍♀️‍➡️': 6, + '🧎🏻‍♂️': 4, + '🧎🏻‍♂️‍➡️': 6, + '🧎🏻‍➡️': 4, + '🧎🏼‍♀️': 4, + '🧎🏼‍♀️‍➡️': 6, + '🧎🏼‍♂️': 4, + '🧎🏼‍♂️‍➡️': 6, + '🧎🏼‍➡️': 4, + '🧎🏽‍♀️': 4, + '🧎🏽‍♀️‍➡️': 6, + '🧎🏽‍♂️': 4, + '🧎🏽‍♂️‍➡️': 6, + '🧎🏽‍➡️': 4, + '🧎🏾‍♀️': 4, + '🧎🏾‍♀️‍➡️': 6, + '🧎🏾‍♂️': 4, + '🧎🏾‍♂️‍➡️': 6, + '🧎🏾‍➡️': 4, + '🧎🏿‍♀️': 4, + '🧎🏿‍♀️‍➡️': 6, + '🧎🏿‍♂️': 4, + '🧎🏿‍♂️‍➡️': 6, + '🧎🏿‍➡️': 4, + '🧏‍♀️': 4, + '🧏‍♂️': 4, + '🧏🏻‍♀️': 4, + '🧏🏻‍♂️': 4, + '🧏🏼‍♀️': 4, + '🧏🏼‍♂️': 4, + '🧏🏽‍♀️': 4, + '🧏🏽‍♂️': 4, + '🧏🏾‍♀️': 4, + '🧏🏾‍♂️': 4, + '🧏🏿‍♀️': 4, + '🧏🏿‍♂️': 4, + '🧑‍⚕️': 4, + '🧑‍⚖️': 4, + '🧑‍✈️': 4, + '🧑‍🌾': 4, + '🧑‍🍳': 4, + '🧑‍🍼': 4, + '🧑‍🎄': 4, + '🧑‍🎓': 4, + '🧑‍🎤': 4, + '🧑‍🎨': 4, + '🧑‍🏫': 4, + '🧑‍🏭': 4, + '🧑‍💻': 4, + '🧑‍💼': 4, + '🧑‍🔧': 4, + '🧑‍🔬': 4, + '🧑‍🚀': 4, + '🧑‍🚒': 4, + '🧑‍🤝‍🧑': 6, + '🧑‍🦯': 4, + '🧑‍🦯‍➡️': 6, + '🧑‍🦰': 4, + '🧑‍🦱': 4, + '🧑‍🦲': 4, + '🧑‍🦳': 4, + '🧑‍🦼': 4, + '🧑‍🦼‍➡️': 6, + '🧑‍🦽': 4, + '🧑‍🦽‍➡️': 6, + '🧑‍🧑‍🧒': 6, + '🧑‍🧑‍🧒‍🧒': 8, + '🧑‍🧒': 4, + '🧑‍🧒‍🧒': 6, + '🧑‍🩰': 4, + '🧑🏻‍⚕️': 4, + '🧑🏻‍⚖️': 4, + '🧑🏻‍✈️': 4, + '🧑🏻‍❤️‍💋‍🧑🏿': 8, + '🧑🏻‍🌾': 4, + '🧑🏻‍🍳': 4, + '🧑🏻‍🍼': 4, + '🧑🏻‍🎄': 4, + '🧑🏻‍🎓': 4, + '🧑🏻‍🎤': 4, + '🧑🏻‍🎨': 4, + '🧑🏻‍🏫': 4, + '🧑🏻‍🏭': 4, + '🧑🏻‍🐰‍🧑🏽': 6, + '🧑🏻‍🐰‍🧑🏾': 6, + '🧑🏻‍🐰‍🧑🏿': 6, + '🧑🏻‍💻': 4, + '🧑🏻‍💼': 4, + '🧑🏻‍🔧': 4, + '🧑🏻‍🔬': 4, + '🧑🏻‍🚀': 4, + '🧑🏻‍🚒': 4, + '🧑🏻‍🤝‍🧑🏻': 6, + '🧑🏻‍🤝‍🧑🏼': 6, + '🧑🏻‍🤝‍🧑🏽': 6, + '🧑🏻‍🤝‍🧑🏾': 6, + '🧑🏻‍🤝‍🧑🏿': 6, + '🧑🏻‍🦯': 4, + '🧑🏻‍🦯‍➡️': 6, + '🧑🏻‍🦰': 4, + '🧑🏻‍🦱': 4, + '🧑🏻‍🦲': 4, + '🧑🏻‍🦳': 4, + '🧑🏻‍🦼': 4, + '🧑🏻‍🦼‍➡️': 6, + '🧑🏻‍🦽': 4, + '🧑🏻‍🦽‍➡️': 6, + '🧑🏻‍🩰': 4, + '🧑🏻‍🫯‍🧑🏼': 6, + '🧑🏻‍🫯‍🧑🏽': 6, + '🧑🏻‍🫯‍🧑🏾': 6, + '🧑🏻‍🫯‍🧑🏿': 6, + '🧑🏼‍⚕️': 4, + '🧑🏼‍⚖️': 4, + '🧑🏼‍✈️': 4, + '🧑🏼‍🌾': 4, + '🧑🏼‍🍳': 4, + '🧑🏼‍🍼': 4, + '🧑🏼‍🎄': 4, + '🧑🏼‍🎓': 4, + '🧑🏼‍🎤': 4, + '🧑🏼‍🎨': 4, + '🧑🏼‍🏫': 4, + '🧑🏼‍🏭': 4, + '🧑🏼‍🐰‍🧑🏿': 6, + '🧑🏼‍💻': 4, + '🧑🏼‍💼': 4, + '🧑🏼‍🔧': 4, + '🧑🏼‍🔬': 4, + '🧑🏼‍🚀': 4, + '🧑🏼‍🚒': 4, + '🧑🏼‍🤝‍🧑🏻': 6, + '🧑🏼‍🤝‍🧑🏼': 6, + '🧑🏼‍🤝‍🧑🏽': 6, + '🧑🏼‍🤝‍🧑🏿': 6, + '🧑🏼‍🦯': 4, + '🧑🏼‍🦯‍➡️': 6, + '🧑🏼‍🦰': 4, + '🧑🏼‍🦱': 4, + '🧑🏼‍🦲': 4, + '🧑🏼‍🦳': 4, + '🧑🏼‍🦼': 4, + '🧑🏼‍🦽': 4, + '🧑🏼‍🩰': 4, + '🧑🏼‍🫯‍🧑🏻': 6, + '🧑🏼‍🫯‍🧑🏽': 6, + '🧑🏼‍🫯‍🧑🏿': 6, + '🧑🏽‍⚕️': 4, + '🧑🏽‍⚖️': 4, + '🧑🏽‍✈️': 4, + '🧑🏽‍❤️‍💋‍🧑🏿': 8, + '🧑🏽‍🌾': 4, + '🧑🏽‍🍳': 4, + '🧑🏽‍🍼': 4, + '🧑🏽‍🎄': 4, + '🧑🏽‍🎓': 4, + '🧑🏽‍🎤': 4, + '🧑🏽‍🎨': 4, + '🧑🏽‍🏫': 4, + '🧑🏽‍🏭': 4, + '🧑🏽‍🐰‍🧑🏻': 6, + '🧑🏽‍🐰‍🧑🏿': 6, + '🧑🏽‍💻': 4, + '🧑🏽‍💼': 4, + '🧑🏽‍🔧': 4, + '🧑🏽‍🔬': 4, + '🧑🏽‍🚀': 4, + '🧑🏽‍🚒': 4, + '🧑🏽‍🤝‍🧑🏻': 6, + '🧑🏽‍🤝‍🧑🏼': 6, + '🧑🏽‍🤝‍🧑🏽': 6, + '🧑🏽‍🤝‍🧑🏾': 6, + '🧑🏽‍🤝‍🧑🏿': 6, + '🧑🏽‍🦯': 4, + '🧑🏽‍🦯‍➡️': 6, + '🧑🏽‍🦰': 4, + '🧑🏽‍🦱': 4, + '🧑🏽‍🦲': 4, + '🧑🏽‍🦳': 4, + '🧑🏽‍🦼': 4, + '🧑🏽‍🦼‍➡️': 6, + '🧑🏽‍🦽': 4, + '🧑🏽‍🦽‍➡️': 6, + '🧑🏽‍🩰': 4, + '🧑🏽‍🫯‍🧑🏻': 6, + '🧑🏽‍🫯‍🧑🏼': 6, + '🧑🏽‍🫯‍🧑🏾': 6, + '🧑🏽‍🫯‍🧑🏿': 6, + '🧑🏾‍⚕️': 4, + '🧑🏾‍⚖️': 4, + '🧑🏾‍✈️': 4, + '🧑🏾‍🌾': 4, + '🧑🏾‍🍳': 4, + '🧑🏾‍🍼': 4, + '🧑🏾‍🎄': 4, + '🧑🏾‍🎓': 4, + '🧑🏾‍🎤': 4, + '🧑🏾‍🎨': 4, + '🧑🏾‍🏫': 4, + '🧑🏾‍🏭': 4, + '🧑🏾‍🐰‍🧑🏻': 6, + '🧑🏾‍🐰‍🧑🏿': 6, + '🧑🏾‍💻': 4, + '🧑🏾‍💼': 4, + '🧑🏾‍🔧': 4, + '🧑🏾‍🔬': 4, + '🧑🏾‍🚀': 4, + '🧑🏾‍🚒': 4, + '🧑🏾‍🤝‍🧑🏻': 6, + '🧑🏾‍🤝‍🧑🏽': 6, + '🧑🏾‍🤝‍🧑🏾': 6, + '🧑🏾‍🤝‍🧑🏿': 6, + '🧑🏾‍🦯': 4, + '🧑🏾‍🦯‍➡️': 6, + '🧑🏾‍🦰': 4, + '🧑🏾‍🦱': 4, + '🧑🏾‍🦲': 4, + '🧑🏾‍🦳': 4, + '🧑🏾‍🦼': 4, + '🧑🏾‍🦽': 4, + '🧑🏾‍🩰': 4, + '🧑🏾‍🫯‍🧑🏻': 6, + '🧑🏾‍🫯‍🧑🏽': 6, + '🧑🏾‍🫯‍🧑🏿': 6, + '🧑🏿‍⚕️': 4, + '🧑🏿‍⚖️': 4, + '🧑🏿‍✈️': 4, + '🧑🏿‍❤️‍💋‍🧑🏻': 8, + '🧑🏿‍❤️‍💋‍🧑🏽': 8, + '🧑🏿‍🌾': 4, + '🧑🏿‍🍳': 4, + '🧑🏿‍🍼': 4, + '🧑🏿‍🎄': 4, + '🧑🏿‍🎓': 4, + '🧑🏿‍🎤': 4, + '🧑🏿‍🎨': 4, + '🧑🏿‍🏫': 4, + '🧑🏿‍🏭': 4, + '🧑🏿‍🐰‍🧑🏻': 6, + '🧑🏿‍🐰‍🧑🏼': 6, + '🧑🏿‍🐰‍🧑🏽': 6, + '🧑🏿‍🐰‍🧑🏾': 6, + '🧑🏿‍💻': 4, + '🧑🏿‍💼': 4, + '🧑🏿‍🔧': 4, + '🧑🏿‍🔬': 4, + '🧑🏿‍🚀': 4, + '🧑🏿‍🚒': 4, + '🧑🏿‍🤝‍🧑🏻': 6, + '🧑🏿‍🤝‍🧑🏼': 6, + '🧑🏿‍🤝‍🧑🏽': 6, + '🧑🏿‍🤝‍🧑🏾': 6, + '🧑🏿‍🤝‍🧑🏿': 6, + '🧑🏿‍🦯': 4, + '🧑🏿‍🦯‍➡️': 6, + '🧑🏿‍🦰': 4, + '🧑🏿‍🦱': 4, + '🧑🏿‍🦲': 4, + '🧑🏿‍🦳': 4, + '🧑🏿‍🦼': 4, + '🧑🏿‍🦼‍➡️': 6, + '🧑🏿‍🦽': 4, + '🧑🏿‍🦽‍➡️': 6, + '🧑🏿‍🩰': 4, + '🧑🏿‍🫯‍🧑🏻': 6, + '🧑🏿‍🫯‍🧑🏼': 6, + '🧑🏿‍🫯‍🧑🏽': 6, + '🧑🏿‍🫯‍🧑🏾': 6, + '🧔‍♀️': 4, + '🧔‍♂️': 4, + '🧔🏻‍♀️': 4, + '🧔🏻‍♂️': 4, + '🧔🏼‍♀️': 4, + '🧔🏼‍♂️': 4, + '🧔🏽‍♀️': 4, + '🧔🏽‍♂️': 4, + '🧔🏾‍♀️': 4, + '🧔🏾‍♂️': 4, + '🧔🏿‍♀️': 4, + '🧔🏿‍♂️': 4, + '🧖‍♀️': 4, + '🧖‍♂️': 4, + '🧖🏻‍♀️': 4, + '🧖🏻‍♂️': 4, + '🧖🏼‍♀️': 4, + '🧖🏼‍♂️': 4, + '🧖🏽‍♀️': 4, + '🧖🏽‍♂️': 4, + '🧖🏾‍♀️': 4, + '🧖🏾‍♂️': 4, + '🧖🏿‍♀️': 4, + '🧖🏿‍♂️': 4, + '🧗‍♀️': 4, + '🧗‍♂️': 4, + '🧗🏻‍♀️': 4, + '🧗🏻‍♂️': 4, + '🧗🏼‍♀️': 4, + '🧗🏼‍♂️': 4, + '🧗🏽‍♀️': 4, + '🧗🏽‍♂️': 4, + '🧗🏾‍♀️': 4, + '🧗🏾‍♂️': 4, + '🧗🏿‍♀️': 4, + '🧗🏿‍♂️': 4, + '🧘‍♀️': 4, + '🧘‍♂️': 4, + '🧘🏻‍♀️': 4, + '🧘🏻‍♂️': 4, + '🧘🏼‍♀️': 4, + '🧘🏼‍♂️': 4, + '🧘🏽‍♀️': 4, + '🧘🏽‍♂️': 4, + '🧘🏾‍♀️': 4, + '🧘🏾‍♂️': 4, + '🧘🏿‍♀️': 4, + '🧘🏿‍♂️': 4, + '🧙‍♀️': 4, + '🧙‍♂️': 4, + '🧙🏻‍♀️': 4, + '🧙🏻‍♂️': 4, + '🧙🏼‍♀️': 4, + '🧙🏼‍♂️': 4, + '🧙🏽‍♀️': 4, + '🧙🏽‍♂️': 4, + '🧙🏾‍♀️': 4, + '🧙🏾‍♂️': 4, + '🧙🏿‍♀️': 4, + '🧙🏿‍♂️': 4, + '🧚‍♀️': 4, + '🧚‍♂️': 4, + '🧚🏻‍♀️': 4, + '🧚🏻‍♂️': 4, + '🧚🏼‍♀️': 4, + '🧚🏼‍♂️': 4, + '🧚🏽‍♀️': 4, + '🧚🏽‍♂️': 4, + '🧚🏾‍♀️': 4, + '🧚🏾‍♂️': 4, + '🧚🏿‍♀️': 4, + '🧚🏿‍♂️': 4, + '🧛‍♀️': 4, + '🧛‍♂️': 4, + '🧛🏻‍♀️': 4, + '🧛🏻‍♂️': 4, + '🧛🏼‍♀️': 4, + '🧛🏼‍♂️': 4, + '🧛🏽‍♀️': 4, + '🧛🏽‍♂️': 4, + '🧛🏾‍♀️': 4, + '🧛🏾‍♂️': 4, + '🧛🏿‍♀️': 4, + '🧛🏿‍♂️': 4, + '🧜‍♀️': 4, + '🧜‍♂️': 4, + '🧜🏻‍♀️': 4, + '🧜🏻‍♂️': 4, + '🧜🏼‍♀️': 4, + '🧜🏼‍♂️': 4, + '🧜🏽‍♀️': 4, + '🧜🏽‍♂️': 4, + '🧜🏾‍♀️': 4, + '🧜🏾‍♂️': 4, + '🧜🏿‍♀️': 4, + '🧜🏿‍♂️': 4, + '🧝‍♀️': 4, + '🧝‍♂️': 4, + '🧝🏻‍♀️': 4, + '🧝🏻‍♂️': 4, + '🧝🏼‍♀️': 4, + '🧝🏼‍♂️': 4, + '🧝🏽‍♀️': 4, + '🧝🏽‍♂️': 4, + '🧝🏾‍♀️': 4, + '🧝🏾‍♂️': 4, + '🧝🏿‍♀️': 4, + '🧝🏿‍♂️': 4, + '🧞‍♀️': 4, + '🧞‍♂️': 4, + '🧟‍♀️': 4, + '🧟‍♂️': 4, + '🫱🏻‍🫲🏼': 4, + '🫱🏻‍🫲🏽': 4, + '🫱🏻‍🫲🏾': 4, + '🫱🏻‍🫲🏿': 4, + '🫱🏼‍🫲🏻': 4, + '🫱🏼‍🫲🏽': 4, + '🫱🏼‍🫲🏾': 4, + '🫱🏼‍🫲🏿': 4, + '🫱🏽‍🫲🏻': 4, + '🫱🏽‍🫲🏼': 4, + '🫱🏽‍🫲🏾': 4, + '🫱🏽‍🫲🏿': 4, + '🫱🏾‍🫲🏻': 4, + '🫱🏾‍🫲🏼': 4, + '🫱🏾‍🫲🏽': 4, + '🫱🏾‍🫲🏿': 4, + '🫱🏿‍🫲🏻': 4, + '🫱🏿‍🫲🏼': 4, + '🫱🏿‍🫲🏽': 4, + '🫱🏿‍🫲🏾': 4, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_c0a2cdbf.py b/libs/wcwidth/table_grapheme_overrides/_known_c0a2cdbf.py new file mode 100644 index 000000000..ecd6a8095 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_c0a2cdbf.py @@ -0,0 +1,3617 @@ +""" +Grapheme overrides for _known_c0a2cdbf. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: kitty +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 0, + 'का': 1, + 'कां': 1, + 'कि': 1, + 'किं': 1, + 'की': 1, + 'केा': 1, + 'को': 1, + 'कौ': 1, + 'क्क': 1, + 'क्का': 1, + 'क्कां': 1, + 'क्को': 1, + 'क्घ्घि्': 1, + 'क्छ': 1, + 'क्त': 1, + 'क्ता': 1, + 'क्ति': 1, + 'क्ती': 1, + 'क्तीं': 1, + 'क्तुं': 1, + 'क्तेः': 1, + 'क्त्योः': 1, + 'क्ना': 1, + 'क्नि': 1, + 'क्य': 1, + 'क्या': 1, + 'क्र': 1, + 'क्रा': 1, + 'क्रि': 1, + 'क्रू': 1, + 'क्ष': 1, + 'क्षं': 1, + 'क्षा': 1, + 'क्षां': 1, + 'क्षाः': 1, + 'क्षि': 1, + 'क्षे': 1, + 'क्षै': 1, + 'क्षो': 1, + 'क्ष्ण': 1, + 'क्ष्य': 1, + 'क्ष्या': 1, + 'क्सा': 1, + 'खा': 1, + 'खि': 1, + 'खी': 1, + 'खो': 1, + 'ख्त': 1, + 'ख्ने': 1, + 'ख्य': 1, + 'ख्यः': 1, + 'ख्या': 1, + 'ख्‍ने': 1, + 'ग़ा': 1, + 'गा': 1, + 'गाँ': 1, + 'गां': 1, + 'गि': 1, + 'गी': 1, + 'गो': 1, + 'गों': 1, + 'गौ': 1, + 'ग्ण': 1, + 'ग्न': 1, + 'ग्ने': 1, + 'ग्भे': 1, + 'ग्य': 1, + 'ग्या': 1, + 'ग्ये': 1, + 'ग्यैः': 1, + 'ग्र': 1, + 'ग्रे': 1, + 'ग्र्य': 1, + 'घा': 1, + 'घि': 1, + 'घी': 1, + 'घेि': 1, + 'घो': 1, + 'घ्ंत': 1, + 'घ्का': 1, + 'घ्त': 1, + 'घ्र': 1, + 'घ्र्व्य': 1, + 'घ्लि': 1, + 'घ्व': 1, + 'घ्श्य': 1, + 'घ्स्ट': 1, + 'ङ्ग': 1, + 'ङ्घ': 1, + 'चा': 1, + 'चां': 1, + 'चि': 1, + 'ची': 1, + 'चो': 1, + 'चौ': 1, + 'च्च': 1, + 'च्चा': 1, + 'च्चे': 1, + 'च्चों': 1, + 'च्छ': 1, + 'च्छं': 1, + 'च्छा': 1, + 'च्छि': 1, + 'च्छु': 1, + 'च्छे': 1, + 'च्या': 1, + 'च्यां': 1, + 'च्यो': 1, + 'छि': 1, + 'छुी': 1, + 'छो': 1, + 'जा': 1, + 'जि': 1, + 'जिं': 1, + 'जी': 1, + 'जो': 1, + 'ज्ज': 1, + 'ज्ञा': 1, + 'ज्ञे': 1, + 'ज्ने': 1, + 'ज्य': 1, + 'ज्या': 1, + 'ज्या‌': 1, + 'ज्रा': 1, + 'झा': 1, + 'ञ्च': 1, + 'ञ्चा': 1, + 'ञ्चि': 1, + 'टा': 1, + 'टां': 1, + 'टि': 1, + 'टी': 1, + 'टों': 1, + 'ट्ज़': 1, + 'ट्टि': 1, + 'ट्टी': 1, + 'ट्या': 1, + 'ट्र': 1, + 'ट्रा': 1, + 'ट्रे': 1, + 'ठा': 1, + 'ठी': 1, + 'ठैः': 1, + 'ड़ा': 1, + 'डा': 1, + 'डि': 1, + 'डी': 1, + 'डो': 1, + 'ढ़ा': 1, + 'ढ़ि': 1, + 'ढ़ी': 1, + 'ढ़ो': 1, + 'ढा': 1, + 'ढाँ': 1, + 'ढि': 1, + 'णा': 1, + 'णां': 1, + 'णि': 1, + 'णी': 1, + 'णो': 1, + 'णों': 1, + 'ण्ड': 1, + 'ण्डं': 1, + 'ण्डः': 1, + 'ण्डि': 1, + 'ण्य': 1, + 'ण्या': 1, + 'ण्यां': 1, + 'तः': 1, + 'ता': 1, + 'तां': 1, + 'ताः': 1, + 'ति': 1, + 'तिं': 1, + 'ती': 1, + 'तो': 1, + 'तों': 1, + 'तौ': 1, + 'त्का': 1, + 'त्त': 1, + 'त्ता': 1, + 'त्ति': 1, + 'त्ते': 1, + 'त्त्थि': 1, + 'त्त्व': 1, + 'त्त्वा': 1, + 'त्त्वे': 1, + 'त्न': 1, + 'त्नी': 1, + 'त्नों': 1, + 'त्प': 1, + 'त्पा': 1, + 'त्पी': 1, + 'त्पू': 1, + 'त्म': 1, + 'त्मा': 1, + 'त्य': 1, + 'त्यं': 1, + 'त्या': 1, + 'त्यां': 1, + 'त्यि': 1, + 'त्ये': 1, + 'त्यै': 1, + 'त्र': 1, + 'त्रा': 1, + 'त्राा': 1, + 'त्राि': 1, + 'त्रि': 1, + 'त्री': 1, + 'त्रे': 1, + 'त्र्त': 1, + 'त्र्य': 1, + 'त्र्या': 1, + 'त्र्ये': 1, + 'त्व': 1, + 'त्वं': 1, + 'त्वा': 1, + 'त्वां': 1, + 'त्वे': 1, + 'त्सं': 1, + 'त्सा': 1, + 'त्सो': 1, + 'त्स्वा': 1, + 'था': 1, + 'थि': 1, + 'थ्य': 1, + 'दः': 1, + 'दा': 1, + 'दां': 1, + 'दि': 1, + 'दी': 1, + 'दो': 1, + 'दौ': 1, + 'द्घ': 1, + 'द्घि': 1, + 'द्घो': 1, + 'द्द': 1, + 'द्दा': 1, + 'द्दि': 1, + 'द्दी': 1, + 'द्दे': 1, + 'द्ध': 1, + 'द्धः': 1, + 'द्धा': 1, + 'द्धां': 1, + 'द्धि': 1, + 'द्धिं': 1, + 'द्धी': 1, + 'द्धीं': 1, + 'द्धैः': 1, + 'द्भ': 1, + 'द्भा': 1, + 'द्य': 1, + 'द्या': 1, + 'द्र': 1, + 'द्रो': 1, + 'द्व': 1, + 'द्वा': 1, + 'द्वि': 1, + 'द्वे': 1, + 'द्स': 1, + 'धा': 1, + 'धां': 1, + 'धाः': 1, + 'धि': 1, + 'धी': 1, + 'धेः': 1, + 'धो': 1, + 'धों': 1, + 'ध्य': 1, + 'ध्या': 1, + 'ध्ये': 1, + 'ध्व': 1, + 'नः': 1, + 'ना': 1, + 'नां': 1, + 'नाः': 1, + 'ना‌': 1, + 'नि': 1, + 'निँ': 1, + 'निः': 1, + 'नी': 1, + 'नैः': 1, + 'नो': 1, + 'नों': 1, + 'नौ': 1, + 'न्का': 1, + 'न्चि': 1, + 'न्छे': 1, + 'न्जे': 1, + 'न्ञ्या': 1, + 'न्त': 1, + 'न्तं': 1, + 'न्तः': 1, + 'न्ता': 1, + 'न्ति': 1, + 'न्तु': 1, + 'न्तुं': 1, + 'न्ते': 1, + 'न्तो': 1, + 'न्तों': 1, + 'न्त्र': 1, + 'न्त्रा': 1, + 'न्त्राः': 1, + 'न्त्रि': 1, + 'न्त्री': 1, + 'न्त्र्य': 1, + 'न्त्र्यं': 1, + 'न्त्र्या': 1, + 'न्त्र्ये': 1, + 'न्द': 1, + 'न्दा': 1, + 'न्दी': 1, + 'न्दो': 1, + 'न्ध': 1, + 'न्धा': 1, + 'न्धि': 1, + 'न्धी': 1, + 'न्धु': 1, + 'न्धों': 1, + 'न्न': 1, + 'न्ना': 1, + 'न्नाः': 1, + 'न्नि': 1, + 'न्ने': 1, + 'न्म': 1, + 'न्मा': 1, + 'न्मु': 1, + 'न्मे': 1, + 'न्य': 1, + 'न्या': 1, + 'न्ये': 1, + 'न्यै': 1, + 'न्यो': 1, + 'न्व': 1, + 'न्वे': 1, + 'न्स': 1, + 'न्हा': 1, + 'न्हें': 1, + 'न्ह्यां': 1, + 'पा': 1, + 'पाँ': 1, + 'पां': 1, + 'पि': 1, + 'पी': 1, + 'पो': 1, + 'पों': 1, + 'प्त': 1, + 'प्तं': 1, + 'प्ति': 1, + 'प्तुं': 1, + 'प्तौ': 1, + 'प्य': 1, + 'प्यं': 1, + 'प्या': 1, + 'प्र': 1, + 'प्रा': 1, + 'फा': 1, + 'फाँ': 1, + 'फौ': 1, + 'फ्त': 1, + 'फ्ता': 1, + 'फ्ना': 1, + 'फ्नो': 1, + 'फ्रां': 1, + 'बा': 1, + 'बाँ': 1, + 'बि': 1, + 'बी': 1, + 'बो': 1, + 'ब्द': 1, + 'ब्ध': 1, + 'ब्धि': 1, + 'ब्यू': 1, + 'भा': 1, + 'भि': 1, + 'भिः': 1, + 'भी': 1, + 'भो': 1, + 'भौ': 1, + 'भ्य': 1, + 'भ्यः': 1, + 'भ्या': 1, + 'भ्यां': 1, + 'भ्यु': 1, + 'भ्यो': 1, + 'भ्र': 1, + 'मा': 1, + 'मां': 1, + 'मि': 1, + 'मी': 1, + 'मो': 1, + 'मों': 1, + 'मौ': 1, + 'म्ति': 1, + 'म्न': 1, + 'म्नि': 1, + 'म्प': 1, + 'म्पा': 1, + 'म्प्र': 1, + 'म्ब': 1, + 'म्बु': 1, + 'म्भि': 1, + 'म्म': 1, + 'म्मा': 1, + 'म्मि': 1, + 'म्मे': 1, + 'म्या': 1, + 'म्से': 1, + 'म्ह': 1, + 'म्ही': 1, + 'म्हो': 1, + 'यः': 1, + 'या': 1, + 'यां': 1, + 'याः': 1, + 'यि': 1, + 'यी': 1, + 'यैः': 1, + 'यो': 1, + 'यों': 1, + 'य्य': 1, + 'य्यो': 1, + 'रः': 1, + 'रा': 1, + 'रां': 1, + 'राः': 1, + 'रि': 1, + 'रिं': 1, + 'री': 1, + 'रो': 1, + 'रों': 1, + 'रौ': 1, + 'र्क': 1, + 'र्कि': 1, + 'र्ग': 1, + 'र्गी': 1, + 'र्गे': 1, + 'र्चि': 1, + 'र्छ': 1, + 'र्ज': 1, + 'र्जा': 1, + 'र्जि': 1, + 'र्जी': 1, + 'र्ट': 1, + 'र्ढ': 1, + 'र्ण': 1, + 'र्णं': 1, + 'र्णां': 1, + 'र्णी': 1, + 'र्त': 1, + 'र्ता': 1, + 'र्ति': 1, + 'र्तिः': 1, + 'र्ती': 1, + 'र्तुं': 1, + 'र्त्त': 1, + 'र्त्ति': 1, + 'र्थ': 1, + 'र्थं': 1, + 'र्थि': 1, + 'र्थी': 1, + 'र्द': 1, + 'र्दा': 1, + 'र्दि': 1, + 'र्दु': 1, + 'र्दे': 1, + 'र्दै': 1, + 'र्दो': 1, + 'र्द्ध': 1, + 'र्ध': 1, + 'र्धा': 1, + 'र्धि': 1, + 'र्न': 1, + 'र्नि': 1, + 'र्नु': 1, + 'र्नू': 1, + 'र्ने': 1, + 'र्फ': 1, + 'र्ब': 1, + 'र्बं': 1, + 'र्बा': 1, + 'र्भ': 1, + 'र्भा': 1, + 'र्म': 1, + 'र्मं': 1, + 'र्मा': 1, + 'र्मि': 1, + 'र्य': 1, + 'र्यं': 1, + 'र्या': 1, + 'र्यां': 1, + 'र्ये': 1, + 'र्यों': 1, + 'र्रा': 1, + 'र्व': 1, + 'र्वा': 1, + 'र्वां': 1, + 'र्वृ': 1, + 'र्वे': 1, + 'र्वो': 1, + 'र्श': 1, + 'र्ष': 1, + 'र्षे': 1, + 'र्स': 1, + 'र्सा': 1, + 'र्ह': 1, + 'र्हाः': 1, + 'र्हि': 1, + 'र्हो': 1, + 'र्‍या': 1, + 'ला': 1, + 'लां': 1, + 'लाः': 1, + 'लि': 1, + 'लिं': 1, + 'ली': 1, + 'लीं': 1, + 'लो': 1, + 'लों': 1, + 'लौ': 1, + 'ल्क': 1, + 'ल्का': 1, + 'ल्ने': 1, + 'ल्प': 1, + 'ल्पि': 1, + 'ल्म': 1, + 'ल्य': 1, + 'ल्यं': 1, + 'ल्या': 1, + 'ल्यां': 1, + 'ल्याः': 1, + 'ल्ये': 1, + 'ल्ल': 1, + 'ल्लं': 1, + 'ल्ला': 1, + 'ल्लि': 1, + 'ल्ले': 1, + 'ल्ह': 1, + 'ळा': 1, + 'ळी': 1, + 'वः': 1, + 'वा': 1, + 'वां': 1, + 'वाः': 1, + 'वि': 1, + 'वी': 1, + 'वो': 1, + 'वों': 1, + 'व्य': 1, + 'व्या': 1, + 'व्ये': 1, + 'व्यों': 1, + 'शः': 1, + 'शा': 1, + 'शां': 1, + 'शाः': 1, + 'शि': 1, + 'शी': 1, + 'शो': 1, + 'शों': 1, + 'श्च': 1, + 'श्चि': 1, + 'श्य': 1, + 'श्या': 1, + 'श्यि': 1, + 'श्यों': 1, + 'श्र': 1, + 'श्रा': 1, + 'श्रां': 1, + 'श्रु': 1, + 'श्व': 1, + 'श्वा': 1, + 'श्वि': 1, + 'षा': 1, + 'षां': 1, + 'षाः': 1, + 'षि': 1, + 'षी': 1, + 'षों': 1, + 'ष्का': 1, + 'ष्ट': 1, + 'ष्टः': 1, + 'ष्टा': 1, + 'ष्टां': 1, + 'ष्टाः': 1, + 'ष्टि': 1, + 'ष्टी': 1, + 'ष्टीं': 1, + 'ष्ट्र': 1, + 'ष्ट्रा': 1, + 'ष्ट्रां': 1, + 'ष्ट्रि': 1, + 'ष्ट्री': 1, + 'ष्ट्रे': 1, + 'ष्ट्रो': 1, + 'ष्ट्रों': 1, + 'ष्ठा': 1, + 'ष्ठे': 1, + 'ष्ठों': 1, + 'ष्णु': 1, + 'ष्प': 1, + 'ष्प्र': 1, + 'ष्य': 1, + 'ष्यः': 1, + 'ष्यों': 1, + 'सः': 1, + 'सा': 1, + 'साँ': 1, + 'सां': 1, + 'सि': 1, + 'सी': 1, + 'सो': 1, + 'सौ': 1, + 'स्क': 1, + 'स्का': 1, + 'स्काः': 1, + 'स्कु': 1, + 'स्कू': 1, + 'स्कृ': 1, + 'स्ट': 1, + 'स्ट्या': 1, + 'स्त': 1, + 'स्ता': 1, + 'स्ति': 1, + 'स्ती': 1, + 'स्तु': 1, + 'स्तुं': 1, + 'स्तै': 1, + 'स्तो': 1, + 'स्त्र': 1, + 'स्त्रा': 1, + 'स्त्रि': 1, + 'स्त्री': 1, + 'स्थ': 1, + 'स्था': 1, + 'स्थां': 1, + 'स्थि': 1, + 'स्थी': 1, + 'स्थे': 1, + 'स्थ्य': 1, + 'स्धी': 1, + 'स्प': 1, + 'स्पे': 1, + 'स्म': 1, + 'स्मि': 1, + 'स्मिं': 1, + 'स्मै': 1, + 'स्य': 1, + 'स्या': 1, + 'स्यां': 1, + 'स्याः': 1, + 'स्युः': 1, + 'स्यों': 1, + 'स्रो': 1, + 'स्ल': 1, + 'स्व': 1, + 'स्वं': 1, + 'स्वा': 1, + 'स्वी': 1, + 'स्वे': 1, + 'स्स': 1, + 'स्सा': 1, + 'हा': 1, + 'हाँ': 1, + 'हां': 1, + 'हि': 1, + 'हिं': 1, + 'ही': 1, + 'हीं': 1, + 'हैः': 1, + 'हो': 1, + 'हों': 1, + 'हौ': 1, + 'ह्या': 1, + 'ह्रा': 1, + 'क़ा': 1, + 'क़ि': 1, + 'ज़ा': 1, + 'ड़ा': 1, + 'ढ़ा': 1, + 'फ़ा': 1, + 'फ़्ता': 1, + 'অং': 1, + 'কা': 1, + 'কা‌': 1, + 'কি': 1, + 'কিং': 1, + 'কে': 1, + 'কে‌': 1, + 'কো': 1, + 'ক্ত': 1, + 'ক্তি': 1, + 'ক্য': 1, + 'ক্র': 1, + 'ক্রী': 1, + 'ক্ষ': 1, + 'ক্ষা': 1, + 'ক্ষি': 1, + 'ক্ষে': 1, + 'ক্ষ্য': 1, + 'ক্ষ্যে': 1, + 'খা': 1, + 'খি': 1, + 'খে': 1, + 'খ্যা': 1, + 'গি': 1, + 'গে': 1, + 'গো': 1, + 'গ্য': 1, + 'গ্র': 1, + 'গ্রা': 1, + 'গ্রে': 1, + 'ঘা': 1, + 'ঘে': 1, + 'ঘো': 1, + 'ঘো': 1, + 'ঙ্খা': 1, + 'ঙ্গ': 1, + 'ঙ্গী': 1, + 'ঙ্গে': 1, + 'ঙ্ঘ': 1, + 'চা': 1, + 'চি': 1, + 'চে': 1, + 'চো': 1, + 'চৌ': 1, + 'চ্চ': 1, + 'চ্ছ': 1, + 'চ্ছা': 1, + 'চ্ছা‌': 1, + 'চ্ছু': 1, + 'চ্ছে': 1, + 'ছা': 1, + 'ছি': 1, + 'ছে': 1, + 'জা': 1, + 'জি': 1, + 'জী': 1, + 'জে': 1, + 'জে‌': 1, + 'জ্ঞ': 1, + 'জ্ঞা': 1, + 'জ্য': 1, + 'ঝো': 1, + 'ঞ্চি': 1, + 'ঞ্ছি': 1, + 'টা': 1, + 'টি': 1, + 'ট্রে': 1, + 'ঠি': 1, + 'ড়া': 1, + 'ণা': 1, + 'ণি': 1, + 'ণে': 1, + 'ণ্ড': 1, + 'ণ্ডে': 1, + 'ণ্য': 1, + 'তঃ': 1, + 'তা': 1, + 'তাঁ': 1, + 'তা‌': 1, + 'তি': 1, + 'তি‌': 1, + 'তী': 1, + 'তে': 1, + 'তে‌': 1, + 'ত্ত': 1, + 'ত্তা': 1, + 'ত্তি': 1, + 'ত্ন': 1, + 'ত্ব': 1, + 'ত্বে': 1, + 'ত্ম': 1, + 'ত্য': 1, + 'ত্যা': 1, + 'ত্যি': 1, + 'ত্যে': 1, + 'ত্র': 1, + 'ত্রা': 1, + 'ত্রে': 1, + 'ত্‍প': 1, + 'ত্‍পী': 1, + 'ত্‍সা': 1, + 'থা': 1, + 'থি': 1, + 'থে': 1, + 'থ্য': 1, + 'দা': 1, + 'দি': 1, + 'দে': 1, + 'দে‌': 1, + 'দৈ': 1, + 'দো': 1, + 'দ্দি': 1, + 'দ্দে': 1, + 'দ্ধ': 1, + 'দ্ধা': 1, + 'দ্ধি': 1, + 'দ্ধে': 1, + 'দ্বা': 1, + 'দ্ভ': 1, + 'দ্ভূ': 1, + 'দ্য': 1, + 'দ্রো': 1, + 'ধা': 1, + 'ধি': 1, + 'ধী': 1, + 'ধে': 1, + 'ধ্য': 1, + 'ধ্যে': 1, + 'না': 1, + 'না‌': 1, + 'নি': 1, + 'নী': 1, + 'নে': 1, + 'নৈ': 1, + 'নো': 1, + 'ন্ড': 1, + 'ন্ত': 1, + 'ন্তা': 1, + 'ন্তি': 1, + 'ন্তে': 1, + 'ন্ত্রি': 1, + 'ন্ত্রে': 1, + 'ন্থী': 1, + 'ন্ধ': 1, + 'ন্ধা': 1, + 'ন্ধু': 1, + 'ন্ন': 1, + 'ন্ম': 1, + 'ন্মা': 1, + 'ন্মু': 1, + 'ন্য': 1, + 'ন্যা': 1, + 'ন্যে': 1, + 'পা': 1, + 'পা‌': 1, + 'পি': 1, + 'পী': 1, + 'পে': 1, + 'পো': 1, + 'প্ত': 1, + 'প্তা': 1, + 'প্র': 1, + 'প্রা': 1, + 'ফে': 1, + 'ফৌ': 1, + 'বং': 1, + 'বা': 1, + 'বা‌': 1, + 'বি': 1, + 'বী': 1, + 'বে': 1, + 'বৈ': 1, + 'বো': 1, + 'ব্ধি': 1, + 'ব্য': 1, + 'ব্যা': 1, + 'ব্র': 1, + 'ভা': 1, + 'ভি': 1, + 'ভে': 1, + 'ভো': 1, + 'ভৌ': 1, + 'ভ্য': 1, + 'ভ্রা': 1, + 'মা': 1, + 'মা‌': 1, + 'মি': 1, + 'মে': 1, + 'মৌ': 1, + 'ম্প': 1, + 'ম্পা': 1, + 'ম্ব': 1, + 'ম্ভ': 1, + 'ম্ম': 1, + 'ম্মা': 1, + 'ম্য': 1, + 'য়া': 1, + 'য়ি': 1, + 'য়ে': 1, + 'য়ো': 1, + 'যা': 1, + 'যে': 1, + 'যো': 1, + 'য্য': 1, + 'রা': 1, + 'রাং': 1, + 'রি': 1, + 'রী': 1, + 'রে': 1, + 'রো': 1, + 'র্ক': 1, + 'র্জ': 1, + 'র্জা': 1, + 'র্ণ': 1, + 'র্ত': 1, + 'র্থ': 1, + 'র্থে': 1, + 'র্দো': 1, + 'র্ধ': 1, + 'র্ধা': 1, + 'র্নী': 1, + 'র্ব': 1, + 'র্বা': 1, + 'র্বি': 1, + 'র্বো': 1, + 'র্ব্য': 1, + 'র্ভূ': 1, + 'র্ম': 1, + 'র্মে': 1, + 'র্য': 1, + 'র্যা': 1, + 'লা': 1, + 'লি': 1, + 'লী': 1, + 'লে': 1, + 'লে‌': 1, + 'ল্প': 1, + 'ল্য': 1, + 'ল্যা': 1, + 'ল্যে': 1, + 'ল্লে': 1, + 'শং': 1, + 'শা': 1, + 'শি': 1, + 'শী': 1, + 'শৃং': 1, + 'শে': 1, + 'শৈ': 1, + 'শ্চ': 1, + 'শ্চি': 1, + 'শ্বা': 1, + 'শ্বে': 1, + 'শ্য': 1, + 'শ্যে': 1, + 'শ্র': 1, + 'শ্রা': 1, + 'ষা': 1, + 'ষি': 1, + 'ষী': 1, + 'ষে': 1, + 'ষ্ট': 1, + 'ষ্টা': 1, + 'ষ্টি': 1, + 'ষ্ট্র': 1, + 'ষ্ট্রী': 1, + 'ষ্ট্রে': 1, + 'ষ্ঠা': 1, + 'ষ্ঠি': 1, + 'ষ্ঠী': 1, + 'ষ্ঠু': 1, + 'ষ্ণু': 1, + 'সং': 1, + 'সা': 1, + 'সাং': 1, + 'সি': 1, + 'সী': 1, + 'সে': 1, + 'সে‌': 1, + 'স্ক': 1, + 'স্কৃ': 1, + 'স্ত': 1, + 'স্তি': 1, + 'স্তি‌': 1, + 'স্ত্র': 1, + 'স্থ': 1, + 'স্থা': 1, + 'স্থ্য': 1, + 'স্ব': 1, + 'স্বা': 1, + 'স্বী': 1, + 'স্য': 1, + 'স্যা': 1, + 'স্যে': 1, + 'হা': 1, + 'হি': 1, + 'হে': 1, + 'হো': 1, + 'হ্য': 1, + 'য়ে': 1, + 'ਕਾ': 1, + 'ਕਾਂ': 1, + 'ਕਿ': 1, + 'ਕਿ੍': 1, + 'ਕਿੱ': 1, + 'ਕੀ': 1, + 'ਖਾ': 1, + 'ਖਾਂ': 1, + 'ਖਿ': 1, + 'ਖਿ਼': 1, + 'ਖੀ': 1, + 'ਗਾ': 1, + 'ਗਿ': 1, + 'ਗਿ੍': 1, + 'ਗੀ': 1, + 'ਚਾ': 1, + 'ਚਿ': 1, + 'ਚਿੱ': 1, + 'ਚੀ': 1, + 'ਜਾ': 1, + 'ਜਾਂ': 1, + 'ਜਿ': 1, + 'ਜਿ਼': 1, + 'ਜਿ਼ੰ': 1, + 'ਜੀ': 1, + 'ਝਿ': 1, + 'ਝੀ': 1, + 'ਟਾ': 1, + 'ਟਿ': 1, + 'ਟਿ੍': 1, + 'ਟੀ': 1, + 'ਠੀ': 1, + 'ਡਾ': 1, + 'ਣਾ': 1, + 'ਣਾਂ': 1, + 'ਣੀ': 1, + 'ਤਾ': 1, + 'ਤਾਂ': 1, + 'ਤਿ': 1, + 'ਤੀ': 1, + 'ਦਾ': 1, + 'ਦਾਂ': 1, + 'ਦਿ': 1, + 'ਦਿੱ': 1, + 'ਦੀ': 1, + 'ਧਾ': 1, + 'ਧਾਂ': 1, + 'ਧਿ': 1, + 'ਧੀ': 1, + 'ਨਾ': 1, + 'ਨਾਂ': 1, + 'ਨਿ': 1, + 'ਨੀ': 1, + 'ਨੀਂ': 1, + 'ਪਾ': 1, + 'ਪਿ': 1, + 'ਪੀ': 1, + 'ਫਿ': 1, + 'ਬਾ': 1, + 'ਬਿ': 1, + 'ਬੀ': 1, + 'ਭਾ': 1, + 'ਭਿ': 1, + 'ਮਾ': 1, + 'ਮਾਂ': 1, + 'ਮਿ': 1, + 'ਮੀ': 1, + 'ਯਾ': 1, + 'ਰਾ': 1, + 'ਰਾਂ': 1, + 'ਰਿ': 1, + 'ਰੀ': 1, + 'ਲਾ': 1, + 'ਲਾਂ': 1, + 'ਲਿ': 1, + 'ਲਿੰ': 1, + 'ਲੀ': 1, + 'ਵਾ': 1, + 'ਵਾਂ': 1, + 'ਵਿ': 1, + 'ਵਿੱ': 1, + 'ਵੀ': 1, + 'ਸ਼ਾ': 1, + 'ਸ਼ਾਂ': 1, + 'ਸ਼ਿ': 1, + 'ਸ਼ੀ': 1, + 'ਸਾ': 1, + 'ਸਾਂ': 1, + 'ਸਿ': 1, + 'ਸਿੱ': 1, + 'ਸੀ': 1, + 'ਹਾ': 1, + 'ਹਾਂ': 1, + 'ਹਿ': 1, + 'ਹਿੰ': 1, + 'ਹਿੱ': 1, + 'ਹੀ': 1, + 'ਹੀਂ': 1, + 'ਜ਼ਾ': 1, + 'ਜ਼ੀ': 1, + 'ੜਾ': 1, + 'ੜੀਂ': 1, + 'ਫ਼ਾ': 1, + 'કઃ': 1, + 'કા': 1, + 'કાં': 1, + 'કિ': 1, + 'કી': 1, + 'કો': 1, + 'ક્ક': 1, + 'ક્કા': 1, + 'ક્કો': 1, + 'ક્તિ': 1, + 'ક્ર': 1, + 'ક્ષ': 1, + 'ક્ષા': 1, + 'ખા': 1, + 'ખી': 1, + 'ખો': 1, + 'ખ્ત': 1, + 'ગા': 1, + 'ગિ': 1, + 'ગી': 1, + 'ગો': 1, + 'ગ્ન': 1, + 'ગ્ય': 1, + 'ઘા': 1, + 'ઘો': 1, + 'ચા': 1, + 'ચિ': 1, + 'ચી': 1, + 'ચ્ચ': 1, + 'ચ્ચા': 1, + 'ચ્ચે': 1, + 'ચ્છા': 1, + 'ચ્છે': 1, + 'છા': 1, + 'છો': 1, + 'જા': 1, + 'જાં': 1, + 'જિ': 1, + 'જી': 1, + 'જો': 1, + 'જ્ઞા': 1, + 'જ્ય': 1, + 'જ્યો': 1, + 'ટઃ': 1, + 'ટ્ર': 1, + 'ટ્રે': 1, + 'ડા': 1, + 'ડાં': 1, + 'ડી': 1, + 'ઢા': 1, + 'ઢિ': 1, + 'ણા': 1, + 'ણી': 1, + 'તઃ': 1, + 'તા': 1, + 'તાં': 1, + 'તિ': 1, + 'તી': 1, + 'તો': 1, + 'ત્તા': 1, + 'ત્તિ': 1, + 'ત્ત્વ': 1, + 'ત્ત્વા': 1, + 'ત્ન': 1, + 'ત્મ': 1, + 'ત્ય': 1, + 'ત્યા': 1, + 'ત્યાં': 1, + 'ત્યિ': 1, + 'ત્યે': 1, + 'ત્યો': 1, + 'ત્ર': 1, + 'ત્રી': 1, + 'ત્ર્ય': 1, + 'ત્વ': 1, + 'થા': 1, + 'થી': 1, + 'દા': 1, + 'દી': 1, + 'દો': 1, + 'દ્ધ': 1, + 'દ્ધા': 1, + 'દ્ધાં': 1, + 'દ્ધિ': 1, + 'દ્ભ': 1, + 'દ્વા': 1, + 'ધા': 1, + 'ધાં': 1, + 'ધિ': 1, + 'ધી': 1, + 'ધો': 1, + 'ધ્ય': 1, + 'ધ્યા': 1, + 'નઃ': 1, + 'ના': 1, + 'નાં': 1, + 'નિ': 1, + 'ની': 1, + 'નો': 1, + 'ન્ત': 1, + 'ન્તો': 1, + 'ન્મ': 1, + 'ન્મા': 1, + 'ન્મે': 1, + 'ન્ય': 1, + 'ન્યા': 1, + 'ન્યૂં': 1, + 'પા': 1, + 'પિ': 1, + 'પી': 1, + 'પો': 1, + 'પ્ત': 1, + 'પ્ર': 1, + 'પ્રા': 1, + 'ફા': 1, + 'ફો': 1, + 'બા': 1, + 'બાં': 1, + 'બી': 1, + 'બ્ધ': 1, + 'ભા': 1, + 'ભાં': 1, + 'ભિ': 1, + 'ભો': 1, + 'ભૌ': 1, + 'ભ્ય': 1, + 'મઃ': 1, + 'મા': 1, + 'માં': 1, + 'મિ': 1, + 'મી': 1, + 'મો': 1, + 'મ્ય': 1, + 'મ્યાં': 1, + 'યા': 1, + 'યિ': 1, + 'યી': 1, + 'યો': 1, + 'રા': 1, + 'રિ': 1, + 'રી': 1, + 'રો': 1, + 'ર્જ': 1, + 'ર્ણ': 1, + 'ર્ત': 1, + 'ર્તા': 1, + 'ર્થ': 1, + 'ર્થિ': 1, + 'ર્દો': 1, + 'ર્મ': 1, + 'ર્મિ': 1, + 'ર્મે': 1, + 'ર્ય': 1, + 'ર્યા': 1, + 'ર્યો': 1, + 'ર્વ': 1, + 'લા': 1, + 'લાં': 1, + 'લિ': 1, + 'લિં': 1, + 'લી': 1, + 'લો': 1, + 'લ્મ': 1, + 'લ્ય': 1, + 'ળા': 1, + 'ળી': 1, + 'વા': 1, + 'વાં': 1, + 'વિ': 1, + 'વી': 1, + 'વો': 1, + 'વ્ય': 1, + 'વ્યા': 1, + 'વ્યો': 1, + 'શા': 1, + 'શાં': 1, + 'શિ': 1, + 'શી': 1, + 'શો': 1, + 'શ્કે': 1, + 'શ્ચ': 1, + 'શ્ય': 1, + 'શ્ર': 1, + 'શ્વ': 1, + 'ષા': 1, + 'ષી': 1, + 'ષો': 1, + 'ષ્ટિ': 1, + 'ષ્ટ્ર': 1, + 'ષ્ટ્રી': 1, + 'ષ્ટ્રો': 1, + 'ષ્ઠા': 1, + 'ષ્ણુ': 1, + 'ષ્પ': 1, + 'સા': 1, + 'સાં': 1, + 'સિ': 1, + 'સી': 1, + 'સો': 1, + 'સ્કૃ': 1, + 'સ્ટ': 1, + 'સ્તિ': 1, + 'સ્તી': 1, + 'સ્ત્રી': 1, + 'સ્થા': 1, + 'સ્થિ': 1, + 'સ્પ': 1, + 'સ્રી': 1, + 'સ્વ': 1, + 'સ્વા': 1, + 'સ્વી': 1, + 'સ્સા': 1, + 'હા': 1, + 'હિ': 1, + 'હી': 1, + 'હો': 1, + 'கா': 1, + 'கி': 1, + 'கு': 1, + 'கூ': 1, + 'கெ': 1, + 'கே': 1, + 'கை': 1, + 'கொ': 1, + 'கோ': 1, + 'சா': 1, + 'சி': 1, + 'சு': 1, + 'சூ': 1, + 'செ': 1, + 'சே': 1, + 'சை': 1, + 'சொ': 1, + 'டா': 1, + 'டி': 1, + 'டு': 1, + 'டெ': 1, + 'டை': 1, + 'ணா': 1, + 'ணி': 1, + 'ணு': 1, + 'ணை': 1, + 'தா': 1, + 'தி': 1, + 'து': 1, + 'தூ': 1, + 'தெ': 1, + 'தே': 1, + 'தை': 1, + 'தொ': 1, + 'தோ': 1, + 'நா': 1, + 'நி': 1, + 'நு': 1, + 'நெ': 1, + 'நே': 1, + 'நோ': 1, + 'னா': 1, + 'னி': 1, + 'னு': 1, + 'னே': 1, + 'னை': 1, + 'னொ': 1, + 'பா': 1, + 'பி': 1, + 'பு': 1, + 'பெ': 1, + 'பே': 1, + 'பை': 1, + 'பொ': 1, + 'போ': 1, + 'மா': 1, + 'மி': 1, + 'மு': 1, + 'மூ': 1, + 'மெ': 1, + 'மே': 1, + 'மை': 1, + 'மொ': 1, + 'மோ': 1, + 'யா': 1, + 'யி': 1, + 'யு': 1, + 'யே': 1, + 'யை': 1, + 'யோ': 1, + 'ரா': 1, + 'ரி': 1, + 'ரு': 1, + 'ரெ': 1, + 'ரை': 1, + 'றா': 1, + 'றி': 1, + 'று': 1, + 'றே': 1, + 'றை': 1, + 'றொ': 1, + 'றோ': 1, + 'லா': 1, + 'லி': 1, + 'லு': 1, + 'லெ': 1, + 'லே': 1, + 'லை': 1, + 'லோ': 1, + 'ளா': 1, + 'ளி': 1, + 'ளு': 1, + 'ளெ': 1, + 'ளை': 1, + 'ழி': 1, + 'ழு': 1, + 'ழை': 1, + 'வா': 1, + 'வி': 1, + 'வு': 1, + 'வூ': 1, + 'வெ': 1, + 'வே': 1, + 'வை': 1, + 'வொ': 1, + 'வோ': 1, + 'అం': 1, + 'ఆం': 1, + 'ఒం': 1, + 'కం': 1, + 'కాం': 1, + 'కు': 1, + 'కుం': 1, + 'కూ': 1, + 'కృ': 1, + 'కెం': 1, + 'క్క': 1, + 'క్కు': 1, + 'క్త': 1, + 'క్తి': 1, + 'క్తీ': 1, + 'క్తు': 1, + 'క్య': 1, + 'క్ర': 1, + 'క్రాం': 1, + 'క్రి': 1, + 'క్రూ': 1, + 'క్ష': 1, + 'క్షా': 1, + 'క్షి': 1, + 'క్షిం': 1, + 'క్షే': 1, + 'క్షో': 1, + 'క్స్వా': 1, + 'ఖ్య': 1, + 'ఖ్యా': 1, + 'గిం': 1, + 'గు': 1, + 'గూ': 1, + 'గృ': 1, + 'గ్భా': 1, + 'గ్మ': 1, + 'గ్య': 1, + 'గ్ర': 1, + 'గ్రి': 1, + 'చు': 1, + 'చెం': 1, + 'చ్చు': 1, + 'చ్ఛ': 1, + 'చ్ఛా': 1, + 'చ్ఛే': 1, + 'ఛ్ఛా': 1, + 'జ్ఞ': 1, + 'జ్ఞా': 1, + 'జ్య': 1, + 'టం': 1, + 'టిం': 1, + 'టు': 1, + 'టుం': 1, + 'ట్టి': 1, + 'ట్లు': 1, + 'డు': 1, + 'డ్డ': 1, + 'డ్రు': 1, + 'ణిం': 1, + 'తం': 1, + 'తః': 1, + 'తు': 1, + 'తృ': 1, + 'త్కా': 1, + 'త్త': 1, + 'త్తి': 1, + 'త్తు': 1, + 'త్న': 1, + 'త్నిం': 1, + 'త్ప': 1, + 'త్ఫ': 1, + 'త్య': 1, + 'త్యం': 1, + 'త్యా': 1, + 'త్ర': 1, + 'త్రు': 1, + 'త్ర్య': 1, + 'త్వ': 1, + 'త్వా': 1, + 'త్స': 1, + 'దం': 1, + 'దిం': 1, + 'దు': 1, + 'దుం': 1, + 'దృ': 1, + 'ద్ఘో': 1, + 'ద్ది': 1, + 'ద్దు': 1, + 'ద్దే': 1, + 'ద్ధ': 1, + 'ద్ధి': 1, + 'ద్ధిం': 1, + 'ద్య': 1, + 'ద్యా': 1, + 'ద్యో': 1, + 'ద్ర్య': 1, + 'ద్వా': 1, + 'ధిం': 1, + 'ధు': 1, + 'ధ్య': 1, + 'నం': 1, + 'నః': 1, + 'నాం': 1, + 'నిం': 1, + 'ను': 1, + 'నుం': 1, + 'నొం': 1, + 'న్ధ': 1, + 'న్న': 1, + 'న్నా': 1, + 'న్ని': 1, + 'న్ను': 1, + 'న్మ': 1, + 'న్య': 1, + 'న్యా': 1, + 'న్వే': 1, + 'పం': 1, + 'పిం': 1, + 'పు': 1, + 'పూ': 1, + 'పెం': 1, + 'పొం': 1, + 'ప్త': 1, + 'ప్ర': 1, + 'ప్రా': 1, + 'ప్రే': 1, + 'బం': 1, + 'బ్ర': 1, + 'భం': 1, + 'భు': 1, + 'భ్య': 1, + 'భ్యు': 1, + 'భ్ర': 1, + 'భ్రా': 1, + 'మిం': 1, + 'ము': 1, + 'మూ': 1, + 'మ్య': 1, + 'యం': 1, + 'యః': 1, + 'యిం': 1, + 'యు': 1, + 'యుం': 1, + 'రం': 1, + 'రిం': 1, + 'రు': 1, + 'రూ': 1, + 'ర్గ': 1, + 'ర్చి': 1, + 'ర్చు': 1, + 'ర్ణ': 1, + 'ర్ణాం': 1, + 'ర్ణు': 1, + 'ర్త': 1, + 'ర్తి': 1, + 'ర్తిం': 1, + 'ర్తృ': 1, + 'ర్థ': 1, + 'ర్థి': 1, + 'ర్థిం': 1, + 'ర్ద': 1, + 'ర్దో': 1, + 'ర్ధ': 1, + 'ర్ప': 1, + 'ర్బం': 1, + 'ర్భ': 1, + 'ర్భా': 1, + 'ర్య': 1, + 'ర్యా': 1, + 'ర్రా': 1, + 'ర్వ': 1, + 'ర్వా': 1, + 'ర్హ': 1, + 'ర్హాం': 1, + 'ర్హు': 1, + 'ఱు': 1, + 'లం': 1, + 'లిం': 1, + 'లు': 1, + 'ల్క': 1, + 'ల్గొ': 1, + 'ల్ప': 1, + 'ల్ల': 1, + 'ల్లి': 1, + 'విం': 1, + 'వు': 1, + 'వృ': 1, + 'వ్య': 1, + 'వ్యా': 1, + 'వ్వ': 1, + 'శాం': 1, + 'శిం': 1, + 'శు': 1, + 'శ్చ': 1, + 'శ్చి': 1, + 'శ్య': 1, + 'శ్ర': 1, + 'శ్రాం': 1, + 'శ్లే': 1, + 'శ్వా': 1, + 'షిం': 1, + 'షు': 1, + 'ష్ట': 1, + 'ష్టు': 1, + 'ష్ట్ర': 1, + 'ష్ట్రీ': 1, + 'ష్ఠ': 1, + 'ష్ఠా': 1, + 'ష్ఠు': 1, + 'ష్థ': 1, + 'ష్ప': 1, + 'సం': 1, + 'సాం': 1, + 'సు': 1, + 'సూ': 1, + 'స్కృ': 1, + 'స్తా': 1, + 'స్తి': 1, + 'స్త్ర': 1, + 'స్త్రీ': 1, + 'స్థ': 1, + 'స్థా': 1, + 'స్థి': 1, + 'స్థ్య': 1, + 'స్ప': 1, + 'స్య': 1, + 'స్వ': 1, + 'స్వా': 1, + 'స్వీ': 1, + 'స్వే': 1, + 'హిం': 1, + 'ಅಂ': 1, + 'ಇಂ': 1, + 'ಉಂ': 1, + 'ಎಂ': 1, + 'ಒಂ': 1, + 'ಕಾ': 1, + 'ಕಾಂ': 1, + 'ಕಿಂ': 1, + 'ಕೀ': 1, + 'ಕು': 1, + 'ಕುಂ': 1, + 'ಕೂ': 1, + 'ಕೃ': 1, + 'ಕೆಂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಕೊಂ': 1, + 'ಖಾಂ': 1, + 'ಗಾ': 1, + 'ಗಾಂ': 1, + 'ಗೀ': 1, + 'ಗು': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೊಂ': 1, + 'ಗೋ': 1, + 'ಚಾ': 1, + 'ಚೀ': 1, + 'ಚು': 1, + 'ಛಾ': 1, + 'ಜಾ': 1, + 'ಜೀ': 1, + 'ಟು': 1, + 'ಟುಂ': 1, + 'ಟೇ': 1, + 'ಠಾ': 1, + 'ಡಾ': 1, + 'ಡು': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣಂ': 1, + 'ಣಾ': 1, + 'ಣು': 1, + 'ಣೈ': 1, + 'ತಂ': 1, + 'ತಃ': 1, + 'ತಾ': 1, + 'ತೀ': 1, + 'ತು': 1, + 'ತೃ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ಥಾ': 1, + 'ದಂ': 1, + 'ದಾ': 1, + 'ದಿಂ': 1, + 'ದು': 1, + 'ದೂ': 1, + 'ದೃ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧಾ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಃ': 1, + 'ನಾ': 1, + 'ನಾಂ': 1, + 'ನಿಂ': 1, + 'ನು': 1, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪಂ': 1, + 'ಪಾ': 1, + 'ಪು': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬಂ': 1, + 'ಬಾ': 1, + 'ಬು': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭಾ': 1, + 'ಭೀ': 1, + 'ಭು': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮಾ': 1, + 'ಮೀ': 1, + 'ಮು': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯಂ': 1, + 'ಯಾ': 1, + 'ಯಿಂ': 1, + 'ಯು': 1, + 'ಯುಂ': 1, + 'ಯೂ': 1, + 'ಯೆಂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಂ': 1, + 'ರಾ': 1, + 'ರಾಂ': 1, + 'ರಿಂ': 1, + 'ರೀ': 1, + 'ರು': 1, + 'ರೂ': 1, + 'ರೆಂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲಂ': 1, + 'ಲಾ': 1, + 'ಲೀ': 1, + 'ಲು': 1, + 'ಲೂ': 1, + 'ಲೆಂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳಾ': 1, + 'ಳಿಂ': 1, + 'ಳು': 1, + 'ಳೂ': 1, + 'ವಾ': 1, + 'ವೀ': 1, + 'ವು': 1, + 'ವುಂ': 1, + 'ವೂ': 1, + 'ವೃ': 1, + 'ವೆಂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾ': 1, + 'ಶಾಂ': 1, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷಾ': 1, + 'ಷೇ': 1, + 'ಸಂ': 1, + 'ಸಾ': 1, + 'ಸಾಂ': 1, + 'ಸು': 1, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹಾ': 1, + 'ಹು': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೊಂ': 1, + 'ಹೋ': 1, + 'അം': 1, + 'കാ': 1, + 'കി': 1, + 'കെ': 1, + 'കേ': 1, + 'കൈ': 1, + 'കൊ': 1, + 'കോ': 1, + 'ക്ക': 1, + 'ക്കാ': 1, + 'ക്കി': 1, + 'ക്കു': 1, + 'ക്കും': 1, + 'ക്കെ': 1, + 'ക്കേ': 1, + 'ക്കൊ': 1, + 'ക്കോ': 1, + 'ക്ക്‌': 1, + 'ക്തി': 1, + 'ക്യ': 1, + 'ക്യം': 1, + 'ക്ര': 1, + 'ക്രൂ': 1, + 'ക്ഷ': 1, + 'ക്ഷാ': 1, + 'ക്ഷി': 1, + 'ക്ഷേ': 1, + 'ഖ്യാ': 1, + 'ഗം': 1, + 'ഗി': 1, + 'ഗീ': 1, + 'ഗ്ഗം': 1, + 'ഗ്ദാ': 1, + 'ഗ്യ': 1, + 'ഗ്യം': 1, + 'ഗ്ര': 1, + 'ഘി': 1, + 'ങ്കി': 1, + 'ങ്കെ': 1, + 'ങ്കേ': 1, + 'ങ്ങ': 1, + 'ങ്ങി': 1, + 'ചാ': 1, + 'ചി': 1, + 'ചെ': 1, + 'ചേ': 1, + 'ചോ': 1, + 'ച്ച': 1, + 'ച്ചി': 1, + 'ച്ചു': 1, + 'ച്ചും': 1, + 'ച്ചെ': 1, + 'ച്ചേ': 1, + 'ജാ': 1, + 'ജി': 1, + 'ജീ': 1, + 'ജ്യ': 1, + 'ജ്യാ': 1, + 'ഞ്ച': 1, + 'ഞ്ഞ': 1, + 'ഞ്ഞി': 1, + 'ഞ്ഞെ': 1, + 'ടാ': 1, + 'ടി': 1, + 'ടും': 1, + 'ടെ': 1, + 'ടേ': 1, + 'ട്ട': 1, + 'ട്ടാ': 1, + 'ട്ടി': 1, + 'ട്ടു': 1, + 'ട്ടോ': 1, + 'ഠി': 1, + 'ണം': 1, + 'ണാ': 1, + 'ണി': 1, + 'ണെ': 1, + 'ണ്ട': 1, + 'ണ്ടാ': 1, + 'ണ്ടി': 1, + 'ണ്ടു': 1, + 'ണ്ടും': 1, + 'ണ്ടെ': 1, + 'ണ്ട്‌': 1, + 'ണ്ഡ': 1, + 'ണ്ണ': 1, + 'തം': 1, + 'താ': 1, + 'തി': 1, + 'തീ': 1, + 'തും': 1, + 'തെ': 1, + 'തേ': 1, + 'തൊ': 1, + 'തോ': 1, + 'ത്ത': 1, + 'ത്താ': 1, + 'ത്തി': 1, + 'ത്തു': 1, + 'ത്തും': 1, + 'ത്തെ': 1, + 'ത്തേ': 1, + 'ത്തോ': 1, + 'ത്ത്‌': 1, + 'ത്ന': 1, + 'ത്നം': 1, + 'ത്നി': 1, + 'ത്യ': 1, + 'ത്യാ': 1, + 'ത്യേ': 1, + 'ത്ര': 1, + 'ത്വ': 1, + 'ത്വം': 1, + 'ദം': 1, + 'ദാ': 1, + 'ദി': 1, + 'ദേ': 1, + 'ദ്ദ': 1, + 'ദ്ധ': 1, + 'ദ്ധി': 1, + 'ദ്യ': 1, + 'ദ്യാ': 1, + 'ദ്ര': 1, + 'ധാ': 1, + 'ധി': 1, + 'ധ്യ': 1, + 'നം': 1, + 'നാ': 1, + 'നി': 1, + 'നീ': 1, + 'നും': 1, + 'നെ': 1, + 'നേ': 1, + 'ന്ത': 1, + 'ന്തം': 1, + 'ന്തെ': 1, + 'ന്ത്ര': 1, + 'ന്ത്ര്യ': 1, + 'ന്ത്ര്യം': 1, + 'ന്ഥ': 1, + 'ന്ദ': 1, + 'ന്ധ': 1, + 'ന്ധി': 1, + 'ന്ന': 1, + 'ന്നാ': 1, + 'ന്നി': 1, + 'ന്നീ': 1, + 'ന്നു': 1, + 'ന്നും': 1, + 'ന്നെ': 1, + 'ന്നേ': 1, + 'ന്നോ': 1, + 'ന്ന്': 1, + 'ന്മ': 1, + 'ന്മാ': 1, + 'ന്മു': 1, + 'ന്മൂ': 1, + 'ന്യ': 1, + 'ന്യം': 1, + 'ന്യാ': 1, + 'ന്യേ': 1, + 'ന്യോ': 1, + 'ന്റെ': 1, + 'ന്‍കൂ': 1, + 'ന്‍പി': 1, + 'പാ': 1, + 'പി': 1, + 'പീ': 1, + 'പെ': 1, + 'പൈ': 1, + 'പൊ': 1, + 'പോ': 1, + 'പൌ': 1, + 'പ്പ': 1, + 'പ്പി': 1, + 'പ്പു': 1, + 'പ്പെ': 1, + 'പ്പോ': 1, + 'പ്പ്‌': 1, + 'പ്ര': 1, + 'പ്രാ': 1, + 'ബം': 1, + 'ബാ': 1, + 'ബോ': 1, + 'ബ്ലി': 1, + 'ഭാ': 1, + 'ഭി': 1, + 'ഭേ': 1, + 'ഭൌ': 1, + 'ഭ്യാ': 1, + 'ഭ്രാ': 1, + 'മം': 1, + 'മാ': 1, + 'മി': 1, + 'മെ': 1, + 'മേ': 1, + 'മൊ': 1, + 'മോ': 1, + 'മൌ': 1, + 'മ്പ': 1, + 'മ്പാ': 1, + 'മ്പ്ര': 1, + 'മ്മ': 1, + 'മ്മി': 1, + 'യം': 1, + 'യാ': 1, + 'യി': 1, + 'യും': 1, + 'യെ': 1, + 'യേ': 1, + 'യോ': 1, + 'യ്ക്കു': 1, + 'യ്തി': 1, + 'യ്തു': 1, + 'യ്മ': 1, + 'യ്യാ': 1, + 'യ്യി': 1, + 'രം': 1, + 'രാ': 1, + 'രി': 1, + 'രീ': 1, + 'രും': 1, + 'രെ': 1, + 'രേ': 1, + 'രോ': 1, + 'ര്യ': 1, + 'ര്യം': 1, + 'ര്‍ക്കു': 1, + 'ര്‍ക്കും': 1, + 'ര്‍ക്ക്‌': 1, + 'ര്‍ഗ്ഗം': 1, + 'ര്‍ച്ച': 1, + 'ര്‍ണ്ണ': 1, + 'ര്‍ത്ത': 1, + 'ര്‍ത്തി': 1, + 'ര്‍ത്തു': 1, + 'ര്‍ത്തേ': 1, + 'ര്‍ദേ': 1, + 'ര്‍ദ്ദ': 1, + 'ര്‍പ്പെ': 1, + 'ര്‍ബ': 1, + 'ര്‍ഭ': 1, + 'ര്‍മ്മി': 1, + 'ര്‍രാ': 1, + 'ര്‍വ്വ': 1, + 'ര്‍ഹ': 1, + 'ര്‍ഹി': 1, + 'റം': 1, + 'റി': 1, + 'റെ': 1, + 'റ്റ': 1, + 'റ്റം': 1, + 'റ്റു': 1, + 'റ്റേ': 1, + 'ലം': 1, + 'ലാ': 1, + 'ലി': 1, + 'ലും': 1, + 'ലെ': 1, + 'ലേ': 1, + 'ലോ': 1, + 'ല്യ': 1, + 'ല്യാ': 1, + 'ല്ല': 1, + 'ല്ലാ': 1, + 'ല്ലാം': 1, + 'ല്ലെ': 1, + 'ല്‍കി': 1, + 'ല്‍കു': 1, + 'ല്‍കേ': 1, + 'ല്‍ത': 1, + 'ല്‍നി': 1, + 'ല്‍പ്പ': 1, + 'ളം': 1, + 'ളാ': 1, + 'ളി': 1, + 'ളും': 1, + 'ളെ': 1, + 'ളേ': 1, + 'ളോ': 1, + 'ള്ള': 1, + 'ള്ളാ': 1, + 'ള്ളി': 1, + 'ള്ളൂ': 1, + 'ള്‍കൊ': 1, + 'ള്‍ക്ക': 1, + 'ള്‍ക്കി': 1, + 'ള്‍ക്കു': 1, + 'ള്‍ക്കും': 1, + 'ള്‍ക്കെ': 1, + 'ള്‍ക്ക്‌': 1, + 'ഴി': 1, + 'വാ': 1, + 'വി': 1, + 'വും': 1, + 'വെ': 1, + 'വേ': 1, + 'വൈ': 1, + 'വോ': 1, + 'വ്യ': 1, + 'വ്യം': 1, + 'വ്യാ': 1, + 'വ്വ': 1, + 'ശാ': 1, + 'ശി': 1, + 'ശീ': 1, + 'ശേ': 1, + 'ശ്യ': 1, + 'ശ്ര': 1, + 'ശ്വാ': 1, + 'ഷം': 1, + 'ഷി': 1, + 'ഷ്ട': 1, + 'ഷ്ട്യാ': 1, + 'ഷ്ട്ര': 1, + 'ഷ്ട്രീ': 1, + 'ഷ്ണു': 1, + 'ഷ്യ': 1, + 'ഷ്യാ': 1, + 'സം': 1, + 'സാ': 1, + 'സാം': 1, + 'സി': 1, + 'സൌ': 1, + 'സ്കാ': 1, + 'സ്തു': 1, + 'സ്ത്രം': 1, + 'സ്ത്രീ': 1, + 'സ്ഥ': 1, + 'സ്ഥാ': 1, + 'സ്ഥി': 1, + 'സ്യ': 1, + 'സ്റ്റ്‌': 1, + 'സ്വ': 1, + 'സ്വാ': 1, + 'സ്വീ': 1, + 'സ്സ': 1, + 'സ്സാ': 1, + 'സ്സി': 1, + 'സ്സും': 1, + 'സ്സോ': 1, + 'ഹം': 1, + 'ഹാ': 1, + 'ഹി': 1, + 'ഹിം': 1, + 'කා': 1, + 'කැ': 1, + 'කෘ': 1, + 'කෙ': 1, + 'කො': 1, + 'කෝ': 1, + 'කෲ': 1, + 'ගා': 1, + 'ගැ': 1, + 'ගෙ': 1, + 'ගේ': 1, + 'චා': 1, + 'චැ': 1, + 'චෝ': 1, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ටේ': 1, + 'ඨා': 1, + 'ඩං': 1, + 'ඩා': 1, + 'තා': 1, + 'තැ': 1, + 'තෘ': 1, + 'තෙ': 1, + 'තේ': 1, + 'තො': 1, + 'තෝ': 1, + 'ථා': 1, + 'දා': 1, + 'දැ': 1, + 'දිං': 1, + 'දෙ': 1, + 'දේ': 1, + 'දෝ': 1, + 'ධා': 1, + 'නා': 1, + 'නැ': 1, + 'නෑ': 1, + 'නෙ': 1, + 'නේ': 1, + 'නො': 1, + 'ඳා': 1, + 'පා': 1, + 'පැ': 1, + 'පෑ': 1, + 'පෙ': 1, + 'පේ': 1, + 'පො': 1, + 'පෞ': 1, + 'බා': 1, + 'බැ': 1, + 'බෙ': 1, + 'බේ': 1, + 'බෝ': 1, + 'භා': 1, + 'භෞ': 1, + 'මා': 1, + 'මැ': 1, + 'මෙ': 1, + 'මේ': 1, + 'යා': 1, + 'යෙ': 1, + 'යේ': 1, + 'යො': 1, + 'යෝ': 1, + 'රා': 1, + 'රැ': 1, + 'රෙ': 1, + 'රේ': 1, + 'රෝ': 1, + 'ලං': 1, + 'ලා': 1, + 'ලැ': 1, + 'ලෙ': 1, + 'ලේ': 1, + 'ලෝ': 1, + 'වං': 1, + 'වා': 1, + 'වැ': 1, + 'වෘ': 1, + 'වෙ': 1, + 'වේ': 1, + 'වෝ': 1, + 'ශේ': 1, + 'ෂා': 1, + 'සං': 1, + 'සා': 1, + 'සැ': 1, + 'සෑ': 1, + 'සෙ': 1, + 'සේ': 1, + 'සෞ': 1, + 'හා': 1, + 'හැ': 1, + 'හිං': 1, + 'හෘ': 1, + 'හෙ': 1, + 'හො': 1, + 'හෝ': 1, + 'ළැ': 1, + 'กำ': 1, + 'คำ': 1, + 'จำ': 1, + 'ดำ': 1, + 'ต่ำ': 1, + 'ทำ': 1, + 'ธำ': 1, + 'นำ': 1, + 'บำ': 1, + 'พำ': 1, + 'ย่ำ': 1, + 'รำ': 1, + 'ลำ': 1, + 'สำ': 1, + 'อำ': 1, + 'ກຳ': 1, + 'ຄຳ': 1, + 'ຈຳ': 1, + 'ດຳ': 1, + 'ຕຳ': 1, + 'ທຳ': 1, + 'ນຳ': 1, + 'ລຳ': 1, + 'ສຳ': 1, + 'ອຳ': 1, + 'ကေ': 1, + 'ကေု': 1, + 'က္က': 1, + 'က္ခ': 1, + 'က္ဍ': 1, + 'က္ဍို': 1, + 'က်ျ': 1, + 'ကျ': 1, + 'ကျို': 1, + 'ကျူ': 1, + 'ကျေ': 1, + 'ကျွ': 1, + 'ကြ': 1, + 'ကြို': 1, + 'ကြီ': 1, + 'ကြု': 1, + 'ကြေ': 1, + 'ကြံ': 1, + 'ကြွေ': 1, + 'ကၠေ': 1, + 'ကၠေံ': 1, + 'ခေ': 1, + 'ချ': 1, + 'ချိ': 1, + 'ချို': 1, + 'ချို့': 1, + 'ချီ': 1, + 'ချု': 1, + 'ချေ': 1, + 'ချဳ': 1, + 'ခြ': 1, + 'ခြုံ': 1, + 'ခြေ': 1, + 'ဂ္ဂ': 1, + 'ဂ္ဂို': 1, + 'ဂျို': 1, + 'ဂၠေ': 1, + 'င်္ဂ': 1, + 'ငြ': 1, + 'ငြိ': 1, + 'ငွေ': 1, + 'ငႄ': 1, + 'စေ': 1, + 'စ္စ': 1, + 'စှ်ေ': 1, + 'စၞေ': 1, + 'စၠေ': 1, + 'ဆေ': 1, + 'ဇြ': 1, + 'ဇြေ': 1, + 'ဇြဳ': 1, + 'ဇၞေ': 1, + 'ညေ': 1, + 'ဋ္ဌ': 1, + 'တေ': 1, + 'တေံ': 1, + 'တ္တ': 1, + 'တ္ၚဲ': 1, + 'တြ': 1, + 'တြုံ': 1, + 'တွေ': 1, + 'တှ်ေ': 1, + 'တၟေ': 1, + 'ထေ': 1, + 'ထ္ၜ': 1, + 'ထွေ': 1, + 'ဒေ': 1, + 'ဒေံ': 1, + 'ဒ္ဂေ': 1, + 'ဒြ': 1, + 'နေ': 1, + 'နေ့': 1, + 'န္ဒ': 1, + 'န္နိ': 1, + 'နှေ': 1, + 'ပေ': 1, + 'ပေဲ': 1, + 'ပ္ဍဲ': 1, + 'ပ္တံ': 1, + 'ပ္ပ': 1, + 'ပ္ပံ': 1, + 'ပျ': 1, + 'ပျေ': 1, + 'ပျဲ': 1, + 'ပြ': 1, + 'ပြီ': 1, + 'ပြု': 1, + 'ပြေ': 1, + 'ပြဳ': 1, + 'ပြံ': 1, + 'ဖေ': 1, + 'ဖျ': 1, + 'ဖျု': 1, + 'ဖျေံ': 1, + 'ဖြ': 1, + 'ဖြို': 1, + 'ဗ္တေ': 1, + 'ဗ္ၜေ': 1, + 'ဗျဵု': 1, + 'ဗြေ': 1, + 'ဗြဴ': 1, + 'ဗၠေ': 1, + 'ဘေ': 1, + 'မေ': 1, + 'မ္ဂ': 1, + 'မ္တီ': 1, + 'မ္ဘ': 1, + 'မ္မ': 1, + 'မ္ၚဵု': 1, + 'မျ': 1, + 'မျို': 1, + 'မျီု': 1, + 'မျှ': 1, + 'မြ': 1, + 'မြေ': 1, + 'မြှ': 1, + 'မွေ': 1, + 'မွေ့': 1, + 'မှေ': 1, + 'မၠေ': 1, + 'မႄ': 1, + 'ယေ': 1, + 'ယျ': 1, + 'ယျဵု': 1, + 'ယြဴ': 1, + 'ရေ': 1, + 'ရေံ': 1, + 'ရွေ': 1, + 'ရွှေ့': 1, + 'ရှေ': 1, + 'ရှေ့': 1, + 'ရှ်ေ': 1, + 'လေ': 1, + 'လ္ပ': 1, + 'လ္ၚ': 1, + 'လျ': 1, + 'လျို': 1, + 'လျေ': 1, + 'လျှ': 1, + 'လျှို့': 1, + 'လျှေ': 1, + 'လၟေ': 1, + 'လႄ': 1, + 'ဝေ': 1, + 'သေ': 1, + 'သ္က': 1, + 'သ္ကံ': 1, + 'သ္ဂု': 1, + 'သ္ဂေ': 1, + 'သ္ဇို': 1, + 'သ္ဒ': 1, + 'သ္ပ': 1, + 'သ္ၚိ': 1, + 'သြ': 1, + 'သြို': 1, + 'သှ်ေ': 1, + 'သၞေ': 1, + 'ဟေ': 1, + 'အေ': 1, + 'ၜေ': 1, + 'ၵေ': 1, + 'ၵႄ': 1, + 'ၶေ': 1, + 'ၼေ': 1, + 'ၼႄ': 1, + 'ၽႄ': 1, + 'ႁေ': 1, + 'ႁႄ': 1, + 'កា': 1, + 'កើ': 1, + 'កេ': 1, + 'កោ': 1, + 'ក្ខ': 1, + 'ក្ដិ': 1, + 'ក្ដី': 1, + 'ក្ដៅ': 1, + 'ក្នុ': 1, + 'ក្រ': 1, + 'ក្រុ': 1, + 'ក្រោ': 1, + 'ក្ស': 1, + 'ក្សា': 1, + 'ខា': 1, + 'ខែ': 1, + 'ខ្ជា': 1, + 'ខ្ព': 1, + 'ខ្លា': 1, + 'ខ្លួ': 1, + 'គា': 1, + 'គាំ': 1, + 'គេ': 1, + 'គោ': 1, + 'គ្គ': 1, + 'គ្នា': 1, + 'គ្មា': 1, + 'គ្រ': 1, + 'គ្រា': 1, + 'គ្រឹះ': 1, + 'គ្រួ': 1, + 'ឃា': 1, + 'ឃើ': 1, + 'ឃោ': 1, + 'ឃៅ': 1, + 'ឃ្ន': 1, + 'ងា': 1, + 'ង្ក': 1, + 'ង្កើ': 1, + 'ង្ខិ': 1, + 'ង្ខំ': 1, + 'ង្គ': 1, + 'ង្រី': 1, + 'ង្រឹ': 1, + 'ង្រៀ': 1, + 'ង្ហា': 1, + 'ង្អ': 1, + 'ចា': 1, + 'ចាំ': 1, + 'ចេ': 1, + 'ចែ': 1, + 'ចោ': 1, + 'ច្ច': 1, + 'ច្ចេ': 1, + 'ច្បា': 1, + 'ឆាំ': 1, + 'ឆ្នាំ': 1, + 'ឆ្នោ': 1, + 'ឆ្ល': 1, + 'ឆ្លើ': 1, + 'ជា': 1, + 'ជៀ': 1, + 'ជ្ជ': 1, + 'ជ្ជា': 1, + 'ជ្ញា': 1, + 'ជ្យ': 1, + 'ជ្រ': 1, + 'ជ្រើ': 1, + 'ជ្រៀ': 1, + 'ជ្រែ': 1, + 'ឈា': 1, + 'ឈ្មោះ': 1, + 'ញុះ': 1, + 'ញ្ចូ': 1, + 'ញ្ជា': 1, + 'ញ្ញ': 1, + 'ញ្ញា': 1, + 'ញ្ញៈ': 1, + 'ដា': 1, + 'ដើ': 1, + 'ដែ': 1, + 'ដៃ': 1, + 'ដោ': 1, + 'ដៅ': 1, + 'ដ្ឋ': 1, + 'ដ្ឋា': 1, + 'ឋា': 1, + 'ឌ្ឍ': 1, + 'ណា': 1, + 'ណើ': 1, + 'ណែ': 1, + 'ណោ': 1, + 'ណោះ': 1, + 'ណៈ': 1, + 'ណ្ដា': 1, + 'ណ្ដុះ': 1, + 'ណ្ឌ': 1, + 'ណ្ណោះ': 1, + 'តា': 1, + 'តេ': 1, + 'តែ': 1, + 'ត្ដ': 1, + 'ត្ដិ': 1, + 'ត្ថ': 1, + 'ត្ថិ': 1, + 'ត្យ': 1, + 'ត្រ': 1, + 'ត្រា': 1, + 'ត្រឹ': 1, + 'ត្រូ': 1, + 'ត្ស': 1, + 'ថា': 1, + 'ថែ': 1, + 'ថ្ងៃ': 1, + 'ថ្នា': 1, + 'ថ្នូ': 1, + 'ថ្មី': 1, + 'ថ្លៃ': 1, + 'ទា': 1, + 'ទាំ': 1, + 'ទៀ': 1, + 'ទេ': 1, + 'ទៃ': 1, + 'ទោ': 1, + 'ទោះ': 1, + 'ទៅ': 1, + 'ទ្ធ': 1, + 'ទ្ធិ': 1, + 'ទ្យា': 1, + 'ទ្រ': 1, + 'ធា': 1, + 'ធ្ង': 1, + 'ធ្នា': 1, + 'ធ្នូ': 1, + 'ធ្យា': 1, + 'ធ្យោ': 1, + 'ធ្វើ': 1, + 'នា': 1, + 'នាំ': 1, + 'នឿ': 1, + 'នេះ': 1, + 'នៃ': 1, + 'នោះ': 1, + 'នៅ': 1, + 'នៈ': 1, + 'ន្ដ': 1, + 'ន្ដិ': 1, + 'ន្ថែ': 1, + 'ន្ថោ': 1, + 'ន្ទា': 1, + 'ន្ទៈ': 1, + 'ន្ធ': 1, + 'ន្និ': 1, + 'ន្ម': 1, + 'ន្យា': 1, + 'ន្លែ': 1, + 'បា': 1, + 'បើ': 1, + 'បៀ': 1, + 'បែ': 1, + 'បោ': 1, + 'បោះ': 1, + 'បះ': 1, + 'ប៉ះ': 1, + 'ប្ដី': 1, + 'ប្ដូ': 1, + 'ប្ដេ': 1, + 'ប្ប': 1, + 'ប្បី': 1, + 'ប្រ': 1, + 'ប្រា': 1, + 'ប្រឹ': 1, + 'ប្រុ': 1, + 'ប្រើ': 1, + 'ប្រែ': 1, + 'ប្អូ': 1, + 'ផ្ដ': 1, + 'ផ្ដា': 1, + 'ផ្ទា': 1, + 'ផ្ទុ': 1, + 'ផ្ទេ': 1, + 'ផ្នែ': 1, + 'ផ្លា': 1, + 'ផ្លិ': 1, + 'ផ្ស': 1, + 'ផ្សា': 1, + 'ផ្សេ': 1, + 'ផ្សៃ': 1, + 'ពា': 1, + 'ពើ': 1, + 'ពេ': 1, + 'ពោះ': 1, + 'ព្យ': 1, + 'ព្យា': 1, + 'ព្រ': 1, + 'ព្រឹ': 1, + 'ព្រៀ': 1, + 'ព្រៃ': 1, + 'ព្រំ': 1, + 'ព្វ': 1, + 'ភា': 1, + 'ភេ': 1, + 'មា': 1, + 'មើ': 1, + 'មេ': 1, + 'មែ': 1, + 'ម៉ា': 1, + 'ម៉ោ': 1, + 'ម្ងា': 1, + 'ម្ចា': 1, + 'ម្ដា': 1, + 'ម្ដែ': 1, + 'ម្នា': 1, + 'ម្នេ': 1, + 'ម្ប': 1, + 'ម្បី': 1, + 'ម្បុ': 1, + 'ម្ភា': 1, + 'ម្ម': 1, + 'ម្រ': 1, + 'ម្រា': 1, + 'ម្រិ': 1, + 'ម្រូ': 1, + 'ម្រើ': 1, + 'ម្រេ': 1, + 'ម្លៀ': 1, + 'ម្លៃ': 1, + 'យា': 1, + 'យោ': 1, + 'យៈ': 1, + 'យ៉ា': 1, + 'យ្យ': 1, + 'រា': 1, + 'រាំ': 1, + 'រើ': 1, + 'រឿ': 1, + 'រៀ': 1, + 'រះ': 1, + 'រៈ': 1, + 'រ្ដិ៍': 1, + 'លា': 1, + 'លុះ': 1, + 'លើ': 1, + 'លេ': 1, + 'លោ': 1, + 'លៈ': 1, + 'ល្បៈ': 1, + 'ល្មើ': 1, + 'វា': 1, + 'វេ': 1, + 'វៈ': 1, + 'សា': 1, + 'សើ': 1, + 'សេ': 1, + 'សៈ': 1, + 'ស្គា': 1, + 'ស្ដី': 1, + 'ស្ដ្រី': 1, + 'ស្ត្រ': 1, + 'ស្ថា': 1, + 'ស្ថិ': 1, + 'ស្ទួ': 1, + 'ស្នា': 1, + 'ស្មា': 1, + 'ស្មើ': 1, + 'ស្រ': 1, + 'ស្រា': 1, + 'ស្រី': 1, + 'ស្រ័': 1, + 'ស្លា': 1, + 'ស្វែ': 1, + 'ស្វ័': 1, + 'ស្ស': 1, + 'ហា': 1, + 'ហើ': 1, + 'ហេ': 1, + 'ហែ': 1, + 'ហោ': 1, + 'ហ្ម': 1, + 'ឡា': 1, + 'ឡើ': 1, + 'អា': 1, + 'អើ': 1, + 'អៀ': 1, + 'អ្ន': 1, + 'អ្វី': 1, + 'ឱ្យ': 1, + 'ᨠ᩠ᨠ': 1, + 'ᨠ᩠ᨦᩢᩢ': 1, + 'ᨠ᩠ᨦᩨ᩵': 1, + 'ᨠ᩠ᨭᩫ': 1, + 'ᨠ᩠ᨯᩢ': 1, + 'ᨠ᩠ᨶᩢ': 1, + 'ᨠ᩠ᨶᩥ': 1, + 'ᨠ᩠ᩅ': 1, + 'ᨠ᩠ᩅᩢ': 1, + 'ᨠ᩠ᩅᩫ': 1, + 'ᨠ᩠ᩋ᩵': 1, + 'ᨠᩢ᩠ᨷ': 1, + 'ᨠᩮ᩠': 1, + 'ᨠᩮᩢ᩵': 1, + 'ᨠᩮ᩵': 1, + 'ᨠᩯ᩠': 1, + 'ᨠᩯ᩵': 1, + 'ᨡ᩠ᨦᩢ': 1, + 'ᨡ᩠ᨯᩢ': 1, + 'ᨡ᩠ᨶ': 1, + 'ᨡ᩠ᨶᩢ': 1, + 'ᨡ᩠ᨶᩨ᩶': 1, + 'ᨡ᩠ᩅ': 1, + 'ᨡ᩠ᩋ': 1, + 'ᨡᩮ᩠': 1, + 'ᨡᩮᩢ': 1, + 'ᨡᩮᩢᩢ': 1, + 'ᨡᩯ᩠': 1, + 'ᨡᩱᩢ': 1, + 'ᨣᩕ': 1, + 'ᨣᩕ᩠': 1, + 'ᨣᩕᩮ᩠': 1, + 'ᨣ᩠ᨣ': 1, + 'ᨣ᩠ᨯᩧ': 1, + 'ᨣ᩠ᨶᩢ': 1, + 'ᨣ᩠ᨾᩫ': 1, + 'ᨣ᩠ᩅ': 1, + 'ᨣ᩠ᩋᩢ': 1, + 'ᨣᩢ᩠ᨷ': 1, + 'ᨣᩭ': 1, + 'ᨣᩮᩢᩢ': 1, + 'ᨣᩯ᩠': 1, + 'ᨤ᩠ᨯᩥ': 1, + 'ᨤ᩠ᨶᩨ': 1, + 'ᨤ᩠ᨶᩫ': 1, + 'ᨤ᩠ᩅᩫ': 1, + 'ᨤ᩠ᩋ': 1, + 'ᨤᩢ᩠ᨬ': 1, + 'ᨤᩮᩢ': 1, + 'ᨤᩱ᩵': 1, + 'ᨧ᩠ᨦᩥ᩵': 1, + 'ᨧ᩠ᨯᩢ': 1, + 'ᨧᩮ': 1, + 'ᨧᩮ᩠': 1, + 'ᨧᩮᩢᩢ': 1, + 'ᨧᩱ': 1, + 'ᨧᩱ᩠': 1, + 'ᨨ᩠ᨶᩢ': 1, + 'ᨩ᩠ᨶᩢᩢ': 1, + 'ᨩ᩠ᨶ᩼': 1, + 'ᨩᩭ᩵': 1, + 'ᨩᩮ᩠': 1, + 'ᨩᩱᩢ': 1, + 'ᨩ᩵ᩭ': 1, + 'ᨪ᩠ᨦᩨ᩵': 1, + 'ᨪᩮᩢ': 1, + 'ᨬ᩠ᨬ': 1, + 'ᨯ᩠ᨦᩢ᩵': 1, + 'ᨯ᩠ᨰ': 1, + 'ᨯ᩠ᨶᩥ': 1, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 1, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 1, + 'ᨯᩪᩯ': 1, + 'ᨯᩯ᩠': 1, + 'ᨯᩰ᩠': 1, + 'ᨯᩱ': 1, + 'ᨯᩱᩢ': 1, + 'ᨯᩴ᩠ᨿ': 1, + 'ᨱ᩠ᨱ': 1, + 'ᨲᩕ': 1, + 'ᨲᩕᩦ': 1, + 'ᨲᩕ᩼': 1, + 'ᨲ᩠ᨠᩫ': 1, + 'ᨲ᩠ᨦᩢᩢ': 1, + 'ᨲ᩠ᨯᩢ': 1, + 'ᨲ᩠ᨲᩥ': 1, + 'ᨲ᩠ᨳᩩ': 1, + 'ᨲ᩠ᨶᩫ': 1, + 'ᨲ᩠ᨶᩫᩢ': 1, + 'ᨲ᩠ᩅᩫ': 1, + 'ᨲ᩠ᩋᩢ': 1, + 'ᨲᩮ᩠': 1, + 'ᨲᩯ᩠': 1, + 'ᨲᩯ᩵': 1, + 'ᨲᩱ': 1, + 'ᨲᩱ᩠': 1, + 'ᨲᩱᩢ': 1, + 'ᨳ᩠ᨦᩨ': 1, + 'ᨳ᩠ᩅᩢ': 1, + 'ᨳᩫ᩠ᨷ': 1, + 'ᨳᩮ᩠': 1, + 'ᨳᩮᩢᩢ': 1, + 'ᨳᩯ᩠': 1, + 'ᨴ᩠ᨦᩢ': 1, + 'ᨴ᩠ᨦᩨ': 1, + 'ᨴ᩠ᨵᩥ': 1, + 'ᨴ᩠ᨿ': 1, + 'ᨴ᩠ᨿ᩵': 1, + 'ᨴ᩠ᩅᩢ': 1, + 'ᨴ᩠ᩅᩫ᩵': 1, + 'ᨴᩮ᩠': 1, + 'ᨴᩮᩢᩢ': 1, + 'ᨴᩮᩢ᩵': 1, + 'ᨴᩯ᩠': 1, + 'ᨴᩯᩢ': 1, + 'ᨴᩰ᩠': 1, + 'ᨶ᩠ᨲᩥ': 1, + 'ᨶ᩠ᨶᩢᩢ': 1, + 'ᨶ᩠ᨶᩥ': 1, + 'ᨶ᩠ᩋ': 1, + 'ᨶ᩠ᩋᩢ': 1, + 'ᨶᩢ᩠ᨷ': 1, + 'ᨶᩮ': 1, + 'ᨶᩯ᩠': 1, + 'ᨶᩱ': 1, + 'ᨷᩕ': 1, + 'ᨷ᩠ᨦᩢ': 1, + 'ᨷ᩠ᨯᩢ': 1, + 'ᨷ᩠ᨲᩢ': 1, + 'ᨷ᩠ᨲᩥ': 1, + 'ᨷ᩠ᨴᩫ': 1, + 'ᨷ᩠ᨿ': 1, + 'ᨷ᩠ᩅᩫ': 1, + 'ᨷ᩠ᩋ': 1, + 'ᨷᩮ᩠': 1, + 'ᨸ᩠ᨠᩫ': 1, + 'ᨸ᩠ᨯᩨ': 1, + 'ᨸ᩠ᨶᩢ': 1, + 'ᨸ᩠ᩋ': 1, + 'ᨸᩮ᩠': 1, + 'ᨸᩮᩢ᩵': 1, + 'ᨸ᩠᩵ᨿ': 1, + 'ᨹ᩠ᨯᩥ': 1, + 'ᨹ᩠ᨿ᩵': 1, + 'ᨹ᩠ᩅᩥ': 1, + 'ᨹᩪᩢᩱ': 1, + 'ᨹᩫ᩠ᩃ': 1, + 'ᨹᩮᩨ᩠': 1, + 'ᨻᩕ᩠': 1, + 'ᨻ᩠ᨦ': 1, + 'ᨻ᩠ᨶᩨ᩶': 1, + 'ᨻ᩠ᨶᩫ᩶': 1, + 'ᨻ᩠ᨿ': 1, + 'ᨻ᩠ᩋ': 1, + 'ᨻᩮ᩠': 1, + 'ᨻᩱ': 1, + 'ᨽᩱ᩠': 1, + 'ᨾ᩠ᨲᩥ': 1, + 'ᨾ᩠ᨾ': 1, + 'ᨾ᩠ᨾ᩼': 1, + 'ᨾ᩠ᩅ': 1, + 'ᨾ᩠ᩅ᩵': 1, + 'ᨾᩮ᩠': 1, + 'ᨾᩯ᩵': 1, + 'ᨾᩱ': 1, + 'ᨿ᩠ᨦᩢ': 1, + 'ᨿ᩠ᨦᩢᩢ': 1, + 'ᨿ᩠ᨦᩥ': 1, + 'ᨿ᩠ᨦᩥ᩵': 1, + 'ᨿ᩠ᩋ': 1, + 'ᨿᩢ᩠ᨷ': 1, + 'ᨿᩫ᩠ᩈ': 1, + 'ᨿᩮ᩠': 1, + 'ᨿᩰ': 1, + 'ᩁ᩠ᨠᩢ': 1, + 'ᩁ᩠ᨮᩢ': 1, + 'ᩁ᩠ᨾᩫ': 1, + 'ᩁ᩠ᨿ': 1, + 'ᩁ᩠ᩅ': 1, + 'ᩁ᩠ᩅ᩵': 1, + 'ᩁ᩠ᩋ': 1, + 'ᩁᩢ᩠ᨷ': 1, + 'ᩁᩢᩭ': 1, + 'ᩁᩫ᩠ᨷ': 1, + 'ᩁᩮ᩠': 1, + 'ᩁᩮᩨ᩠': 1, + 'ᩁᩯ᩠': 1, + 'ᩃ᩠ᨦᩫ': 1, + 'ᩃ᩠ᩅ᩵': 1, + 'ᩃᩢ᩠ᨿ': 1, + 'ᩃᩨ᩠ᨷ': 1, + 'ᩃᩮ᩠': 1, + 'ᩃᩯ': 1, + 'ᩃᩰ᩠': 1, + 'ᩅ᩠ᨲᩥ': 1, + 'ᩅ᩠ᨶᩢ': 1, + 'ᩅ᩠ᨿ': 1, + 'ᩅ᩠ᩅ᩵': 1, + 'ᩅᩮ': 1, + 'ᩅᩮ᩠': 1, + 'ᩅᩱᩢ': 1, + 'ᩈ᩠ᨠᩢ': 1, + 'ᩈ᩠ᨠᩧ': 1, + 'ᩈ᩠ᨦᩢ': 1, + 'ᩈ᩠ᨦᩥ᩵': 1, + 'ᩈ᩠ᨦᩫ': 1, + 'ᩈ᩠ᨶᩫ': 1, + 'ᩈ᩠ᨾᩫ': 1, + 'ᩈ᩠ᨿ': 1, + 'ᩈ᩠ᩅ᩵': 1, + 'ᩈ᩠ᩋ': 1, + 'ᩈᩢ᩠ᨿ': 1, + 'ᩈᩫ᩠ᨾ': 1, + 'ᩈᩮ': 1, + 'ᩈᩮ᩠': 1, + 'ᩈᩯ᩠': 1, + 'ᩈᩱ': 1, + 'ᩈᩱ᩠': 1, + 'ᩉᩕᩨ': 1, + 'ᩉ᩠ᨶᩢ': 1, + 'ᩉ᩠ᨶᩦ': 1, + 'ᩉ᩠ᨶᩧ': 1, + 'ᩉ᩠ᨶᩧ᩵': 1, + 'ᩉ᩠ᨶᩫ': 1, + 'ᩉ᩠ᨾ': 1, + 'ᩉ᩠ᨾᩢ': 1, + 'ᩉ᩠ᨾᩢᩢ': 1, + 'ᩉ᩠ᨾᩪ᩵': 1, + 'ᩉ᩠ᨾᩳ': 1, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 1, + 'ᩉ᩠ᨿᩩ': 1, + 'ᩉ᩠ᩅᩫ': 1, + 'ᩉ᩠ᩅ᩵': 1, + 'ᩉᩮᩖᩨ': 1, + 'ᩉᩮ᩠': 1, + 'ᩉᩯ᩠': 1, + 'ᩉᩯ᩵': 1, + 'ᩉᩱ᩠': 1, + 'ᩋ᩠ᨶᩢ': 1, + 'ᩋ᩠ᨶᩨ᩵': 1, + 'ᩋ᩠ᩋ': 1, + 'ᩓ᩠ᩅᩢ': 1, + 'ꦏꦃ': 1, + 'ꦏꦺ': 1, + 'ꦏꦺꦴ': 1, + 'ꦏꦿ': 1, + 'ꦏꦿꦁ': 1, + 'ꦏꦿꦂ': 1, + 'ꦏ꧀ꦏ': 1, + 'ꦏ꧀ꦏꦁ': 1, + 'ꦏ꧀ꦏꦺ': 1, + 'ꦏ꧀ꦏꦼ': 1, + 'ꦏ꧀ꦔ': 1, + 'ꦏ꧀ꦔꦸ': 1, + 'ꦏ꧀ꦔꦿ': 1, + 'ꦏ꧀ꦕ': 1, + 'ꦏ꧀ꦠ': 1, + 'ꦏ꧀ꦠꦶ': 1, + 'ꦏ꧀ꦠꦸ': 1, + 'ꦏ꧀ꦠꦺ': 1, + 'ꦏ꧀ꦢ': 1, + 'ꦏ꧀ꦢꦂ': 1, + 'ꦏ꧀ꦢꦶ': 1, + 'ꦏ꧀ꦤ': 1, + 'ꦏ꧀ꦤꦶꦁ': 1, + 'ꦏ꧀ꦥ': 1, + 'ꦏ꧀ꦥꦶ': 1, + 'ꦏ꧀ꦥꦿꦶ': 1, + 'ꦏ꧀ꦧ': 1, + 'ꦏ꧀ꦧꦶ': 1, + 'ꦏ꧀ꦧꦺ': 1, + 'ꦏ꧀ꦩ': 1, + 'ꦏ꧀ꦩꦸ': 1, + 'ꦏ꧀ꦪ': 1, + 'ꦏ꧀ꦭ': 1, + 'ꦏ꧀ꦮ': 1, + 'ꦏ꧀ꦱ': 1, + 'ꦏ꧀ꦱꦂ': 1, + 'ꦏ꧀ꦱꦶ': 1, + 'ꦏ꧀ꦱꦸ': 1, + 'ꦏ꧀ꦱꦺ': 1, + 'ꦏ꧀ꦲ': 1, + 'ꦏ꧀ꦲꦁ': 1, + 'ꦏ꧀ꦲꦶꦁ': 1, + 'ꦏ꧀ꦲꦸ': 1, + 'ꦏ꧀ꦲꦺ': 1, + 'ꦏ꧀ꦲꦺꦴ': 1, + 'ꦒꦸꦃ': 1, + 'ꦒꦺꦴ': 1, + 'ꦒꦿ': 1, + 'ꦒꦿꦺ': 1, + 'ꦒ꧀ꦒ': 1, + 'ꦔꦃ': 1, + 'ꦔꦺ': 1, + 'ꦔꦺꦴ': 1, + 'ꦔꦿ': 1, + 'ꦔ꧀ꦒꦼ': 1, + 'ꦔ꧀ꦒꦿꦼꦁ': 1, + 'ꦔ꧀ꦭ': 1, + 'ꦔ꧀ꦭꦁ': 1, + 'ꦔ꧀ꦭꦸ': 1, + 'ꦔ꧀ꦭꦼ': 1, + 'ꦕꦃ': 1, + 'ꦕꦺꦴ': 1, + 'ꦗꦃ': 1, + 'ꦗꦺ': 1, + 'ꦗꦿꦺꦴ': 1, + 'ꦚ꧀ꦕ': 1, + 'ꦚ꧀ꦕꦶ': 1, + 'ꦚ꧀ꦗ': 1, + 'ꦚ꧀ꦗꦶ': 1, + 'ꦚ꧀ꦗꦸꦁ': 1, + 'ꦚ꧀ꦗꦸꦂ': 1, + 'ꦝꦃ': 1, + 'ꦝꦺ': 1, + 'ꦝꦺꦴ': 1, + 'ꦝꦺꦴꦂ': 1, + 'ꦠꦸꦃ': 1, + 'ꦠꦺ': 1, + 'ꦠꦺꦃ': 1, + 'ꦠꦺꦴ': 1, + 'ꦠꦾ': 1, + 'ꦠꦿ': 1, + 'ꦠꦿꦶ': 1, + 'ꦠ꧀ꦏꦁ': 1, + 'ꦠ꧀ꦏꦸ': 1, + 'ꦠ꧀ꦒ': 1, + 'ꦠ꧀ꦔꦸ': 1, + 'ꦠ꧀ꦚ': 1, + 'ꦠ꧀ꦠ': 1, + 'ꦠ꧀ꦠꦶ': 1, + 'ꦠ꧀ꦠꦶꦁ': 1, + 'ꦠ꧀ꦠꦸ': 1, + 'ꦠ꧀ꦠꦺ': 1, + 'ꦠ꧀ꦤ': 1, + 'ꦠ꧀ꦤꦶ': 1, + 'ꦠ꧀ꦩ': 1, + 'ꦠ꧀ꦩꦼ': 1, + 'ꦠ꧀ꦭ': 1, + 'ꦠ꧀ꦭꦺ': 1, + 'ꦠ꧀ꦱ': 1, + 'ꦠ꧀ꦱꦶꦁ': 1, + 'ꦠ꧀ꦱꦶꦃ': 1, + 'ꦠ꧀ꦱꦾ': 1, + 'ꦠ꧀ꦱ꧀ꦮ': 1, + 'ꦠ꧀ꦲ': 1, + 'ꦠ꧀ꦲꦶꦁ': 1, + 'ꦠ꧀ꦲꦺꦴ': 1, + 'ꦢꦃ': 1, + 'ꦢꦺ': 1, + 'ꦢꦺꦴ': 1, + 'ꦢꦾ': 1, + 'ꦢꦿ': 1, + 'ꦢ꧀ꦏ': 1, + 'ꦢ꧀ꦠ': 1, + 'ꦢ꧀ꦠꦶꦁ': 1, + 'ꦢ꧀ꦢ': 1, + 'ꦢ꧀ꦥ': 1, + 'ꦢ꧀ꦩꦺꦴ': 1, + 'ꦢ꧀ꦲ': 1, + 'ꦤꦺ': 1, + 'ꦤꦺꦃ': 1, + 'ꦤꦺꦴ': 1, + 'ꦤ꧀ꦏ': 1, + 'ꦤ꧀ꦏꦁ': 1, + 'ꦤ꧀ꦏꦸ': 1, + 'ꦤ꧀ꦏꦼ': 1, + 'ꦤ꧀ꦒ': 1, + 'ꦤ꧀ꦔ': 1, + 'ꦤ꧀ꦔꦁ': 1, + 'ꦤ꧀ꦔꦶ': 1, + 'ꦤ꧀ꦔꦸ': 1, + 'ꦤ꧀ꦔꦸꦂ': 1, + 'ꦤ꧀ꦔꦺ': 1, + 'ꦤ꧀ꦔꦼ': 1, + 'ꦤ꧀ꦔꦿꦸ': 1, + 'ꦤ꧀ꦗ': 1, + 'ꦤ꧀ꦗꦸ': 1, + 'ꦤ꧀ꦗꦼ': 1, + 'ꦤ꧀ꦚꦶ': 1, + 'ꦤ꧀ꦛꦃ': 1, + 'ꦤ꧀ꦛꦶ': 1, + 'ꦤ꧀ꦝ': 1, + 'ꦤ꧀ꦝꦁ': 1, + 'ꦤ꧀ꦝꦶ': 1, + 'ꦤ꧀ꦝꦸ': 1, + 'ꦤ꧀ꦝꦸꦁ': 1, + 'ꦤ꧀ꦝꦺꦁ': 1, + 'ꦤ꧀ꦝꦺꦴꦁ': 1, + 'ꦤ꧀ꦝꦼ': 1, + 'ꦤ꧀ꦠ': 1, + 'ꦤ꧀ꦠꦂ': 1, + 'ꦤ꧀ꦠꦃ': 1, + 'ꦤ꧀ꦠꦶ': 1, + 'ꦤ꧀ꦠꦸ': 1, + 'ꦤ꧀ꦠꦸꦁ': 1, + 'ꦤ꧀ꦠꦺ': 1, + 'ꦤ꧀ꦠꦼ': 1, + 'ꦤ꧀ꦠꦼꦂ': 1, + 'ꦤ꧀ꦠꦽ': 1, + 'ꦤ꧀ꦢ': 1, + 'ꦤ꧀ꦢꦁ': 1, + 'ꦤ꧀ꦢꦂ': 1, + 'ꦤ꧀ꦢꦶ': 1, + 'ꦤ꧀ꦢꦸ': 1, + 'ꦤ꧀ꦢꦺ': 1, + 'ꦤ꧀ꦢꦼ': 1, + 'ꦤ꧀ꦤ': 1, + 'ꦤ꧀ꦤꦶ': 1, + 'ꦤ꧀ꦤꦶꦁ': 1, + 'ꦤ꧀ꦤꦺ': 1, + 'ꦤ꧀ꦤꦼ': 1, + 'ꦤ꧀ꦥ': 1, + 'ꦤ꧀ꦥꦁ': 1, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 1, + 'ꦤ꧀ꦥꦶ': 1, + 'ꦤ꧀ꦥꦸ': 1, + 'ꦤ꧀ꦥꦼ': 1, + 'ꦤ꧀ꦥꦿ': 1, + 'ꦤ꧀ꦥꦿꦶ': 1, + 'ꦤ꧀ꦧ': 1, + 'ꦤ꧀ꦧꦁ': 1, + 'ꦤ꧀ꦧꦶ': 1, + 'ꦤ꧀ꦧꦸ': 1, + 'ꦤ꧀ꦧꦺ': 1, + 'ꦤ꧀ꦧꦺꦴ': 1, + 'ꦤ꧀ꦧꦼ': 1, + 'ꦤ꧀ꦩ': 1, + 'ꦤ꧀ꦩꦂ': 1, + 'ꦤ꧀ꦩꦶ': 1, + 'ꦤ꧀ꦩꦸꦂ': 1, + 'ꦤ꧀ꦩꦼ': 1, + 'ꦤ꧀ꦭ': 1, + 'ꦤ꧀ꦭꦶ': 1, + 'ꦤ꧀ꦭꦸ': 1, + 'ꦤ꧀ꦮ': 1, + 'ꦤ꧀ꦮꦂ': 1, + 'ꦤ꧀ꦮꦶ': 1, + 'ꦤ꧀ꦮꦸ': 1, + 'ꦤ꧀ꦮꦼ': 1, + 'ꦤ꧀ꦱ': 1, + 'ꦤ꧀ꦱꦂ': 1, + 'ꦤ꧀ꦱꦃ': 1, + 'ꦤ꧀ꦱꦶ': 1, + 'ꦤ꧀ꦱꦶꦁ': 1, + 'ꦤ꧀ꦱꦸ': 1, + 'ꦤ꧀ꦱꦺꦴ': 1, + 'ꦤ꧀ꦱꦼ': 1, + 'ꦤ꧀ꦲ': 1, + 'ꦤ꧀ꦲꦁ': 1, + 'ꦤ꧀ꦲꦶ': 1, + 'ꦤ꧀ꦲꦶꦁ': 1, + 'ꦤ꧀ꦲꦸ': 1, + 'ꦤ꧀ꦲꦺꦴ': 1, + 'ꦥꦃ': 1, + 'ꦥꦺ': 1, + 'ꦥꦺꦴ': 1, + 'ꦥꦿ': 1, + 'ꦥꦿꦃ': 1, + 'ꦥꦿꦶ': 1, + 'ꦥꦿꦺꦴ': 1, + 'ꦥ꧀ꦏ': 1, + 'ꦥ꧀ꦏꦁ': 1, + 'ꦥ꧀ꦠ': 1, + 'ꦥ꧀ꦠꦶ': 1, + 'ꦥ꧀ꦢꦸ': 1, + 'ꦥ꧀ꦤ': 1, + 'ꦥ꧀ꦤꦶ': 1, + 'ꦥ꧀ꦥ': 1, + 'ꦥ꧀ꦥꦶ': 1, + 'ꦥ꧀ꦥꦺ': 1, + 'ꦥ꧀ꦧꦺ': 1, + 'ꦥ꧀ꦩ': 1, + 'ꦥ꧀ꦩꦂ': 1, + 'ꦥ꧀ꦫꦺ': 1, + 'ꦥ꧀ꦭ': 1, + 'ꦥ꧀ꦭꦸ': 1, + 'ꦥ꧀ꦱ': 1, + 'ꦥ꧀ꦱꦶꦂ': 1, + 'ꦥ꧀ꦱꦼ': 1, + 'ꦥ꧀ꦲ': 1, + 'ꦧꦺ': 1, + 'ꦧꦺꦃ': 1, + 'ꦧꦺꦴ': 1, + 'ꦧꦿ': 1, + 'ꦧ꧀ꦏ': 1, + 'ꦧ꧀ꦠꦸ': 1, + 'ꦧ꧀ꦥ': 1, + 'ꦧ꧀ꦧ': 1, + 'ꦧ꧀ꦧꦺ': 1, + 'ꦧ꧀ꦲ': 1, + 'ꦧ꧀ꦲꦶ': 1, + 'ꦩꦃ': 1, + 'ꦩꦺ': 1, + 'ꦩꦺꦃ': 1, + 'ꦩꦺꦴ': 1, + 'ꦩꦿ': 1, + 'ꦩꦿꦃ': 1, + 'ꦩꦿꦶꦃ': 1, + 'ꦩ꧀ꦏꦁ': 1, + 'ꦩ꧀ꦔꦼ': 1, + 'ꦩ꧀ꦠꦸ': 1, + 'ꦩ꧀ꦠꦺꦴ': 1, + 'ꦩ꧀ꦤꦶꦁ': 1, + 'ꦩ꧀ꦥ': 1, + 'ꦩ꧀ꦥꦶ': 1, + 'ꦩ꧀ꦥꦸ': 1, + 'ꦩ꧀ꦥꦼ': 1, + 'ꦩ꧀ꦧ': 1, + 'ꦩ꧀ꦧꦁ': 1, + 'ꦩ꧀ꦧꦂ': 1, + 'ꦩ꧀ꦧꦃ': 1, + 'ꦩ꧀ꦧꦶ': 1, + 'ꦩ꧀ꦧꦸ': 1, + 'ꦩ꧀ꦧꦸꦁ': 1, + 'ꦩ꧀ꦧꦺ': 1, + 'ꦩ꧀ꦧꦺꦂ': 1, + 'ꦩ꧀ꦧꦼꦂ': 1, + 'ꦩ꧀ꦧꦾꦸꦂ': 1, + 'ꦩ꧀ꦩ': 1, + 'ꦩ꧀ꦩꦶ': 1, + 'ꦩ꧀ꦫ': 1, + 'ꦩ꧀ꦫꦃ': 1, + 'ꦩ꧀ꦭ': 1, + 'ꦩ꧀ꦲꦶ': 1, + 'ꦪꦃ': 1, + 'ꦪꦸꦃ': 1, + 'ꦪꦺꦴ': 1, + 'ꦫꦶꦃ': 1, + 'ꦫꦺ': 1, + 'ꦫꦺꦴ': 1, + 'ꦫꦾ': 1, + 'ꦭꦃ': 1, + 'ꦭꦶꦃ': 1, + 'ꦭꦺ': 1, + 'ꦭꦺꦃ': 1, + 'ꦭꦺꦴ': 1, + 'ꦭꦺꦴꦁ': 1, + 'ꦭ꧀ꦏꦁ': 1, + 'ꦭ꧀ꦚꦼꦁ': 1, + 'ꦭ꧀ꦠꦸ': 1, + 'ꦭ꧀ꦧꦸ': 1, + 'ꦭ꧀ꦩꦸ': 1, + 'ꦭ꧀ꦭ': 1, + 'ꦭ꧀ꦭꦶ': 1, + 'ꦭ꧀ꦭꦶꦁ': 1, + 'ꦭ꧀ꦭꦺ': 1, + 'ꦭ꧀ꦮ': 1, + 'ꦭ꧀ꦱ': 1, + 'ꦭ꧀ꦱꦂ': 1, + 'ꦭ꧀ꦱꦶꦁ': 1, + 'ꦭ꧀ꦱꦸ': 1, + 'ꦭ꧀ꦲ': 1, + 'ꦭ꧀ꦲꦁ': 1, + 'ꦭ꧀ꦲꦶꦁ': 1, + 'ꦭ꧀ꦲꦸ': 1, + 'ꦮꦃ': 1, + 'ꦮꦶꦃ': 1, + 'ꦮꦸꦃ': 1, + 'ꦮꦺ': 1, + 'ꦮꦺꦴ': 1, + 'ꦮꦺꦴꦁ': 1, + 'ꦮꦺꦴꦃ': 1, + 'ꦮꦿꦸꦃ': 1, + 'ꦱꦶꦃ': 1, + 'ꦱꦺ': 1, + 'ꦱꦺꦴ': 1, + 'ꦱꦾ': 1, + 'ꦱꦿ': 1, + 'ꦱꦿꦃ': 1, + 'ꦱ꧀ꦏ': 1, + 'ꦱ꧀ꦏꦁ': 1, + 'ꦱ꧀ꦏꦿꦶ': 1, + 'ꦱ꧀ꦒ': 1, + 'ꦱ꧀ꦔꦸ': 1, + 'ꦱ꧀ꦔꦿ': 1, + 'ꦱ꧀ꦛꦶ': 1, + 'ꦱ꧀ꦠ': 1, + 'ꦱ꧀ꦠꦿ': 1, + 'ꦱ꧀ꦢꦂ': 1, + 'ꦱ꧀ꦢꦶ': 1, + 'ꦱ꧀ꦢꦸ': 1, + 'ꦱ꧀ꦥ': 1, + 'ꦱ꧀ꦧ': 1, + 'ꦱ꧀ꦧꦁ': 1, + 'ꦱ꧀ꦩ': 1, + 'ꦱ꧀ꦩꦶ': 1, + 'ꦱ꧀ꦩꦼ': 1, + 'ꦱ꧀ꦩꦿ': 1, + 'ꦱ꧀ꦭ': 1, + 'ꦱ꧀ꦮ': 1, + 'ꦱ꧀ꦱ': 1, + 'ꦱ꧀ꦱꦂ': 1, + 'ꦱ꧀ꦱꦶ': 1, + 'ꦱ꧀ꦱꦶꦁ': 1, + 'ꦱ꧀ꦱꦸ': 1, + 'ꦱ꧀ꦱꦺ': 1, + 'ꦱ꧀ꦲ': 1, + 'ꦱ꧀ꦲꦁ': 1, + 'ꦱ꧀ꦲꦶ': 1, + 'ꦱ꧀ꦲꦶꦁ': 1, + 'ꦱ꧀ꦲꦸ': 1, + 'ꦲꦺ': 1, + 'ꦲꦺꦴ': 1, + '𑄃𑄬': 1, + '𑄃𑄬𑄭': 1, + '𑄃𑄳𑄃': 1, + '𑄃𑄳𑄦𑄧': 1, + '𑄃𑄳𑄦𑄨': 1, + '𑄇𑄬': 1, + '𑄇𑄳𑄠': 1, + '𑄇𑄳𑄠𑄬': 1, + '𑄇𑄳𑄠𑄴': 1, + '𑄇𑄳𑄢𑄨': 1, + '𑄇𑄳𑄦𑄧': 1, + '𑄇𑄳𑄦𑄴': 1, + '𑄈𑄬': 1, + '𑄈𑄳𑄠': 1, + '𑄈𑄳𑄠𑄬': 1, + '𑄉𑄬': 1, + '𑄉𑄬𑄭': 1, + '𑄉𑄳𑄠': 1, + '𑄉𑄳𑄢𑄧': 1, + '𑄊𑄬': 1, + '𑄌𑄬': 1, + '𑄌𑄳𑄠𑄬': 1, + '𑄌𑄳𑄠𑄴': 1, + '𑄎𑄬': 1, + '𑄎𑄬𑄭': 1, + '𑄎𑄳𑄠𑄧': 1, + '𑄎𑄳𑄠𑄬': 1, + '𑄏𑄬': 1, + '𑄑𑄬': 1, + '𑄑𑄳𑄢𑄬': 1, + '𑄖𑄬': 1, + '𑄖𑄳𑄠𑄧': 1, + '𑄖𑄳𑄠𑄬': 1, + '𑄖𑄳𑄠𑄴': 1, + '𑄖𑄳𑄢𑄨': 1, + '𑄖𑄳𑄢𑄬': 1, + '𑄖𑄳𑄦𑄴': 1, + '𑄗𑄬': 1, + '𑄘𑄬': 1, + '𑄘𑄳𑄠𑄧': 1, + '𑄘𑄳𑄠𑄬': 1, + '𑄙𑄬': 1, + '𑄙𑄳𑄠': 1, + '𑄙𑄳𑄠𑄧': 1, + '𑄚𑄬': 1, + '𑄚𑄬𑄭': 1, + '𑄚𑄳𑄠𑄧': 1, + '𑄚𑄳𑄠𑄬': 1, + '𑄚𑄳𑄠𑄴': 1, + '𑄚𑄳𑄢𑄧𑄧': 1, + '𑄚𑄳𑄦𑄧': 1, + '𑄚𑄳𑄦𑄨': 1, + '𑄚𑄳𑄦𑄴': 1, + '𑄛𑄬': 1, + '𑄛𑄳𑄢': 1, + '𑄛𑄳𑄢𑄧': 1, + '𑄛𑄳𑄦': 1, + '𑄜𑄬': 1, + '𑄝𑄬': 1, + '𑄝𑄳𑄠': 1, + '𑄝𑄳𑄢𑄨': 1, + '𑄝𑄳𑄢𑄬': 1, + '𑄞𑄬': 1, + '𑄞𑄬𑄭': 1, + '𑄟𑄬': 1, + '𑄟𑄳𑄠𑄬': 1, + '𑄠𑄬': 1, + '𑄠𑄬𑄭': 1, + '𑄡𑄬': 1, + '𑄡𑄬𑄭': 1, + '𑄡𑄳𑄠𑄬': 1, + '𑄢𑄬': 1, + '𑄢𑄬𑄭': 1, + '𑄢𑄳𑄦𑄧𑄱𑄱': 1, + '𑄣𑄬': 1, + '𑄣𑄬𑄭': 1, + '𑄣𑄳𑄦𑄬': 1, + '𑄣𑄳𑄦𑄴': 1, + '𑄥𑄬': 1, + '𑄥𑄳𑄠': 1, + '𑄥𑄳𑄠𑄧': 1, + '𑄥𑄳𑄢𑄧': 1, + '𑄥𑄳𑄢𑄨𑄁': 1, + '𑄦𑄬': 1, + '𑌅𑌂': 1, + '𑌕𑌂': 1, + '𑌕𑌾': 1, + '𑌕𑌾𑌂': 1, + '𑌕𑌾𑌃': 1, + '𑌕𑌿': 1, + '𑌕𑍂': 1, + '𑌕𑍃': 1, + '𑌕𑍇': 1, + '𑌕𑍋': 1, + '𑌗𑌾': 1, + '𑌗𑌿': 1, + '𑌗𑍁': 1, + '𑌗𑍃': 1, + '𑌗𑍇': 1, + '𑌗𑍌': 1, + '𑌘𑌾': 1, + '𑌘𑍇': 1, + '𑌘𑍋': 1, + '𑌚𑌾': 1, + '𑌚𑌿': 1, + '𑌚𑍇': 1, + '𑌚𑍈': 1, + '𑌛𑌾': 1, + '𑌛𑍁': 1, + '𑌛𑍇': 1, + '𑌜𑌾': 1, + '𑌜𑌿': 1, + '𑌜𑍇': 1, + '𑌞𑌾': 1, + '𑌟𑌃': 1, + '𑌟𑌾': 1, + '𑌟𑌾𑌂': 1, + '𑌟𑌾𑌃': 1, + '𑌠𑌾': 1, + '𑌠𑍇': 1, + '𑌠𑍈𑌃': 1, + '𑌡𑌂': 1, + '𑌡𑌃': 1, + '𑌡𑌿': 1, + '𑌣𑌂': 1, + '𑌣𑌾': 1, + '𑌣𑌾𑌂': 1, + '𑌣𑌿': 1, + '𑌣𑍁': 1, + '𑌣𑍇': 1, + '𑌤𑌂': 1, + '𑌤𑌃': 1, + '𑌤𑌾': 1, + '𑌤𑌾𑌂': 1, + '𑌤𑌾𑌃': 1, + '𑌤𑌿': 1, + '𑌤𑌿𑌂': 1, + '𑌤𑌿𑌃': 1, + '𑌤𑍁': 1, + '𑌤𑍁𑌂': 1, + '𑌤𑍃': 1, + '𑌤𑍇': 1, + '𑌤𑍇𑌃': 1, + '𑌤𑍈': 1, + '𑌤𑍋': 1, + '𑌤𑍌': 1, + '𑌥𑌂': 1, + '𑌥𑌾': 1, + '𑌥𑌾𑌂': 1, + '𑌥𑌿': 1, + '𑌦𑌂': 1, + '𑌦𑌃': 1, + '𑌦𑌾': 1, + '𑌦𑌿': 1, + '𑌦𑍁': 1, + '𑌦𑍃': 1, + '𑌦𑍇': 1, + '𑌦𑍋': 1, + '𑌧𑌃': 1, + '𑌧𑌾': 1, + '𑌧𑌾𑌃': 1, + '𑌧𑌿': 1, + '𑌧𑌿𑌂': 1, + '𑌧𑍁': 1, + '𑌧𑍃': 1, + '𑌧𑍇': 1, + '𑌧𑍇𑌃': 1, + '𑌧𑍈𑌃': 1, + '𑌨𑌂': 1, + '𑌨𑌃': 1, + '𑌨𑌾': 1, + '𑌨𑌾𑌂': 1, + '𑌨𑌾𑌃': 1, + '𑌨𑌿': 1, + '𑌨𑌿𑌃': 1, + '𑌨𑍁': 1, + '𑌨𑍂': 1, + '𑌨𑍇': 1, + '𑌨𑍈': 1, + '𑌨𑍈𑌃': 1, + '𑌨𑍋': 1, + '𑌪𑌂': 1, + '𑌪𑌾': 1, + '𑌪𑌿': 1, + '𑌪𑍁': 1, + '𑌪𑍂': 1, + '𑌪𑍇': 1, + '𑌪𑍋': 1, + '𑌬𑌾': 1, + '𑌬𑍃': 1, + '𑌭𑌾': 1, + '𑌭𑌿': 1, + '𑌭𑌿𑌃': 1, + '𑌭𑍁': 1, + '𑌭𑍂': 1, + '𑌭𑍇': 1, + '𑌮𑌂': 1, + '𑌮𑌾': 1, + '𑌮𑌿': 1, + '𑌮𑌿𑌂': 1, + '𑌮𑍁': 1, + '𑌮𑍂': 1, + '𑌮𑍇': 1, + '𑌮𑍈': 1, + '𑌮𑍋': 1, + '𑌮𑍌': 1, + '𑌯𑌂': 1, + '𑌯𑌃': 1, + '𑌯𑌾': 1, + '𑌯𑌾𑌂': 1, + '𑌯𑌾𑌃': 1, + '𑌯𑌿': 1, + '𑌯𑍁': 1, + '𑌯𑍁𑌃': 1, + '𑌯𑍇': 1, + '𑌯𑍈': 1, + '𑌯𑍈𑌃': 1, + '𑌯𑍋': 1, + '𑌯𑍋𑌃': 1, + '𑌰𑌂': 1, + '𑌰𑌃': 1, + '𑌰𑌾': 1, + '𑌰𑌾𑌃': 1, + '𑌰𑌿': 1, + '𑌰𑌿𑌂': 1, + '𑌰𑍁': 1, + '𑌰𑍂': 1, + '𑌰𑍇': 1, + '𑌰𑍋': 1, + '𑌰𑍌': 1, + '𑌲𑌂': 1, + '𑌲𑌾': 1, + '𑌲𑌾𑌃': 1, + '𑌲𑌿': 1, + '𑌲𑌿𑌂': 1, + '𑌲𑍇': 1, + '𑌲𑍈': 1, + '𑌲𑍋': 1, + '𑌵𑌂': 1, + '𑌵𑌃': 1, + '𑌵𑌾': 1, + '𑌵𑌾𑌂': 1, + '𑌵𑌾𑌃': 1, + '𑌵𑌿': 1, + '𑌵𑍃': 1, + '𑌵𑍇': 1, + '𑌵𑍈': 1, + '𑌵𑍋': 1, + '𑌶𑌂': 1, + '𑌶𑌃': 1, + '𑌶𑌾': 1, + '𑌶𑌾𑌃': 1, + '𑌶𑌿': 1, + '𑌶𑍁': 1, + '𑌶𑍂': 1, + '𑌶𑍇': 1, + '𑌶𑍋': 1, + '𑌷𑌂': 1, + '𑌷𑌾': 1, + '𑌷𑌾𑌂': 1, + '𑌷𑌾𑌃': 1, + '𑌷𑌿': 1, + '𑌷𑍁': 1, + '𑌷𑍇': 1, + '𑌷𑍈': 1, + '𑌷𑍋': 1, + '𑌸𑌂': 1, + '𑌸𑌃': 1, + '𑌸𑌾': 1, + '𑌸𑌾𑌂': 1, + '𑌸𑌿': 1, + '𑌸𑍁': 1, + '𑌸𑍂': 1, + '𑌸𑍇': 1, + '𑌸𑍋': 1, + '𑌸𑍌': 1, + '𑌹𑌾': 1, + '𑌹𑌾𑌃': 1, + '𑌹𑌿': 1, + '𑌹𑍇': 1, + '𑌹𑍋': 1, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_c2157f7e.py b/libs/wcwidth/table_grapheme_overrides/_known_c2157f7e.py new file mode 100644 index 000000000..a3c2ad607 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_c2157f7e.py @@ -0,0 +1,1153 @@ +""" +Grapheme overrides for _known_c2157f7e. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: konsole +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'केा': 1, + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्त्योः': 4, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 3, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ग़ा': 1, + 'ग्या': 3, + 'ग्र्य': 3, + 'घेि': 1, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'छुी': 1, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 3, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ठैः': 1, + 'ड़ा': 1, + 'ढ़ा': 1, + 'ढ़ि': 1, + 'ढ़ी': 1, + 'ढ़ो': 1, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 3, + 'त्राि': 3, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'धेः': 1, + 'ध्या': 3, + 'नैः': 1, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 4, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 3, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'यैः': 1, + 'य्यो': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 3, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 3, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 3, + 'ल्ला': 3, + 'ल्लि': 3, + 'व्या': 3, + 'व्यों': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 3, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 3, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'हैः': 1, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 3, + 'চ্ছে': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ড়া': 1, + 'ণ্ডে': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'প্তা': 3, + 'প্রা': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'য়া': 1, + 'য়ি': 1, + 'য়ে': 1, + 'য়ো': 1, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শৃং': 1, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'স্তি': 3, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కాం': 1, + 'కెం': 1, + 'క్కు': 3, + 'క్తు': 3, + 'క్రూ': 3, + 'క్స్వా': 3, + 'గిం': 1, + 'చెం': 1, + 'చ్చు': 3, + 'టిం': 1, + 'ట్లు': 3, + 'డ్రు': 3, + 'ణిం': 1, + 'త్తు': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దిం': 1, + 'ద్దు': 3, + 'ద్ర్య': 3, + 'ధిం': 1, + 'నాం': 1, + 'నిం': 1, + 'నొం': 1, + 'న్ను': 3, + 'పిం': 1, + 'పెం': 1, + 'పొం': 1, + 'భ్యు': 3, + 'మిం': 1, + 'యిం': 1, + 'రిం': 1, + 'ర్చు': 3, + 'ర్ణు': 3, + 'ర్తృ': 3, + 'ర్బం': 3, + 'ర్హు': 3, + 'లిం': 1, + 'విం': 1, + 'శాం': 1, + 'శిం': 1, + 'షిం': 1, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'సాం': 1, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'హిం': 1, + 'ಕಿಂ': 1, + 'ಕೆಂ': 1, + 'ದಿಂ': 1, + 'ನಿಂ': 1, + 'ಯಿಂ': 1, + 'ಯೆಂ': 1, + 'ರಿಂ': 1, + 'ರೆಂ': 1, + 'ಲೆಂ': 1, + 'ಳಿಂ': 1, + 'ವೆಂ': 1, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ടും': 1, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടെ': 3, + 'തും': 1, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'നും': 1, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‍പി': 3, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യും': 1, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'രും': 1, + 'ര്യം': 3, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 3, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 4, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 3, + 'ര്‍ഹി': 3, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ലും': 1, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 3, + 'ല്ലെ': 3, + 'ല്‍കി': 3, + 'ല്‍കേ': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 3, + 'ളും': 1, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 3, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 3, + 'വും': 1, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 3, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സോ': 3, + 'දිං': 1, + 'හිං': 1, + 'က်ျ': 1, + 'ကၠေ': 1, + 'ကၠေံ': 1, + 'ဂၠေ': 1, + 'ငွေ': 1, + 'စှ်ေ': 1, + 'စၞေ': 1, + 'စၠေ': 1, + 'ဇၞေ': 1, + 'တွေ': 1, + 'တှ်ေ': 1, + 'တၟေ': 1, + 'ထွေ': 1, + 'ဒ္ဂေ': 3, + 'နှေ': 1, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗၠေ': 1, + 'မွေ': 1, + 'မွေ့': 1, + 'မှေ': 1, + 'မၠေ': 1, + 'ရွေ': 1, + 'ရွှေ့': 1, + 'ရှေ': 1, + 'ရှေ့': 1, + 'ရှ်ေ': 1, + 'လၟေ': 1, + 'သ္ဂေ': 3, + 'သှ်ေ': 1, + 'သၞေ': 1, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 3, + 'ញុះ': 1, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណ្ដា': 3, + 'ណ្ណោះ': 3, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'ប៉ះ': 1, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម៉ា': 1, + 'ម៉ោ': 1, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'យ៉ា': 1, + 'លុះ': 1, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨩ᩵ᩭ': 1, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᨯᩪᩯ': 1, + 'ᨹᩪᩢᩱ': 1, + 'ᩁᩢᩭ': 1, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 4, + 'ꦒꦸꦃ': 1, + 'ꦒ꧀ꦒ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦠꦸꦃ': 1, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 3, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 4, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 4, + 'ꦢ꧀ꦲ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 4, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 3, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 4, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 4, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 4, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦸꦃ': 1, + 'ꦫꦶꦃ': 1, + 'ꦭꦶꦃ': 1, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦶꦃ': 1, + 'ꦮꦸꦃ': 1, + 'ꦱꦶꦃ': 1, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑍍': 2, + '𑌣𑍍': 2, + '𑌤𑍍': 2, + '𑌦𑍍': 2, + '𑌧𑍍': 2, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑍍': 2, + '𑌮𑍍': 2, + '𑌯𑍍': 2, + '𑌰𑍍': 2, + '𑌲𑍍': 2, + '𑌵𑍍': 2, + '𑌶𑍍': 2, + '𑌷𑍍': 2, + '𑌸𑍍': 2, + '👨🏻‍🫯‍👨🏼': 3, + '👨🏻‍🫯‍👨🏽': 3, + '👨🏻‍🫯‍👨🏾': 3, + '👨🏻‍🫯‍👨🏿': 3, + '👨🏼‍🫯‍👨🏻': 3, + '👨🏼‍🫯‍👨🏽': 3, + '👨🏼‍🫯‍👨🏾': 3, + '👨🏼‍🫯‍👨🏿': 3, + '👨🏽‍🫯‍👨🏻': 3, + '👨🏽‍🫯‍👨🏼': 3, + '👨🏽‍🫯‍👨🏾': 3, + '👨🏽‍🫯‍👨🏿': 3, + '👨🏾‍🫯‍👨🏻': 3, + '👨🏾‍🫯‍👨🏼': 3, + '👨🏾‍🫯‍👨🏽': 3, + '👨🏾‍🫯‍👨🏿': 3, + '👨🏿‍🫯‍👨🏻': 3, + '👨🏿‍🫯‍👨🏼': 3, + '👨🏿‍🫯‍👨🏽': 3, + '👨🏿‍🫯‍👨🏾': 3, + '👩🏻‍🫯‍👩🏼': 3, + '👩🏻‍🫯‍👩🏽': 3, + '👩🏻‍🫯‍👩🏾': 3, + '👩🏻‍🫯‍👩🏿': 3, + '👩🏼‍🫯‍👩🏻': 3, + '👩🏼‍🫯‍👩🏽': 3, + '👩🏼‍🫯‍👩🏾': 3, + '👩🏼‍🫯‍👩🏿': 3, + '👩🏽‍🫯‍👩🏻': 3, + '👩🏽‍🫯‍👩🏼': 3, + '👩🏽‍🫯‍👩🏾': 3, + '👩🏽‍🫯‍👩🏿': 3, + '👩🏾‍🫯‍👩🏻': 3, + '👩🏾‍🫯‍👩🏼': 3, + '👩🏾‍🫯‍👩🏽': 3, + '👩🏾‍🫯‍👩🏿': 3, + '👩🏿‍🫯‍👩🏻': 3, + '👩🏿‍🫯‍👩🏼': 3, + '👩🏿‍🫯‍👩🏽': 3, + '👩🏿‍🫯‍👩🏾': 3, + '🧑🏻‍🫯‍🧑🏼': 3, + '🧑🏻‍🫯‍🧑🏽': 3, + '🧑🏻‍🫯‍🧑🏾': 3, + '🧑🏻‍🫯‍🧑🏿': 3, + '🧑🏼‍🫯‍🧑🏻': 3, + '🧑🏼‍🫯‍🧑🏽': 3, + '🧑🏼‍🫯‍🧑🏿': 3, + '🧑🏽‍🫯‍🧑🏻': 3, + '🧑🏽‍🫯‍🧑🏼': 3, + '🧑🏽‍🫯‍🧑🏾': 3, + '🧑🏽‍🫯‍🧑🏿': 3, + '🧑🏾‍🫯‍🧑🏻': 3, + '🧑🏾‍🫯‍🧑🏽': 3, + '🧑🏾‍🫯‍🧑🏿': 3, + '🧑🏿‍🫯‍🧑🏻': 3, + '🧑🏿‍🫯‍🧑🏼': 3, + '🧑🏿‍🫯‍🧑🏽': 3, + '🧑🏿‍🫯‍🧑🏾': 3, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_c3db41c0.py b/libs/wcwidth/table_grapheme_overrides/_known_c3db41c0.py new file mode 100644 index 000000000..6879ce6af --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_c3db41c0.py @@ -0,0 +1,3401 @@ +""" +Grapheme overrides for _known_c3db41c0. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: mlterm +""" +# pylint: skip-file +GRAPHEMES = { + 'ت‌': 2, + 'ن‌': 2, + 'ه‌': 2, + 'ی‌': 2, + 'ے‌': 2, + 'का': 1, + 'कां': 1, + 'कि': 1, + 'किं': 1, + 'की': 1, + 'केा': 1, + 'को': 1, + 'कौ': 1, + 'क्घ्घि्': 3, + 'क्त्योः': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 3, + 'खा': 1, + 'खि': 1, + 'खी': 1, + 'खो': 1, + 'ख्‍ने': 3, + 'ग़ा': 1, + 'गा': 1, + 'गाँ': 1, + 'गां': 1, + 'गि': 1, + 'गी': 1, + 'गो': 1, + 'गों': 1, + 'गौ': 1, + 'ग्र्य': 3, + 'घा': 1, + 'घि': 1, + 'घी': 1, + 'घेि': 1, + 'घो': 1, + 'घ्र्व्य': 4, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'चा': 1, + 'चां': 1, + 'चि': 1, + 'ची': 1, + 'चो': 1, + 'चौ': 1, + 'छि': 1, + 'छुी': 1, + 'छो': 1, + 'जा': 1, + 'जि': 1, + 'जिं': 1, + 'जी': 1, + 'जो': 1, + 'ज्या‌': 3, + 'झा': 1, + 'टा': 1, + 'टां': 1, + 'टि': 1, + 'टी': 1, + 'टों': 1, + 'ठा': 1, + 'ठी': 1, + 'ठैः': 1, + 'ड़ा': 1, + 'डा': 1, + 'डि': 1, + 'डी': 1, + 'डो': 1, + 'ढ़ा': 1, + 'ढ़ि': 1, + 'ढ़ी': 1, + 'ढ़ो': 1, + 'ढा': 1, + 'ढाँ': 1, + 'ढि': 1, + 'णा': 1, + 'णां': 1, + 'णि': 1, + 'णी': 1, + 'णो': 1, + 'णों': 1, + 'तः': 1, + 'ता': 1, + 'तां': 1, + 'ताः': 1, + 'ति': 1, + 'तिं': 1, + 'ती': 1, + 'तो': 1, + 'तों': 1, + 'तौ': 1, + 'त्त्थि': 3, + 'त्त्व': 3, + 'त्त्वा': 3, + 'त्त्वे': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 3, + 'त्र्ये': 3, + 'त्स्वा': 3, + 'था': 1, + 'थि': 1, + 'दः': 1, + 'दा': 1, + 'दां': 1, + 'दि': 1, + 'दी': 1, + 'दो': 1, + 'दौ': 1, + 'द्‌': 2, + 'धा': 1, + 'धां': 1, + 'धाः': 1, + 'धि': 1, + 'धी': 1, + 'धेः': 1, + 'धो': 1, + 'धों': 1, + 'नः': 1, + 'ना': 1, + 'नां': 1, + 'नाः': 1, + 'नि': 1, + 'निँ': 1, + 'निः': 1, + 'नी': 1, + 'नैः': 1, + 'नो': 1, + 'नों': 1, + 'नौ': 1, + 'न्ञ्या': 3, + 'न्त्र': 3, + 'न्त्रा': 3, + 'न्त्राः': 3, + 'न्त्रि': 3, + 'न्त्री': 3, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 4, + 'न्त्र्ये': 4, + 'न्ह्यां': 3, + 'पा': 1, + 'पाँ': 1, + 'पां': 1, + 'पि': 1, + 'पी': 1, + 'पो': 1, + 'पों': 1, + 'फा': 1, + 'फाँ': 1, + 'फौ': 1, + 'बा': 1, + 'बाँ': 1, + 'बि': 1, + 'बी': 1, + 'बो': 1, + 'भा': 1, + 'भि': 1, + 'भिः': 1, + 'भी': 1, + 'भो': 1, + 'भौ': 1, + 'मा': 1, + 'मां': 1, + 'मि': 1, + 'मी': 1, + 'मो': 1, + 'मों': 1, + 'मौ': 1, + 'म्प्र': 3, + 'यः': 1, + 'या': 1, + 'यां': 1, + 'याः': 1, + 'यि': 1, + 'यी': 1, + 'यैः': 1, + 'यो': 1, + 'यों': 1, + 'रः': 1, + 'रा': 1, + 'रां': 1, + 'राः': 1, + 'रि': 1, + 'रिं': 1, + 'री': 1, + 'रो': 1, + 'रों': 1, + 'रौ': 1, + 'र्त्त': 3, + 'र्त्ति': 3, + 'र्द्ध': 3, + 'र्‍या': 3, + 'ला': 1, + 'लां': 1, + 'लाः': 1, + 'लि': 1, + 'लिं': 1, + 'ली': 1, + 'लीं': 1, + 'लो': 1, + 'लों': 1, + 'लौ': 1, + 'ळा': 1, + 'ळी': 1, + 'वः': 1, + 'वा': 1, + 'वां': 1, + 'वाः': 1, + 'वि': 1, + 'वी': 1, + 'वो': 1, + 'वों': 1, + 'शः': 1, + 'शा': 1, + 'शां': 1, + 'शाः': 1, + 'शि': 1, + 'शी': 1, + 'शो': 1, + 'शों': 1, + 'षा': 1, + 'षां': 1, + 'षाः': 1, + 'षि': 1, + 'षी': 1, + 'षों': 1, + 'ष्ट्र': 3, + 'ष्ट्रा': 3, + 'ष्ट्रां': 3, + 'ष्ट्रि': 3, + 'ष्ट्री': 3, + 'ष्ट्रे': 3, + 'ष्ट्रो': 3, + 'ष्ट्रों': 3, + 'ष्प्र': 3, + 'सः': 1, + 'सा': 1, + 'साँ': 1, + 'सां': 1, + 'सि': 1, + 'सी': 1, + 'सो': 1, + 'सौ': 1, + 'स्ट्या': 3, + 'स्त्र': 3, + 'स्त्रा': 3, + 'स्त्रि': 3, + 'स्त्री': 3, + 'स्थ्य': 3, + 'हा': 1, + 'हाँ': 1, + 'हां': 1, + 'हि': 1, + 'हिं': 1, + 'ही': 1, + 'हीं': 1, + 'हैः': 1, + 'हो': 1, + 'हों': 1, + 'हौ': 1, + 'क़ा': 1, + 'क़ि': 1, + 'ज़ा': 1, + 'ड़ा': 1, + 'ढ़ा': 1, + 'फ़ा': 1, + 'অং': 1, + 'আ‌': 2, + 'ই‌': 2, + 'এ‌': 2, + 'কা': 1, + 'কি': 1, + 'কিং': 1, + 'কে': 1, + 'কো': 1, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 3, + 'খা': 1, + 'খি': 1, + 'খে': 1, + 'গি': 1, + 'গে': 1, + 'গো': 1, + 'ঘা': 1, + 'ঘে': 1, + 'ঘো': 1, + 'ঘো': 1, + 'চা': 1, + 'চি': 1, + 'চে': 1, + 'চো': 1, + 'চৌ': 1, + 'চ্ছা‌': 3, + 'ছা': 1, + 'ছি': 1, + 'ছে': 1, + 'জা': 1, + 'জি': 1, + 'জী': 1, + 'জে': 1, + 'ঝো': 1, + 'টা': 1, + 'টি': 1, + 'ঠি': 1, + 'ড়া': 1, + 'ণা': 1, + 'ণি': 1, + 'ণে': 1, + 'তঃ': 1, + 'তা': 1, + 'তাঁ': 1, + 'তি': 1, + 'তী': 1, + 'তে': 1, + 'ত্‍': 2, + 'ত্‍প': 3, + 'ত্‍পী': 3, + 'ত্‍সা': 3, + 'থা': 1, + 'থি': 1, + 'থে': 1, + 'দা': 1, + 'দি': 1, + 'দে': 1, + 'দৈ': 1, + 'দো': 1, + 'ধা': 1, + 'ধি': 1, + 'ধী': 1, + 'ধে': 1, + 'না': 1, + 'নি': 1, + 'নী': 1, + 'নে': 1, + 'নৈ': 1, + 'নো': 1, + 'ন্ত্রি': 3, + 'ন্ত্রে': 3, + 'ন‌': 2, + 'পা': 1, + 'পি': 1, + 'পী': 1, + 'পে': 1, + 'পো': 1, + 'ফে': 1, + 'ফৌ': 1, + 'বং': 1, + 'বা': 1, + 'বি': 1, + 'বী': 1, + 'বে': 1, + 'বৈ': 1, + 'বো': 1, + 'ভা': 1, + 'ভি': 1, + 'ভে': 1, + 'ভো': 1, + 'ভৌ': 1, + 'মা': 1, + 'মি': 1, + 'মে': 1, + 'মৌ': 1, + 'য়া': 1, + 'য়ি': 1, + 'য়ে': 1, + 'য়ো': 1, + 'যা': 1, + 'যে': 1, + 'যো': 1, + 'রা': 1, + 'রাং': 1, + 'রি': 1, + 'রী': 1, + 'রে': 1, + 'রো': 1, + 'র্ব্য': 3, + 'র‌': 2, + 'লা': 1, + 'লি': 1, + 'লী': 1, + 'লে': 1, + 'শং': 1, + 'শা': 1, + 'শি': 1, + 'শী': 1, + 'শৃং': 1, + 'শে': 1, + 'শৈ': 1, + 'ষা': 1, + 'ষি': 1, + 'ষী': 1, + 'ষে': 1, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 3, + 'ষ্ট্রে': 3, + 'সং': 1, + 'সা': 1, + 'সাং': 1, + 'সি': 1, + 'সী': 1, + 'সে': 1, + 'স্তি‌': 3, + 'স্ত্র': 3, + 'স্থ্য': 3, + 'হা': 1, + 'হি': 1, + 'হে': 1, + 'হো': 1, + 'হ‌': 2, + 'য়ে': 1, + 'ਕਾ': 1, + 'ਕਾਂ': 1, + 'ਕਿ': 1, + 'ਕਿ੍': 1, + 'ਕਿੱ': 1, + 'ਕੀ': 1, + 'ਖਾ': 1, + 'ਖਾਂ': 1, + 'ਖਿ': 1, + 'ਖਿ਼': 1, + 'ਖੀ': 1, + 'ਗਾ': 1, + 'ਗਿ': 1, + 'ਗਿ੍': 1, + 'ਗੀ': 1, + 'ਚਾ': 1, + 'ਚਿ': 1, + 'ਚਿੱ': 1, + 'ਚੀ': 1, + 'ਜਾ': 1, + 'ਜਾਂ': 1, + 'ਜਿ': 1, + 'ਜਿ਼': 1, + 'ਜਿ਼ੰ': 1, + 'ਜੀ': 1, + 'ਝਿ': 1, + 'ਝੀ': 1, + 'ਟਾ': 1, + 'ਟਿ': 1, + 'ਟਿ੍': 1, + 'ਟੀ': 1, + 'ਠੀ': 1, + 'ਡਾ': 1, + 'ਣਾ': 1, + 'ਣਾਂ': 1, + 'ਣੀ': 1, + 'ਤਾ': 1, + 'ਤਾਂ': 1, + 'ਤਿ': 1, + 'ਤੀ': 1, + 'ਦਾ': 1, + 'ਦਾਂ': 1, + 'ਦਿ': 1, + 'ਦਿੱ': 1, + 'ਦੀ': 1, + 'ਧਾ': 1, + 'ਧਾਂ': 1, + 'ਧਿ': 1, + 'ਧੀ': 1, + 'ਨਾ': 1, + 'ਨਾਂ': 1, + 'ਨਿ': 1, + 'ਨੀ': 1, + 'ਨੀਂ': 1, + 'ਪਾ': 1, + 'ਪਿ': 1, + 'ਪੀ': 1, + 'ਫਿ': 1, + 'ਬਾ': 1, + 'ਬਿ': 1, + 'ਬੀ': 1, + 'ਭਾ': 1, + 'ਭਿ': 1, + 'ਮਾ': 1, + 'ਮਾਂ': 1, + 'ਮਿ': 1, + 'ਮੀ': 1, + 'ਯਾ': 1, + 'ਰਾ': 1, + 'ਰਾਂ': 1, + 'ਰਿ': 1, + 'ਰੀ': 1, + 'ਲਾ': 1, + 'ਲਾਂ': 1, + 'ਲਿ': 1, + 'ਲਿੰ': 1, + 'ਲੀ': 1, + 'ਵਾ': 1, + 'ਵਾਂ': 1, + 'ਵਿ': 1, + 'ਵਿੱ': 1, + 'ਵੀ': 1, + 'ਸ਼ਾ': 1, + 'ਸ਼ਾਂ': 1, + 'ਸ਼ਿ': 1, + 'ਸ਼ੀ': 1, + 'ਸਾ': 1, + 'ਸਾਂ': 1, + 'ਸਿ': 1, + 'ਸਿੱ': 1, + 'ਸੀ': 1, + 'ਹਾ': 1, + 'ਹਾਂ': 1, + 'ਹਿ': 1, + 'ਹਿੰ': 1, + 'ਹਿੱ': 1, + 'ਹੀ': 1, + 'ਹੀਂ': 1, + 'ਜ਼ਾ': 1, + 'ਜ਼ੀ': 1, + 'ੜਾ': 1, + 'ੜੀਂ': 1, + 'ਫ਼ਾ': 1, + 'કઃ': 1, + 'કા': 1, + 'કાં': 1, + 'કિ': 1, + 'કી': 1, + 'કો': 1, + 'ખા': 1, + 'ખી': 1, + 'ખો': 1, + 'ગા': 1, + 'ગિ': 1, + 'ગી': 1, + 'ગો': 1, + 'ઘા': 1, + 'ઘો': 1, + 'ચા': 1, + 'ચિ': 1, + 'ચી': 1, + 'છા': 1, + 'છો': 1, + 'જા': 1, + 'જાં': 1, + 'જિ': 1, + 'જી': 1, + 'જો': 1, + 'ટઃ': 1, + 'ડા': 1, + 'ડાં': 1, + 'ડી': 1, + 'ઢા': 1, + 'ઢિ': 1, + 'ણા': 1, + 'ણી': 1, + 'તઃ': 1, + 'તા': 1, + 'તાં': 1, + 'તિ': 1, + 'તી': 1, + 'તો': 1, + 'ત્ત્વ': 3, + 'ત્ત્વા': 3, + 'ત્ર્ય': 3, + 'થા': 1, + 'થી': 1, + 'દા': 1, + 'દી': 1, + 'દો': 1, + 'ધા': 1, + 'ધાં': 1, + 'ધિ': 1, + 'ધી': 1, + 'ધો': 1, + 'નઃ': 1, + 'ના': 1, + 'નાં': 1, + 'નિ': 1, + 'ની': 1, + 'નો': 1, + 'પા': 1, + 'પિ': 1, + 'પી': 1, + 'પો': 1, + 'ફા': 1, + 'ફો': 1, + 'બા': 1, + 'બાં': 1, + 'બી': 1, + 'ભા': 1, + 'ભાં': 1, + 'ભિ': 1, + 'ભો': 1, + 'ભૌ': 1, + 'મઃ': 1, + 'મા': 1, + 'માં': 1, + 'મિ': 1, + 'મી': 1, + 'મો': 1, + 'યા': 1, + 'યિ': 1, + 'યી': 1, + 'યો': 1, + 'રા': 1, + 'રિ': 1, + 'રી': 1, + 'રો': 1, + 'લા': 1, + 'લાં': 1, + 'લિ': 1, + 'લિં': 1, + 'લી': 1, + 'લો': 1, + 'ળા': 1, + 'ળી': 1, + 'વા': 1, + 'વાં': 1, + 'વિ': 1, + 'વી': 1, + 'વો': 1, + 'શા': 1, + 'શાં': 1, + 'શિ': 1, + 'શી': 1, + 'શો': 1, + 'ષા': 1, + 'ષી': 1, + 'ષો': 1, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 3, + 'ષ્ટ્રો': 3, + 'સા': 1, + 'સાં': 1, + 'સિ': 1, + 'સી': 1, + 'સો': 1, + 'સ્ત્રી': 3, + 'હા': 1, + 'હિ': 1, + 'હી': 1, + 'હો': 1, + 'கா': 1, + 'கி': 1, + 'கு': 1, + 'கூ': 1, + 'கெ': 1, + 'கே': 1, + 'கை': 1, + 'கொ': 1, + 'கோ': 1, + 'சா': 1, + 'சி': 1, + 'சு': 1, + 'சூ': 1, + 'செ': 1, + 'சே': 1, + 'சை': 1, + 'சொ': 1, + 'டா': 1, + 'டி': 1, + 'டு': 1, + 'டெ': 1, + 'டை': 1, + 'ணா': 1, + 'ணி': 1, + 'ணு': 1, + 'ணை': 1, + 'தா': 1, + 'தி': 1, + 'து': 1, + 'தூ': 1, + 'தெ': 1, + 'தே': 1, + 'தை': 1, + 'தொ': 1, + 'தோ': 1, + 'நா': 1, + 'நி': 1, + 'நு': 1, + 'நெ': 1, + 'நே': 1, + 'நோ': 1, + 'னா': 1, + 'னி': 1, + 'னு': 1, + 'னே': 1, + 'னை': 1, + 'னொ': 1, + 'பா': 1, + 'பி': 1, + 'பு': 1, + 'பெ': 1, + 'பே': 1, + 'பை': 1, + 'பொ': 1, + 'போ': 1, + 'மா': 1, + 'மி': 1, + 'மு': 1, + 'மூ': 1, + 'மெ': 1, + 'மே': 1, + 'மை': 1, + 'மொ': 1, + 'மோ': 1, + 'யா': 1, + 'யி': 1, + 'யு': 1, + 'யே': 1, + 'யை': 1, + 'யோ': 1, + 'ரா': 1, + 'ரி': 1, + 'ரு': 1, + 'ரெ': 1, + 'ரை': 1, + 'றா': 1, + 'றி': 1, + 'று': 1, + 'றே': 1, + 'றை': 1, + 'றொ': 1, + 'றோ': 1, + 'லா': 1, + 'லி': 1, + 'லு': 1, + 'லெ': 1, + 'லே': 1, + 'லை': 1, + 'லோ': 1, + 'ளா': 1, + 'ளி': 1, + 'ளு': 1, + 'ளெ': 1, + 'ளை': 1, + 'ழி': 1, + 'ழு': 1, + 'ழை': 1, + 'வா': 1, + 'வி': 1, + 'வு': 1, + 'வூ': 1, + 'வெ': 1, + 'வே': 1, + 'வை': 1, + 'வொ': 1, + 'வோ': 1, + 'అం': 1, + 'ఆం': 1, + 'ఒం': 1, + 'కం': 1, + 'కాం': 1, + 'కు': 1, + 'కుం': 1, + 'కూ': 1, + 'కృ': 1, + 'కెం': 1, + 'క్స్వా': 3, + 'గిం': 1, + 'గు': 1, + 'గూ': 1, + 'గృ': 1, + 'చు': 1, + 'చెం': 1, + 'టం': 1, + 'టిం': 1, + 'టు': 1, + 'టుం': 1, + 'డు': 1, + 'ణిం': 1, + 'తం': 1, + 'తః': 1, + 'తు': 1, + 'తృ': 1, + 'త్ర్య': 3, + 'దం': 1, + 'దిం': 1, + 'దు': 1, + 'దుం': 1, + 'దృ': 1, + 'ద్ర్య': 3, + 'ధిం': 1, + 'ధు': 1, + 'నం': 1, + 'నః': 1, + 'నాం': 1, + 'నిం': 1, + 'ను': 1, + 'నుం': 1, + 'నొం': 1, + 'పం': 1, + 'పిం': 1, + 'పు': 1, + 'పూ': 1, + 'పెం': 1, + 'పొం': 1, + 'బం': 1, + 'భం': 1, + 'భు': 1, + 'మిం': 1, + 'ము': 1, + 'మూ': 1, + 'యం': 1, + 'యః': 1, + 'యిం': 1, + 'యు': 1, + 'యుం': 1, + 'రం': 1, + 'రిం': 1, + 'రు': 1, + 'రూ': 1, + 'ఱు': 1, + 'లం': 1, + 'లిం': 1, + 'లు': 1, + 'విం': 1, + 'వు': 1, + 'వృ': 1, + 'శాం': 1, + 'శిం': 1, + 'శు': 1, + 'షిం': 1, + 'షు': 1, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'సం': 1, + 'సాం': 1, + 'సు': 1, + 'సూ': 1, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'హిం': 1, + 'ಅಂ': 1, + 'ಇಂ': 1, + 'ಉಂ': 1, + 'ಎಂ': 1, + 'ಒಂ': 1, + 'ಕಾ': 1, + 'ಕಾಂ': 1, + 'ಕಿಂ': 1, + 'ಕೀ': 1, + 'ಕು': 1, + 'ಕುಂ': 1, + 'ಕೂ': 1, + 'ಕೃ': 1, + 'ಕೆಂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಕೊಂ': 1, + 'ಕ್‌': 2, + 'ಖಾಂ': 1, + 'ಗಾ': 1, + 'ಗಾಂ': 1, + 'ಗೀ': 1, + 'ಗು': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೊಂ': 1, + 'ಗೋ': 1, + 'ಚಾ': 1, + 'ಚೀ': 1, + 'ಚು': 1, + 'ಛಾ': 1, + 'ಜಾ': 1, + 'ಜೀ': 1, + 'ಟು': 1, + 'ಟುಂ': 1, + 'ಟೇ': 1, + 'ಠಾ': 1, + 'ಡಾ': 1, + 'ಡು': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣಂ': 1, + 'ಣಾ': 1, + 'ಣು': 1, + 'ಣೈ': 1, + 'ತಂ': 1, + 'ತಃ': 1, + 'ತಾ': 1, + 'ತೀ': 1, + 'ತು': 1, + 'ತೃ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ಥಾ': 1, + 'ದಂ': 1, + 'ದಾ': 1, + 'ದಿಂ': 1, + 'ದು': 1, + 'ದೂ': 1, + 'ದೃ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧಾ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಃ': 1, + 'ನಾ': 1, + 'ನಾಂ': 1, + 'ನಿಂ': 1, + 'ನು': 1, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪಂ': 1, + 'ಪಾ': 1, + 'ಪು': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬಂ': 1, + 'ಬಾ': 1, + 'ಬು': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭಾ': 1, + 'ಭೀ': 1, + 'ಭು': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮಾ': 1, + 'ಮೀ': 1, + 'ಮು': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯಂ': 1, + 'ಯಾ': 1, + 'ಯಿಂ': 1, + 'ಯು': 1, + 'ಯುಂ': 1, + 'ಯೂ': 1, + 'ಯೆಂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಂ': 1, + 'ರಾ': 1, + 'ರಾಂ': 1, + 'ರಿಂ': 1, + 'ರೀ': 1, + 'ರು': 1, + 'ರೂ': 1, + 'ರೆಂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲಂ': 1, + 'ಲಾ': 1, + 'ಲೀ': 1, + 'ಲು': 1, + 'ಲೂ': 1, + 'ಲೆಂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳಾ': 1, + 'ಳಿಂ': 1, + 'ಳು': 1, + 'ಳೂ': 1, + 'ವಾ': 1, + 'ವೀ': 1, + 'ವು': 1, + 'ವುಂ': 1, + 'ವೂ': 1, + 'ವೃ': 1, + 'ವೆಂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾ': 1, + 'ಶಾಂ': 1, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷಾ': 1, + 'ಷೇ': 1, + 'ಸಂ': 1, + 'ಸಾ': 1, + 'ಸಾಂ': 1, + 'ಸು': 1, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹಾ': 1, + 'ಹು': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೊಂ': 1, + 'ಹೋ': 1, + 'അം': 1, + 'കാ': 1, + 'കി': 1, + 'കെ': 1, + 'കേ': 1, + 'കൈ': 1, + 'കൊ': 1, + 'കോ': 1, + 'ക്ക്‌': 3, + 'ഗം': 1, + 'ഗി': 1, + 'ഗീ': 1, + 'ഘി': 1, + 'ചാ': 1, + 'ചി': 1, + 'ചെ': 1, + 'ചേ': 1, + 'ചോ': 1, + 'ജാ': 1, + 'ജി': 1, + 'ജീ': 1, + 'ടാ': 1, + 'ടി': 1, + 'ടും': 1, + 'ടെ': 1, + 'ടേ': 1, + 'ട്‌': 2, + 'ഠി': 1, + 'ണം': 1, + 'ണാ': 1, + 'ണി': 1, + 'ണെ': 1, + 'ണ്ട്‌': 3, + 'ണ്‌': 2, + 'തം': 1, + 'താ': 1, + 'തി': 1, + 'തീ': 1, + 'തും': 1, + 'തെ': 1, + 'തേ': 1, + 'തൊ': 1, + 'തോ': 1, + 'ത്ത്‌': 3, + 'ത്‌': 2, + 'ദം': 1, + 'ദാ': 1, + 'ദി': 1, + 'ദേ': 1, + 'ധാ': 1, + 'ധി': 1, + 'നം': 1, + 'നാ': 1, + 'നി': 1, + 'നീ': 1, + 'നും': 1, + 'നെ': 1, + 'നേ': 1, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 4, + 'ന്‌': 2, + 'ന്‍': 2, + 'ന്‍കൂ': 3, + 'ന്‍പി': 3, + 'പാ': 1, + 'പി': 1, + 'പീ': 1, + 'പെ': 1, + 'പൈ': 1, + 'പൊ': 1, + 'പോ': 1, + 'പൌ': 1, + 'പ്പ്‌': 3, + 'ബം': 1, + 'ബാ': 1, + 'ബോ': 1, + 'ഭാ': 1, + 'ഭി': 1, + 'ഭേ': 1, + 'ഭൌ': 1, + 'മം': 1, + 'മാ': 1, + 'മി': 1, + 'മെ': 1, + 'മേ': 1, + 'മൊ': 1, + 'മോ': 1, + 'മൌ': 1, + 'മ്പ്ര': 3, + 'യം': 1, + 'യാ': 1, + 'യി': 1, + 'യും': 1, + 'യെ': 1, + 'യേ': 1, + 'യോ': 1, + 'യ്ക്കു': 3, + 'രം': 1, + 'രാ': 1, + 'രി': 1, + 'രീ': 1, + 'രും': 1, + 'രെ': 1, + 'രേ': 1, + 'രോ': 1, + 'ര്‍ക്കു': 4, + 'ര്‍ക്കും': 4, + 'ര്‍ക്ക്‌': 5, + 'ര്‍ഗ്ഗം': 4, + 'ര്‍ച്ച': 4, + 'ര്‍ണ്ണ': 4, + 'ര്‍ത്ത': 4, + 'ര്‍ത്തി': 4, + 'ര്‍ത്തു': 4, + 'ര്‍ത്തേ': 4, + 'ര്‍ദേ': 3, + 'ര്‍ദ്ദ': 4, + 'ര്‍പ്പെ': 4, + 'ര്‍ബ': 3, + 'ര്‍ഭ': 3, + 'ര്‍മ്മി': 4, + 'ര്‍രാ': 3, + 'ര്‍വ്വ': 4, + 'ര്‍ഹ': 3, + 'ര്‍ഹി': 3, + 'റം': 1, + 'റി': 1, + 'റെ': 1, + 'ലം': 1, + 'ലാ': 1, + 'ലി': 1, + 'ലും': 1, + 'ലെ': 1, + 'ലേ': 1, + 'ലോ': 1, + 'ല്‍': 2, + 'ല്‍കി': 3, + 'ല്‍കു': 3, + 'ല്‍കേ': 3, + 'ല്‍ത': 3, + 'ല്‍നി': 3, + 'ല്‍പ്പ': 4, + 'ളം': 1, + 'ളാ': 1, + 'ളി': 1, + 'ളും': 1, + 'ളെ': 1, + 'ളേ': 1, + 'ളോ': 1, + 'ള്‍': 2, + 'ള്‍കൊ': 3, + 'ള്‍ക്ക': 4, + 'ള്‍ക്കി': 4, + 'ള്‍ക്കു': 4, + 'ള്‍ക്കും': 4, + 'ള്‍ക്കെ': 4, + 'ള്‍ക്ക്‌': 5, + 'ഴി': 1, + 'വാ': 1, + 'വി': 1, + 'വും': 1, + 'വെ': 1, + 'വേ': 1, + 'വൈ': 1, + 'വോ': 1, + 'ശാ': 1, + 'ശി': 1, + 'ശീ': 1, + 'ശേ': 1, + 'ഷം': 1, + 'ഷി': 1, + 'ഷ്ട്യാ': 3, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 3, + 'സം': 1, + 'സാ': 1, + 'സാം': 1, + 'സി': 1, + 'സൌ': 1, + 'സ്ത്രം': 3, + 'സ്ത്രീ': 3, + 'സ്റ്റ്‌': 4, + 'ഹം': 1, + 'ഹാ': 1, + 'ഹി': 1, + 'ഹിം': 1, + 'ක්‍': 2, + 'කා': 1, + 'කැ': 1, + 'කෘ': 1, + 'කෙ': 1, + 'කො': 1, + 'කෝ': 1, + 'කෲ': 1, + 'ඛ්‍': 2, + 'ගා': 1, + 'ගැ': 1, + 'ගෙ': 1, + 'ගේ': 1, + 'චා': 1, + 'චැ': 1, + 'චෝ': 1, + 'ජ්‍': 2, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ටේ': 1, + 'ඨා': 1, + 'ඩං': 1, + 'ඩා': 1, + 'ත්‍': 2, + 'තා': 1, + 'තැ': 1, + 'තෘ': 1, + 'තෙ': 1, + 'තේ': 1, + 'තො': 1, + 'තෝ': 1, + 'ථා': 1, + 'ද්‍': 2, + 'දා': 1, + 'දැ': 1, + 'දිං': 1, + 'දෙ': 1, + 'දේ': 1, + 'දෝ': 1, + 'ධ්‍': 2, + 'ධා': 1, + 'න්‍': 2, + 'නා': 1, + 'නැ': 1, + 'නෑ': 1, + 'නෙ': 1, + 'නේ': 1, + 'නො': 1, + 'ඳා': 1, + 'ප්‍': 2, + 'පා': 1, + 'පැ': 1, + 'පෑ': 1, + 'පෙ': 1, + 'පේ': 1, + 'පො': 1, + 'පෞ': 1, + 'බා': 1, + 'බැ': 1, + 'බෙ': 1, + 'බේ': 1, + 'බෝ': 1, + 'භා': 1, + 'භෞ': 1, + 'මා': 1, + 'මැ': 1, + 'මෙ': 1, + 'මේ': 1, + 'යා': 1, + 'යෙ': 1, + 'යේ': 1, + 'යො': 1, + 'යෝ': 1, + 'රා': 1, + 'රැ': 1, + 'රෙ': 1, + 'රේ': 1, + 'රෝ': 1, + 'ලං': 1, + 'ලා': 1, + 'ලැ': 1, + 'ලෙ': 1, + 'ලේ': 1, + 'ලෝ': 1, + 'වං': 1, + 'ව්‍': 2, + 'වා': 1, + 'වැ': 1, + 'වෘ': 1, + 'වෙ': 1, + 'වේ': 1, + 'වෝ': 1, + 'ශ්‍': 2, + 'ශේ': 1, + 'ෂ්‍': 2, + 'ෂා': 1, + 'සං': 1, + 'සා': 1, + 'සැ': 1, + 'සෑ': 1, + 'සෙ': 1, + 'සේ': 1, + 'සෞ': 1, + 'හා': 1, + 'හැ': 1, + 'හිං': 1, + 'හෘ': 1, + 'හෙ': 1, + 'හො': 1, + 'හෝ': 1, + 'ළැ': 1, + 'ကေ': 1, + 'ကေု': 1, + 'က်ျ': 1, + 'ကျ': 1, + 'ကျို': 1, + 'ကျူ': 1, + 'ကျေ': 1, + 'ကျွ': 1, + 'ကြ': 1, + 'ကြို': 1, + 'ကြီ': 1, + 'ကြု': 1, + 'ကြေ': 1, + 'ကြံ': 1, + 'ကြွေ': 1, + 'ကၠေ': 1, + 'ကၠေံ': 1, + 'ခေ': 1, + 'ချ': 1, + 'ချိ': 1, + 'ချို': 1, + 'ချို့': 1, + 'ချီ': 1, + 'ချု': 1, + 'ချေ': 1, + 'ချဳ': 1, + 'ခြ': 1, + 'ခြုံ': 1, + 'ခြေ': 1, + 'ဂျို': 1, + 'ဂၠေ': 1, + 'ငြ': 1, + 'ငြိ': 1, + 'ငွေ': 1, + 'ငႄ': 1, + 'စေ': 1, + 'စှ်ေ': 1, + 'စၞေ': 1, + 'စၠေ': 1, + 'ဆေ': 1, + 'ဇြ': 1, + 'ဇြေ': 1, + 'ဇြဳ': 1, + 'ဇၞေ': 1, + 'ညေ': 1, + 'တေ': 1, + 'တေံ': 1, + 'တြ': 1, + 'တြုံ': 1, + 'တွေ': 1, + 'တှ်ေ': 1, + 'တၟေ': 1, + 'ထေ': 1, + 'ထွေ': 1, + 'ဒေ': 1, + 'ဒေံ': 1, + 'ဒြ': 1, + 'နေ': 1, + 'နေ့': 1, + 'နှေ': 1, + 'ပေ': 1, + 'ပေဲ': 1, + 'ပျ': 1, + 'ပျေ': 1, + 'ပျဲ': 1, + 'ပြ': 1, + 'ပြီ': 1, + 'ပြု': 1, + 'ပြေ': 1, + 'ပြဳ': 1, + 'ပြံ': 1, + 'ဖေ': 1, + 'ဖျ': 1, + 'ဖျု': 1, + 'ဖျေံ': 1, + 'ဖြ': 1, + 'ဖြို': 1, + 'ဗျဵု': 1, + 'ဗြေ': 1, + 'ဗြဴ': 1, + 'ဗၠေ': 1, + 'ဘေ': 1, + 'မေ': 1, + 'မျ': 1, + 'မျို': 1, + 'မျီု': 1, + 'မျှ': 1, + 'မြ': 1, + 'မြေ': 1, + 'မြှ': 1, + 'မွေ': 1, + 'မွေ့': 1, + 'မှေ': 1, + 'မၠေ': 1, + 'မႄ': 1, + 'ယေ': 1, + 'ယျ': 1, + 'ယျဵု': 1, + 'ယြဴ': 1, + 'ရေ': 1, + 'ရေံ': 1, + 'ရွေ': 1, + 'ရွှေ့': 1, + 'ရှေ': 1, + 'ရှေ့': 1, + 'ရှ်ေ': 1, + 'လေ': 1, + 'လျ': 1, + 'လျို': 1, + 'လျေ': 1, + 'လျှ': 1, + 'လျှို့': 1, + 'လျှေ': 1, + 'လၟေ': 1, + 'လႄ': 1, + 'ဝေ': 1, + 'သေ': 1, + 'သြ': 1, + 'သြို': 1, + 'သှ်ေ': 1, + 'သၞေ': 1, + 'ဟေ': 1, + 'အေ': 1, + 'ၜေ': 1, + 'ၵေ': 1, + 'ၵႄ': 1, + 'ၶေ': 1, + 'ၼေ': 1, + 'ၼႄ': 1, + 'ၽႄ': 1, + 'ႁေ': 1, + 'ႁႄ': 1, + 'កា': 1, + 'កើ': 1, + 'កេ': 1, + 'កោ': 1, + 'ខា': 1, + 'ខែ': 1, + 'គា': 1, + 'គាំ': 1, + 'គេ': 1, + 'គោ': 1, + 'ឃា': 1, + 'ឃើ': 1, + 'ឃោ': 1, + 'ឃៅ': 1, + 'ងា': 1, + 'ចា': 1, + 'ចាំ': 1, + 'ចេ': 1, + 'ចែ': 1, + 'ចោ': 1, + 'ឆាំ': 1, + 'ជា': 1, + 'ជៀ': 1, + 'ឈា': 1, + 'ញុះ': 1, + 'ដា': 1, + 'ដើ': 1, + 'ដែ': 1, + 'ដៃ': 1, + 'ដោ': 1, + 'ដៅ': 1, + 'ឋា': 1, + 'ណា': 1, + 'ណើ': 1, + 'ណែ': 1, + 'ណោ': 1, + 'ណោះ': 1, + 'ណៈ': 1, + 'តា': 1, + 'តេ': 1, + 'តែ': 1, + 'ថា': 1, + 'ថែ': 1, + 'ទា': 1, + 'ទាំ': 1, + 'ទៀ': 1, + 'ទេ': 1, + 'ទៃ': 1, + 'ទោ': 1, + 'ទោះ': 1, + 'ទៅ': 1, + 'ធា': 1, + 'នា': 1, + 'នាំ': 1, + 'នឿ': 1, + 'នេះ': 1, + 'នៃ': 1, + 'នោះ': 1, + 'នៅ': 1, + 'នៈ': 1, + 'បា': 1, + 'បើ': 1, + 'បៀ': 1, + 'បែ': 1, + 'បោ': 1, + 'បោះ': 1, + 'បះ': 1, + 'ប៉ះ': 1, + 'ពា': 1, + 'ពើ': 1, + 'ពេ': 1, + 'ពោះ': 1, + 'ភា': 1, + 'ភេ': 1, + 'មា': 1, + 'មើ': 1, + 'មេ': 1, + 'មែ': 1, + 'ម៉ា': 1, + 'ម៉ោ': 1, + 'យា': 1, + 'យោ': 1, + 'យៈ': 1, + 'យ៉ា': 1, + 'រា': 1, + 'រាំ': 1, + 'រើ': 1, + 'រឿ': 1, + 'រៀ': 1, + 'រះ': 1, + 'រៈ': 1, + 'លា': 1, + 'លុះ': 1, + 'លើ': 1, + 'លេ': 1, + 'លោ': 1, + 'លៈ': 1, + 'វា': 1, + 'វេ': 1, + 'វៈ': 1, + 'សា': 1, + 'សើ': 1, + 'សេ': 1, + 'សៈ': 1, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ហា': 1, + 'ហើ': 1, + 'ហេ': 1, + 'ហែ': 1, + 'ហោ': 1, + 'ឡា': 1, + 'ឡើ': 1, + 'អា': 1, + 'អើ': 1, + 'អៀ': 1, + 'ᨠᩮ᩠': 1, + 'ᨠᩮᩢ᩵': 1, + 'ᨠᩮ᩵': 1, + 'ᨠᩯ᩠': 1, + 'ᨠᩯ᩵': 1, + 'ᨡᩮ᩠': 1, + 'ᨡᩮᩢ': 1, + 'ᨡᩮᩢᩢ': 1, + 'ᨡᩯ᩠': 1, + 'ᨡᩱᩢ': 1, + 'ᨣᩕ': 1, + 'ᨣᩕ᩠': 1, + 'ᨣᩕᩮ᩠': 1, + 'ᨣᩭ': 1, + 'ᨣᩮᩢᩢ': 1, + 'ᨣᩯ᩠': 1, + 'ᨤᩮᩢ': 1, + 'ᨤᩱ᩵': 1, + 'ᨧᩮ': 1, + 'ᨧᩮ᩠': 1, + 'ᨧᩮᩢᩢ': 1, + 'ᨧᩱ': 1, + 'ᨧᩱ᩠': 1, + 'ᨩᩭ᩵': 1, + 'ᨩᩮ᩠': 1, + 'ᨩᩱᩢ': 1, + 'ᨩ᩵ᩭ': 1, + 'ᨪᩮᩢ': 1, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᨯᩪᩯ': 1, + 'ᨯᩯ᩠': 1, + 'ᨯᩰ᩠': 1, + 'ᨯᩱ': 1, + 'ᨯᩱᩢ': 1, + 'ᨲᩕ': 1, + 'ᨲᩕᩦ': 1, + 'ᨲᩕ᩼': 1, + 'ᨲᩮ᩠': 1, + 'ᨲᩯ᩠': 1, + 'ᨲᩯ᩵': 1, + 'ᨲᩱ': 1, + 'ᨲᩱ᩠': 1, + 'ᨲᩱᩢ': 1, + 'ᨳᩮ᩠': 1, + 'ᨳᩮᩢᩢ': 1, + 'ᨳᩯ᩠': 1, + 'ᨴᩮ᩠': 1, + 'ᨴᩮᩢᩢ': 1, + 'ᨴᩮᩢ᩵': 1, + 'ᨴᩯ᩠': 1, + 'ᨴᩯᩢ': 1, + 'ᨴᩰ᩠': 1, + 'ᨶᩮ': 1, + 'ᨶᩯ᩠': 1, + 'ᨶᩱ': 1, + 'ᨷᩕ': 1, + 'ᨷᩮ᩠': 1, + 'ᨸᩮ᩠': 1, + 'ᨸᩮᩢ᩵': 1, + 'ᨹᩪᩢᩱ': 1, + 'ᨹᩮᩨ᩠': 1, + 'ᨻᩕ᩠': 1, + 'ᨻᩮ᩠': 1, + 'ᨻᩱ': 1, + 'ᨽᩱ᩠': 1, + 'ᨾᩮ᩠': 1, + 'ᨾᩯ᩵': 1, + 'ᨾᩱ': 1, + 'ᨿᩮ᩠': 1, + 'ᨿᩰ': 1, + 'ᩁᩢᩭ': 1, + 'ᩁᩮ᩠': 1, + 'ᩁᩮᩨ᩠': 1, + 'ᩁᩯ᩠': 1, + 'ᩃᩮ᩠': 1, + 'ᩃᩯ': 1, + 'ᩃᩰ᩠': 1, + 'ᩅᩮ': 1, + 'ᩅᩮ᩠': 1, + 'ᩅᩱᩢ': 1, + 'ᩈᩮ': 1, + 'ᩈᩮ᩠': 1, + 'ᩈᩯ᩠': 1, + 'ᩈᩱ': 1, + 'ᩈᩱ᩠': 1, + 'ᩉᩕᩨ': 1, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + 'ᩉᩮᩖᩨ': 1, + 'ᩉᩮ᩠': 1, + 'ᩉᩯ᩠': 1, + 'ᩉᩯ᩵': 1, + 'ᩉᩱ᩠': 1, + '⛓️‍💥': 4, + '⛹️‍♀️': 3, + '⛹️‍♂️': 3, + '⛹🏻‍♀️': 5, + '⛹🏻‍♂️': 5, + '⛹🏼‍♀️': 5, + '⛹🏼‍♂️': 5, + '⛹🏽‍♀️': 5, + '⛹🏽‍♂️': 5, + '⛹🏾‍♀️': 5, + '⛹🏾‍♂️': 5, + '⛹🏿‍♀️': 5, + '⛹🏿‍♂️': 5, + '❤️‍🔥': 4, + '❤️‍🩹': 4, + 'ꦏꦃ': 1, + 'ꦏꦺ': 1, + 'ꦏꦺꦴ': 1, + 'ꦏꦿ': 1, + 'ꦏꦿꦁ': 1, + 'ꦏꦿꦂ': 1, + 'ꦒꦸꦃ': 1, + 'ꦒꦺꦴ': 1, + 'ꦒꦿ': 1, + 'ꦒꦿꦺ': 1, + 'ꦔꦃ': 1, + 'ꦔꦺ': 1, + 'ꦔꦺꦴ': 1, + 'ꦔꦿ': 1, + 'ꦕꦃ': 1, + 'ꦕꦺꦴ': 1, + 'ꦗꦃ': 1, + 'ꦗꦺ': 1, + 'ꦗꦿꦺꦴ': 1, + 'ꦝꦃ': 1, + 'ꦝꦺ': 1, + 'ꦝꦺꦴ': 1, + 'ꦝꦺꦴꦂ': 1, + 'ꦠꦸꦃ': 1, + 'ꦠꦺ': 1, + 'ꦠꦺꦃ': 1, + 'ꦠꦺꦴ': 1, + 'ꦠꦾ': 1, + 'ꦠꦿ': 1, + 'ꦠꦿꦶ': 1, + 'ꦠ꧀ꦱ꧀ꦮ': 3, + 'ꦢꦃ': 1, + 'ꦢꦺ': 1, + 'ꦢꦺꦴ': 1, + 'ꦢꦾ': 1, + 'ꦢꦿ': 1, + 'ꦤꦺ': 1, + 'ꦤꦺꦃ': 1, + 'ꦤꦺꦴ': 1, + 'ꦥꦃ': 1, + 'ꦥꦺ': 1, + 'ꦥꦺꦴ': 1, + 'ꦥꦿ': 1, + 'ꦥꦿꦃ': 1, + 'ꦥꦿꦶ': 1, + 'ꦥꦿꦺꦴ': 1, + 'ꦧꦺ': 1, + 'ꦧꦺꦃ': 1, + 'ꦧꦺꦴ': 1, + 'ꦧꦿ': 1, + 'ꦩꦃ': 1, + 'ꦩꦺ': 1, + 'ꦩꦺꦃ': 1, + 'ꦩꦺꦴ': 1, + 'ꦩꦿ': 1, + 'ꦩꦿꦃ': 1, + 'ꦩꦿꦶꦃ': 1, + 'ꦪꦃ': 1, + 'ꦪꦸꦃ': 1, + 'ꦪꦺꦴ': 1, + 'ꦫꦶꦃ': 1, + 'ꦫꦺ': 1, + 'ꦫꦺꦴ': 1, + 'ꦫꦾ': 1, + 'ꦭꦃ': 1, + 'ꦭꦶꦃ': 1, + 'ꦭꦺ': 1, + 'ꦭꦺꦃ': 1, + 'ꦭꦺꦴ': 1, + 'ꦭꦺꦴꦁ': 1, + 'ꦮꦃ': 1, + 'ꦮꦶꦃ': 1, + 'ꦮꦸꦃ': 1, + 'ꦮꦺ': 1, + 'ꦮꦺꦴ': 1, + 'ꦮꦺꦴꦁ': 1, + 'ꦮꦺꦴꦃ': 1, + 'ꦮꦿꦸꦃ': 1, + 'ꦱꦶꦃ': 1, + 'ꦱꦺ': 1, + 'ꦱꦺꦴ': 1, + 'ꦱꦾ': 1, + 'ꦱꦿ': 1, + 'ꦱꦿꦃ': 1, + 'ꦲꦺ': 1, + 'ꦲꦺꦴ': 1, + '𑄃𑄬': 1, + '𑄃𑄬𑄭': 1, + '𑄇𑄬': 1, + '𑄈𑄬': 1, + '𑄉𑄬': 1, + '𑄉𑄬𑄭': 1, + '𑄊𑄬': 1, + '𑄌𑄬': 1, + '𑄎𑄬': 1, + '𑄎𑄬𑄭': 1, + '𑄏𑄬': 1, + '𑄑𑄬': 1, + '𑄖𑄬': 1, + '𑄗𑄬': 1, + '𑄘𑄬': 1, + '𑄙𑄬': 1, + '𑄚𑄬': 1, + '𑄚𑄬𑄭': 1, + '𑄛𑄬': 1, + '𑄜𑄬': 1, + '𑄝𑄬': 1, + '𑄞𑄬': 1, + '𑄞𑄬𑄭': 1, + '𑄟𑄬': 1, + '𑄠𑄬': 1, + '𑄠𑄬𑄭': 1, + '𑄡𑄬': 1, + '𑄡𑄬𑄭': 1, + '𑄢𑄬': 1, + '𑄢𑄬𑄭': 1, + '𑄣𑄬': 1, + '𑄣𑄬𑄭': 1, + '𑄥𑄬': 1, + '𑄦𑄬': 1, + '𑌅𑌂': 1, + '𑌕𑌂': 1, + '𑌕𑌾': 1, + '𑌕𑌾𑌂': 1, + '𑌕𑌾𑌃': 1, + '𑌕𑌿': 1, + '𑌕𑍂': 1, + '𑌕𑍃': 1, + '𑌕𑍇': 1, + '𑌕𑍋': 1, + '𑌗𑌾': 1, + '𑌗𑌿': 1, + '𑌗𑍁': 1, + '𑌗𑍃': 1, + '𑌗𑍇': 1, + '𑌗𑍌': 1, + '𑌘𑌾': 1, + '𑌘𑍇': 1, + '𑌘𑍋': 1, + '𑌚𑌾': 1, + '𑌚𑌿': 1, + '𑌚𑍇': 1, + '𑌚𑍈': 1, + '𑌛𑌾': 1, + '𑌛𑍁': 1, + '𑌛𑍇': 1, + '𑌜𑌾': 1, + '𑌜𑌿': 1, + '𑌜𑍇': 1, + '𑌞𑌾': 1, + '𑌟𑌃': 1, + '𑌟𑌾': 1, + '𑌟𑌾𑌂': 1, + '𑌟𑌾𑌃': 1, + '𑌠𑌾': 1, + '𑌠𑍇': 1, + '𑌠𑍈𑌃': 1, + '𑌡𑌂': 1, + '𑌡𑌃': 1, + '𑌡𑌿': 1, + '𑌣𑌂': 1, + '𑌣𑌾': 1, + '𑌣𑌾𑌂': 1, + '𑌣𑌿': 1, + '𑌣𑍁': 1, + '𑌣𑍇': 1, + '𑌤𑌂': 1, + '𑌤𑌃': 1, + '𑌤𑌾': 1, + '𑌤𑌾𑌂': 1, + '𑌤𑌾𑌃': 1, + '𑌤𑌿': 1, + '𑌤𑌿𑌂': 1, + '𑌤𑌿𑌃': 1, + '𑌤𑍁': 1, + '𑌤𑍁𑌂': 1, + '𑌤𑍃': 1, + '𑌤𑍇': 1, + '𑌤𑍇𑌃': 1, + '𑌤𑍈': 1, + '𑌤𑍋': 1, + '𑌤𑍌': 1, + '𑌥𑌂': 1, + '𑌥𑌾': 1, + '𑌥𑌾𑌂': 1, + '𑌥𑌿': 1, + '𑌦𑌂': 1, + '𑌦𑌃': 1, + '𑌦𑌾': 1, + '𑌦𑌿': 1, + '𑌦𑍁': 1, + '𑌦𑍃': 1, + '𑌦𑍇': 1, + '𑌦𑍋': 1, + '𑌧𑌃': 1, + '𑌧𑌾': 1, + '𑌧𑌾𑌃': 1, + '𑌧𑌿': 1, + '𑌧𑌿𑌂': 1, + '𑌧𑍁': 1, + '𑌧𑍃': 1, + '𑌧𑍇': 1, + '𑌧𑍇𑌃': 1, + '𑌧𑍈𑌃': 1, + '𑌨𑌂': 1, + '𑌨𑌃': 1, + '𑌨𑌾': 1, + '𑌨𑌾𑌂': 1, + '𑌨𑌾𑌃': 1, + '𑌨𑌿': 1, + '𑌨𑌿𑌃': 1, + '𑌨𑍁': 1, + '𑌨𑍂': 1, + '𑌨𑍇': 1, + '𑌨𑍈': 1, + '𑌨𑍈𑌃': 1, + '𑌨𑍋': 1, + '𑌪𑌂': 1, + '𑌪𑌾': 1, + '𑌪𑌿': 1, + '𑌪𑍁': 1, + '𑌪𑍂': 1, + '𑌪𑍇': 1, + '𑌪𑍋': 1, + '𑌬𑌾': 1, + '𑌬𑍃': 1, + '𑌭𑌾': 1, + '𑌭𑌿': 1, + '𑌭𑌿𑌃': 1, + '𑌭𑍁': 1, + '𑌭𑍂': 1, + '𑌭𑍇': 1, + '𑌮𑌂': 1, + '𑌮𑌾': 1, + '𑌮𑌿': 1, + '𑌮𑌿𑌂': 1, + '𑌮𑍁': 1, + '𑌮𑍂': 1, + '𑌮𑍇': 1, + '𑌮𑍈': 1, + '𑌮𑍋': 1, + '𑌮𑍌': 1, + '𑌯𑌂': 1, + '𑌯𑌃': 1, + '𑌯𑌾': 1, + '𑌯𑌾𑌂': 1, + '𑌯𑌾𑌃': 1, + '𑌯𑌿': 1, + '𑌯𑍁': 1, + '𑌯𑍁𑌃': 1, + '𑌯𑍇': 1, + '𑌯𑍈': 1, + '𑌯𑍈𑌃': 1, + '𑌯𑍋': 1, + '𑌯𑍋𑌃': 1, + '𑌰𑌂': 1, + '𑌰𑌃': 1, + '𑌰𑌾': 1, + '𑌰𑌾𑌃': 1, + '𑌰𑌿': 1, + '𑌰𑌿𑌂': 1, + '𑌰𑍁': 1, + '𑌰𑍂': 1, + '𑌰𑍇': 1, + '𑌰𑍋': 1, + '𑌰𑍌': 1, + '𑌲𑌂': 1, + '𑌲𑌾': 1, + '𑌲𑌾𑌃': 1, + '𑌲𑌿': 1, + '𑌲𑌿𑌂': 1, + '𑌲𑍇': 1, + '𑌲𑍈': 1, + '𑌲𑍋': 1, + '𑌵𑌂': 1, + '𑌵𑌃': 1, + '𑌵𑌾': 1, + '𑌵𑌾𑌂': 1, + '𑌵𑌾𑌃': 1, + '𑌵𑌿': 1, + '𑌵𑍃': 1, + '𑌵𑍇': 1, + '𑌵𑍈': 1, + '𑌵𑍋': 1, + '𑌶𑌂': 1, + '𑌶𑌃': 1, + '𑌶𑌾': 1, + '𑌶𑌾𑌃': 1, + '𑌶𑌿': 1, + '𑌶𑍁': 1, + '𑌶𑍂': 1, + '𑌶𑍇': 1, + '𑌶𑍋': 1, + '𑌷𑌂': 1, + '𑌷𑌾': 1, + '𑌷𑌾𑌂': 1, + '𑌷𑌾𑌃': 1, + '𑌷𑌿': 1, + '𑌷𑍁': 1, + '𑌷𑍇': 1, + '𑌷𑍈': 1, + '𑌷𑍋': 1, + '𑌸𑌂': 1, + '𑌸𑌃': 1, + '𑌸𑌾': 1, + '𑌸𑌾𑌂': 1, + '𑌸𑌿': 1, + '𑌸𑍁': 1, + '𑌸𑍂': 1, + '𑌸𑍇': 1, + '𑌸𑍋': 1, + '𑌸𑍌': 1, + '𑌹𑌾': 1, + '𑌹𑌾𑌃': 1, + '𑌹𑌿': 1, + '𑌹𑍇': 1, + '𑌹𑍋': 1, + '🍄‍🟫': 5, + '🍋‍🟩': 5, + '🏃‍♀️': 4, + '🏃‍♀️‍➡️': 6, + '🏃‍♂️': 4, + '🏃‍♂️‍➡️': 6, + '🏃‍➡️': 4, + '🏃🏻‍♀️': 6, + '🏃🏻‍♀️‍➡️': 8, + '🏃🏻‍♂️': 6, + '🏃🏻‍♂️‍➡️': 8, + '🏃🏻‍➡️': 6, + '🏃🏼‍♀️': 6, + '🏃🏼‍♀️‍➡️': 8, + '🏃🏼‍♂️': 6, + '🏃🏼‍♂️‍➡️': 8, + '🏃🏼‍➡️': 6, + '🏃🏽‍♀️': 6, + '🏃🏽‍♀️‍➡️': 8, + '🏃🏽‍♂️': 6, + '🏃🏽‍♂️‍➡️': 8, + '🏃🏽‍➡️': 6, + '🏃🏾‍♀️': 6, + '🏃🏾‍♀️‍➡️': 8, + '🏃🏾‍♂️': 6, + '🏃🏾‍♂️‍➡️': 8, + '🏃🏾‍➡️': 6, + '🏃🏿‍♀️': 6, + '🏃🏿‍♀️‍➡️': 8, + '🏃🏿‍♂️': 6, + '🏃🏿‍♂️‍➡️': 8, + '🏃🏿‍➡️': 6, + '🏄‍♀️': 4, + '🏄‍♂️': 4, + '🏄🏻‍♀️': 6, + '🏄🏻‍♂️': 6, + '🏄🏼‍♀️': 6, + '🏄🏼‍♂️': 6, + '🏄🏽‍♀️': 6, + '🏄🏽‍♂️': 6, + '🏄🏾‍♀️': 6, + '🏄🏾‍♂️': 6, + '🏄🏿‍♀️': 6, + '🏄🏿‍♂️': 6, + '🏊‍♀️': 4, + '🏊‍♂️': 4, + '🏊🏻‍♀️': 6, + '🏊🏻‍♂️': 6, + '🏊🏼‍♀️': 6, + '🏊🏼‍♂️': 6, + '🏊🏽‍♀️': 6, + '🏊🏽‍♂️': 6, + '🏊🏾‍♀️': 6, + '🏊🏾‍♂️': 6, + '🏊🏿‍♀️': 6, + '🏊🏿‍♂️': 6, + '🏋️‍♀️': 3, + '🏋️‍♂️': 3, + '🏋🏻‍♀️': 5, + '🏋🏻‍♂️': 5, + '🏋🏼‍♀️': 5, + '🏋🏼‍♂️': 5, + '🏋🏽‍♀️': 5, + '🏋🏽‍♂️': 5, + '🏋🏾‍♀️': 5, + '🏋🏾‍♂️': 5, + '🏋🏿‍♀️': 5, + '🏋🏿‍♂️': 5, + '🏌️‍♀️': 3, + '🏌️‍♂️': 3, + '🏌🏻‍♀️': 5, + '🏌🏻‍♂️': 5, + '🏌🏼‍♀️': 5, + '🏌🏼‍♂️': 5, + '🏌🏽‍♀️': 5, + '🏌🏽‍♂️': 5, + '🏌🏾‍♀️': 5, + '🏌🏾‍♂️': 5, + '🏌🏿‍♀️': 5, + '🏌🏿‍♂️': 5, + '🏳️‍⚧️': 3, + '🏳️‍🌈': 4, + '🏴‍☠️': 4, + '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 8, + '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 8, + '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 8, + '🐈‍⬛': 5, + '🐕‍🦺': 5, + '🐦‍⬛': 5, + '🐦‍🔥': 5, + '🐻‍❄️': 4, + '👁️‍🗨️': 3, + '👨‍⚕️': 4, + '👨‍⚖️': 4, + '👨‍✈️': 4, + '👨‍❤️‍👨': 7, + '👨‍❤️‍💋‍👨': 10, + '👨‍🌾': 5, + '👨‍🍳': 5, + '👨‍🍼': 5, + '👨‍🎓': 5, + '👨‍🎤': 5, + '👨‍🎨': 5, + '👨‍🏫': 5, + '👨‍🏭': 5, + '👨‍👦': 5, + '👨‍👦‍👦': 8, + '👨‍👧': 5, + '👨‍👧‍👦': 8, + '👨‍👧‍👧': 8, + '👨‍👨‍👦': 8, + '👨‍👨‍👦‍👦': 11, + '👨‍👨‍👧': 8, + '👨‍👨‍👧‍👦': 11, + '👨‍👨‍👧‍👧': 11, + '👨‍👩‍👦': 8, + '👨‍👩‍👦‍👦': 11, + '👨‍👩‍👧': 8, + '👨‍👩‍👧‍👦': 11, + '👨‍👩‍👧‍👧': 11, + '👨‍💻': 5, + '👨‍💼': 5, + '👨‍🔧': 5, + '👨‍🔬': 5, + '👨‍🚀': 5, + '👨‍🚒': 5, + '👨‍🦯': 5, + '👨‍🦯‍➡️': 7, + '👨‍🦰': 5, + '👨‍🦱': 5, + '👨‍🦲': 5, + '👨‍🦳': 5, + '👨‍🦼': 5, + '👨‍🦼‍➡️': 7, + '👨‍🦽': 5, + '👨‍🦽‍➡️': 7, + '👨🏻‍⚕️': 6, + '👨🏻‍⚖️': 6, + '👨🏻‍✈️': 6, + '👨🏻‍❤️‍👨🏻': 11, + '👨🏻‍❤️‍👨🏽': 11, + '👨🏻‍❤️‍👨🏿': 11, + '👨🏻‍❤️‍💋‍👨🏻': 14, + '👨🏻‍❤️‍💋‍👨🏽': 14, + '👨🏻‍❤️‍💋‍👨🏾': 14, + '👨🏻‍❤️‍💋‍👨🏿': 14, + '👨🏻‍🌾': 7, + '👨🏻‍🍳': 7, + '👨🏻‍🍼': 7, + '👨🏻‍🎓': 7, + '👨🏻‍🎤': 7, + '👨🏻‍🎨': 7, + '👨🏻‍🏫': 7, + '👨🏻‍🏭': 7, + '👨🏻‍🐰‍👨🏼': 12, + '👨🏻‍🐰‍👨🏽': 12, + '👨🏻‍🐰‍👨🏾': 12, + '👨🏻‍🐰‍👨🏿': 12, + '👨🏻‍💻': 7, + '👨🏻‍💼': 7, + '👨🏻‍🔧': 7, + '👨🏻‍🔬': 7, + '👨🏻‍🚀': 7, + '👨🏻‍🚒': 7, + '👨🏻‍🤝‍👨🏼': 12, + '👨🏻‍🤝‍👨🏽': 12, + '👨🏻‍🤝‍👨🏾': 12, + '👨🏻‍🤝‍👨🏿': 12, + '👨🏻‍🦯': 7, + '👨🏻‍🦯‍➡️': 9, + '👨🏻‍🦰': 7, + '👨🏻‍🦱': 7, + '👨🏻‍🦲': 7, + '👨🏻‍🦳': 7, + '👨🏻‍🦼': 7, + '👨🏻‍🦼‍➡️': 9, + '👨🏻‍🦽': 7, + '👨🏻‍🦽‍➡️': 9, + '👨🏻‍🫯‍👨🏼': 11, + '👨🏻‍🫯‍👨🏽': 11, + '👨🏻‍🫯‍👨🏾': 11, + '👨🏻‍🫯‍👨🏿': 11, + '👨🏼‍⚕️': 6, + '👨🏼‍⚖️': 6, + '👨🏼‍✈️': 6, + '👨🏼‍❤️‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏼': 14, + '👨🏼‍❤️‍💋‍👨🏿': 14, + '👨🏼‍🌾': 7, + '👨🏼‍🍳': 7, + '👨🏼‍🍼': 7, + '👨🏼‍🎓': 7, + '👨🏼‍🎤': 7, + '👨🏼‍🎨': 7, + '👨🏼‍🏫': 7, + '👨🏼‍🏭': 7, + '👨🏼‍🐰‍👨🏻': 12, + '👨🏼‍🐰‍👨🏽': 12, + '👨🏼‍🐰‍👨🏿': 12, + '👨🏼‍💻': 7, + '👨🏼‍💼': 7, + '👨🏼‍🔧': 7, + '👨🏼‍🔬': 7, + '👨🏼‍🚀': 7, + '👨🏼‍🚒': 7, + '👨🏼‍🤝‍👨🏻': 12, + '👨🏼‍🤝‍👨🏽': 12, + '👨🏼‍🤝‍👨🏿': 12, + '👨🏼‍🦯': 7, + '👨🏼‍🦯‍➡️': 9, + '👨🏼‍🦰': 7, + '👨🏼‍🦱': 7, + '👨🏼‍🦲': 7, + '👨🏼‍🦳': 7, + '👨🏼‍🦼': 7, + '👨🏼‍🦽': 7, + '👨🏼‍🦽‍➡️': 9, + '👨🏼‍🫯‍👨🏻': 11, + '👨🏼‍🫯‍👨🏽': 11, + '👨🏼‍🫯‍👨🏾': 11, + '👨🏼‍🫯‍👨🏿': 11, + '👨🏽‍⚕️': 6, + '👨🏽‍⚖️': 6, + '👨🏽‍✈️': 6, + '👨🏽‍❤️‍👨🏻': 11, + '👨🏽‍❤️‍👨🏽': 11, + '👨🏽‍❤️‍👨🏿': 11, + '👨🏽‍❤️‍💋‍👨🏻': 14, + '👨🏽‍❤️‍💋‍👨🏽': 14, + '👨🏽‍❤️‍💋‍👨🏿': 14, + '👨🏽‍🌾': 7, + '👨🏽‍🍳': 7, + '👨🏽‍🍼': 7, + '👨🏽‍🎓': 7, + '👨🏽‍🎤': 7, + '👨🏽‍🎨': 7, + '👨🏽‍🏫': 7, + '👨🏽‍🏭': 7, + '👨🏽‍🐰‍👨🏻': 12, + '👨🏽‍🐰‍👨🏼': 12, + '👨🏽‍🐰‍👨🏾': 12, + '👨🏽‍🐰‍👨🏿': 12, + '👨🏽‍💻': 7, + '👨🏽‍💼': 7, + '👨🏽‍🔧': 7, + '👨🏽‍🔬': 7, + '👨🏽‍🚀': 7, + '👨🏽‍🚒': 7, + '👨🏽‍🤝‍👨🏻': 12, + '👨🏽‍🤝‍👨🏼': 12, + '👨🏽‍🤝‍👨🏾': 12, + '👨🏽‍🤝‍👨🏿': 12, + '👨🏽‍🦯': 7, + '👨🏽‍🦯‍➡️': 9, + '👨🏽‍🦰': 7, + '👨🏽‍🦱': 7, + '👨🏽‍🦲': 7, + '👨🏽‍🦳': 7, + '👨🏽‍🦼': 7, + '👨🏽‍🦼‍➡️': 9, + '👨🏽‍🦽': 7, + '👨🏽‍🦽‍➡️': 9, + '👨🏽‍🫯‍👨🏻': 11, + '👨🏽‍🫯‍👨🏼': 11, + '👨🏽‍🫯‍👨🏾': 11, + '👨🏽‍🫯‍👨🏿': 11, + '👨🏾‍⚕️': 6, + '👨🏾‍⚖️': 6, + '👨🏾‍✈️': 6, + '👨🏾‍❤️‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏻': 14, + '👨🏾‍❤️‍💋‍👨🏾': 14, + '👨🏾‍❤️‍💋‍👨🏿': 14, + '👨🏾‍🌾': 7, + '👨🏾‍🍳': 7, + '👨🏾‍🍼': 7, + '👨🏾‍🎓': 7, + '👨🏾‍🎤': 7, + '👨🏾‍🎨': 7, + '👨🏾‍🏫': 7, + '👨🏾‍🏭': 7, + '👨🏾‍🐰‍👨🏻': 12, + '👨🏾‍🐰‍👨🏽': 12, + '👨🏾‍🐰‍👨🏿': 12, + '👨🏾‍💻': 7, + '👨🏾‍💼': 7, + '👨🏾‍🔧': 7, + '👨🏾‍🔬': 7, + '👨🏾‍🚀': 7, + '👨🏾‍🚒': 7, + '👨🏾‍🤝‍👨🏻': 12, + '👨🏾‍🤝‍👨🏽': 12, + '👨🏾‍🤝‍👨🏿': 12, + '👨🏾‍🦯': 7, + '👨🏾‍🦯‍➡️': 9, + '👨🏾‍🦰': 7, + '👨🏾‍🦱': 7, + '👨🏾‍🦲': 7, + '👨🏾‍🦳': 7, + '👨🏾‍🦼': 7, + '👨🏾‍🦽': 7, + '👨🏾‍🦽‍➡️': 9, + '👨🏾‍🫯‍👨🏻': 11, + '👨🏾‍🫯‍👨🏼': 11, + '👨🏾‍🫯‍👨🏽': 11, + '👨🏾‍🫯‍👨🏿': 11, + '👨🏿‍⚕️': 6, + '👨🏿‍⚖️': 6, + '👨🏿‍✈️': 6, + '👨🏿‍❤️‍👨🏻': 11, + '👨🏿‍❤️‍👨🏽': 11, + '👨🏿‍❤️‍👨🏿': 11, + '👨🏿‍❤️‍💋‍👨🏻': 14, + '👨🏿‍❤️‍💋‍👨🏼': 14, + '👨🏿‍❤️‍💋‍👨🏽': 14, + '👨🏿‍❤️‍💋‍👨🏾': 14, + '👨🏿‍❤️‍💋‍👨🏿': 14, + '👨🏿‍🌾': 7, + '👨🏿‍🍳': 7, + '👨🏿‍🍼': 7, + '👨🏿‍🎓': 7, + '👨🏿‍🎤': 7, + '👨🏿‍🎨': 7, + '👨🏿‍🏫': 7, + '👨🏿‍🏭': 7, + '👨🏿‍🐰‍👨🏻': 12, + '👨🏿‍🐰‍👨🏼': 12, + '👨🏿‍🐰‍👨🏽': 12, + '👨🏿‍🐰‍👨🏾': 12, + '👨🏿‍💻': 7, + '👨🏿‍💼': 7, + '👨🏿‍🔧': 7, + '👨🏿‍🔬': 7, + '👨🏿‍🚀': 7, + '👨🏿‍🚒': 7, + '👨🏿‍🤝‍👨🏻': 12, + '👨🏿‍🤝‍👨🏼': 12, + '👨🏿‍🤝‍👨🏽': 12, + '👨🏿‍🤝‍👨🏾': 12, + '👨🏿‍🦯': 7, + '👨🏿‍🦯‍➡️': 9, + '👨🏿‍🦰': 7, + '👨🏿‍🦱': 7, + '👨🏿‍🦲': 7, + '👨🏿‍🦳': 7, + '👨🏿‍🦼': 7, + '👨🏿‍🦼‍➡️': 9, + '👨🏿‍🦽': 7, + '👨🏿‍🦽‍➡️': 9, + '👨🏿‍🫯‍👨🏻': 11, + '👨🏿‍🫯‍👨🏼': 11, + '👨🏿‍🫯‍👨🏽': 11, + '👨🏿‍🫯‍👨🏾': 11, + '👩‍⚕️': 4, + '👩‍⚖️': 4, + '👩‍✈️': 4, + '👩‍❤️‍👨': 7, + '👩‍❤️‍👩': 7, + '👩‍❤️‍💋‍👨': 10, + '👩‍❤️‍💋‍👩': 10, + '👩‍🌾': 5, + '👩‍🍳': 5, + '👩‍🍼': 5, + '👩‍🎓': 5, + '👩‍🎤': 5, + '👩‍🎨': 5, + '👩‍🏫': 5, + '👩‍🏭': 5, + '👩‍👦': 5, + '👩‍👦‍👦': 8, + '👩‍👧': 5, + '👩‍👧‍👦': 8, + '👩‍👧‍👧': 8, + '👩‍👩‍👦': 8, + '👩‍👩‍👦‍👦': 11, + '👩‍👩‍👧': 8, + '👩‍👩‍👧‍👦': 11, + '👩‍👩‍👧‍👧': 11, + '👩‍💻': 5, + '👩‍💼': 5, + '👩‍🔧': 5, + '👩‍🔬': 5, + '👩‍🚀': 5, + '👩‍🚒': 5, + '👩‍🦯': 5, + '👩‍🦯‍➡️': 7, + '👩‍🦰': 5, + '👩‍🦱': 5, + '👩‍🦲': 5, + '👩‍🦳': 5, + '👩‍🦼': 5, + '👩‍🦼‍➡️': 7, + '👩‍🦽': 5, + '👩‍🦽‍➡️': 7, + '👩🏻‍⚕️': 6, + '👩🏻‍⚖️': 6, + '👩🏻‍✈️': 6, + '👩🏻‍❤️‍👨🏻': 11, + '👩🏻‍❤️‍👨🏿': 11, + '👩🏻‍❤️‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👨🏻': 14, + '👩🏻‍❤️‍💋‍👨🏽': 14, + '👩🏻‍❤️‍💋‍👨🏿': 14, + '👩🏻‍❤️‍💋‍👩🏻': 14, + '👩🏻‍❤️‍💋‍👩🏽': 14, + '👩🏻‍❤️‍💋‍👩🏿': 14, + '👩🏻‍🌾': 7, + '👩🏻‍🍳': 7, + '👩🏻‍🍼': 7, + '👩🏻‍🎓': 7, + '👩🏻‍🎤': 7, + '👩🏻‍🎨': 7, + '👩🏻‍🏫': 7, + '👩🏻‍🏭': 7, + '👩🏻‍🐰‍👩🏼': 12, + '👩🏻‍🐰‍👩🏽': 12, + '👩🏻‍🐰‍👩🏾': 12, + '👩🏻‍🐰‍👩🏿': 12, + '👩🏻‍💻': 7, + '👩🏻‍💼': 7, + '👩🏻‍🔧': 7, + '👩🏻‍🔬': 7, + '👩🏻‍🚀': 7, + '👩🏻‍🚒': 7, + '👩🏻‍🤝‍👨🏽': 12, + '👩🏻‍🤝‍👨🏿': 12, + '👩🏻‍🤝‍👩🏼': 12, + '👩🏻‍🤝‍👩🏽': 12, + '👩🏻‍🤝‍👩🏾': 12, + '👩🏻‍🤝‍👩🏿': 12, + '👩🏻‍🦯': 7, + '👩🏻‍🦯‍➡️': 9, + '👩🏻‍🦰': 7, + '👩🏻‍🦱': 7, + '👩🏻‍🦲': 7, + '👩🏻‍🦳': 7, + '👩🏻‍🦼': 7, + '👩🏻‍🦼‍➡️': 9, + '👩🏻‍🦽': 7, + '👩🏻‍🦽‍➡️': 9, + '👩🏻‍🫯‍👩🏼': 11, + '👩🏻‍🫯‍👩🏽': 11, + '👩🏻‍🫯‍👩🏾': 11, + '👩🏻‍🫯‍👩🏿': 11, + '👩🏼‍⚕️': 6, + '👩🏼‍⚖️': 6, + '👩🏼‍✈️': 6, + '👩🏼‍❤️‍👨🏼': 11, + '👩🏼‍❤️‍👩🏼': 11, + '👩🏼‍❤️‍💋‍👨🏼': 14, + '👩🏼‍❤️‍💋‍👩🏼': 14, + '👩🏼‍🌾': 7, + '👩🏼‍🍳': 7, + '👩🏼‍🍼': 7, + '👩🏼‍🎓': 7, + '👩🏼‍🎤': 7, + '👩🏼‍🎨': 7, + '👩🏼‍🏫': 7, + '👩🏼‍🏭': 7, + '👩🏼‍🐰‍👩🏻': 12, + '👩🏼‍🐰‍👩🏿': 12, + '👩🏼‍💻': 7, + '👩🏼‍💼': 7, + '👩🏼‍🔧': 7, + '👩🏼‍🔬': 7, + '👩🏼‍🚀': 7, + '👩🏼‍🚒': 7, + '👩🏼‍🤝‍👩🏻': 12, + '👩🏼‍🤝‍👩🏽': 12, + '👩🏼‍🤝‍👩🏿': 12, + '👩🏼‍🦯': 7, + '👩🏼‍🦯‍➡️': 9, + '👩🏼‍🦰': 7, + '👩🏼‍🦱': 7, + '👩🏼‍🦲': 7, + '👩🏼‍🦳': 7, + '👩🏼‍🦼': 7, + '👩🏼‍🦽': 7, + '👩🏼‍🫯‍👩🏻': 11, + '👩🏼‍🫯‍👩🏽': 11, + '👩🏼‍🫯‍👩🏾': 11, + '👩🏼‍🫯‍👩🏿': 11, + '👩🏽‍⚕️': 6, + '👩🏽‍⚖️': 6, + '👩🏽‍✈️': 6, + '👩🏽‍❤️‍👨🏽': 11, + '👩🏽‍❤️‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👨🏻': 14, + '👩🏽‍❤️‍💋‍👨🏽': 14, + '👩🏽‍❤️‍💋‍👨🏿': 14, + '👩🏽‍❤️‍💋‍👩🏻': 14, + '👩🏽‍❤️‍💋‍👩🏽': 14, + '👩🏽‍❤️‍💋‍👩🏿': 14, + '👩🏽‍🌾': 7, + '👩🏽‍🍳': 7, + '👩🏽‍🍼': 7, + '👩🏽‍🎓': 7, + '👩🏽‍🎤': 7, + '👩🏽‍🎨': 7, + '👩🏽‍🏫': 7, + '👩🏽‍🏭': 7, + '👩🏽‍🐰‍👩🏻': 12, + '👩🏽‍🐰‍👩🏾': 12, + '👩🏽‍🐰‍👩🏿': 12, + '👩🏽‍💻': 7, + '👩🏽‍💼': 7, + '👩🏽‍🔧': 7, + '👩🏽‍🔬': 7, + '👩🏽‍🚀': 7, + '👩🏽‍🚒': 7, + '👩🏽‍🤝‍👨🏻': 12, + '👩🏽‍🤝‍👨🏿': 12, + '👩🏽‍🤝‍👩🏻': 12, + '👩🏽‍🤝‍👩🏼': 12, + '👩🏽‍🤝‍👩🏾': 12, + '👩🏽‍🤝‍👩🏿': 12, + '👩🏽‍🦯': 7, + '👩🏽‍🦯‍➡️': 9, + '👩🏽‍🦰': 7, + '👩🏽‍🦱': 7, + '👩🏽‍🦲': 7, + '👩🏽‍🦳': 7, + '👩🏽‍🦼': 7, + '👩🏽‍🦼‍➡️': 9, + '👩🏽‍🦽': 7, + '👩🏽‍🦽‍➡️': 9, + '👩🏽‍🫯‍👩🏻': 11, + '👩🏽‍🫯‍👩🏼': 11, + '👩🏽‍🫯‍👩🏾': 11, + '👩🏽‍🫯‍👩🏿': 11, + '👩🏾‍⚕️': 6, + '👩🏾‍⚖️': 6, + '👩🏾‍✈️': 6, + '👩🏾‍❤️‍👨🏾': 11, + '👩🏾‍❤️‍👩🏾': 11, + '👩🏾‍❤️‍💋‍👨🏾': 14, + '👩🏾‍❤️‍💋‍👩🏾': 14, + '👩🏾‍🌾': 7, + '👩🏾‍🍳': 7, + '👩🏾‍🍼': 7, + '👩🏾‍🎓': 7, + '👩🏾‍🎤': 7, + '👩🏾‍🎨': 7, + '👩🏾‍🏫': 7, + '👩🏾‍🏭': 7, + '👩🏾‍🐰‍👩🏻': 12, + '👩🏾‍🐰‍👩🏽': 12, + '👩🏾‍🐰‍👩🏿': 12, + '👩🏾‍💻': 7, + '👩🏾‍💼': 7, + '👩🏾‍🔧': 7, + '👩🏾‍🔬': 7, + '👩🏾‍🚀': 7, + '👩🏾‍🚒': 7, + '👩🏾‍🤝‍👩🏻': 12, + '👩🏾‍🤝‍👩🏽': 12, + '👩🏾‍🤝‍👩🏿': 12, + '👩🏾‍🦯': 7, + '👩🏾‍🦯‍➡️': 9, + '👩🏾‍🦰': 7, + '👩🏾‍🦱': 7, + '👩🏾‍🦲': 7, + '👩🏾‍🦳': 7, + '👩🏾‍🦼': 7, + '👩🏾‍🦽': 7, + '👩🏾‍🦽‍➡️': 9, + '👩🏾‍🫯‍👩🏻': 11, + '👩🏾‍🫯‍👩🏼': 11, + '👩🏾‍🫯‍👩🏽': 11, + '👩🏾‍🫯‍👩🏿': 11, + '👩🏿‍⚕️': 6, + '👩🏿‍⚖️': 6, + '👩🏿‍✈️': 6, + '👩🏿‍❤️‍👨🏻': 11, + '👩🏿‍❤️‍👨🏿': 11, + '👩🏿‍❤️‍👩🏿': 11, + '👩🏿‍❤️‍💋‍👨🏻': 14, + '👩🏿‍❤️‍💋‍👨🏽': 14, + '👩🏿‍❤️‍💋‍👨🏿': 14, + '👩🏿‍❤️‍💋‍👩🏻': 14, + '👩🏿‍❤️‍💋‍👩🏽': 14, + '👩🏿‍❤️‍💋‍👩🏿': 14, + '👩🏿‍🌾': 7, + '👩🏿‍🍳': 7, + '👩🏿‍🍼': 7, + '👩🏿‍🎓': 7, + '👩🏿‍🎤': 7, + '👩🏿‍🎨': 7, + '👩🏿‍🏫': 7, + '👩🏿‍🏭': 7, + '👩🏿‍🐰‍👩🏻': 12, + '👩🏿‍🐰‍👩🏼': 12, + '👩🏿‍🐰‍👩🏽': 12, + '👩🏿‍🐰‍👩🏾': 12, + '👩🏿‍💻': 7, + '👩🏿‍💼': 7, + '👩🏿‍🔧': 7, + '👩🏿‍🔬': 7, + '👩🏿‍🚀': 7, + '👩🏿‍🚒': 7, + '👩🏿‍🤝‍👨🏻': 12, + '👩🏿‍🤝‍👨🏽': 12, + '👩🏿‍🤝‍👩🏻': 12, + '👩🏿‍🤝‍👩🏼': 12, + '👩🏿‍🤝‍👩🏽': 12, + '👩🏿‍🤝‍👩🏾': 12, + '👩🏿‍🦯': 7, + '👩🏿‍🦯‍➡️': 9, + '👩🏿‍🦰': 7, + '👩🏿‍🦱': 7, + '👩🏿‍🦲': 7, + '👩🏿‍🦳': 7, + '👩🏿‍🦼': 7, + '👩🏿‍🦼‍➡️': 9, + '👩🏿‍🦽': 7, + '👩🏿‍🦽‍➡️': 9, + '👩🏿‍🫯‍👩🏻': 11, + '👩🏿‍🫯‍👩🏼': 11, + '👩🏿‍🫯‍👩🏽': 11, + '👩🏿‍🫯‍👩🏾': 11, + '👮‍♀️': 4, + '👮‍♂️': 4, + '👮🏻‍♀️': 6, + '👮🏻‍♂️': 6, + '👮🏼‍♀️': 6, + '👮🏼‍♂️': 6, + '👮🏽‍♀️': 6, + '👮🏽‍♂️': 6, + '👮🏾‍♀️': 6, + '👮🏾‍♂️': 6, + '👮🏿‍♀️': 6, + '👮🏿‍♂️': 6, + '👯‍♀️': 4, + '👯‍♂️': 4, + '👯🏻‍♀️': 6, + '👯🏻‍♂️': 6, + '👯🏼‍♀️': 6, + '👯🏼‍♂️': 6, + '👯🏽‍♀️': 6, + '👯🏽‍♂️': 6, + '👯🏾‍♀️': 6, + '👯🏾‍♂️': 6, + '👯🏿‍♀️': 6, + '👯🏿‍♂️': 6, + '👰‍♀️': 4, + '👰‍♂️': 4, + '👰🏻‍♀️': 6, + '👰🏻‍♂️': 6, + '👰🏼‍♀️': 6, + '👰🏼‍♂️': 6, + '👰🏽‍♀️': 6, + '👰🏽‍♂️': 6, + '👰🏾‍♀️': 6, + '👰🏾‍♂️': 6, + '👰🏿‍♀️': 6, + '👰🏿‍♂️': 6, + '👱‍♀️': 4, + '👱‍♂️': 4, + '👱🏻‍♀️': 6, + '👱🏻‍♂️': 6, + '👱🏼‍♀️': 6, + '👱🏼‍♂️': 6, + '👱🏽‍♀️': 6, + '👱🏽‍♂️': 6, + '👱🏾‍♀️': 6, + '👱🏾‍♂️': 6, + '👱🏿‍♀️': 6, + '👱🏿‍♂️': 6, + '👳‍♀️': 4, + '👳‍♂️': 4, + '👳🏻‍♀️': 6, + '👳🏻‍♂️': 6, + '👳🏼‍♀️': 6, + '👳🏼‍♂️': 6, + '👳🏽‍♀️': 6, + '👳🏽‍♂️': 6, + '👳🏾‍♀️': 6, + '👳🏾‍♂️': 6, + '👳🏿‍♀️': 6, + '👳🏿‍♂️': 6, + '👷‍♀️': 4, + '👷‍♂️': 4, + '👷🏻‍♀️': 6, + '👷🏻‍♂️': 6, + '👷🏼‍♀️': 6, + '👷🏼‍♂️': 6, + '👷🏽‍♀️': 6, + '👷🏽‍♂️': 6, + '👷🏾‍♀️': 6, + '👷🏾‍♂️': 6, + '👷🏿‍♀️': 6, + '👷🏿‍♂️': 6, + '💁‍♀️': 4, + '💁‍♂️': 4, + '💁🏻‍♀️': 6, + '💁🏻‍♂️': 6, + '💁🏼‍♀️': 6, + '💁🏼‍♂️': 6, + '💁🏽‍♀️': 6, + '💁🏽‍♂️': 6, + '💁🏾‍♀️': 6, + '💁🏾‍♂️': 6, + '💁🏿‍♀️': 6, + '💁🏿‍♂️': 6, + '💂‍♀️': 4, + '💂‍♂️': 4, + '💂🏻‍♀️': 6, + '💂🏻‍♂️': 6, + '💂🏼‍♀️': 6, + '💂🏼‍♂️': 6, + '💂🏽‍♀️': 6, + '💂🏽‍♂️': 6, + '💂🏾‍♀️': 6, + '💂🏾‍♂️': 6, + '💂🏿‍♀️': 6, + '💂🏿‍♂️': 6, + '💆‍♀️': 4, + '💆‍♂️': 4, + '💆🏻‍♀️': 6, + '💆🏻‍♂️': 6, + '💆🏼‍♀️': 6, + '💆🏼‍♂️': 6, + '💆🏽‍♀️': 6, + '💆🏽‍♂️': 6, + '💆🏾‍♀️': 6, + '💆🏾‍♂️': 6, + '💆🏿‍♀️': 6, + '💆🏿‍♂️': 6, + '💇‍♀️': 4, + '💇‍♂️': 4, + '💇🏻‍♀️': 6, + '💇🏻‍♂️': 6, + '💇🏼‍♀️': 6, + '💇🏼‍♂️': 6, + '💇🏽‍♀️': 6, + '💇🏽‍♂️': 6, + '💇🏾‍♀️': 6, + '💇🏾‍♂️': 6, + '💇🏿‍♀️': 6, + '💇🏿‍♂️': 6, + '🕵️‍♀️': 3, + '🕵️‍♂️': 3, + '🕵🏻‍♀️': 5, + '🕵🏻‍♂️': 5, + '🕵🏼‍♀️': 5, + '🕵🏼‍♂️': 5, + '🕵🏽‍♀️': 5, + '🕵🏽‍♂️': 5, + '🕵🏾‍♀️': 5, + '🕵🏾‍♂️': 5, + '🕵🏿‍♀️': 5, + '🕵🏿‍♂️': 5, + '😮‍💨': 5, + '😵‍💫': 5, + '😶‍🌫️': 4, + '🙂‍↔️': 4, + '🙂‍↕️': 4, + '🙅‍♀️': 4, + '🙅‍♂️': 4, + '🙅🏻‍♀️': 6, + '🙅🏻‍♂️': 6, + '🙅🏼‍♀️': 6, + '🙅🏼‍♂️': 6, + '🙅🏽‍♀️': 6, + '🙅🏽‍♂️': 6, + '🙅🏾‍♀️': 6, + '🙅🏾‍♂️': 6, + '🙅🏿‍♀️': 6, + '🙅🏿‍♂️': 6, + '🙆‍♀️': 4, + '🙆‍♂️': 4, + '🙆🏻‍♀️': 6, + '🙆🏻‍♂️': 6, + '🙆🏼‍♀️': 6, + '🙆🏼‍♂️': 6, + '🙆🏽‍♀️': 6, + '🙆🏽‍♂️': 6, + '🙆🏾‍♀️': 6, + '🙆🏾‍♂️': 6, + '🙆🏿‍♀️': 6, + '🙆🏿‍♂️': 6, + '🙇‍♀️': 4, + '🙇‍♂️': 4, + '🙇🏻‍♀️': 6, + '🙇🏻‍♂️': 6, + '🙇🏼‍♀️': 6, + '🙇🏼‍♂️': 6, + '🙇🏽‍♀️': 6, + '🙇🏽‍♂️': 6, + '🙇🏾‍♀️': 6, + '🙇🏾‍♂️': 6, + '🙇🏿‍♀️': 6, + '🙇🏿‍♂️': 6, + '🙋‍♀️': 4, + '🙋‍♂️': 4, + '🙋🏻‍♀️': 6, + '🙋🏻‍♂️': 6, + '🙋🏼‍♀️': 6, + '🙋🏼‍♂️': 6, + '🙋🏽‍♀️': 6, + '🙋🏽‍♂️': 6, + '🙋🏾‍♀️': 6, + '🙋🏾‍♂️': 6, + '🙋🏿‍♀️': 6, + '🙋🏿‍♂️': 6, + '🙍‍♀️': 4, + '🙍‍♂️': 4, + '🙍🏻‍♀️': 6, + '🙍🏻‍♂️': 6, + '🙍🏼‍♀️': 6, + '🙍🏼‍♂️': 6, + '🙍🏽‍♀️': 6, + '🙍🏽‍♂️': 6, + '🙍🏾‍♀️': 6, + '🙍🏾‍♂️': 6, + '🙍🏿‍♀️': 6, + '🙍🏿‍♂️': 6, + '🙎‍♀️': 4, + '🙎‍♂️': 4, + '🙎🏻‍♀️': 6, + '🙎🏻‍♂️': 6, + '🙎🏼‍♀️': 6, + '🙎🏼‍♂️': 6, + '🙎🏽‍♀️': 6, + '🙎🏽‍♂️': 6, + '🙎🏾‍♀️': 6, + '🙎🏾‍♂️': 6, + '🙎🏿‍♀️': 6, + '🙎🏿‍♂️': 6, + '🚣‍♀️': 4, + '🚣‍♂️': 4, + '🚣🏻‍♀️': 6, + '🚣🏻‍♂️': 6, + '🚣🏼‍♀️': 6, + '🚣🏼‍♂️': 6, + '🚣🏽‍♀️': 6, + '🚣🏽‍♂️': 6, + '🚣🏾‍♀️': 6, + '🚣🏾‍♂️': 6, + '🚣🏿‍♀️': 6, + '🚣🏿‍♂️': 6, + '🚴‍♀️': 4, + '🚴‍♂️': 4, + '🚴🏻‍♀️': 6, + '🚴🏻‍♂️': 6, + '🚴🏼‍♀️': 6, + '🚴🏼‍♂️': 6, + '🚴🏽‍♀️': 6, + '🚴🏽‍♂️': 6, + '🚴🏾‍♀️': 6, + '🚴🏾‍♂️': 6, + '🚴🏿‍♀️': 6, + '🚴🏿‍♂️': 6, + '🚵‍♀️': 4, + '🚵‍♂️': 4, + '🚵🏻‍♀️': 6, + '🚵🏻‍♂️': 6, + '🚵🏼‍♀️': 6, + '🚵🏼‍♂️': 6, + '🚵🏽‍♀️': 6, + '🚵🏽‍♂️': 6, + '🚵🏾‍♀️': 6, + '🚵🏾‍♂️': 6, + '🚵🏿‍♀️': 6, + '🚵🏿‍♂️': 6, + '🚶‍♀️': 4, + '🚶‍♀️‍➡️': 6, + '🚶‍♂️': 4, + '🚶‍♂️‍➡️': 6, + '🚶‍➡️': 4, + '🚶🏻‍♀️': 6, + '🚶🏻‍♀️‍➡️': 8, + '🚶🏻‍♂️': 6, + '🚶🏻‍♂️‍➡️': 8, + '🚶🏻‍➡️': 6, + '🚶🏼‍♀️': 6, + '🚶🏼‍♀️‍➡️': 8, + '🚶🏼‍♂️': 6, + '🚶🏼‍♂️‍➡️': 8, + '🚶🏼‍➡️': 6, + '🚶🏽‍♀️': 6, + '🚶🏽‍♀️‍➡️': 8, + '🚶🏽‍♂️': 6, + '🚶🏽‍♂️‍➡️': 8, + '🚶🏽‍➡️': 6, + '🚶🏾‍♀️': 6, + '🚶🏾‍♀️‍➡️': 8, + '🚶🏾‍♂️': 6, + '🚶🏾‍♂️‍➡️': 8, + '🚶🏾‍➡️': 6, + '🚶🏿‍♀️': 6, + '🚶🏿‍♀️‍➡️': 8, + '🚶🏿‍♂️': 6, + '🚶🏿‍♂️‍➡️': 8, + '🚶🏿‍➡️': 6, + '🤦‍♀️': 4, + '🤦‍♂️': 4, + '🤦🏻‍♀️': 6, + '🤦🏻‍♂️': 6, + '🤦🏼‍♀️': 6, + '🤦🏼‍♂️': 6, + '🤦🏽‍♀️': 6, + '🤦🏽‍♂️': 6, + '🤦🏾‍♀️': 6, + '🤦🏾‍♂️': 6, + '🤦🏿‍♀️': 6, + '🤦🏿‍♂️': 6, + '🤵‍♀️': 4, + '🤵‍♂️': 4, + '🤵🏻‍♀️': 6, + '🤵🏻‍♂️': 6, + '🤵🏼‍♀️': 6, + '🤵🏼‍♂️': 6, + '🤵🏽‍♀️': 6, + '🤵🏽‍♂️': 6, + '🤵🏾‍♀️': 6, + '🤵🏾‍♂️': 6, + '🤵🏿‍♀️': 6, + '🤵🏿‍♂️': 6, + '🤷‍♀️': 4, + '🤷‍♂️': 4, + '🤷🏻‍♀️': 6, + '🤷🏻‍♂️': 6, + '🤷🏼‍♀️': 6, + '🤷🏼‍♂️': 6, + '🤷🏽‍♀️': 6, + '🤷🏽‍♂️': 6, + '🤷🏾‍♀️': 6, + '🤷🏾‍♂️': 6, + '🤷🏿‍♀️': 6, + '🤷🏿‍♂️': 6, + '🤸‍♀️': 4, + '🤸‍♂️': 4, + '🤸🏻‍♀️': 6, + '🤸🏻‍♂️': 6, + '🤸🏼‍♀️': 6, + '🤸🏼‍♂️': 6, + '🤸🏽‍♀️': 6, + '🤸🏽‍♂️': 6, + '🤸🏾‍♀️': 6, + '🤸🏾‍♂️': 6, + '🤸🏿‍♀️': 6, + '🤸🏿‍♂️': 6, + '🤹‍♀️': 4, + '🤹‍♂️': 4, + '🤹🏻‍♀️': 6, + '🤹🏻‍♂️': 6, + '🤹🏼‍♀️': 6, + '🤹🏼‍♂️': 6, + '🤹🏽‍♀️': 6, + '🤹🏽‍♂️': 6, + '🤹🏾‍♀️': 6, + '🤹🏾‍♂️': 6, + '🤹🏿‍♀️': 6, + '🤹🏿‍♂️': 6, + '🤼‍♀️': 4, + '🤼‍♂️': 4, + '🤼🏻‍♀️': 6, + '🤼🏻‍♂️': 6, + '🤼🏼‍♀️': 6, + '🤼🏼‍♂️': 6, + '🤼🏽‍♀️': 6, + '🤼🏽‍♂️': 6, + '🤼🏾‍♀️': 6, + '🤼🏾‍♂️': 6, + '🤼🏿‍♀️': 6, + '🤼🏿‍♂️': 6, + '🤽‍♀️': 4, + '🤽‍♂️': 4, + '🤽🏻‍♀️': 6, + '🤽🏻‍♂️': 6, + '🤽🏼‍♀️': 6, + '🤽🏼‍♂️': 6, + '🤽🏽‍♀️': 6, + '🤽🏽‍♂️': 6, + '🤽🏾‍♀️': 6, + '🤽🏾‍♂️': 6, + '🤽🏿‍♀️': 6, + '🤽🏿‍♂️': 6, + '🤾‍♀️': 4, + '🤾‍♂️': 4, + '🤾🏻‍♀️': 6, + '🤾🏻‍♂️': 6, + '🤾🏼‍♀️': 6, + '🤾🏼‍♂️': 6, + '🤾🏽‍♀️': 6, + '🤾🏽‍♂️': 6, + '🤾🏾‍♀️': 6, + '🤾🏾‍♂️': 6, + '🤾🏿‍♀️': 6, + '🤾🏿‍♂️': 6, + '🦸‍♀️': 4, + '🦸‍♂️': 4, + '🦸🏻‍♀️': 6, + '🦸🏻‍♂️': 6, + '🦸🏼‍♀️': 6, + '🦸🏼‍♂️': 6, + '🦸🏽‍♀️': 6, + '🦸🏽‍♂️': 6, + '🦸🏾‍♀️': 6, + '🦸🏾‍♂️': 6, + '🦸🏿‍♀️': 6, + '🦸🏿‍♂️': 6, + '🦹‍♀️': 4, + '🦹‍♂️': 4, + '🦹🏻‍♀️': 6, + '🦹🏻‍♂️': 6, + '🦹🏼‍♀️': 6, + '🦹🏼‍♂️': 6, + '🦹🏽‍♀️': 6, + '🦹🏽‍♂️': 6, + '🦹🏾‍♀️': 6, + '🦹🏾‍♂️': 6, + '🦹🏿‍♀️': 6, + '🦹🏿‍♂️': 6, + '🧍‍♀️': 4, + '🧍‍♂️': 4, + '🧍🏻‍♀️': 6, + '🧍🏻‍♂️': 6, + '🧍🏼‍♀️': 6, + '🧍🏼‍♂️': 6, + '🧍🏽‍♀️': 6, + '🧍🏽‍♂️': 6, + '🧍🏾‍♀️': 6, + '🧍🏾‍♂️': 6, + '🧍🏿‍♀️': 6, + '🧍🏿‍♂️': 6, + '🧎‍♀️': 4, + '🧎‍♀️‍➡️': 6, + '🧎‍♂️': 4, + '🧎‍♂️‍➡️': 6, + '🧎‍➡️': 4, + '🧎🏻‍♀️': 6, + '🧎🏻‍♀️‍➡️': 8, + '🧎🏻‍♂️': 6, + '🧎🏻‍♂️‍➡️': 8, + '🧎🏻‍➡️': 6, + '🧎🏼‍♀️': 6, + '🧎🏼‍♀️‍➡️': 8, + '🧎🏼‍♂️': 6, + '🧎🏼‍♂️‍➡️': 8, + '🧎🏼‍➡️': 6, + '🧎🏽‍♀️': 6, + '🧎🏽‍♀️‍➡️': 8, + '🧎🏽‍♂️': 6, + '🧎🏽‍♂️‍➡️': 8, + '🧎🏽‍➡️': 6, + '🧎🏾‍♀️': 6, + '🧎🏾‍♀️‍➡️': 8, + '🧎🏾‍♂️': 6, + '🧎🏾‍♂️‍➡️': 8, + '🧎🏾‍➡️': 6, + '🧎🏿‍♀️': 6, + '🧎🏿‍♀️‍➡️': 8, + '🧎🏿‍♂️': 6, + '🧎🏿‍♂️‍➡️': 8, + '🧎🏿‍➡️': 6, + '🧏‍♀️': 4, + '🧏‍♂️': 4, + '🧏🏻‍♀️': 6, + '🧏🏻‍♂️': 6, + '🧏🏼‍♀️': 6, + '🧏🏼‍♂️': 6, + '🧏🏽‍♀️': 6, + '🧏🏽‍♂️': 6, + '🧏🏾‍♀️': 6, + '🧏🏾‍♂️': 6, + '🧏🏿‍♀️': 6, + '🧏🏿‍♂️': 6, + '🧑‍⚕️': 4, + '🧑‍⚖️': 4, + '🧑‍✈️': 4, + '🧑‍🌾': 5, + '🧑‍🍳': 5, + '🧑‍🍼': 5, + '🧑‍🎄': 5, + '🧑‍🎓': 5, + '🧑‍🎤': 5, + '🧑‍🎨': 5, + '🧑‍🏫': 5, + '🧑‍🏭': 5, + '🧑‍💻': 5, + '🧑‍💼': 5, + '🧑‍🔧': 5, + '🧑‍🔬': 5, + '🧑‍🚀': 5, + '🧑‍🚒': 5, + '🧑‍🤝‍🧑': 8, + '🧑‍🦯': 5, + '🧑‍🦯‍➡️': 7, + '🧑‍🦰': 5, + '🧑‍🦱': 5, + '🧑‍🦲': 5, + '🧑‍🦳': 5, + '🧑‍🦼': 5, + '🧑‍🦼‍➡️': 7, + '🧑‍🦽': 5, + '🧑‍🦽‍➡️': 7, + '🧑‍🧑‍🧒': 8, + '🧑‍🧑‍🧒‍🧒': 11, + '🧑‍🧒': 5, + '🧑‍🧒‍🧒': 8, + '🧑‍🩰': 5, + '🧑🏻‍⚕️': 6, + '🧑🏻‍⚖️': 6, + '🧑🏻‍✈️': 6, + '🧑🏻‍❤️‍💋‍🧑🏿': 14, + '🧑🏻‍🌾': 7, + '🧑🏻‍🍳': 7, + '🧑🏻‍🍼': 7, + '🧑🏻‍🎄': 7, + '🧑🏻‍🎓': 7, + '🧑🏻‍🎤': 7, + '🧑🏻‍🎨': 7, + '🧑🏻‍🏫': 7, + '🧑🏻‍🏭': 7, + '🧑🏻‍🐰‍🧑🏽': 12, + '🧑🏻‍🐰‍🧑🏾': 12, + '🧑🏻‍🐰‍🧑🏿': 12, + '🧑🏻‍💻': 7, + '🧑🏻‍💼': 7, + '🧑🏻‍🔧': 7, + '🧑🏻‍🔬': 7, + '🧑🏻‍🚀': 7, + '🧑🏻‍🚒': 7, + '🧑🏻‍🤝‍🧑🏻': 12, + '🧑🏻‍🤝‍🧑🏼': 12, + '🧑🏻‍🤝‍🧑🏽': 12, + '🧑🏻‍🤝‍🧑🏾': 12, + '🧑🏻‍🤝‍🧑🏿': 12, + '🧑🏻‍🦯': 7, + '🧑🏻‍🦯‍➡️': 9, + '🧑🏻‍🦰': 7, + '🧑🏻‍🦱': 7, + '🧑🏻‍🦲': 7, + '🧑🏻‍🦳': 7, + '🧑🏻‍🦼': 7, + '🧑🏻‍🦼‍➡️': 9, + '🧑🏻‍🦽': 7, + '🧑🏻‍🦽‍➡️': 9, + '🧑🏻‍🩰': 7, + '🧑🏻‍🫯‍🧑🏼': 11, + '🧑🏻‍🫯‍🧑🏽': 11, + '🧑🏻‍🫯‍🧑🏾': 11, + '🧑🏻‍🫯‍🧑🏿': 11, + '🧑🏼‍⚕️': 6, + '🧑🏼‍⚖️': 6, + '🧑🏼‍✈️': 6, + '🧑🏼‍🌾': 7, + '🧑🏼‍🍳': 7, + '🧑🏼‍🍼': 7, + '🧑🏼‍🎄': 7, + '🧑🏼‍🎓': 7, + '🧑🏼‍🎤': 7, + '🧑🏼‍🎨': 7, + '🧑🏼‍🏫': 7, + '🧑🏼‍🏭': 7, + '🧑🏼‍🐰‍🧑🏿': 12, + '🧑🏼‍💻': 7, + '🧑🏼‍💼': 7, + '🧑🏼‍🔧': 7, + '🧑🏼‍🔬': 7, + '🧑🏼‍🚀': 7, + '🧑🏼‍🚒': 7, + '🧑🏼‍🤝‍🧑🏻': 12, + '🧑🏼‍🤝‍🧑🏼': 12, + '🧑🏼‍🤝‍🧑🏽': 12, + '🧑🏼‍🤝‍🧑🏿': 12, + '🧑🏼‍🦯': 7, + '🧑🏼‍🦯‍➡️': 9, + '🧑🏼‍🦰': 7, + '🧑🏼‍🦱': 7, + '🧑🏼‍🦲': 7, + '🧑🏼‍🦳': 7, + '🧑🏼‍🦼': 7, + '🧑🏼‍🦽': 7, + '🧑🏼‍🩰': 7, + '🧑🏼‍🫯‍🧑🏻': 11, + '🧑🏼‍🫯‍🧑🏽': 11, + '🧑🏼‍🫯‍🧑🏿': 11, + '🧑🏽‍⚕️': 6, + '🧑🏽‍⚖️': 6, + '🧑🏽‍✈️': 6, + '🧑🏽‍❤️‍💋‍🧑🏿': 14, + '🧑🏽‍🌾': 7, + '🧑🏽‍🍳': 7, + '🧑🏽‍🍼': 7, + '🧑🏽‍🎄': 7, + '🧑🏽‍🎓': 7, + '🧑🏽‍🎤': 7, + '🧑🏽‍🎨': 7, + '🧑🏽‍🏫': 7, + '🧑🏽‍🏭': 7, + '🧑🏽‍🐰‍🧑🏻': 12, + '🧑🏽‍🐰‍🧑🏿': 12, + '🧑🏽‍💻': 7, + '🧑🏽‍💼': 7, + '🧑🏽‍🔧': 7, + '🧑🏽‍🔬': 7, + '🧑🏽‍🚀': 7, + '🧑🏽‍🚒': 7, + '🧑🏽‍🤝‍🧑🏻': 12, + '🧑🏽‍🤝‍🧑🏼': 12, + '🧑🏽‍🤝‍🧑🏽': 12, + '🧑🏽‍🤝‍🧑🏾': 12, + '🧑🏽‍🤝‍🧑🏿': 12, + '🧑🏽‍🦯': 7, + '🧑🏽‍🦯‍➡️': 9, + '🧑🏽‍🦰': 7, + '🧑🏽‍🦱': 7, + '🧑🏽‍🦲': 7, + '🧑🏽‍🦳': 7, + '🧑🏽‍🦼': 7, + '🧑🏽‍🦼‍➡️': 9, + '🧑🏽‍🦽': 7, + '🧑🏽‍🦽‍➡️': 9, + '🧑🏽‍🩰': 7, + '🧑🏽‍🫯‍🧑🏻': 11, + '🧑🏽‍🫯‍🧑🏼': 11, + '🧑🏽‍🫯‍🧑🏾': 11, + '🧑🏽‍🫯‍🧑🏿': 11, + '🧑🏾‍⚕️': 6, + '🧑🏾‍⚖️': 6, + '🧑🏾‍✈️': 6, + '🧑🏾‍🌾': 7, + '🧑🏾‍🍳': 7, + '🧑🏾‍🍼': 7, + '🧑🏾‍🎄': 7, + '🧑🏾‍🎓': 7, + '🧑🏾‍🎤': 7, + '🧑🏾‍🎨': 7, + '🧑🏾‍🏫': 7, + '🧑🏾‍🏭': 7, + '🧑🏾‍🐰‍🧑🏻': 12, + '🧑🏾‍🐰‍🧑🏿': 12, + '🧑🏾‍💻': 7, + '🧑🏾‍💼': 7, + '🧑🏾‍🔧': 7, + '🧑🏾‍🔬': 7, + '🧑🏾‍🚀': 7, + '🧑🏾‍🚒': 7, + '🧑🏾‍🤝‍🧑🏻': 12, + '🧑🏾‍🤝‍🧑🏽': 12, + '🧑🏾‍🤝‍🧑🏾': 12, + '🧑🏾‍🤝‍🧑🏿': 12, + '🧑🏾‍🦯': 7, + '🧑🏾‍🦯‍➡️': 9, + '🧑🏾‍🦰': 7, + '🧑🏾‍🦱': 7, + '🧑🏾‍🦲': 7, + '🧑🏾‍🦳': 7, + '🧑🏾‍🦼': 7, + '🧑🏾‍🦽': 7, + '🧑🏾‍🩰': 7, + '🧑🏾‍🫯‍🧑🏻': 11, + '🧑🏾‍🫯‍🧑🏽': 11, + '🧑🏾‍🫯‍🧑🏿': 11, + '🧑🏿‍⚕️': 6, + '🧑🏿‍⚖️': 6, + '🧑🏿‍✈️': 6, + '🧑🏿‍❤️‍💋‍🧑🏻': 14, + '🧑🏿‍❤️‍💋‍🧑🏽': 14, + '🧑🏿‍🌾': 7, + '🧑🏿‍🍳': 7, + '🧑🏿‍🍼': 7, + '🧑🏿‍🎄': 7, + '🧑🏿‍🎓': 7, + '🧑🏿‍🎤': 7, + '🧑🏿‍🎨': 7, + '🧑🏿‍🏫': 7, + '🧑🏿‍🏭': 7, + '🧑🏿‍🐰‍🧑🏻': 12, + '🧑🏿‍🐰‍🧑🏼': 12, + '🧑🏿‍🐰‍🧑🏽': 12, + '🧑🏿‍🐰‍🧑🏾': 12, + '🧑🏿‍💻': 7, + '🧑🏿‍💼': 7, + '🧑🏿‍🔧': 7, + '🧑🏿‍🔬': 7, + '🧑🏿‍🚀': 7, + '🧑🏿‍🚒': 7, + '🧑🏿‍🤝‍🧑🏻': 12, + '🧑🏿‍🤝‍🧑🏼': 12, + '🧑🏿‍🤝‍🧑🏽': 12, + '🧑🏿‍🤝‍🧑🏾': 12, + '🧑🏿‍🤝‍🧑🏿': 12, + '🧑🏿‍🦯': 7, + '🧑🏿‍🦯‍➡️': 9, + '🧑🏿‍🦰': 7, + '🧑🏿‍🦱': 7, + '🧑🏿‍🦲': 7, + '🧑🏿‍🦳': 7, + '🧑🏿‍🦼': 7, + '🧑🏿‍🦼‍➡️': 9, + '🧑🏿‍🦽': 7, + '🧑🏿‍🦽‍➡️': 9, + '🧑🏿‍🩰': 7, + '🧑🏿‍🫯‍🧑🏻': 11, + '🧑🏿‍🫯‍🧑🏼': 11, + '🧑🏿‍🫯‍🧑🏽': 11, + '🧑🏿‍🫯‍🧑🏾': 11, + '🧔‍♀️': 4, + '🧔‍♂️': 4, + '🧔🏻‍♀️': 6, + '🧔🏻‍♂️': 6, + '🧔🏼‍♀️': 6, + '🧔🏼‍♂️': 6, + '🧔🏽‍♀️': 6, + '🧔🏽‍♂️': 6, + '🧔🏾‍♀️': 6, + '🧔🏾‍♂️': 6, + '🧔🏿‍♀️': 6, + '🧔🏿‍♂️': 6, + '🧖‍♀️': 4, + '🧖‍♂️': 4, + '🧖🏻‍♀️': 6, + '🧖🏻‍♂️': 6, + '🧖🏼‍♀️': 6, + '🧖🏼‍♂️': 6, + '🧖🏽‍♀️': 6, + '🧖🏽‍♂️': 6, + '🧖🏾‍♀️': 6, + '🧖🏾‍♂️': 6, + '🧖🏿‍♀️': 6, + '🧖🏿‍♂️': 6, + '🧗‍♀️': 4, + '🧗‍♂️': 4, + '🧗🏻‍♀️': 6, + '🧗🏻‍♂️': 6, + '🧗🏼‍♀️': 6, + '🧗🏼‍♂️': 6, + '🧗🏽‍♀️': 6, + '🧗🏽‍♂️': 6, + '🧗🏾‍♀️': 6, + '🧗🏾‍♂️': 6, + '🧗🏿‍♀️': 6, + '🧗🏿‍♂️': 6, + '🧘‍♀️': 4, + '🧘‍♂️': 4, + '🧘🏻‍♀️': 6, + '🧘🏻‍♂️': 6, + '🧘🏼‍♀️': 6, + '🧘🏼‍♂️': 6, + '🧘🏽‍♀️': 6, + '🧘🏽‍♂️': 6, + '🧘🏾‍♀️': 6, + '🧘🏾‍♂️': 6, + '🧘🏿‍♀️': 6, + '🧘🏿‍♂️': 6, + '🧙‍♀️': 4, + '🧙‍♂️': 4, + '🧙🏻‍♀️': 6, + '🧙🏻‍♂️': 6, + '🧙🏼‍♀️': 6, + '🧙🏼‍♂️': 6, + '🧙🏽‍♀️': 6, + '🧙🏽‍♂️': 6, + '🧙🏾‍♀️': 6, + '🧙🏾‍♂️': 6, + '🧙🏿‍♀️': 6, + '🧙🏿‍♂️': 6, + '🧚‍♀️': 4, + '🧚‍♂️': 4, + '🧚🏻‍♀️': 6, + '🧚🏻‍♂️': 6, + '🧚🏼‍♀️': 6, + '🧚🏼‍♂️': 6, + '🧚🏽‍♀️': 6, + '🧚🏽‍♂️': 6, + '🧚🏾‍♀️': 6, + '🧚🏾‍♂️': 6, + '🧚🏿‍♀️': 6, + '🧚🏿‍♂️': 6, + '🧛‍♀️': 4, + '🧛‍♂️': 4, + '🧛🏻‍♀️': 6, + '🧛🏻‍♂️': 6, + '🧛🏼‍♀️': 6, + '🧛🏼‍♂️': 6, + '🧛🏽‍♀️': 6, + '🧛🏽‍♂️': 6, + '🧛🏾‍♀️': 6, + '🧛🏾‍♂️': 6, + '🧛🏿‍♀️': 6, + '🧛🏿‍♂️': 6, + '🧜‍♀️': 4, + '🧜‍♂️': 4, + '🧜🏻‍♀️': 6, + '🧜🏻‍♂️': 6, + '🧜🏼‍♀️': 6, + '🧜🏼‍♂️': 6, + '🧜🏽‍♀️': 6, + '🧜🏽‍♂️': 6, + '🧜🏾‍♀️': 6, + '🧜🏾‍♂️': 6, + '🧜🏿‍♀️': 6, + '🧜🏿‍♂️': 6, + '🧝‍♀️': 4, + '🧝‍♂️': 4, + '🧝🏻‍♀️': 6, + '🧝🏻‍♂️': 6, + '🧝🏼‍♀️': 6, + '🧝🏼‍♂️': 6, + '🧝🏽‍♀️': 6, + '🧝🏽‍♂️': 6, + '🧝🏾‍♀️': 6, + '🧝🏾‍♂️': 6, + '🧝🏿‍♀️': 6, + '🧝🏿‍♂️': 6, + '🧞‍♀️': 4, + '🧞‍♂️': 4, + '🧟‍♀️': 4, + '🧟‍♂️': 4, + '🫱🏻‍🫲🏼': 9, + '🫱🏻‍🫲🏽': 9, + '🫱🏻‍🫲🏾': 9, + '🫱🏻‍🫲🏿': 9, + '🫱🏼‍🫲🏻': 9, + '🫱🏼‍🫲🏽': 9, + '🫱🏼‍🫲🏾': 9, + '🫱🏼‍🫲🏿': 9, + '🫱🏽‍🫲🏻': 9, + '🫱🏽‍🫲🏼': 9, + '🫱🏽‍🫲🏾': 9, + '🫱🏽‍🫲🏿': 9, + '🫱🏾‍🫲🏻': 9, + '🫱🏾‍🫲🏼': 9, + '🫱🏾‍🫲🏽': 9, + '🫱🏾‍🫲🏿': 9, + '🫱🏿‍🫲🏻': 9, + '🫱🏿‍🫲🏼': 9, + '🫱🏿‍🫲🏽': 9, + '🫱🏿‍🫲🏾': 9, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_da9ceb0a.py b/libs/wcwidth/table_grapheme_overrides/_known_da9ceb0a.py new file mode 100644 index 000000000..17a0e25ce --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_da9ceb0a.py @@ -0,0 +1,1910 @@ +""" +Grapheme overrides for _known_da9ceb0a. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: wezterm +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'का': 1, + 'कां': 1, + 'कि': 1, + 'किं': 1, + 'की': 1, + 'केा': 1, + 'को': 1, + 'कौ': 1, + 'क्घ्घि्': 3, + 'क्त्योः': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 3, + 'खा': 1, + 'खि': 1, + 'खी': 1, + 'खो': 1, + 'ग़ा': 1, + 'गा': 1, + 'गाँ': 1, + 'गां': 1, + 'गि': 1, + 'गी': 1, + 'गो': 1, + 'गों': 1, + 'गौ': 1, + 'ग्र्य': 3, + 'घा': 1, + 'घि': 1, + 'घी': 1, + 'घेि': 1, + 'घो': 1, + 'घ्र्व्य': 4, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'चा': 1, + 'चां': 1, + 'चि': 1, + 'ची': 1, + 'चो': 1, + 'चौ': 1, + 'छि': 1, + 'छुी': 1, + 'छो': 1, + 'जा': 1, + 'जि': 1, + 'जिं': 1, + 'जी': 1, + 'जो': 1, + 'झा': 1, + 'टा': 1, + 'टां': 1, + 'टि': 1, + 'टी': 1, + 'टों': 1, + 'ठा': 1, + 'ठी': 1, + 'ठैः': 1, + 'ड़ा': 1, + 'डा': 1, + 'डि': 1, + 'डी': 1, + 'डो': 1, + 'ढ़ा': 1, + 'ढ़ि': 1, + 'ढ़ी': 1, + 'ढ़ो': 1, + 'ढा': 1, + 'ढाँ': 1, + 'ढि': 1, + 'णा': 1, + 'णां': 1, + 'णि': 1, + 'णी': 1, + 'णो': 1, + 'णों': 1, + 'तः': 1, + 'ता': 1, + 'तां': 1, + 'ताः': 1, + 'ति': 1, + 'तिं': 1, + 'ती': 1, + 'तो': 1, + 'तों': 1, + 'तौ': 1, + 'त्त्थि': 3, + 'त्त्व': 3, + 'त्त्वा': 3, + 'त्त्वे': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 3, + 'त्र्ये': 3, + 'त्स्वा': 3, + 'था': 1, + 'थि': 1, + 'दः': 1, + 'दा': 1, + 'दां': 1, + 'दि': 1, + 'दी': 1, + 'दो': 1, + 'दौ': 1, + 'धा': 1, + 'धां': 1, + 'धाः': 1, + 'धि': 1, + 'धी': 1, + 'धेः': 1, + 'धो': 1, + 'धों': 1, + 'नः': 1, + 'ना': 1, + 'नां': 1, + 'नाः': 1, + 'ना‌': 1, + 'नि': 1, + 'निँ': 1, + 'निः': 1, + 'नी': 1, + 'नैः': 1, + 'नो': 1, + 'नों': 1, + 'नौ': 1, + 'न्ञ्या': 3, + 'न्त्र': 3, + 'न्त्रा': 3, + 'न्त्राः': 3, + 'न्त्रि': 3, + 'न्त्री': 3, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 4, + 'न्त्र्ये': 4, + 'न्ह्यां': 3, + 'पा': 1, + 'पाँ': 1, + 'पां': 1, + 'पि': 1, + 'पी': 1, + 'पो': 1, + 'पों': 1, + 'फा': 1, + 'फाँ': 1, + 'फौ': 1, + 'बा': 1, + 'बाँ': 1, + 'बि': 1, + 'बी': 1, + 'बो': 1, + 'भा': 1, + 'भि': 1, + 'भिः': 1, + 'भी': 1, + 'भो': 1, + 'भौ': 1, + 'मा': 1, + 'मां': 1, + 'मि': 1, + 'मी': 1, + 'मो': 1, + 'मों': 1, + 'मौ': 1, + 'म्प्र': 3, + 'यः': 1, + 'या': 1, + 'यां': 1, + 'याः': 1, + 'यि': 1, + 'यी': 1, + 'यैः': 1, + 'यो': 1, + 'यों': 1, + 'रः': 1, + 'रा': 1, + 'रां': 1, + 'राः': 1, + 'रि': 1, + 'रिं': 1, + 'री': 1, + 'रो': 1, + 'रों': 1, + 'रौ': 1, + 'र्त्त': 3, + 'र्त्ति': 3, + 'र्द्ध': 3, + 'ला': 1, + 'लां': 1, + 'लाः': 1, + 'लि': 1, + 'लिं': 1, + 'ली': 1, + 'लीं': 1, + 'लो': 1, + 'लों': 1, + 'लौ': 1, + 'ळा': 1, + 'ळी': 1, + 'वः': 1, + 'वा': 1, + 'वां': 1, + 'वाः': 1, + 'वि': 1, + 'वी': 1, + 'वो': 1, + 'वों': 1, + 'शः': 1, + 'शा': 1, + 'शां': 1, + 'शाः': 1, + 'शि': 1, + 'शी': 1, + 'शो': 1, + 'शों': 1, + 'षा': 1, + 'षां': 1, + 'षाः': 1, + 'षि': 1, + 'षी': 1, + 'षों': 1, + 'ष्ट्र': 3, + 'ष्ट्रा': 3, + 'ष्ट्रां': 3, + 'ष्ट्रि': 3, + 'ष्ट्री': 3, + 'ष्ट्रे': 3, + 'ष्ट्रो': 3, + 'ष्ट्रों': 3, + 'ष्प्र': 3, + 'सः': 1, + 'सा': 1, + 'साँ': 1, + 'सां': 1, + 'सि': 1, + 'सी': 1, + 'सो': 1, + 'सौ': 1, + 'स्ट्या': 3, + 'स्त्र': 3, + 'स्त्रा': 3, + 'स्त्रि': 3, + 'स्त्री': 3, + 'स्थ्य': 3, + 'हा': 1, + 'हाँ': 1, + 'हां': 1, + 'हि': 1, + 'हिं': 1, + 'ही': 1, + 'हीं': 1, + 'हैः': 1, + 'हो': 1, + 'हों': 1, + 'हौ': 1, + 'क़ा': 1, + 'क़ि': 1, + 'ज़ा': 1, + 'ड़ा': 1, + 'ढ़ा': 1, + 'फ़ा': 1, + 'অং': 1, + 'কা': 1, + 'কা‌': 1, + 'কি': 1, + 'কিং': 1, + 'কে': 1, + 'কে‌': 1, + 'কো': 1, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 3, + 'খা': 1, + 'খি': 1, + 'খে': 1, + 'গি': 1, + 'গে': 1, + 'গো': 1, + 'ঘা': 1, + 'ঘে': 1, + 'ঘো': 1, + 'ঘো': 1, + 'চা': 1, + 'চি': 1, + 'চে': 1, + 'চো': 1, + 'চৌ': 1, + 'ছা': 1, + 'ছি': 1, + 'ছে': 1, + 'জা': 1, + 'জি': 1, + 'জী': 1, + 'জে': 1, + 'জে‌': 1, + 'ঝো': 1, + 'টা': 1, + 'টি': 1, + 'ঠি': 1, + 'ড়া': 1, + 'ণা': 1, + 'ণি': 1, + 'ণে': 1, + 'তঃ': 1, + 'তা': 1, + 'তাঁ': 1, + 'তা‌': 1, + 'তি': 1, + 'তি‌': 1, + 'তী': 1, + 'তে': 1, + 'তে‌': 1, + 'থা': 1, + 'থি': 1, + 'থে': 1, + 'দা': 1, + 'দি': 1, + 'দে': 1, + 'দে‌': 1, + 'দৈ': 1, + 'দো': 1, + 'ধা': 1, + 'ধি': 1, + 'ধী': 1, + 'ধে': 1, + 'না': 1, + 'না‌': 1, + 'নি': 1, + 'নী': 1, + 'নে': 1, + 'নৈ': 1, + 'নো': 1, + 'ন্ত্রি': 3, + 'ন্ত্রে': 3, + 'পা': 1, + 'পা‌': 1, + 'পি': 1, + 'পী': 1, + 'পে': 1, + 'পো': 1, + 'ফে': 1, + 'ফৌ': 1, + 'বং': 1, + 'বা': 1, + 'বা‌': 1, + 'বি': 1, + 'বী': 1, + 'বে': 1, + 'বৈ': 1, + 'বো': 1, + 'ভা': 1, + 'ভি': 1, + 'ভে': 1, + 'ভো': 1, + 'ভৌ': 1, + 'মা': 1, + 'মা‌': 1, + 'মি': 1, + 'মে': 1, + 'মৌ': 1, + 'য়া': 1, + 'য়ি': 1, + 'য়ে': 1, + 'য়ো': 1, + 'যা': 1, + 'যে': 1, + 'যো': 1, + 'রা': 1, + 'রাং': 1, + 'রি': 1, + 'রী': 1, + 'রে': 1, + 'রো': 1, + 'র্ব্য': 3, + 'লা': 1, + 'লি': 1, + 'লী': 1, + 'লে': 1, + 'লে‌': 1, + 'শং': 1, + 'শা': 1, + 'শি': 1, + 'শী': 1, + 'শৃং': 1, + 'শে': 1, + 'শৈ': 1, + 'ষা': 1, + 'ষি': 1, + 'ষী': 1, + 'ষে': 1, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 3, + 'ষ্ট্রে': 3, + 'সং': 1, + 'সা': 1, + 'সাং': 1, + 'সি': 1, + 'সী': 1, + 'সে': 1, + 'সে‌': 1, + 'স্ত্র': 3, + 'স্থ্য': 3, + 'হা': 1, + 'হি': 1, + 'হে': 1, + 'হো': 1, + 'য়ে': 1, + 'ਕਾ': 1, + 'ਕਾਂ': 1, + 'ਕਿ': 1, + 'ਕਿ੍': 1, + 'ਕਿੱ': 1, + 'ਕੀ': 1, + 'ਖਾ': 1, + 'ਖਾਂ': 1, + 'ਖਿ': 1, + 'ਖਿ਼': 1, + 'ਖੀ': 1, + 'ਗਾ': 1, + 'ਗਿ': 1, + 'ਗਿ੍': 1, + 'ਗੀ': 1, + 'ਚਾ': 1, + 'ਚਿ': 1, + 'ਚਿੱ': 1, + 'ਚੀ': 1, + 'ਜਾ': 1, + 'ਜਾਂ': 1, + 'ਜਿ': 1, + 'ਜਿ਼': 1, + 'ਜਿ਼ੰ': 1, + 'ਜੀ': 1, + 'ਝਿ': 1, + 'ਝੀ': 1, + 'ਟਾ': 1, + 'ਟਿ': 1, + 'ਟਿ੍': 1, + 'ਟੀ': 1, + 'ਠੀ': 1, + 'ਡਾ': 1, + 'ਣਾ': 1, + 'ਣਾਂ': 1, + 'ਣੀ': 1, + 'ਤਾ': 1, + 'ਤਾਂ': 1, + 'ਤਿ': 1, + 'ਤੀ': 1, + 'ਦਾ': 1, + 'ਦਾਂ': 1, + 'ਦਿ': 1, + 'ਦਿੱ': 1, + 'ਦੀ': 1, + 'ਧਾ': 1, + 'ਧਾਂ': 1, + 'ਧਿ': 1, + 'ਧੀ': 1, + 'ਨਾ': 1, + 'ਨਾਂ': 1, + 'ਨਿ': 1, + 'ਨੀ': 1, + 'ਨੀਂ': 1, + 'ਪਾ': 1, + 'ਪਿ': 1, + 'ਪੀ': 1, + 'ਫਿ': 1, + 'ਬਾ': 1, + 'ਬਿ': 1, + 'ਬੀ': 1, + 'ਭਾ': 1, + 'ਭਿ': 1, + 'ਮਾ': 1, + 'ਮਾਂ': 1, + 'ਮਿ': 1, + 'ਮੀ': 1, + 'ਯਾ': 1, + 'ਰਾ': 1, + 'ਰਾਂ': 1, + 'ਰਿ': 1, + 'ਰੀ': 1, + 'ਲਾ': 1, + 'ਲਾਂ': 1, + 'ਲਿ': 1, + 'ਲਿੰ': 1, + 'ਲੀ': 1, + 'ਵਾ': 1, + 'ਵਾਂ': 1, + 'ਵਿ': 1, + 'ਵਿੱ': 1, + 'ਵੀ': 1, + 'ਸ਼ਾ': 1, + 'ਸ਼ਾਂ': 1, + 'ਸ਼ਿ': 1, + 'ਸ਼ੀ': 1, + 'ਸਾ': 1, + 'ਸਾਂ': 1, + 'ਸਿ': 1, + 'ਸਿੱ': 1, + 'ਸੀ': 1, + 'ਹਾ': 1, + 'ਹਾਂ': 1, + 'ਹਿ': 1, + 'ਹਿੰ': 1, + 'ਹਿੱ': 1, + 'ਹੀ': 1, + 'ਹੀਂ': 1, + 'ਜ਼ਾ': 1, + 'ਜ਼ੀ': 1, + 'ੜਾ': 1, + 'ੜੀਂ': 1, + 'ਫ਼ਾ': 1, + 'કઃ': 1, + 'કા': 1, + 'કાં': 1, + 'કિ': 1, + 'કી': 1, + 'કો': 1, + 'ખા': 1, + 'ખી': 1, + 'ખો': 1, + 'ગા': 1, + 'ગિ': 1, + 'ગી': 1, + 'ગો': 1, + 'ઘા': 1, + 'ઘો': 1, + 'ચા': 1, + 'ચિ': 1, + 'ચી': 1, + 'છા': 1, + 'છો': 1, + 'જા': 1, + 'જાં': 1, + 'જિ': 1, + 'જી': 1, + 'જો': 1, + 'ટઃ': 1, + 'ડા': 1, + 'ડાં': 1, + 'ડી': 1, + 'ઢા': 1, + 'ઢિ': 1, + 'ણા': 1, + 'ણી': 1, + 'તઃ': 1, + 'તા': 1, + 'તાં': 1, + 'તિ': 1, + 'તી': 1, + 'તો': 1, + 'ત્ત્વ': 3, + 'ત્ત્વા': 3, + 'ત્ર્ય': 3, + 'થા': 1, + 'થી': 1, + 'દા': 1, + 'દી': 1, + 'દો': 1, + 'ધા': 1, + 'ધાં': 1, + 'ધિ': 1, + 'ધી': 1, + 'ધો': 1, + 'નઃ': 1, + 'ના': 1, + 'નાં': 1, + 'નિ': 1, + 'ની': 1, + 'નો': 1, + 'પા': 1, + 'પિ': 1, + 'પી': 1, + 'પો': 1, + 'ફા': 1, + 'ફો': 1, + 'બા': 1, + 'બાં': 1, + 'બી': 1, + 'ભા': 1, + 'ભાં': 1, + 'ભિ': 1, + 'ભો': 1, + 'ભૌ': 1, + 'મઃ': 1, + 'મા': 1, + 'માં': 1, + 'મિ': 1, + 'મી': 1, + 'મો': 1, + 'યા': 1, + 'યિ': 1, + 'યી': 1, + 'યો': 1, + 'રા': 1, + 'રિ': 1, + 'રી': 1, + 'રો': 1, + 'લા': 1, + 'લાં': 1, + 'લિ': 1, + 'લિં': 1, + 'લી': 1, + 'લો': 1, + 'ળા': 1, + 'ળી': 1, + 'વા': 1, + 'વાં': 1, + 'વિ': 1, + 'વી': 1, + 'વો': 1, + 'શા': 1, + 'શાં': 1, + 'શિ': 1, + 'શી': 1, + 'શો': 1, + 'ષા': 1, + 'ષી': 1, + 'ષો': 1, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 3, + 'ષ્ટ્રો': 3, + 'સા': 1, + 'સાં': 1, + 'સિ': 1, + 'સી': 1, + 'સો': 1, + 'સ્ત્રી': 3, + 'હા': 1, + 'હિ': 1, + 'હી': 1, + 'હો': 1, + 'கா': 1, + 'கி': 1, + 'கு': 1, + 'கூ': 1, + 'கெ': 1, + 'கே': 1, + 'கை': 1, + 'கொ': 1, + 'கோ': 1, + 'சா': 1, + 'சி': 1, + 'சு': 1, + 'சூ': 1, + 'செ': 1, + 'சே': 1, + 'சை': 1, + 'சொ': 1, + 'டா': 1, + 'டி': 1, + 'டு': 1, + 'டெ': 1, + 'டை': 1, + 'ணா': 1, + 'ணி': 1, + 'ணு': 1, + 'ணை': 1, + 'தா': 1, + 'தி': 1, + 'து': 1, + 'தூ': 1, + 'தெ': 1, + 'தே': 1, + 'தை': 1, + 'தொ': 1, + 'தோ': 1, + 'நா': 1, + 'நி': 1, + 'நு': 1, + 'நெ': 1, + 'நே': 1, + 'நோ': 1, + 'னா': 1, + 'னி': 1, + 'னு': 1, + 'னே': 1, + 'னை': 1, + 'னொ': 1, + 'பா': 1, + 'பி': 1, + 'பு': 1, + 'பெ': 1, + 'பே': 1, + 'பை': 1, + 'பொ': 1, + 'போ': 1, + 'மா': 1, + 'மி': 1, + 'மு': 1, + 'மூ': 1, + 'மெ': 1, + 'மே': 1, + 'மை': 1, + 'மொ': 1, + 'மோ': 1, + 'யா': 1, + 'யி': 1, + 'யு': 1, + 'யே': 1, + 'யை': 1, + 'யோ': 1, + 'ரா': 1, + 'ரி': 1, + 'ரு': 1, + 'ரெ': 1, + 'ரை': 1, + 'றா': 1, + 'றி': 1, + 'று': 1, + 'றே': 1, + 'றை': 1, + 'றொ': 1, + 'றோ': 1, + 'லா': 1, + 'லி': 1, + 'லு': 1, + 'லெ': 1, + 'லே': 1, + 'லை': 1, + 'லோ': 1, + 'ளா': 1, + 'ளி': 1, + 'ளு': 1, + 'ளெ': 1, + 'ளை': 1, + 'ழி': 1, + 'ழு': 1, + 'ழை': 1, + 'வா': 1, + 'வி': 1, + 'வு': 1, + 'வூ': 1, + 'வெ': 1, + 'வே': 1, + 'வை': 1, + 'வொ': 1, + 'வோ': 1, + 'అం': 1, + 'ఆం': 1, + 'ఒం': 1, + 'కం': 1, + 'కాం': 1, + 'కు': 1, + 'కుం': 1, + 'కూ': 1, + 'కృ': 1, + 'కెం': 1, + 'క్స్వా': 3, + 'గిం': 1, + 'గు': 1, + 'గూ': 1, + 'గృ': 1, + 'చు': 1, + 'చెం': 1, + 'టం': 1, + 'టిం': 1, + 'టు': 1, + 'టుం': 1, + 'డు': 1, + 'ణిం': 1, + 'తం': 1, + 'తః': 1, + 'తు': 1, + 'తృ': 1, + 'త్ర్య': 3, + 'దం': 1, + 'దిం': 1, + 'దు': 1, + 'దుం': 1, + 'దృ': 1, + 'ద్ర్య': 3, + 'ధిం': 1, + 'ధు': 1, + 'నం': 1, + 'నః': 1, + 'నాం': 1, + 'నిం': 1, + 'ను': 1, + 'నుం': 1, + 'నొం': 1, + 'పం': 1, + 'పిం': 1, + 'పు': 1, + 'పూ': 1, + 'పెం': 1, + 'పొం': 1, + 'బం': 1, + 'భం': 1, + 'భు': 1, + 'మిం': 1, + 'ము': 1, + 'మూ': 1, + 'యం': 1, + 'యః': 1, + 'యిం': 1, + 'యు': 1, + 'యుం': 1, + 'రం': 1, + 'రిం': 1, + 'రు': 1, + 'రూ': 1, + 'ఱు': 1, + 'లం': 1, + 'లిం': 1, + 'లు': 1, + 'విం': 1, + 'వు': 1, + 'వృ': 1, + 'శాం': 1, + 'శిం': 1, + 'శు': 1, + 'షిం': 1, + 'షు': 1, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'సం': 1, + 'సాం': 1, + 'సు': 1, + 'సూ': 1, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'హిం': 1, + 'ಅಂ': 1, + 'ಇಂ': 1, + 'ಉಂ': 1, + 'ಎಂ': 1, + 'ಒಂ': 1, + 'ಕಾ': 1, + 'ಕಾಂ': 1, + 'ಕಿಂ': 1, + 'ಕೀ': 1, + 'ಕು': 1, + 'ಕುಂ': 1, + 'ಕೂ': 1, + 'ಕೃ': 1, + 'ಕೆಂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಕೊಂ': 1, + 'ಖಾಂ': 1, + 'ಗಾ': 1, + 'ಗಾಂ': 1, + 'ಗೀ': 1, + 'ಗು': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೊಂ': 1, + 'ಗೋ': 1, + 'ಚಾ': 1, + 'ಚೀ': 1, + 'ಚು': 1, + 'ಛಾ': 1, + 'ಜಾ': 1, + 'ಜೀ': 1, + 'ಟು': 1, + 'ಟುಂ': 1, + 'ಟೇ': 1, + 'ಠಾ': 1, + 'ಡಾ': 1, + 'ಡು': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣಂ': 1, + 'ಣಾ': 1, + 'ಣು': 1, + 'ಣೈ': 1, + 'ತಂ': 1, + 'ತಃ': 1, + 'ತಾ': 1, + 'ತೀ': 1, + 'ತು': 1, + 'ತೃ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ಥಾ': 1, + 'ದಂ': 1, + 'ದಾ': 1, + 'ದಿಂ': 1, + 'ದು': 1, + 'ದೂ': 1, + 'ದೃ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧಾ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಃ': 1, + 'ನಾ': 1, + 'ನಾಂ': 1, + 'ನಿಂ': 1, + 'ನು': 1, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪಂ': 1, + 'ಪಾ': 1, + 'ಪು': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬಂ': 1, + 'ಬಾ': 1, + 'ಬು': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭಾ': 1, + 'ಭೀ': 1, + 'ಭು': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮಾ': 1, + 'ಮೀ': 1, + 'ಮು': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯಂ': 1, + 'ಯಾ': 1, + 'ಯಿಂ': 1, + 'ಯು': 1, + 'ಯುಂ': 1, + 'ಯೂ': 1, + 'ಯೆಂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಂ': 1, + 'ರಾ': 1, + 'ರಾಂ': 1, + 'ರಿಂ': 1, + 'ರೀ': 1, + 'ರು': 1, + 'ರೂ': 1, + 'ರೆಂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲಂ': 1, + 'ಲಾ': 1, + 'ಲೀ': 1, + 'ಲು': 1, + 'ಲೂ': 1, + 'ಲೆಂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳಾ': 1, + 'ಳಿಂ': 1, + 'ಳು': 1, + 'ಳೂ': 1, + 'ವಾ': 1, + 'ವೀ': 1, + 'ವು': 1, + 'ವುಂ': 1, + 'ವೂ': 1, + 'ವೃ': 1, + 'ವೆಂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾ': 1, + 'ಶಾಂ': 1, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷಾ': 1, + 'ಷೇ': 1, + 'ಸಂ': 1, + 'ಸಾ': 1, + 'ಸಾಂ': 1, + 'ಸು': 1, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹಾ': 1, + 'ಹು': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೊಂ': 1, + 'ಹೋ': 1, + 'അം': 1, + 'കാ': 1, + 'കി': 1, + 'കെ': 1, + 'കേ': 1, + 'കൈ': 1, + 'കൊ': 1, + 'കോ': 1, + 'ഗം': 1, + 'ഗി': 1, + 'ഗീ': 1, + 'ഘി': 1, + 'ചാ': 1, + 'ചി': 1, + 'ചെ': 1, + 'ചേ': 1, + 'ചോ': 1, + 'ജാ': 1, + 'ജി': 1, + 'ജീ': 1, + 'ടാ': 1, + 'ടി': 1, + 'ടും': 1, + 'ടെ': 1, + 'ടേ': 1, + 'ഠി': 1, + 'ണം': 1, + 'ണാ': 1, + 'ണി': 1, + 'ണെ': 1, + 'തം': 1, + 'താ': 1, + 'തി': 1, + 'തീ': 1, + 'തും': 1, + 'തെ': 1, + 'തേ': 1, + 'തൊ': 1, + 'തോ': 1, + 'ദം': 1, + 'ദാ': 1, + 'ദി': 1, + 'ദേ': 1, + 'ധാ': 1, + 'ധി': 1, + 'നം': 1, + 'നാ': 1, + 'നി': 1, + 'നീ': 1, + 'നും': 1, + 'നെ': 1, + 'നേ': 1, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 4, + 'പാ': 1, + 'പി': 1, + 'പീ': 1, + 'പെ': 1, + 'പൈ': 1, + 'പൊ': 1, + 'പോ': 1, + 'പൌ': 1, + 'ബം': 1, + 'ബാ': 1, + 'ബോ': 1, + 'ഭാ': 1, + 'ഭി': 1, + 'ഭേ': 1, + 'ഭൌ': 1, + 'മം': 1, + 'മാ': 1, + 'മി': 1, + 'മെ': 1, + 'മേ': 1, + 'മൊ': 1, + 'മോ': 1, + 'മൌ': 1, + 'മ്പ്ര': 3, + 'യം': 1, + 'യാ': 1, + 'യി': 1, + 'യും': 1, + 'യെ': 1, + 'യേ': 1, + 'യോ': 1, + 'യ്ക്കു': 3, + 'രം': 1, + 'രാ': 1, + 'രി': 1, + 'രീ': 1, + 'രും': 1, + 'രെ': 1, + 'രേ': 1, + 'രോ': 1, + 'ര്‍ക്കു': 3, + 'ര്‍ക്കും': 3, + 'ര്‍ക്ക്‌': 3, + 'ര്‍ഗ്ഗം': 3, + 'ര്‍ച്ച': 3, + 'ര്‍ണ്ണ': 3, + 'ര്‍ത്ത': 3, + 'ര്‍ത്തി': 3, + 'ര്‍ത്തു': 3, + 'ര്‍ത്തേ': 3, + 'ര്‍ദ്ദ': 3, + 'ര്‍പ്പെ': 3, + 'ര്‍മ്മി': 3, + 'ര്‍വ്വ': 3, + 'റം': 1, + 'റി': 1, + 'റെ': 1, + 'ലം': 1, + 'ലാ': 1, + 'ലി': 1, + 'ലും': 1, + 'ലെ': 1, + 'ലേ': 1, + 'ലോ': 1, + 'ല്‍പ്പ': 3, + 'ളം': 1, + 'ളാ': 1, + 'ളി': 1, + 'ളും': 1, + 'ളെ': 1, + 'ളേ': 1, + 'ളോ': 1, + 'ള്‍ക്ക': 3, + 'ള്‍ക്കി': 3, + 'ള്‍ക്കു': 3, + 'ള്‍ക്കും': 3, + 'ള്‍ക്കെ': 3, + 'ള്‍ക്ക്‌': 3, + 'ഴി': 1, + 'വാ': 1, + 'വി': 1, + 'വും': 1, + 'വെ': 1, + 'വേ': 1, + 'വൈ': 1, + 'വോ': 1, + 'ശാ': 1, + 'ശി': 1, + 'ശീ': 1, + 'ശേ': 1, + 'ഷം': 1, + 'ഷി': 1, + 'ഷ്ട്യാ': 3, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 3, + 'സം': 1, + 'സാ': 1, + 'സാം': 1, + 'സി': 1, + 'സൌ': 1, + 'സ്ത്രം': 3, + 'സ്ത്രീ': 3, + 'സ്റ്റ്‌': 3, + 'ഹം': 1, + 'ഹാ': 1, + 'ഹി': 1, + 'ഹിം': 1, + 'කා': 1, + 'කැ': 1, + 'කෘ': 1, + 'කෙ': 1, + 'කො': 1, + 'කෝ': 1, + 'කෲ': 1, + 'ගා': 1, + 'ගැ': 1, + 'ගෙ': 1, + 'ගේ': 1, + 'චා': 1, + 'චැ': 1, + 'චෝ': 1, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ටේ': 1, + 'ඨා': 1, + 'ඩං': 1, + 'ඩා': 1, + 'තා': 1, + 'තැ': 1, + 'තෘ': 1, + 'තෙ': 1, + 'තේ': 1, + 'තො': 1, + 'තෝ': 1, + 'ථා': 1, + 'දා': 1, + 'දැ': 1, + 'දිං': 1, + 'දෙ': 1, + 'දේ': 1, + 'දෝ': 1, + 'ධා': 1, + 'නා': 1, + 'නැ': 1, + 'නෑ': 1, + 'නෙ': 1, + 'නේ': 1, + 'නො': 1, + 'ඳා': 1, + 'පා': 1, + 'පැ': 1, + 'පෑ': 1, + 'පෙ': 1, + 'පේ': 1, + 'පො': 1, + 'පෞ': 1, + 'බා': 1, + 'බැ': 1, + 'බෙ': 1, + 'බේ': 1, + 'බෝ': 1, + 'භා': 1, + 'භෞ': 1, + 'මා': 1, + 'මැ': 1, + 'මෙ': 1, + 'මේ': 1, + 'යා': 1, + 'යෙ': 1, + 'යේ': 1, + 'යො': 1, + 'යෝ': 1, + 'රා': 1, + 'රැ': 1, + 'රෙ': 1, + 'රේ': 1, + 'රෝ': 1, + 'ලං': 1, + 'ලා': 1, + 'ලැ': 1, + 'ලෙ': 1, + 'ලේ': 1, + 'ලෝ': 1, + 'වං': 1, + 'වා': 1, + 'වැ': 1, + 'වෘ': 1, + 'වෙ': 1, + 'වේ': 1, + 'වෝ': 1, + 'ශේ': 1, + 'ෂා': 1, + 'සං': 1, + 'සා': 1, + 'සැ': 1, + 'සෑ': 1, + 'සෙ': 1, + 'සේ': 1, + 'සෞ': 1, + 'හා': 1, + 'හැ': 1, + 'හිං': 1, + 'හෘ': 1, + 'හෙ': 1, + 'හො': 1, + 'හෝ': 1, + 'ළැ': 1, + 'ကေ': 1, + 'ကေု': 1, + 'က်ျ': 1, + 'ကျ': 1, + 'ကျို': 1, + 'ကျူ': 1, + 'ကျေ': 1, + 'ကျွ': 1, + 'ကြ': 1, + 'ကြို': 1, + 'ကြီ': 1, + 'ကြု': 1, + 'ကြေ': 1, + 'ကြံ': 1, + 'ကြွေ': 1, + 'ကၠေ': 1, + 'ကၠေံ': 1, + 'ခေ': 1, + 'ချ': 1, + 'ချိ': 1, + 'ချို': 1, + 'ချို့': 1, + 'ချီ': 1, + 'ချု': 1, + 'ချေ': 1, + 'ချဳ': 1, + 'ခြ': 1, + 'ခြုံ': 1, + 'ခြေ': 1, + 'ဂျို': 1, + 'ဂၠေ': 1, + 'ငြ': 1, + 'ငြိ': 1, + 'ငွေ': 1, + 'ငႄ': 1, + 'စေ': 1, + 'စှ်ေ': 1, + 'စၞေ': 1, + 'စၠေ': 1, + 'ဆေ': 1, + 'ဇြ': 1, + 'ဇြေ': 1, + 'ဇြဳ': 1, + 'ဇၞေ': 1, + 'ညေ': 1, + 'တေ': 1, + 'တေံ': 1, + 'တြ': 1, + 'တြုံ': 1, + 'တွေ': 1, + 'တှ်ေ': 1, + 'တၟေ': 1, + 'ထေ': 1, + 'ထွေ': 1, + 'ဒေ': 1, + 'ဒေံ': 1, + 'ဒြ': 1, + 'နေ': 1, + 'နေ့': 1, + 'နှေ': 1, + 'ပေ': 1, + 'ပေဲ': 1, + 'ပျ': 1, + 'ပျေ': 1, + 'ပျဲ': 1, + 'ပြ': 1, + 'ပြီ': 1, + 'ပြု': 1, + 'ပြေ': 1, + 'ပြဳ': 1, + 'ပြံ': 1, + 'ဖေ': 1, + 'ဖျ': 1, + 'ဖျု': 1, + 'ဖျေံ': 1, + 'ဖြ': 1, + 'ဖြို': 1, + 'ဗျဵု': 1, + 'ဗြေ': 1, + 'ဗြဴ': 1, + 'ဗၠေ': 1, + 'ဘေ': 1, + 'မေ': 1, + 'မျ': 1, + 'မျို': 1, + 'မျီု': 1, + 'မျှ': 1, + 'မြ': 1, + 'မြေ': 1, + 'မြှ': 1, + 'မွေ': 1, + 'မွေ့': 1, + 'မှေ': 1, + 'မၠေ': 1, + 'မႄ': 1, + 'ယေ': 1, + 'ယျ': 1, + 'ယျဵု': 1, + 'ယြဴ': 1, + 'ရေ': 1, + 'ရေံ': 1, + 'ရွေ': 1, + 'ရွှေ့': 1, + 'ရှေ': 1, + 'ရှေ့': 1, + 'ရှ်ေ': 1, + 'လေ': 1, + 'လျ': 1, + 'လျို': 1, + 'လျေ': 1, + 'လျှ': 1, + 'လျှို့': 1, + 'လျှေ': 1, + 'လၟေ': 1, + 'လႄ': 1, + 'ဝေ': 1, + 'သေ': 1, + 'သြ': 1, + 'သြို': 1, + 'သှ်ေ': 1, + 'သၞေ': 1, + 'ဟေ': 1, + 'အေ': 1, + 'ၜေ': 1, + 'ၵေ': 1, + 'ၵႄ': 1, + 'ၶေ': 1, + 'ၼေ': 1, + 'ၼႄ': 1, + 'ၽႄ': 1, + 'ႁေ': 1, + 'ႁႄ': 1, + 'កា': 1, + 'កើ': 1, + 'កេ': 1, + 'កោ': 1, + 'ខា': 1, + 'ខែ': 1, + 'គា': 1, + 'គាំ': 1, + 'គេ': 1, + 'គោ': 1, + 'ឃា': 1, + 'ឃើ': 1, + 'ឃោ': 1, + 'ឃៅ': 1, + 'ងា': 1, + 'ចា': 1, + 'ចាំ': 1, + 'ចេ': 1, + 'ចែ': 1, + 'ចោ': 1, + 'ឆាំ': 1, + 'ជា': 1, + 'ជៀ': 1, + 'ឈា': 1, + 'ញុះ': 1, + 'ដា': 1, + 'ដើ': 1, + 'ដែ': 1, + 'ដៃ': 1, + 'ដោ': 1, + 'ដៅ': 1, + 'ឋា': 1, + 'ណា': 1, + 'ណើ': 1, + 'ណែ': 1, + 'ណោ': 1, + 'ណោះ': 1, + 'ណៈ': 1, + 'តា': 1, + 'តេ': 1, + 'តែ': 1, + 'ថា': 1, + 'ថែ': 1, + 'ទា': 1, + 'ទាំ': 1, + 'ទៀ': 1, + 'ទេ': 1, + 'ទៃ': 1, + 'ទោ': 1, + 'ទោះ': 1, + 'ទៅ': 1, + 'ធា': 1, + 'នា': 1, + 'នាំ': 1, + 'នឿ': 1, + 'នេះ': 1, + 'នៃ': 1, + 'នោះ': 1, + 'នៅ': 1, + 'នៈ': 1, + 'បា': 1, + 'បើ': 1, + 'បៀ': 1, + 'បែ': 1, + 'បោ': 1, + 'បោះ': 1, + 'បះ': 1, + 'ប៉ះ': 1, + 'ពា': 1, + 'ពើ': 1, + 'ពេ': 1, + 'ពោះ': 1, + 'ភា': 1, + 'ភេ': 1, + 'មា': 1, + 'មើ': 1, + 'មេ': 1, + 'មែ': 1, + 'ម៉ា': 1, + 'ម៉ោ': 1, + 'យា': 1, + 'យោ': 1, + 'យៈ': 1, + 'យ៉ា': 1, + 'រា': 1, + 'រាំ': 1, + 'រើ': 1, + 'រឿ': 1, + 'រៀ': 1, + 'រះ': 1, + 'រៈ': 1, + 'លា': 1, + 'លុះ': 1, + 'លើ': 1, + 'លេ': 1, + 'លោ': 1, + 'លៈ': 1, + 'វា': 1, + 'វេ': 1, + 'វៈ': 1, + 'សា': 1, + 'សើ': 1, + 'សេ': 1, + 'សៈ': 1, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ហា': 1, + 'ហើ': 1, + 'ហេ': 1, + 'ហែ': 1, + 'ហោ': 1, + 'ឡា': 1, + 'ឡើ': 1, + 'អា': 1, + 'អើ': 1, + 'អៀ': 1, + 'ᨠᩮ᩠': 1, + 'ᨠᩮᩢ᩵': 1, + 'ᨠᩮ᩵': 1, + 'ᨠᩯ᩠': 1, + 'ᨠᩯ᩵': 1, + 'ᨡᩮ᩠': 1, + 'ᨡᩮᩢ': 1, + 'ᨡᩮᩢᩢ': 1, + 'ᨡᩯ᩠': 1, + 'ᨡᩱᩢ': 1, + 'ᨣᩕ': 1, + 'ᨣᩕ᩠': 1, + 'ᨣᩕᩮ᩠': 1, + 'ᨣᩭ': 1, + 'ᨣᩮᩢᩢ': 1, + 'ᨣᩯ᩠': 1, + 'ᨤᩮᩢ': 1, + 'ᨤᩱ᩵': 1, + 'ᨧᩮ': 1, + 'ᨧᩮ᩠': 1, + 'ᨧᩮᩢᩢ': 1, + 'ᨧᩱ': 1, + 'ᨧᩱ᩠': 1, + 'ᨩᩭ᩵': 1, + 'ᨩᩮ᩠': 1, + 'ᨩᩱᩢ': 1, + 'ᨩ᩵ᩭ': 1, + 'ᨪᩮᩢ': 1, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᨯᩪᩯ': 1, + 'ᨯᩯ᩠': 1, + 'ᨯᩰ᩠': 1, + 'ᨯᩱ': 1, + 'ᨯᩱᩢ': 1, + 'ᨲᩕ': 1, + 'ᨲᩕᩦ': 1, + 'ᨲᩕ᩼': 1, + 'ᨲᩮ᩠': 1, + 'ᨲᩯ᩠': 1, + 'ᨲᩯ᩵': 1, + 'ᨲᩱ': 1, + 'ᨲᩱ᩠': 1, + 'ᨲᩱᩢ': 1, + 'ᨳᩮ᩠': 1, + 'ᨳᩮᩢᩢ': 1, + 'ᨳᩯ᩠': 1, + 'ᨴᩮ᩠': 1, + 'ᨴᩮᩢᩢ': 1, + 'ᨴᩮᩢ᩵': 1, + 'ᨴᩯ᩠': 1, + 'ᨴᩯᩢ': 1, + 'ᨴᩰ᩠': 1, + 'ᨶᩮ': 1, + 'ᨶᩯ᩠': 1, + 'ᨶᩱ': 1, + 'ᨷᩕ': 1, + 'ᨷᩮ᩠': 1, + 'ᨸᩮ᩠': 1, + 'ᨸᩮᩢ᩵': 1, + 'ᨹᩪᩢᩱ': 1, + 'ᨹᩮᩨ᩠': 1, + 'ᨻᩕ᩠': 1, + 'ᨻᩮ᩠': 1, + 'ᨻᩱ': 1, + 'ᨽᩱ᩠': 1, + 'ᨾᩮ᩠': 1, + 'ᨾᩯ᩵': 1, + 'ᨾᩱ': 1, + 'ᨿᩮ᩠': 1, + 'ᨿᩰ': 1, + 'ᩁᩢᩭ': 1, + 'ᩁᩮ᩠': 1, + 'ᩁᩮᩨ᩠': 1, + 'ᩁᩯ᩠': 1, + 'ᩃᩮ᩠': 1, + 'ᩃᩯ': 1, + 'ᩃᩰ᩠': 1, + 'ᩅᩮ': 1, + 'ᩅᩮ᩠': 1, + 'ᩅᩱᩢ': 1, + 'ᩈᩮ': 1, + 'ᩈᩮ᩠': 1, + 'ᩈᩯ᩠': 1, + 'ᩈᩱ': 1, + 'ᩈᩱ᩠': 1, + 'ᩉᩕᩨ': 1, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + 'ᩉᩮᩖᩨ': 1, + 'ᩉᩮ᩠': 1, + 'ᩉᩯ᩠': 1, + 'ᩉᩯ᩵': 1, + 'ᩉᩱ᩠': 1, + 'ꦏꦃ': 1, + 'ꦏꦺ': 1, + 'ꦏꦺꦴ': 1, + 'ꦏꦿ': 1, + 'ꦏꦿꦁ': 1, + 'ꦏꦿꦂ': 1, + 'ꦒꦸꦃ': 1, + 'ꦒꦺꦴ': 1, + 'ꦒꦿ': 1, + 'ꦒꦿꦺ': 1, + 'ꦔꦃ': 1, + 'ꦔꦺ': 1, + 'ꦔꦺꦴ': 1, + 'ꦔꦿ': 1, + 'ꦕꦃ': 1, + 'ꦕꦺꦴ': 1, + 'ꦗꦃ': 1, + 'ꦗꦺ': 1, + 'ꦗꦿꦺꦴ': 1, + 'ꦝꦃ': 1, + 'ꦝꦺ': 1, + 'ꦝꦺꦴ': 1, + 'ꦝꦺꦴꦂ': 1, + 'ꦠꦸꦃ': 1, + 'ꦠꦺ': 1, + 'ꦠꦺꦃ': 1, + 'ꦠꦺꦴ': 1, + 'ꦠꦾ': 1, + 'ꦠꦿ': 1, + 'ꦠꦿꦶ': 1, + 'ꦠ꧀ꦱ꧀ꦮ': 3, + 'ꦢꦃ': 1, + 'ꦢꦺ': 1, + 'ꦢꦺꦴ': 1, + 'ꦢꦾ': 1, + 'ꦢꦿ': 1, + 'ꦤꦺ': 1, + 'ꦤꦺꦃ': 1, + 'ꦤꦺꦴ': 1, + 'ꦥꦃ': 1, + 'ꦥꦺ': 1, + 'ꦥꦺꦴ': 1, + 'ꦥꦿ': 1, + 'ꦥꦿꦃ': 1, + 'ꦥꦿꦶ': 1, + 'ꦥꦿꦺꦴ': 1, + 'ꦧꦺ': 1, + 'ꦧꦺꦃ': 1, + 'ꦧꦺꦴ': 1, + 'ꦧꦿ': 1, + 'ꦩꦃ': 1, + 'ꦩꦺ': 1, + 'ꦩꦺꦃ': 1, + 'ꦩꦺꦴ': 1, + 'ꦩꦿ': 1, + 'ꦩꦿꦃ': 1, + 'ꦩꦿꦶꦃ': 1, + 'ꦪꦃ': 1, + 'ꦪꦸꦃ': 1, + 'ꦪꦺꦴ': 1, + 'ꦫꦶꦃ': 1, + 'ꦫꦺ': 1, + 'ꦫꦺꦴ': 1, + 'ꦫꦾ': 1, + 'ꦭꦃ': 1, + 'ꦭꦶꦃ': 1, + 'ꦭꦺ': 1, + 'ꦭꦺꦃ': 1, + 'ꦭꦺꦴ': 1, + 'ꦭꦺꦴꦁ': 1, + 'ꦮꦃ': 1, + 'ꦮꦶꦃ': 1, + 'ꦮꦸꦃ': 1, + 'ꦮꦺ': 1, + 'ꦮꦺꦴ': 1, + 'ꦮꦺꦴꦁ': 1, + 'ꦮꦺꦴꦃ': 1, + 'ꦮꦿꦸꦃ': 1, + 'ꦱꦶꦃ': 1, + 'ꦱꦺ': 1, + 'ꦱꦺꦴ': 1, + 'ꦱꦾ': 1, + 'ꦱꦿ': 1, + 'ꦱꦿꦃ': 1, + 'ꦲꦺ': 1, + 'ꦲꦺꦴ': 1, + '𑄃𑄬': 1, + '𑄃𑄬𑄭': 1, + '𑄇𑄬': 1, + '𑄈𑄬': 1, + '𑄉𑄬': 1, + '𑄉𑄬𑄭': 1, + '𑄊𑄬': 1, + '𑄌𑄬': 1, + '𑄎𑄬': 1, + '𑄎𑄬𑄭': 1, + '𑄏𑄬': 1, + '𑄑𑄬': 1, + '𑄖𑄬': 1, + '𑄗𑄬': 1, + '𑄘𑄬': 1, + '𑄙𑄬': 1, + '𑄚𑄬': 1, + '𑄚𑄬𑄭': 1, + '𑄛𑄬': 1, + '𑄜𑄬': 1, + '𑄝𑄬': 1, + '𑄞𑄬': 1, + '𑄞𑄬𑄭': 1, + '𑄟𑄬': 1, + '𑄠𑄬': 1, + '𑄠𑄬𑄭': 1, + '𑄡𑄬': 1, + '𑄡𑄬𑄭': 1, + '𑄢𑄬': 1, + '𑄢𑄬𑄭': 1, + '𑄣𑄬': 1, + '𑄣𑄬𑄭': 1, + '𑄥𑄬': 1, + '𑄦𑄬': 1, + '𑌅𑌂': 1, + '𑌕𑌂': 1, + '𑌕𑌾': 1, + '𑌕𑌾𑌂': 1, + '𑌕𑌾𑌃': 1, + '𑌕𑌿': 1, + '𑌕𑍂': 1, + '𑌕𑍃': 1, + '𑌕𑍇': 1, + '𑌕𑍋': 1, + '𑌗𑌾': 1, + '𑌗𑌿': 1, + '𑌗𑍁': 1, + '𑌗𑍃': 1, + '𑌗𑍇': 1, + '𑌗𑍌': 1, + '𑌘𑌾': 1, + '𑌘𑍇': 1, + '𑌘𑍋': 1, + '𑌚𑌾': 1, + '𑌚𑌿': 1, + '𑌚𑍇': 1, + '𑌚𑍈': 1, + '𑌛𑌾': 1, + '𑌛𑍁': 1, + '𑌛𑍇': 1, + '𑌜𑌾': 1, + '𑌜𑌿': 1, + '𑌜𑍇': 1, + '𑌞𑌾': 1, + '𑌟𑌃': 1, + '𑌟𑌾': 1, + '𑌟𑌾𑌂': 1, + '𑌟𑌾𑌃': 1, + '𑌠𑌾': 1, + '𑌠𑍇': 1, + '𑌠𑍈𑌃': 1, + '𑌡𑌂': 1, + '𑌡𑌃': 1, + '𑌡𑌿': 1, + '𑌣𑌂': 1, + '𑌣𑌾': 1, + '𑌣𑌾𑌂': 1, + '𑌣𑌿': 1, + '𑌣𑍁': 1, + '𑌣𑍇': 1, + '𑌤𑌂': 1, + '𑌤𑌃': 1, + '𑌤𑌾': 1, + '𑌤𑌾𑌂': 1, + '𑌤𑌾𑌃': 1, + '𑌤𑌿': 1, + '𑌤𑌿𑌂': 1, + '𑌤𑌿𑌃': 1, + '𑌤𑍁': 1, + '𑌤𑍁𑌂': 1, + '𑌤𑍃': 1, + '𑌤𑍇': 1, + '𑌤𑍇𑌃': 1, + '𑌤𑍈': 1, + '𑌤𑍋': 1, + '𑌤𑍌': 1, + '𑌥𑌂': 1, + '𑌥𑌾': 1, + '𑌥𑌾𑌂': 1, + '𑌥𑌿': 1, + '𑌦𑌂': 1, + '𑌦𑌃': 1, + '𑌦𑌾': 1, + '𑌦𑌿': 1, + '𑌦𑍁': 1, + '𑌦𑍃': 1, + '𑌦𑍇': 1, + '𑌦𑍋': 1, + '𑌧𑌃': 1, + '𑌧𑌾': 1, + '𑌧𑌾𑌃': 1, + '𑌧𑌿': 1, + '𑌧𑌿𑌂': 1, + '𑌧𑍁': 1, + '𑌧𑍃': 1, + '𑌧𑍇': 1, + '𑌧𑍇𑌃': 1, + '𑌧𑍈𑌃': 1, + '𑌨𑌂': 1, + '𑌨𑌃': 1, + '𑌨𑌾': 1, + '𑌨𑌾𑌂': 1, + '𑌨𑌾𑌃': 1, + '𑌨𑌿': 1, + '𑌨𑌿𑌃': 1, + '𑌨𑍁': 1, + '𑌨𑍂': 1, + '𑌨𑍇': 1, + '𑌨𑍈': 1, + '𑌨𑍈𑌃': 1, + '𑌨𑍋': 1, + '𑌪𑌂': 1, + '𑌪𑌾': 1, + '𑌪𑌿': 1, + '𑌪𑍁': 1, + '𑌪𑍂': 1, + '𑌪𑍇': 1, + '𑌪𑍋': 1, + '𑌬𑌾': 1, + '𑌬𑍃': 1, + '𑌭𑌾': 1, + '𑌭𑌿': 1, + '𑌭𑌿𑌃': 1, + '𑌭𑍁': 1, + '𑌭𑍂': 1, + '𑌭𑍇': 1, + '𑌮𑌂': 1, + '𑌮𑌾': 1, + '𑌮𑌿': 1, + '𑌮𑌿𑌂': 1, + '𑌮𑍁': 1, + '𑌮𑍂': 1, + '𑌮𑍇': 1, + '𑌮𑍈': 1, + '𑌮𑍋': 1, + '𑌮𑍌': 1, + '𑌯𑌂': 1, + '𑌯𑌃': 1, + '𑌯𑌾': 1, + '𑌯𑌾𑌂': 1, + '𑌯𑌾𑌃': 1, + '𑌯𑌿': 1, + '𑌯𑍁': 1, + '𑌯𑍁𑌃': 1, + '𑌯𑍇': 1, + '𑌯𑍈': 1, + '𑌯𑍈𑌃': 1, + '𑌯𑍋': 1, + '𑌯𑍋𑌃': 1, + '𑌰𑌂': 1, + '𑌰𑌃': 1, + '𑌰𑌾': 1, + '𑌰𑌾𑌃': 1, + '𑌰𑌿': 1, + '𑌰𑌿𑌂': 1, + '𑌰𑍁': 1, + '𑌰𑍂': 1, + '𑌰𑍇': 1, + '𑌰𑍋': 1, + '𑌰𑍌': 1, + '𑌲𑌂': 1, + '𑌲𑌾': 1, + '𑌲𑌾𑌃': 1, + '𑌲𑌿': 1, + '𑌲𑌿𑌂': 1, + '𑌲𑍇': 1, + '𑌲𑍈': 1, + '𑌲𑍋': 1, + '𑌵𑌂': 1, + '𑌵𑌃': 1, + '𑌵𑌾': 1, + '𑌵𑌾𑌂': 1, + '𑌵𑌾𑌃': 1, + '𑌵𑌿': 1, + '𑌵𑍃': 1, + '𑌵𑍇': 1, + '𑌵𑍈': 1, + '𑌵𑍋': 1, + '𑌶𑌂': 1, + '𑌶𑌃': 1, + '𑌶𑌾': 1, + '𑌶𑌾𑌃': 1, + '𑌶𑌿': 1, + '𑌶𑍁': 1, + '𑌶𑍂': 1, + '𑌶𑍇': 1, + '𑌶𑍋': 1, + '𑌷𑌂': 1, + '𑌷𑌾': 1, + '𑌷𑌾𑌂': 1, + '𑌷𑌾𑌃': 1, + '𑌷𑌿': 1, + '𑌷𑍁': 1, + '𑌷𑍇': 1, + '𑌷𑍈': 1, + '𑌷𑍋': 1, + '𑌸𑌂': 1, + '𑌸𑌃': 1, + '𑌸𑌾': 1, + '𑌸𑌾𑌂': 1, + '𑌸𑌿': 1, + '𑌸𑍁': 1, + '𑌸𑍂': 1, + '𑌸𑍇': 1, + '𑌸𑍋': 1, + '𑌸𑍌': 1, + '𑌹𑌾': 1, + '𑌹𑌾𑌃': 1, + '𑌹𑌿': 1, + '𑌹𑍇': 1, + '𑌹𑍋': 1, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_e08bd75e.py b/libs/wcwidth/table_grapheme_overrides/_known_e08bd75e.py new file mode 100644 index 000000000..0575d5844 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_e08bd75e.py @@ -0,0 +1,2641 @@ +""" +Grapheme overrides for _known_e08bd75e. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: apple_terminal +""" +# pylint: skip-file +GRAPHEMES = { + 'ت‌': 2, + 'ن‌': 2, + 'ه‌': 2, + 'ی‌': 2, + 'ے‌': 2, + 'क्का': 3, + 'क्कां': 3, + 'क्को': 3, + 'क्घ्घि्': 4, + 'क्ता': 3, + 'क्ति': 3, + 'क्ती': 3, + 'क्तीं': 3, + 'क्तेः': 3, + 'क्त्योः': 5, + 'क्ना': 3, + 'क्नि': 3, + 'क्या': 3, + 'क्रा': 3, + 'क्रि': 3, + 'क्षा': 3, + 'क्षां': 3, + 'क्षाः': 4, + 'क्षि': 3, + 'क्षो': 3, + 'क्ष्ण': 3, + 'क्ष्य': 3, + 'क्ष्या': 4, + 'क्सा': 3, + 'ख्यः': 3, + 'ख्या': 3, + 'ख्‍ने': 3, + 'ग्या': 3, + 'ग्यैः': 3, + 'ग्र्य': 3, + 'घ्का': 3, + 'घ्र्व्य': 4, + 'घ्लि': 3, + 'घ्श्य': 3, + 'घ्स्ट': 3, + 'च्चा': 3, + 'च्चों': 3, + 'च्छा': 3, + 'च्छि': 3, + 'च्या': 3, + 'च्यां': 3, + 'च्यो': 3, + 'ज्ञा': 3, + 'ज्या': 3, + 'ज्या‌': 4, + 'ज्रा': 3, + 'ञ्चा': 3, + 'ञ्चि': 3, + 'ट्टि': 3, + 'ट्टी': 3, + 'ट्या': 3, + 'ट्रा': 3, + 'ण्डः': 3, + 'ण्डि': 3, + 'ण्या': 3, + 'ण्यां': 3, + 'ताः': 3, + 'त्का': 3, + 'त्ता': 3, + 'त्ति': 3, + 'त्त्थि': 4, + 'त्त्व': 3, + 'त्त्वा': 4, + 'त्त्वे': 3, + 'त्नी': 3, + 'त्नों': 3, + 'त्पा': 3, + 'त्पी': 3, + 'त्मा': 3, + 'त्या': 3, + 'त्यां': 3, + 'त्यि': 3, + 'त्रा': 3, + 'त्राा': 4, + 'त्राि': 4, + 'त्रि': 3, + 'त्री': 3, + 'त्र्त': 3, + 'त्र्य': 3, + 'त्र्या': 4, + 'त्र्ये': 3, + 'त्वा': 3, + 'त्वां': 3, + 'त्सा': 3, + 'त्सो': 3, + 'त्स्वा': 4, + 'द्घि': 3, + 'द्घो': 3, + 'द्दा': 3, + 'द्दि': 3, + 'द्दी': 3, + 'द्धः': 3, + 'द्धा': 3, + 'द्धां': 3, + 'द्धि': 3, + 'द्धिं': 3, + 'द्धी': 3, + 'द्धीं': 3, + 'द्धैः': 3, + 'द्भा': 3, + 'द्या': 3, + 'द्रो': 3, + 'द्वा': 3, + 'द्वि': 3, + 'द्‌': 2, + 'धाः': 3, + 'ध्या': 3, + 'नाः': 3, + 'ना‌': 3, + 'निः': 3, + 'न्का': 3, + 'न्चि': 3, + 'न्ञ्या': 4, + 'न्तः': 3, + 'न्ता': 3, + 'न्ति': 3, + 'न्तो': 3, + 'न्तों': 3, + 'न्त्र': 3, + 'न्त्रा': 4, + 'न्त्राः': 5, + 'न्त्रि': 4, + 'न्त्री': 4, + 'न्त्र्य': 4, + 'न्त्र्यं': 4, + 'न्त्र्या': 5, + 'न्त्र्ये': 4, + 'न्दा': 3, + 'न्दी': 3, + 'न्दो': 3, + 'न्धा': 3, + 'न्धि': 3, + 'न्धी': 3, + 'न्धों': 3, + 'न्ना': 3, + 'न्नाः': 4, + 'न्नि': 3, + 'न्मा': 3, + 'न्या': 3, + 'न्यो': 3, + 'न्हा': 3, + 'न्ह्यां': 4, + 'प्ति': 3, + 'प्तौ': 3, + 'प्या': 3, + 'प्रा': 3, + 'फ्ता': 3, + 'फ्ना': 3, + 'फ्नो': 3, + 'फ्रां': 3, + 'ब्धि': 3, + 'भिः': 3, + 'भ्यः': 3, + 'भ्या': 3, + 'भ्यां': 3, + 'भ्यो': 3, + 'म्ति': 3, + 'म्नि': 3, + 'म्पा': 3, + 'म्प्र': 3, + 'म्भि': 3, + 'म्मा': 3, + 'म्मि': 3, + 'म्या': 3, + 'म्ही': 3, + 'म्हो': 3, + 'याः': 3, + 'य्यो': 3, + 'राः': 3, + 'र्कि': 3, + 'र्गी': 3, + 'र्चि': 3, + 'र्जा': 3, + 'र्जि': 3, + 'र्जी': 3, + 'र्णां': 3, + 'र्णी': 3, + 'र्ता': 3, + 'र्ति': 3, + 'र्तिः': 4, + 'र्ती': 3, + 'र्त्त': 3, + 'र्त्ति': 4, + 'र्थि': 3, + 'र्थी': 3, + 'र्दा': 3, + 'र्दि': 3, + 'र्दो': 3, + 'र्द्ध': 3, + 'र्धा': 3, + 'र्धि': 3, + 'र्नि': 3, + 'र्बा': 3, + 'र्भा': 3, + 'र्मा': 3, + 'र्मि': 3, + 'र्या': 3, + 'र्यां': 3, + 'र्यों': 3, + 'र्रा': 3, + 'र्वा': 3, + 'र्वां': 3, + 'र्वो': 3, + 'र्सा': 3, + 'र्हाः': 4, + 'र्हि': 3, + 'र्हो': 3, + 'र्‍या': 4, + 'लाः': 3, + 'ल्का': 3, + 'ल्पि': 3, + 'ल्या': 3, + 'ल्यां': 3, + 'ल्याः': 4, + 'ल्ला': 3, + 'ल्लि': 3, + 'वाः': 3, + 'व्या': 3, + 'व्यों': 3, + 'शाः': 3, + 'श्चि': 3, + 'श्या': 3, + 'श्यि': 3, + 'श्यों': 3, + 'श्रा': 3, + 'श्रां': 3, + 'श्वा': 3, + 'श्वि': 3, + 'षाः': 3, + 'ष्का': 3, + 'ष्टः': 3, + 'ष्टा': 3, + 'ष्टां': 3, + 'ष्टाः': 4, + 'ष्टि': 3, + 'ष्टी': 3, + 'ष्टीं': 3, + 'ष्ट्र': 3, + 'ष्ट्रा': 4, + 'ष्ट्रां': 4, + 'ष्ट्रि': 4, + 'ष्ट्री': 4, + 'ष्ट्रे': 3, + 'ष्ट्रो': 4, + 'ष्ट्रों': 4, + 'ष्ठा': 3, + 'ष्ठों': 3, + 'ष्प्र': 3, + 'ष्यः': 3, + 'ष्यों': 3, + 'स्का': 3, + 'स्काः': 4, + 'स्ट्या': 4, + 'स्ता': 3, + 'स्ति': 3, + 'स्ती': 3, + 'स्तो': 3, + 'स्त्र': 3, + 'स्त्रा': 4, + 'स्त्रि': 4, + 'स्त्री': 4, + 'स्था': 3, + 'स्थां': 3, + 'स्थि': 3, + 'स्थी': 3, + 'स्थ्य': 3, + 'स्धी': 3, + 'स्मि': 3, + 'स्मिं': 3, + 'स्या': 3, + 'स्यां': 3, + 'स्याः': 4, + 'स्युः': 3, + 'स्यों': 3, + 'स्रो': 3, + 'स्वा': 3, + 'स्वी': 3, + 'स्सा': 3, + 'ह्या': 3, + 'ह्रा': 3, + 'फ़्ता': 3, + 'আ‌': 2, + 'ই‌': 2, + 'এ‌': 2, + 'কা‌': 3, + 'কিং': 3, + 'কে‌': 3, + 'ক্তি': 3, + 'ক্রী': 3, + 'ক্ষা': 3, + 'ক্ষি': 3, + 'ক্ষে': 3, + 'ক্ষ্য': 3, + 'ক্ষ্যে': 4, + 'খ্যা': 3, + 'গ্রা': 3, + 'গ্রে': 3, + 'ঘো': 3, + 'ঙ্খা': 3, + 'ঙ্গী': 3, + 'ঙ্গে': 3, + 'চ্ছা': 3, + 'চ্ছা‌': 4, + 'চ্ছে': 3, + 'জে‌': 3, + 'জ্ঞা': 3, + 'ঞ্চি': 3, + 'ঞ্ছি': 3, + 'ট্রে': 3, + 'ণ্ডে': 3, + 'তা‌': 3, + 'তি‌': 3, + 'তে‌': 3, + 'ত্তা': 3, + 'ত্তি': 3, + 'ত্বে': 3, + 'ত্যা': 3, + 'ত্যি': 3, + 'ত্যে': 3, + 'ত্রা': 3, + 'ত্রে': 3, + 'ত্‍': 2, + 'ত্‍প': 3, + 'ত্‍পী': 4, + 'ত্‍সা': 4, + 'দে‌': 3, + 'দ্দি': 3, + 'দ্দে': 3, + 'দ্ধা': 3, + 'দ্ধি': 3, + 'দ্ধে': 3, + 'দ্বা': 3, + 'দ্রো': 3, + 'ধ্যে': 3, + 'না‌': 3, + 'ন্তা': 3, + 'ন্তি': 3, + 'ন্তে': 3, + 'ন্ত্রি': 4, + 'ন্ত্রে': 4, + 'ন্থী': 3, + 'ন্ধা': 3, + 'ন্মা': 3, + 'ন্যা': 3, + 'ন্যে': 3, + 'ন‌': 2, + 'পা‌': 3, + 'প্তা': 3, + 'প্রা': 3, + 'বা‌': 3, + 'ব্ধি': 3, + 'ব্যা': 3, + 'ভ্রা': 3, + 'মা‌': 3, + 'ম্পা': 3, + 'ম্মা': 3, + 'রাং': 3, + 'র্জা': 3, + 'র্থে': 3, + 'র্দো': 3, + 'র্ধা': 3, + 'র্নী': 3, + 'র্বা': 3, + 'র্বি': 3, + 'র্বো': 3, + 'র্ব্য': 3, + 'র্মে': 3, + 'র্যা': 3, + 'র‌': 2, + 'লে‌': 3, + 'ল্যা': 3, + 'ল্যে': 3, + 'ল্লে': 3, + 'শ্চি': 3, + 'শ্বা': 3, + 'শ্বে': 3, + 'শ্যে': 3, + 'শ্রা': 3, + 'ষ্টা': 3, + 'ষ্টি': 3, + 'ষ্ট্র': 3, + 'ষ্ট্রী': 4, + 'ষ্ট্রে': 4, + 'ষ্ঠা': 3, + 'ষ্ঠি': 3, + 'ষ্ঠী': 3, + 'সাং': 3, + 'সে‌': 3, + 'স্তি': 3, + 'স্তি‌': 4, + 'স্ত্র': 3, + 'স্থা': 3, + 'স্থ্য': 3, + 'স্বা': 3, + 'স্বী': 3, + 'স্যা': 3, + 'স্যে': 3, + 'হ‌': 2, + 'ક્કા': 3, + 'ક્કો': 3, + 'ક્તિ': 3, + 'ક્ષા': 3, + 'ચ્ચા': 3, + 'ચ્છા': 3, + 'જ્ઞા': 3, + 'જ્યો': 3, + 'ત્તા': 3, + 'ત્તિ': 3, + 'ત્ત્વ': 3, + 'ત્ત્વા': 4, + 'ત્યા': 3, + 'ત્યાં': 3, + 'ત્યિ': 3, + 'ત્યો': 3, + 'ત્રી': 3, + 'ત્ર્ય': 3, + 'દ્ધા': 3, + 'દ્ધાં': 3, + 'દ્ધિ': 3, + 'દ્વા': 3, + 'ધ્યા': 3, + 'ન્તો': 3, + 'ન્મા': 3, + 'ન્યા': 3, + 'પ્રા': 3, + 'મ્યાં': 3, + 'ર્તા': 3, + 'ર્થિ': 3, + 'ર્દો': 3, + 'ર્મિ': 3, + 'ર્યા': 3, + 'ર્યો': 3, + 'વ્યા': 3, + 'વ્યો': 3, + 'ષ્ટિ': 3, + 'ષ્ટ્ર': 3, + 'ષ્ટ્રી': 4, + 'ષ્ટ્રો': 4, + 'ષ્ઠા': 3, + 'સ્તિ': 3, + 'સ્તી': 3, + 'સ્ત્રી': 4, + 'સ્થા': 3, + 'સ્થિ': 3, + 'સ્રી': 3, + 'સ્વા': 3, + 'સ્વી': 3, + 'સ્સા': 3, + 'కుం': 3, + 'క్కు': 3, + 'క్తు': 3, + 'క్రాం': 3, + 'క్రూ': 3, + 'క్షిం': 3, + 'క్స్వా': 3, + 'చ్చు': 3, + 'టుం': 3, + 'ట్లు': 3, + 'డ్రు': 3, + 'త్తు': 3, + 'త్నిం': 3, + 'త్యం': 3, + 'త్రు': 3, + 'త్ర్య': 3, + 'దుం': 3, + 'ద్దు': 3, + 'ద్ధిం': 3, + 'ద్ర్య': 3, + 'నుం': 3, + 'న్ను': 3, + 'భ్యు': 3, + 'యుం': 3, + 'ర్చు': 3, + 'ర్ణాం': 3, + 'ర్ణు': 3, + 'ర్తిం': 3, + 'ర్తృ': 3, + 'ర్థిం': 3, + 'ర్బం': 3, + 'ర్హాం': 3, + 'ర్హు': 3, + 'శ్రాం': 3, + 'ష్టు': 3, + 'ష్ట్ర': 3, + 'ష్ట్రీ': 3, + 'ష్ఠు': 3, + 'స్కృ': 3, + 'స్త్ర': 3, + 'స్త్రీ': 3, + 'స్థ్య': 3, + 'ಕಾಂ': 3, + 'ಕುಂ': 3, + 'ಕೊಂ': 3, + 'ಕ್‌': 2, + 'ಖಾಂ': 3, + 'ಗಾಂ': 3, + 'ಗೊಂ': 3, + 'ಟುಂ': 3, + 'ನಾಂ': 3, + 'ಯುಂ': 3, + 'ರಾಂ': 3, + 'ವುಂ': 3, + 'ಶಾಂ': 3, + 'ಸಾಂ': 3, + 'ಹೊಂ': 3, + 'ക്കാ': 3, + 'ക്കി': 3, + 'ക്കും': 3, + 'ക്കെ': 3, + 'ക്കേ': 3, + 'ക്കൊ': 3, + 'ക്കോ': 3, + 'ക്ക്‌': 3, + 'ക്തി': 3, + 'ക്യം': 3, + 'ക്ഷാ': 3, + 'ക്ഷി': 3, + 'ക്ഷേ': 3, + 'ഖ്യാ': 3, + 'ഗ്ഗം': 3, + 'ഗ്ദാ': 3, + 'ഗ്യം': 3, + 'ങ്കി': 3, + 'ങ്കെ': 3, + 'ങ്കേ': 3, + 'ങ്ങി': 3, + 'ച്ചി': 3, + 'ച്ചും': 3, + 'ച്ചെ': 3, + 'ച്ചേ': 3, + 'ജ്യാ': 3, + 'ഞ്ഞി': 3, + 'ഞ്ഞെ': 3, + 'ട്ടാ': 3, + 'ട്ടി': 3, + 'ട്ടോ': 3, + 'ട്‌': 2, + 'ണ്ടാ': 3, + 'ണ്ടി': 3, + 'ണ്ടും': 3, + 'ണ്ടെ': 3, + 'ണ്ട്‌': 3, + 'ണ്‌': 2, + 'ത്താ': 3, + 'ത്തി': 3, + 'ത്തും': 3, + 'ത്തെ': 3, + 'ത്തേ': 3, + 'ത്തോ': 3, + 'ത്ത്‌': 3, + 'ത്നം': 3, + 'ത്നി': 3, + 'ത്യാ': 3, + 'ത്യേ': 3, + 'ത്വം': 3, + 'ത്‌': 2, + 'ദ്ധി': 3, + 'ദ്യാ': 3, + 'ന്തം': 3, + 'ന്തെ': 3, + 'ന്ത്ര': 3, + 'ന്ത്ര്യ': 4, + 'ന്ത്ര്യം': 5, + 'ന്ധി': 3, + 'ന്നാ': 3, + 'ന്നി': 3, + 'ന്നീ': 3, + 'ന്നും': 3, + 'ന്നെ': 3, + 'ന്നേ': 3, + 'ന്നോ': 3, + 'ന്മാ': 3, + 'ന്യം': 3, + 'ന്യാ': 3, + 'ന്യേ': 3, + 'ന്യോ': 3, + 'ന്റെ': 3, + 'ന്‌': 2, + 'ന്‍': 2, + 'ന്‍കൂ': 3, + 'ന്‍പി': 4, + 'പ്പി': 3, + 'പ്പെ': 3, + 'പ്പോ': 3, + 'പ്പ്‌': 3, + 'പ്രാ': 3, + 'ബ്ലി': 3, + 'ഭ്യാ': 3, + 'ഭ്രാ': 3, + 'മ്പാ': 3, + 'മ്പ്ര': 3, + 'മ്മി': 3, + 'യ്ക്കു': 3, + 'യ്തി': 3, + 'യ്യാ': 3, + 'യ്യി': 3, + 'ര്യം': 3, + 'ര്‍ക്കു': 4, + 'ര്‍ക്കും': 5, + 'ര്‍ക്ക്‌': 5, + 'ര്‍ഗ്ഗം': 5, + 'ര്‍ച്ച': 4, + 'ര്‍ണ്ണ': 4, + 'ര്‍ത്ത': 4, + 'ര്‍ത്തി': 5, + 'ര്‍ത്തു': 4, + 'ര്‍ത്തേ': 5, + 'ര്‍ദേ': 4, + 'ര്‍ദ്ദ': 4, + 'ര്‍പ്പെ': 5, + 'ര്‍ബ': 3, + 'ര്‍ഭ': 3, + 'ര്‍മ്മി': 5, + 'ര്‍രാ': 4, + 'ര്‍വ്വ': 4, + 'ര്‍ഹ': 3, + 'ര്‍ഹി': 4, + 'റ്റം': 3, + 'റ്റേ': 3, + 'ല്യാ': 3, + 'ല്ലാ': 3, + 'ല്ലാം': 4, + 'ല്ലെ': 3, + 'ല്‍': 2, + 'ല്‍കി': 4, + 'ല്‍കു': 3, + 'ല്‍കേ': 4, + 'ല്‍ത': 3, + 'ല്‍നി': 4, + 'ല്‍പ്പ': 4, + 'ള്ളാ': 3, + 'ള്ളി': 3, + 'ള്‍': 2, + 'ള്‍കൊ': 4, + 'ള്‍ക്ക': 4, + 'ള്‍ക്കി': 5, + 'ള്‍ക്കു': 4, + 'ള്‍ക്കും': 5, + 'ള്‍ക്കെ': 5, + 'ള്‍ക്ക്‌': 5, + 'വ്യം': 3, + 'വ്യാ': 3, + 'ശ്വാ': 3, + 'ഷ്ട്യാ': 4, + 'ഷ്ട്ര': 3, + 'ഷ്ട്രീ': 4, + 'ഷ്യാ': 3, + 'സാം': 3, + 'സ്കാ': 3, + 'സ്ത്രം': 4, + 'സ്ത്രീ': 4, + 'സ്ഥാ': 3, + 'സ്ഥി': 3, + 'സ്റ്റ്‌': 4, + 'സ്വാ': 3, + 'സ്വീ': 3, + 'സ്സാ': 3, + 'സ്സി': 3, + 'സ്സും': 3, + 'സ്സോ': 3, + 'ഹിം': 3, + 'ක්‍': 2, + 'ඛ්‍': 2, + 'ජ්‍': 2, + 'ත්‍': 2, + 'ද්‍': 2, + 'ධ්‍': 2, + 'න්‍': 2, + 'ප්‍': 2, + 'ව්‍': 2, + 'ශ්‍': 2, + 'ෂ්‍': 2, + 'ကျေ': 3, + 'ကြေ': 3, + 'ကြွေ': 3, + 'ချေ': 3, + 'ခြေ': 3, + 'ဇြေ': 3, + 'ဒ္ဂေ': 3, + 'ပျေ': 3, + 'ပြေ': 3, + 'ဖျေံ': 3, + 'ဗ္တေ': 3, + 'ဗ္ၜေ': 3, + 'ဗြေ': 3, + 'မြေ': 3, + 'လျေ': 3, + 'လျှေ': 3, + 'သ္ဂေ': 3, + 'ក្ដៅ': 3, + 'ក្រោ': 3, + 'ក្សា': 3, + 'ខ្ជា': 3, + 'ខ្លា': 3, + 'គ្នា': 3, + 'គ្មា': 3, + 'គ្រា': 3, + 'គ្រឹះ': 3, + 'ង្កើ': 3, + 'ង្រៀ': 3, + 'ង្ហា': 3, + 'ច្ចេ': 3, + 'ច្បា': 3, + 'ឆ្នាំ': 3, + 'ឆ្នោ': 3, + 'ឆ្លើ': 3, + 'ជ្ជា': 3, + 'ជ្ញា': 3, + 'ជ្រើ': 3, + 'ជ្រៀ': 3, + 'ជ្រែ': 3, + 'ឈ្មោះ': 4, + 'ញ្ជា': 3, + 'ញ្ញា': 3, + 'ញ្ញៈ': 3, + 'ដ្ឋា': 3, + 'ណោះ': 3, + 'ណ្ដា': 3, + 'ណ្ដុះ': 3, + 'ណ្ណោះ': 4, + 'ត្រា': 3, + 'ថ្ងៃ': 3, + 'ថ្នា': 3, + 'ថ្លៃ': 3, + 'ទោះ': 3, + 'ទ្យា': 3, + 'ធ្នា': 3, + 'ធ្យា': 3, + 'ធ្យោ': 3, + 'ធ្វើ': 3, + 'នេះ': 3, + 'នោះ': 3, + 'ន្ថែ': 3, + 'ន្ថោ': 3, + 'ន្ទា': 3, + 'ន្ទៈ': 3, + 'ន្យា': 3, + 'ន្លែ': 3, + 'បោះ': 3, + 'ប្ដេ': 3, + 'ប្រា': 3, + 'ប្រើ': 3, + 'ប្រែ': 3, + 'ផ្ដា': 3, + 'ផ្ទា': 3, + 'ផ្ទេ': 3, + 'ផ្នែ': 3, + 'ផ្លា': 3, + 'ផ្សា': 3, + 'ផ្សេ': 3, + 'ផ្សៃ': 3, + 'ពោះ': 3, + 'ព្យា': 3, + 'ព្រៀ': 3, + 'ព្រៃ': 3, + 'ម្ងា': 3, + 'ម្ចា': 3, + 'ម្ដា': 3, + 'ម្ដែ': 3, + 'ម្នា': 3, + 'ម្នេ': 3, + 'ម្ភា': 3, + 'ម្រា': 3, + 'ម្រើ': 3, + 'ម្រេ': 3, + 'ម្លៀ': 3, + 'ម្លៃ': 3, + 'ល្បៈ': 3, + 'ល្មើ': 3, + 'ស្គា': 3, + 'ស្ដ្រី': 3, + 'ស្ត្រ': 3, + 'ស្ថា': 3, + 'ស្នា': 3, + 'ស្មា': 3, + 'ស្មើ': 3, + 'ស្រា': 3, + 'ស្លា': 3, + 'ស្វែ': 3, + 'ᨣᩕᩮ᩠': 3, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 3, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 3, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 3, + '⛓️‍💥': 4, + '⛹️‍♀️': 3, + '⛹️‍♂️': 3, + '⛹🏻‍♀️': 5, + '⛹🏻‍♂️': 5, + '⛹🏼‍♀️': 5, + '⛹🏼‍♂️': 5, + '⛹🏽‍♀️': 5, + '⛹🏽‍♂️': 5, + '⛹🏾‍♀️': 5, + '⛹🏾‍♂️': 5, + '⛹🏿‍♀️': 5, + '⛹🏿‍♂️': 5, + '❤️‍🔥': 4, + '❤️‍🩹': 4, + 'ꦏꦺꦴ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 3, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 3, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 3, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 3, + 'ꦏ꧀ꦠꦸ': 3, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 3, + 'ꦏ꧀ꦢꦶ': 3, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 3, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 3, + 'ꦏ꧀ꦥꦿꦶ': 4, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 3, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 3, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 3, + 'ꦏ꧀ꦱꦶ': 3, + 'ꦏ꧀ꦱꦸ': 3, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 3, + 'ꦏ꧀ꦲꦶꦁ': 3, + 'ꦏ꧀ꦲꦸ': 3, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦺꦴ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦺꦴ': 3, + 'ꦔ꧀ꦒꦼ': 3, + 'ꦔ꧀ꦒꦿꦼꦁ': 4, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 3, + 'ꦔ꧀ꦭꦸ': 3, + 'ꦔ꧀ꦭꦼ': 3, + 'ꦕꦺꦴ': 3, + 'ꦗꦿꦺꦴ': 4, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 3, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 3, + 'ꦚ꧀ꦗꦸꦁ': 3, + 'ꦚ꧀ꦗꦸꦂ': 3, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 3, + 'ꦠ꧀ꦏꦸ': 3, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 3, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 3, + 'ꦠ꧀ꦠꦶꦁ': 3, + 'ꦠ꧀ꦠꦸ': 3, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 3, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 3, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 3, + 'ꦠ꧀ꦱꦶꦃ': 4, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 3, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦺꦴ': 3, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 3, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 3, + 'ꦤ꧀ꦏꦸ': 3, + 'ꦤ꧀ꦏꦼ': 3, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 3, + 'ꦤ꧀ꦔꦶ': 3, + 'ꦤ꧀ꦔꦸ': 3, + 'ꦤ꧀ꦔꦸꦂ': 3, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 3, + 'ꦤ꧀ꦔꦿꦸ': 4, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 3, + 'ꦤ꧀ꦗꦼ': 3, + 'ꦤ꧀ꦚꦶ': 3, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 3, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 3, + 'ꦤ꧀ꦝꦶ': 3, + 'ꦤ꧀ꦝꦸ': 3, + 'ꦤ꧀ꦝꦸꦁ': 3, + 'ꦤ꧀ꦝꦺꦁ': 4, + 'ꦤ꧀ꦝꦺꦴꦁ': 5, + 'ꦤ꧀ꦝꦼ': 3, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 3, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 3, + 'ꦤ꧀ꦠꦸ': 3, + 'ꦤ꧀ꦠꦸꦁ': 3, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 3, + 'ꦤ꧀ꦠꦼꦂ': 3, + 'ꦤ꧀ꦠꦽ': 3, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 3, + 'ꦤ꧀ꦢꦂ': 3, + 'ꦤ꧀ꦢꦶ': 3, + 'ꦤ꧀ꦢꦸ': 3, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 3, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 3, + 'ꦤ꧀ꦤꦶꦁ': 3, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 3, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 3, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 5, + 'ꦤ꧀ꦥꦶ': 3, + 'ꦤ꧀ꦥꦸ': 3, + 'ꦤ꧀ꦥꦼ': 3, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 4, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 3, + 'ꦤ꧀ꦧꦶ': 3, + 'ꦤ꧀ꦧꦸ': 3, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 3, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 3, + 'ꦤ꧀ꦩꦶ': 3, + 'ꦤ꧀ꦩꦸꦂ': 3, + 'ꦤ꧀ꦩꦼ': 3, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 3, + 'ꦤ꧀ꦭꦸ': 3, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 3, + 'ꦤ꧀ꦮꦶ': 3, + 'ꦤ꧀ꦮꦸ': 3, + 'ꦤ꧀ꦮꦼ': 3, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 3, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 3, + 'ꦤ꧀ꦱꦶꦁ': 3, + 'ꦤ꧀ꦱꦸ': 3, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 3, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 3, + 'ꦤ꧀ꦲꦶ': 3, + 'ꦤ꧀ꦲꦶꦁ': 3, + 'ꦤ꧀ꦲꦸ': 3, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥ꦳꧀': 2, + 'ꦥꦺꦴ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 3, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 3, + 'ꦥ꧀ꦢꦸ': 3, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 3, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 3, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 3, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 3, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 3, + 'ꦥ꧀ꦱꦼ': 3, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 3, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 3, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 3, + 'ꦩ꧀ꦔꦼ': 3, + 'ꦩ꧀ꦠꦸ': 3, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 3, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 3, + 'ꦩ꧀ꦥꦸ': 3, + 'ꦩ꧀ꦥꦼ': 3, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 3, + 'ꦩ꧀ꦧꦂ': 3, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 3, + 'ꦩ꧀ꦧꦸ': 3, + 'ꦩ꧀ꦧꦸꦁ': 3, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 4, + 'ꦩ꧀ꦧꦼꦂ': 3, + 'ꦩ꧀ꦧꦾꦸꦂ': 4, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 3, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 3, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 3, + 'ꦭ꧀ꦚꦼꦁ': 3, + 'ꦭ꧀ꦠꦸ': 3, + 'ꦭ꧀ꦧꦸ': 3, + 'ꦭ꧀ꦩꦸ': 3, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 3, + 'ꦭ꧀ꦭꦶꦁ': 3, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 3, + 'ꦭ꧀ꦱꦶꦁ': 3, + 'ꦭ꧀ꦱꦸ': 3, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 3, + 'ꦭ꧀ꦲꦶꦁ': 3, + 'ꦭ꧀ꦲꦸ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 3, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦿꦸꦃ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 3, + 'ꦱ꧀ꦏꦿꦶ': 4, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 3, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 3, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 3, + 'ꦱ꧀ꦢꦶ': 3, + 'ꦱ꧀ꦢꦸ': 3, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 3, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 3, + 'ꦱ꧀ꦩꦼ': 3, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 3, + 'ꦱ꧀ꦱꦶ': 3, + 'ꦱ꧀ꦱꦶꦁ': 3, + 'ꦱ꧀ꦱꦸ': 3, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 3, + 'ꦱ꧀ꦲꦶ': 3, + 'ꦱ꧀ꦲꦶꦁ': 3, + 'ꦱ꧀ꦲꦸ': 3, + 'ꦲꦺꦴ': 3, + '𑄇𑄳𑄠𑄬': 3, + '𑄈𑄳𑄠𑄬': 3, + '𑄌𑄳𑄠𑄬': 3, + '𑄎𑄳𑄠𑄬': 3, + '𑄑𑄳𑄢𑄬': 3, + '𑄖𑄳𑄠𑄬': 3, + '𑄖𑄳𑄢𑄬': 3, + '𑄘𑄳𑄠𑄬': 3, + '𑄚𑄳𑄠𑄬': 3, + '𑄝𑄳𑄢𑄬': 3, + '𑄟𑄳𑄠𑄬': 3, + '𑄡𑄳𑄠𑄬': 3, + '𑄣𑄳𑄦𑄬': 3, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍍': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '🍄‍🟫': 5, + '🍋‍🟩': 5, + '🏃‍♀️': 4, + '🏃‍♀️‍➡️': 6, + '🏃‍♂️': 4, + '🏃‍♂️‍➡️': 6, + '🏃‍➡️': 4, + '🏃🏻‍♀️': 6, + '🏃🏻‍♀️‍➡️': 8, + '🏃🏻‍♂️': 6, + '🏃🏻‍♂️‍➡️': 8, + '🏃🏻‍➡️': 6, + '🏃🏼‍♀️': 6, + '🏃🏼‍♀️‍➡️': 8, + '🏃🏼‍♂️': 6, + '🏃🏼‍♂️‍➡️': 8, + '🏃🏼‍➡️': 6, + '🏃🏽‍♀️': 6, + '🏃🏽‍♀️‍➡️': 8, + '🏃🏽‍♂️': 6, + '🏃🏽‍♂️‍➡️': 8, + '🏃🏽‍➡️': 6, + '🏃🏾‍♀️': 6, + '🏃🏾‍♀️‍➡️': 8, + '🏃🏾‍♂️': 6, + '🏃🏾‍♂️‍➡️': 8, + '🏃🏾‍➡️': 6, + '🏃🏿‍♀️': 6, + '🏃🏿‍♀️‍➡️': 8, + '🏃🏿‍♂️': 6, + '🏃🏿‍♂️‍➡️': 8, + '🏃🏿‍➡️': 6, + '🏄‍♀️': 4, + '🏄‍♂️': 4, + '🏄🏻‍♀️': 6, + '🏄🏻‍♂️': 6, + '🏄🏼‍♀️': 6, + '🏄🏼‍♂️': 6, + '🏄🏽‍♀️': 6, + '🏄🏽‍♂️': 6, + '🏄🏾‍♀️': 6, + '🏄🏾‍♂️': 6, + '🏄🏿‍♀️': 6, + '🏄🏿‍♂️': 6, + '🏊‍♀️': 4, + '🏊‍♂️': 4, + '🏊🏻‍♀️': 6, + '🏊🏻‍♂️': 6, + '🏊🏼‍♀️': 6, + '🏊🏼‍♂️': 6, + '🏊🏽‍♀️': 6, + '🏊🏽‍♂️': 6, + '🏊🏾‍♀️': 6, + '🏊🏾‍♂️': 6, + '🏊🏿‍♀️': 6, + '🏊🏿‍♂️': 6, + '🏋️‍♀️': 3, + '🏋️‍♂️': 3, + '🏋🏻‍♀️': 5, + '🏋🏻‍♂️': 5, + '🏋🏼‍♀️': 5, + '🏋🏼‍♂️': 5, + '🏋🏽‍♀️': 5, + '🏋🏽‍♂️': 5, + '🏋🏾‍♀️': 5, + '🏋🏾‍♂️': 5, + '🏋🏿‍♀️': 5, + '🏋🏿‍♂️': 5, + '🏌️‍♀️': 3, + '🏌️‍♂️': 3, + '🏌🏻‍♀️': 5, + '🏌🏻‍♂️': 5, + '🏌🏼‍♀️': 5, + '🏌🏼‍♂️': 5, + '🏌🏽‍♀️': 5, + '🏌🏽‍♂️': 5, + '🏌🏾‍♀️': 5, + '🏌🏾‍♂️': 5, + '🏌🏿‍♀️': 5, + '🏌🏿‍♂️': 5, + '🏳️‍⚧️': 3, + '🏳️‍🌈': 4, + '🏴‍☠️': 4, + '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 8, + '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 8, + '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 8, + '🐈‍⬛': 5, + '🐕‍🦺': 5, + '🐦‍⬛': 5, + '🐦‍🔥': 5, + '🐻‍❄️': 4, + '👁️‍🗨️': 3, + '👨‍⚕️': 4, + '👨‍⚖️': 4, + '👨‍✈️': 4, + '👨‍❤️‍👨': 7, + '👨‍❤️‍💋‍👨': 10, + '👨‍🌾': 5, + '👨‍🍳': 5, + '👨‍🍼': 5, + '👨‍🎓': 5, + '👨‍🎤': 5, + '👨‍🎨': 5, + '👨‍🏫': 5, + '👨‍🏭': 5, + '👨‍👦': 5, + '👨‍👦‍👦': 8, + '👨‍👧': 5, + '👨‍👧‍👦': 8, + '👨‍👧‍👧': 8, + '👨‍👨‍👦': 8, + '👨‍👨‍👦‍👦': 11, + '👨‍👨‍👧': 8, + '👨‍👨‍👧‍👦': 11, + '👨‍👨‍👧‍👧': 11, + '👨‍👩‍👦': 8, + '👨‍👩‍👦‍👦': 11, + '👨‍👩‍👧': 8, + '👨‍👩‍👧‍👦': 11, + '👨‍👩‍👧‍👧': 11, + '👨‍💻': 5, + '👨‍💼': 5, + '👨‍🔧': 5, + '👨‍🔬': 5, + '👨‍🚀': 5, + '👨‍🚒': 5, + '👨‍🦯': 5, + '👨‍🦯‍➡️': 7, + '👨‍🦰': 5, + '👨‍🦱': 5, + '👨‍🦲': 5, + '👨‍🦳': 5, + '👨‍🦼': 5, + '👨‍🦼‍➡️': 7, + '👨‍🦽': 5, + '👨‍🦽‍➡️': 7, + '👨🏻‍⚕️': 6, + '👨🏻‍⚖️': 6, + '👨🏻‍✈️': 6, + '👨🏻‍❤️‍👨🏻': 11, + '👨🏻‍❤️‍👨🏽': 11, + '👨🏻‍❤️‍👨🏿': 11, + '👨🏻‍❤️‍💋‍👨🏻': 14, + '👨🏻‍❤️‍💋‍👨🏽': 14, + '👨🏻‍❤️‍💋‍👨🏾': 14, + '👨🏻‍❤️‍💋‍👨🏿': 14, + '👨🏻‍🌾': 7, + '👨🏻‍🍳': 7, + '👨🏻‍🍼': 7, + '👨🏻‍🎓': 7, + '👨🏻‍🎤': 7, + '👨🏻‍🎨': 7, + '👨🏻‍🏫': 7, + '👨🏻‍🏭': 7, + '👨🏻‍🐰‍👨🏼': 12, + '👨🏻‍🐰‍👨🏽': 12, + '👨🏻‍🐰‍👨🏾': 12, + '👨🏻‍🐰‍👨🏿': 12, + '👨🏻‍💻': 7, + '👨🏻‍💼': 7, + '👨🏻‍🔧': 7, + '👨🏻‍🔬': 7, + '👨🏻‍🚀': 7, + '👨🏻‍🚒': 7, + '👨🏻‍🤝‍👨🏼': 12, + '👨🏻‍🤝‍👨🏽': 12, + '👨🏻‍🤝‍👨🏾': 12, + '👨🏻‍🤝‍👨🏿': 12, + '👨🏻‍🦯': 7, + '👨🏻‍🦯‍➡️': 9, + '👨🏻‍🦰': 7, + '👨🏻‍🦱': 7, + '👨🏻‍🦲': 7, + '👨🏻‍🦳': 7, + '👨🏻‍🦼': 7, + '👨🏻‍🦼‍➡️': 9, + '👨🏻‍🦽': 7, + '👨🏻‍🦽‍➡️': 9, + '👨🏻‍🫯‍👨🏼': 12, + '👨🏻‍🫯‍👨🏽': 12, + '👨🏻‍🫯‍👨🏾': 12, + '👨🏻‍🫯‍👨🏿': 12, + '👨🏼‍⚕️': 6, + '👨🏼‍⚖️': 6, + '👨🏼‍✈️': 6, + '👨🏼‍❤️‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏼': 14, + '👨🏼‍❤️‍💋‍👨🏿': 14, + '👨🏼‍🌾': 7, + '👨🏼‍🍳': 7, + '👨🏼‍🍼': 7, + '👨🏼‍🎓': 7, + '👨🏼‍🎤': 7, + '👨🏼‍🎨': 7, + '👨🏼‍🏫': 7, + '👨🏼‍🏭': 7, + '👨🏼‍🐰‍👨🏻': 12, + '👨🏼‍🐰‍👨🏽': 12, + '👨🏼‍🐰‍👨🏿': 12, + '👨🏼‍💻': 7, + '👨🏼‍💼': 7, + '👨🏼‍🔧': 7, + '👨🏼‍🔬': 7, + '👨🏼‍🚀': 7, + '👨🏼‍🚒': 7, + '👨🏼‍🤝‍👨🏻': 12, + '👨🏼‍🤝‍👨🏽': 12, + '👨🏼‍🤝‍👨🏿': 12, + '👨🏼‍🦯': 7, + '👨🏼‍🦯‍➡️': 9, + '👨🏼‍🦰': 7, + '👨🏼‍🦱': 7, + '👨🏼‍🦲': 7, + '👨🏼‍🦳': 7, + '👨🏼‍🦼': 7, + '👨🏼‍🦽': 7, + '👨🏼‍🦽‍➡️': 9, + '👨🏼‍🫯‍👨🏻': 12, + '👨🏼‍🫯‍👨🏽': 12, + '👨🏼‍🫯‍👨🏾': 12, + '👨🏼‍🫯‍👨🏿': 12, + '👨🏽‍⚕️': 6, + '👨🏽‍⚖️': 6, + '👨🏽‍✈️': 6, + '👨🏽‍❤️‍👨🏻': 11, + '👨🏽‍❤️‍👨🏽': 11, + '👨🏽‍❤️‍👨🏿': 11, + '👨🏽‍❤️‍💋‍👨🏻': 14, + '👨🏽‍❤️‍💋‍👨🏽': 14, + '👨🏽‍❤️‍💋‍👨🏿': 14, + '👨🏽‍🌾': 7, + '👨🏽‍🍳': 7, + '👨🏽‍🍼': 7, + '👨🏽‍🎓': 7, + '👨🏽‍🎤': 7, + '👨🏽‍🎨': 7, + '👨🏽‍🏫': 7, + '👨🏽‍🏭': 7, + '👨🏽‍🐰‍👨🏻': 12, + '👨🏽‍🐰‍👨🏼': 12, + '👨🏽‍🐰‍👨🏾': 12, + '👨🏽‍🐰‍👨🏿': 12, + '👨🏽‍💻': 7, + '👨🏽‍💼': 7, + '👨🏽‍🔧': 7, + '👨🏽‍🔬': 7, + '👨🏽‍🚀': 7, + '👨🏽‍🚒': 7, + '👨🏽‍🤝‍👨🏻': 12, + '👨🏽‍🤝‍👨🏼': 12, + '👨🏽‍🤝‍👨🏾': 12, + '👨🏽‍🤝‍👨🏿': 12, + '👨🏽‍🦯': 7, + '👨🏽‍🦯‍➡️': 9, + '👨🏽‍🦰': 7, + '👨🏽‍🦱': 7, + '👨🏽‍🦲': 7, + '👨🏽‍🦳': 7, + '👨🏽‍🦼': 7, + '👨🏽‍🦼‍➡️': 9, + '👨🏽‍🦽': 7, + '👨🏽‍🦽‍➡️': 9, + '👨🏽‍🫯‍👨🏻': 12, + '👨🏽‍🫯‍👨🏼': 12, + '👨🏽‍🫯‍👨🏾': 12, + '👨🏽‍🫯‍👨🏿': 12, + '👨🏾‍⚕️': 6, + '👨🏾‍⚖️': 6, + '👨🏾‍✈️': 6, + '👨🏾‍❤️‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏻': 14, + '👨🏾‍❤️‍💋‍👨🏾': 14, + '👨🏾‍❤️‍💋‍👨🏿': 14, + '👨🏾‍🌾': 7, + '👨🏾‍🍳': 7, + '👨🏾‍🍼': 7, + '👨🏾‍🎓': 7, + '👨🏾‍🎤': 7, + '👨🏾‍🎨': 7, + '👨🏾‍🏫': 7, + '👨🏾‍🏭': 7, + '👨🏾‍🐰‍👨🏻': 12, + '👨🏾‍🐰‍👨🏽': 12, + '👨🏾‍🐰‍👨🏿': 12, + '👨🏾‍💻': 7, + '👨🏾‍💼': 7, + '👨🏾‍🔧': 7, + '👨🏾‍🔬': 7, + '👨🏾‍🚀': 7, + '👨🏾‍🚒': 7, + '👨🏾‍🤝‍👨🏻': 12, + '👨🏾‍🤝‍👨🏽': 12, + '👨🏾‍🤝‍👨🏿': 12, + '👨🏾‍🦯': 7, + '👨🏾‍🦯‍➡️': 9, + '👨🏾‍🦰': 7, + '👨🏾‍🦱': 7, + '👨🏾‍🦲': 7, + '👨🏾‍🦳': 7, + '👨🏾‍🦼': 7, + '👨🏾‍🦽': 7, + '👨🏾‍🦽‍➡️': 9, + '👨🏾‍🫯‍👨🏻': 12, + '👨🏾‍🫯‍👨🏼': 12, + '👨🏾‍🫯‍👨🏽': 12, + '👨🏾‍🫯‍👨🏿': 12, + '👨🏿‍⚕️': 6, + '👨🏿‍⚖️': 6, + '👨🏿‍✈️': 6, + '👨🏿‍❤️‍👨🏻': 11, + '👨🏿‍❤️‍👨🏽': 11, + '👨🏿‍❤️‍👨🏿': 11, + '👨🏿‍❤️‍💋‍👨🏻': 14, + '👨🏿‍❤️‍💋‍👨🏼': 14, + '👨🏿‍❤️‍💋‍👨🏽': 14, + '👨🏿‍❤️‍💋‍👨🏾': 14, + '👨🏿‍❤️‍💋‍👨🏿': 14, + '👨🏿‍🌾': 7, + '👨🏿‍🍳': 7, + '👨🏿‍🍼': 7, + '👨🏿‍🎓': 7, + '👨🏿‍🎤': 7, + '👨🏿‍🎨': 7, + '👨🏿‍🏫': 7, + '👨🏿‍🏭': 7, + '👨🏿‍🐰‍👨🏻': 12, + '👨🏿‍🐰‍👨🏼': 12, + '👨🏿‍🐰‍👨🏽': 12, + '👨🏿‍🐰‍👨🏾': 12, + '👨🏿‍💻': 7, + '👨🏿‍💼': 7, + '👨🏿‍🔧': 7, + '👨🏿‍🔬': 7, + '👨🏿‍🚀': 7, + '👨🏿‍🚒': 7, + '👨🏿‍🤝‍👨🏻': 12, + '👨🏿‍🤝‍👨🏼': 12, + '👨🏿‍🤝‍👨🏽': 12, + '👨🏿‍🤝‍👨🏾': 12, + '👨🏿‍🦯': 7, + '👨🏿‍🦯‍➡️': 9, + '👨🏿‍🦰': 7, + '👨🏿‍🦱': 7, + '👨🏿‍🦲': 7, + '👨🏿‍🦳': 7, + '👨🏿‍🦼': 7, + '👨🏿‍🦼‍➡️': 9, + '👨🏿‍🦽': 7, + '👨🏿‍🦽‍➡️': 9, + '👨🏿‍🫯‍👨🏻': 12, + '👨🏿‍🫯‍👨🏼': 12, + '👨🏿‍🫯‍👨🏽': 12, + '👨🏿‍🫯‍👨🏾': 12, + '👩‍⚕️': 4, + '👩‍⚖️': 4, + '👩‍✈️': 4, + '👩‍❤️‍👨': 7, + '👩‍❤️‍👩': 7, + '👩‍❤️‍💋‍👨': 10, + '👩‍❤️‍💋‍👩': 10, + '👩‍🌾': 5, + '👩‍🍳': 5, + '👩‍🍼': 5, + '👩‍🎓': 5, + '👩‍🎤': 5, + '👩‍🎨': 5, + '👩‍🏫': 5, + '👩‍🏭': 5, + '👩‍👦': 5, + '👩‍👦‍👦': 8, + '👩‍👧': 5, + '👩‍👧‍👦': 8, + '👩‍👧‍👧': 8, + '👩‍👩‍👦': 8, + '👩‍👩‍👦‍👦': 11, + '👩‍👩‍👧': 8, + '👩‍👩‍👧‍👦': 11, + '👩‍👩‍👧‍👧': 11, + '👩‍💻': 5, + '👩‍💼': 5, + '👩‍🔧': 5, + '👩‍🔬': 5, + '👩‍🚀': 5, + '👩‍🚒': 5, + '👩‍🦯': 5, + '👩‍🦯‍➡️': 7, + '👩‍🦰': 5, + '👩‍🦱': 5, + '👩‍🦲': 5, + '👩‍🦳': 5, + '👩‍🦼': 5, + '👩‍🦼‍➡️': 7, + '👩‍🦽': 5, + '👩‍🦽‍➡️': 7, + '👩🏻‍⚕️': 6, + '👩🏻‍⚖️': 6, + '👩🏻‍✈️': 6, + '👩🏻‍❤️‍👨🏻': 11, + '👩🏻‍❤️‍👨🏿': 11, + '👩🏻‍❤️‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👨🏻': 14, + '👩🏻‍❤️‍💋‍👨🏽': 14, + '👩🏻‍❤️‍💋‍👨🏿': 14, + '👩🏻‍❤️‍💋‍👩🏻': 14, + '👩🏻‍❤️‍💋‍👩🏽': 14, + '👩🏻‍❤️‍💋‍👩🏿': 14, + '👩🏻‍🌾': 7, + '👩🏻‍🍳': 7, + '👩🏻‍🍼': 7, + '👩🏻‍🎓': 7, + '👩🏻‍🎤': 7, + '👩🏻‍🎨': 7, + '👩🏻‍🏫': 7, + '👩🏻‍🏭': 7, + '👩🏻‍🐰‍👩🏼': 12, + '👩🏻‍🐰‍👩🏽': 12, + '👩🏻‍🐰‍👩🏾': 12, + '👩🏻‍🐰‍👩🏿': 12, + '👩🏻‍💻': 7, + '👩🏻‍💼': 7, + '👩🏻‍🔧': 7, + '👩🏻‍🔬': 7, + '👩🏻‍🚀': 7, + '👩🏻‍🚒': 7, + '👩🏻‍🤝‍👨🏽': 12, + '👩🏻‍🤝‍👨🏿': 12, + '👩🏻‍🤝‍👩🏼': 12, + '👩🏻‍🤝‍👩🏽': 12, + '👩🏻‍🤝‍👩🏾': 12, + '👩🏻‍🤝‍👩🏿': 12, + '👩🏻‍🦯': 7, + '👩🏻‍🦯‍➡️': 9, + '👩🏻‍🦰': 7, + '👩🏻‍🦱': 7, + '👩🏻‍🦲': 7, + '👩🏻‍🦳': 7, + '👩🏻‍🦼': 7, + '👩🏻‍🦼‍➡️': 9, + '👩🏻‍🦽': 7, + '👩🏻‍🦽‍➡️': 9, + '👩🏻‍🫯‍👩🏼': 12, + '👩🏻‍🫯‍👩🏽': 12, + '👩🏻‍🫯‍👩🏾': 12, + '👩🏻‍🫯‍👩🏿': 12, + '👩🏼‍⚕️': 6, + '👩🏼‍⚖️': 6, + '👩🏼‍✈️': 6, + '👩🏼‍❤️‍👨🏼': 11, + '👩🏼‍❤️‍👩🏼': 11, + '👩🏼‍❤️‍💋‍👨🏼': 14, + '👩🏼‍❤️‍💋‍👩🏼': 14, + '👩🏼‍🌾': 7, + '👩🏼‍🍳': 7, + '👩🏼‍🍼': 7, + '👩🏼‍🎓': 7, + '👩🏼‍🎤': 7, + '👩🏼‍🎨': 7, + '👩🏼‍🏫': 7, + '👩🏼‍🏭': 7, + '👩🏼‍🐰‍👩🏻': 12, + '👩🏼‍🐰‍👩🏿': 12, + '👩🏼‍💻': 7, + '👩🏼‍💼': 7, + '👩🏼‍🔧': 7, + '👩🏼‍🔬': 7, + '👩🏼‍🚀': 7, + '👩🏼‍🚒': 7, + '👩🏼‍🤝‍👩🏻': 12, + '👩🏼‍🤝‍👩🏽': 12, + '👩🏼‍🤝‍👩🏿': 12, + '👩🏼‍🦯': 7, + '👩🏼‍🦯‍➡️': 9, + '👩🏼‍🦰': 7, + '👩🏼‍🦱': 7, + '👩🏼‍🦲': 7, + '👩🏼‍🦳': 7, + '👩🏼‍🦼': 7, + '👩🏼‍🦽': 7, + '👩🏼‍🫯‍👩🏻': 12, + '👩🏼‍🫯‍👩🏽': 12, + '👩🏼‍🫯‍👩🏾': 12, + '👩🏼‍🫯‍👩🏿': 12, + '👩🏽‍⚕️': 6, + '👩🏽‍⚖️': 6, + '👩🏽‍✈️': 6, + '👩🏽‍❤️‍👨🏽': 11, + '👩🏽‍❤️‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👨🏻': 14, + '👩🏽‍❤️‍💋‍👨🏽': 14, + '👩🏽‍❤️‍💋‍👨🏿': 14, + '👩🏽‍❤️‍💋‍👩🏻': 14, + '👩🏽‍❤️‍💋‍👩🏽': 14, + '👩🏽‍❤️‍💋‍👩🏿': 14, + '👩🏽‍🌾': 7, + '👩🏽‍🍳': 7, + '👩🏽‍🍼': 7, + '👩🏽‍🎓': 7, + '👩🏽‍🎤': 7, + '👩🏽‍🎨': 7, + '👩🏽‍🏫': 7, + '👩🏽‍🏭': 7, + '👩🏽‍🐰‍👩🏻': 12, + '👩🏽‍🐰‍👩🏾': 12, + '👩🏽‍🐰‍👩🏿': 12, + '👩🏽‍💻': 7, + '👩🏽‍💼': 7, + '👩🏽‍🔧': 7, + '👩🏽‍🔬': 7, + '👩🏽‍🚀': 7, + '👩🏽‍🚒': 7, + '👩🏽‍🤝‍👨🏻': 12, + '👩🏽‍🤝‍👨🏿': 12, + '👩🏽‍🤝‍👩🏻': 12, + '👩🏽‍🤝‍👩🏼': 12, + '👩🏽‍🤝‍👩🏾': 12, + '👩🏽‍🤝‍👩🏿': 12, + '👩🏽‍🦯': 7, + '👩🏽‍🦯‍➡️': 9, + '👩🏽‍🦰': 7, + '👩🏽‍🦱': 7, + '👩🏽‍🦲': 7, + '👩🏽‍🦳': 7, + '👩🏽‍🦼': 7, + '👩🏽‍🦼‍➡️': 9, + '👩🏽‍🦽': 7, + '👩🏽‍🦽‍➡️': 9, + '👩🏽‍🫯‍👩🏻': 12, + '👩🏽‍🫯‍👩🏼': 12, + '👩🏽‍🫯‍👩🏾': 12, + '👩🏽‍🫯‍👩🏿': 12, + '👩🏾‍⚕️': 6, + '👩🏾‍⚖️': 6, + '👩🏾‍✈️': 6, + '👩🏾‍❤️‍👨🏾': 11, + '👩🏾‍❤️‍👩🏾': 11, + '👩🏾‍❤️‍💋‍👨🏾': 14, + '👩🏾‍❤️‍💋‍👩🏾': 14, + '👩🏾‍🌾': 7, + '👩🏾‍🍳': 7, + '👩🏾‍🍼': 7, + '👩🏾‍🎓': 7, + '👩🏾‍🎤': 7, + '👩🏾‍🎨': 7, + '👩🏾‍🏫': 7, + '👩🏾‍🏭': 7, + '👩🏾‍🐰‍👩🏻': 12, + '👩🏾‍🐰‍👩🏽': 12, + '👩🏾‍🐰‍👩🏿': 12, + '👩🏾‍💻': 7, + '👩🏾‍💼': 7, + '👩🏾‍🔧': 7, + '👩🏾‍🔬': 7, + '👩🏾‍🚀': 7, + '👩🏾‍🚒': 7, + '👩🏾‍🤝‍👩🏻': 12, + '👩🏾‍🤝‍👩🏽': 12, + '👩🏾‍🤝‍👩🏿': 12, + '👩🏾‍🦯': 7, + '👩🏾‍🦯‍➡️': 9, + '👩🏾‍🦰': 7, + '👩🏾‍🦱': 7, + '👩🏾‍🦲': 7, + '👩🏾‍🦳': 7, + '👩🏾‍🦼': 7, + '👩🏾‍🦽': 7, + '👩🏾‍🦽‍➡️': 9, + '👩🏾‍🫯‍👩🏻': 12, + '👩🏾‍🫯‍👩🏼': 12, + '👩🏾‍🫯‍👩🏽': 12, + '👩🏾‍🫯‍👩🏿': 12, + '👩🏿‍⚕️': 6, + '👩🏿‍⚖️': 6, + '👩🏿‍✈️': 6, + '👩🏿‍❤️‍👨🏻': 11, + '👩🏿‍❤️‍👨🏿': 11, + '👩🏿‍❤️‍👩🏿': 11, + '👩🏿‍❤️‍💋‍👨🏻': 14, + '👩🏿‍❤️‍💋‍👨🏽': 14, + '👩🏿‍❤️‍💋‍👨🏿': 14, + '👩🏿‍❤️‍💋‍👩🏻': 14, + '👩🏿‍❤️‍💋‍👩🏽': 14, + '👩🏿‍❤️‍💋‍👩🏿': 14, + '👩🏿‍🌾': 7, + '👩🏿‍🍳': 7, + '👩🏿‍🍼': 7, + '👩🏿‍🎓': 7, + '👩🏿‍🎤': 7, + '👩🏿‍🎨': 7, + '👩🏿‍🏫': 7, + '👩🏿‍🏭': 7, + '👩🏿‍🐰‍👩🏻': 12, + '👩🏿‍🐰‍👩🏼': 12, + '👩🏿‍🐰‍👩🏽': 12, + '👩🏿‍🐰‍👩🏾': 12, + '👩🏿‍💻': 7, + '👩🏿‍💼': 7, + '👩🏿‍🔧': 7, + '👩🏿‍🔬': 7, + '👩🏿‍🚀': 7, + '👩🏿‍🚒': 7, + '👩🏿‍🤝‍👨🏻': 12, + '👩🏿‍🤝‍👨🏽': 12, + '👩🏿‍🤝‍👩🏻': 12, + '👩🏿‍🤝‍👩🏼': 12, + '👩🏿‍🤝‍👩🏽': 12, + '👩🏿‍🤝‍👩🏾': 12, + '👩🏿‍🦯': 7, + '👩🏿‍🦯‍➡️': 9, + '👩🏿‍🦰': 7, + '👩🏿‍🦱': 7, + '👩🏿‍🦲': 7, + '👩🏿‍🦳': 7, + '👩🏿‍🦼': 7, + '👩🏿‍🦼‍➡️': 9, + '👩🏿‍🦽': 7, + '👩🏿‍🦽‍➡️': 9, + '👩🏿‍🫯‍👩🏻': 12, + '👩🏿‍🫯‍👩🏼': 12, + '👩🏿‍🫯‍👩🏽': 12, + '👩🏿‍🫯‍👩🏾': 12, + '👮‍♀️': 4, + '👮‍♂️': 4, + '👮🏻‍♀️': 6, + '👮🏻‍♂️': 6, + '👮🏼‍♀️': 6, + '👮🏼‍♂️': 6, + '👮🏽‍♀️': 6, + '👮🏽‍♂️': 6, + '👮🏾‍♀️': 6, + '👮🏾‍♂️': 6, + '👮🏿‍♀️': 6, + '👮🏿‍♂️': 6, + '👯‍♀️': 4, + '👯‍♂️': 4, + '👯🏻‍♀️': 6, + '👯🏻‍♂️': 6, + '👯🏼‍♀️': 6, + '👯🏼‍♂️': 6, + '👯🏽‍♀️': 6, + '👯🏽‍♂️': 6, + '👯🏾‍♀️': 6, + '👯🏾‍♂️': 6, + '👯🏿‍♀️': 6, + '👯🏿‍♂️': 6, + '👰‍♀️': 4, + '👰‍♂️': 4, + '👰🏻‍♀️': 6, + '👰🏻‍♂️': 6, + '👰🏼‍♀️': 6, + '👰🏼‍♂️': 6, + '👰🏽‍♀️': 6, + '👰🏽‍♂️': 6, + '👰🏾‍♀️': 6, + '👰🏾‍♂️': 6, + '👰🏿‍♀️': 6, + '👰🏿‍♂️': 6, + '👱‍♀️': 4, + '👱‍♂️': 4, + '👱🏻‍♀️': 6, + '👱🏻‍♂️': 6, + '👱🏼‍♀️': 6, + '👱🏼‍♂️': 6, + '👱🏽‍♀️': 6, + '👱🏽‍♂️': 6, + '👱🏾‍♀️': 6, + '👱🏾‍♂️': 6, + '👱🏿‍♀️': 6, + '👱🏿‍♂️': 6, + '👳‍♀️': 4, + '👳‍♂️': 4, + '👳🏻‍♀️': 6, + '👳🏻‍♂️': 6, + '👳🏼‍♀️': 6, + '👳🏼‍♂️': 6, + '👳🏽‍♀️': 6, + '👳🏽‍♂️': 6, + '👳🏾‍♀️': 6, + '👳🏾‍♂️': 6, + '👳🏿‍♀️': 6, + '👳🏿‍♂️': 6, + '👷‍♀️': 4, + '👷‍♂️': 4, + '👷🏻‍♀️': 6, + '👷🏻‍♂️': 6, + '👷🏼‍♀️': 6, + '👷🏼‍♂️': 6, + '👷🏽‍♀️': 6, + '👷🏽‍♂️': 6, + '👷🏾‍♀️': 6, + '👷🏾‍♂️': 6, + '👷🏿‍♀️': 6, + '👷🏿‍♂️': 6, + '💁‍♀️': 4, + '💁‍♂️': 4, + '💁🏻‍♀️': 6, + '💁🏻‍♂️': 6, + '💁🏼‍♀️': 6, + '💁🏼‍♂️': 6, + '💁🏽‍♀️': 6, + '💁🏽‍♂️': 6, + '💁🏾‍♀️': 6, + '💁🏾‍♂️': 6, + '💁🏿‍♀️': 6, + '💁🏿‍♂️': 6, + '💂‍♀️': 4, + '💂‍♂️': 4, + '💂🏻‍♀️': 6, + '💂🏻‍♂️': 6, + '💂🏼‍♀️': 6, + '💂🏼‍♂️': 6, + '💂🏽‍♀️': 6, + '💂🏽‍♂️': 6, + '💂🏾‍♀️': 6, + '💂🏾‍♂️': 6, + '💂🏿‍♀️': 6, + '💂🏿‍♂️': 6, + '💆‍♀️': 4, + '💆‍♂️': 4, + '💆🏻‍♀️': 6, + '💆🏻‍♂️': 6, + '💆🏼‍♀️': 6, + '💆🏼‍♂️': 6, + '💆🏽‍♀️': 6, + '💆🏽‍♂️': 6, + '💆🏾‍♀️': 6, + '💆🏾‍♂️': 6, + '💆🏿‍♀️': 6, + '💆🏿‍♂️': 6, + '💇‍♀️': 4, + '💇‍♂️': 4, + '💇🏻‍♀️': 6, + '💇🏻‍♂️': 6, + '💇🏼‍♀️': 6, + '💇🏼‍♂️': 6, + '💇🏽‍♀️': 6, + '💇🏽‍♂️': 6, + '💇🏾‍♀️': 6, + '💇🏾‍♂️': 6, + '💇🏿‍♀️': 6, + '💇🏿‍♂️': 6, + '🕵️‍♀️': 3, + '🕵️‍♂️': 3, + '🕵🏻‍♀️': 5, + '🕵🏻‍♂️': 5, + '🕵🏼‍♀️': 5, + '🕵🏼‍♂️': 5, + '🕵🏽‍♀️': 5, + '🕵🏽‍♂️': 5, + '🕵🏾‍♀️': 5, + '🕵🏾‍♂️': 5, + '🕵🏿‍♀️': 5, + '🕵🏿‍♂️': 5, + '😮‍💨': 5, + '😵‍💫': 5, + '😶‍🌫️': 4, + '🙂‍↔️': 4, + '🙂‍↕️': 4, + '🙅‍♀️': 4, + '🙅‍♂️': 4, + '🙅🏻‍♀️': 6, + '🙅🏻‍♂️': 6, + '🙅🏼‍♀️': 6, + '🙅🏼‍♂️': 6, + '🙅🏽‍♀️': 6, + '🙅🏽‍♂️': 6, + '🙅🏾‍♀️': 6, + '🙅🏾‍♂️': 6, + '🙅🏿‍♀️': 6, + '🙅🏿‍♂️': 6, + '🙆‍♀️': 4, + '🙆‍♂️': 4, + '🙆🏻‍♀️': 6, + '🙆🏻‍♂️': 6, + '🙆🏼‍♀️': 6, + '🙆🏼‍♂️': 6, + '🙆🏽‍♀️': 6, + '🙆🏽‍♂️': 6, + '🙆🏾‍♀️': 6, + '🙆🏾‍♂️': 6, + '🙆🏿‍♀️': 6, + '🙆🏿‍♂️': 6, + '🙇‍♀️': 4, + '🙇‍♂️': 4, + '🙇🏻‍♀️': 6, + '🙇🏻‍♂️': 6, + '🙇🏼‍♀️': 6, + '🙇🏼‍♂️': 6, + '🙇🏽‍♀️': 6, + '🙇🏽‍♂️': 6, + '🙇🏾‍♀️': 6, + '🙇🏾‍♂️': 6, + '🙇🏿‍♀️': 6, + '🙇🏿‍♂️': 6, + '🙋‍♀️': 4, + '🙋‍♂️': 4, + '🙋🏻‍♀️': 6, + '🙋🏻‍♂️': 6, + '🙋🏼‍♀️': 6, + '🙋🏼‍♂️': 6, + '🙋🏽‍♀️': 6, + '🙋🏽‍♂️': 6, + '🙋🏾‍♀️': 6, + '🙋🏾‍♂️': 6, + '🙋🏿‍♀️': 6, + '🙋🏿‍♂️': 6, + '🙍‍♀️': 4, + '🙍‍♂️': 4, + '🙍🏻‍♀️': 6, + '🙍🏻‍♂️': 6, + '🙍🏼‍♀️': 6, + '🙍🏼‍♂️': 6, + '🙍🏽‍♀️': 6, + '🙍🏽‍♂️': 6, + '🙍🏾‍♀️': 6, + '🙍🏾‍♂️': 6, + '🙍🏿‍♀️': 6, + '🙍🏿‍♂️': 6, + '🙎‍♀️': 4, + '🙎‍♂️': 4, + '🙎🏻‍♀️': 6, + '🙎🏻‍♂️': 6, + '🙎🏼‍♀️': 6, + '🙎🏼‍♂️': 6, + '🙎🏽‍♀️': 6, + '🙎🏽‍♂️': 6, + '🙎🏾‍♀️': 6, + '🙎🏾‍♂️': 6, + '🙎🏿‍♀️': 6, + '🙎🏿‍♂️': 6, + '🚣‍♀️': 4, + '🚣‍♂️': 4, + '🚣🏻‍♀️': 6, + '🚣🏻‍♂️': 6, + '🚣🏼‍♀️': 6, + '🚣🏼‍♂️': 6, + '🚣🏽‍♀️': 6, + '🚣🏽‍♂️': 6, + '🚣🏾‍♀️': 6, + '🚣🏾‍♂️': 6, + '🚣🏿‍♀️': 6, + '🚣🏿‍♂️': 6, + '🚴‍♀️': 4, + '🚴‍♂️': 4, + '🚴🏻‍♀️': 6, + '🚴🏻‍♂️': 6, + '🚴🏼‍♀️': 6, + '🚴🏼‍♂️': 6, + '🚴🏽‍♀️': 6, + '🚴🏽‍♂️': 6, + '🚴🏾‍♀️': 6, + '🚴🏾‍♂️': 6, + '🚴🏿‍♀️': 6, + '🚴🏿‍♂️': 6, + '🚵‍♀️': 4, + '🚵‍♂️': 4, + '🚵🏻‍♀️': 6, + '🚵🏻‍♂️': 6, + '🚵🏼‍♀️': 6, + '🚵🏼‍♂️': 6, + '🚵🏽‍♀️': 6, + '🚵🏽‍♂️': 6, + '🚵🏾‍♀️': 6, + '🚵🏾‍♂️': 6, + '🚵🏿‍♀️': 6, + '🚵🏿‍♂️': 6, + '🚶‍♀️': 4, + '🚶‍♀️‍➡️': 6, + '🚶‍♂️': 4, + '🚶‍♂️‍➡️': 6, + '🚶‍➡️': 4, + '🚶🏻‍♀️': 6, + '🚶🏻‍♀️‍➡️': 8, + '🚶🏻‍♂️': 6, + '🚶🏻‍♂️‍➡️': 8, + '🚶🏻‍➡️': 6, + '🚶🏼‍♀️': 6, + '🚶🏼‍♀️‍➡️': 8, + '🚶🏼‍♂️': 6, + '🚶🏼‍♂️‍➡️': 8, + '🚶🏼‍➡️': 6, + '🚶🏽‍♀️': 6, + '🚶🏽‍♀️‍➡️': 8, + '🚶🏽‍♂️': 6, + '🚶🏽‍♂️‍➡️': 8, + '🚶🏽‍➡️': 6, + '🚶🏾‍♀️': 6, + '🚶🏾‍♀️‍➡️': 8, + '🚶🏾‍♂️': 6, + '🚶🏾‍♂️‍➡️': 8, + '🚶🏾‍➡️': 6, + '🚶🏿‍♀️': 6, + '🚶🏿‍♀️‍➡️': 8, + '🚶🏿‍♂️': 6, + '🚶🏿‍♂️‍➡️': 8, + '🚶🏿‍➡️': 6, + '🤦‍♀️': 4, + '🤦‍♂️': 4, + '🤦🏻‍♀️': 6, + '🤦🏻‍♂️': 6, + '🤦🏼‍♀️': 6, + '🤦🏼‍♂️': 6, + '🤦🏽‍♀️': 6, + '🤦🏽‍♂️': 6, + '🤦🏾‍♀️': 6, + '🤦🏾‍♂️': 6, + '🤦🏿‍♀️': 6, + '🤦🏿‍♂️': 6, + '🤵‍♀️': 4, + '🤵‍♂️': 4, + '🤵🏻‍♀️': 6, + '🤵🏻‍♂️': 6, + '🤵🏼‍♀️': 6, + '🤵🏼‍♂️': 6, + '🤵🏽‍♀️': 6, + '🤵🏽‍♂️': 6, + '🤵🏾‍♀️': 6, + '🤵🏾‍♂️': 6, + '🤵🏿‍♀️': 6, + '🤵🏿‍♂️': 6, + '🤷‍♀️': 4, + '🤷‍♂️': 4, + '🤷🏻‍♀️': 6, + '🤷🏻‍♂️': 6, + '🤷🏼‍♀️': 6, + '🤷🏼‍♂️': 6, + '🤷🏽‍♀️': 6, + '🤷🏽‍♂️': 6, + '🤷🏾‍♀️': 6, + '🤷🏾‍♂️': 6, + '🤷🏿‍♀️': 6, + '🤷🏿‍♂️': 6, + '🤸‍♀️': 4, + '🤸‍♂️': 4, + '🤸🏻‍♀️': 6, + '🤸🏻‍♂️': 6, + '🤸🏼‍♀️': 6, + '🤸🏼‍♂️': 6, + '🤸🏽‍♀️': 6, + '🤸🏽‍♂️': 6, + '🤸🏾‍♀️': 6, + '🤸🏾‍♂️': 6, + '🤸🏿‍♀️': 6, + '🤸🏿‍♂️': 6, + '🤹‍♀️': 4, + '🤹‍♂️': 4, + '🤹🏻‍♀️': 6, + '🤹🏻‍♂️': 6, + '🤹🏼‍♀️': 6, + '🤹🏼‍♂️': 6, + '🤹🏽‍♀️': 6, + '🤹🏽‍♂️': 6, + '🤹🏾‍♀️': 6, + '🤹🏾‍♂️': 6, + '🤹🏿‍♀️': 6, + '🤹🏿‍♂️': 6, + '🤼‍♀️': 4, + '🤼‍♂️': 4, + '🤼🏻‍♀️': 6, + '🤼🏻‍♂️': 6, + '🤼🏼‍♀️': 6, + '🤼🏼‍♂️': 6, + '🤼🏽‍♀️': 6, + '🤼🏽‍♂️': 6, + '🤼🏾‍♀️': 6, + '🤼🏾‍♂️': 6, + '🤼🏿‍♀️': 6, + '🤼🏿‍♂️': 6, + '🤽‍♀️': 4, + '🤽‍♂️': 4, + '🤽🏻‍♀️': 6, + '🤽🏻‍♂️': 6, + '🤽🏼‍♀️': 6, + '🤽🏼‍♂️': 6, + '🤽🏽‍♀️': 6, + '🤽🏽‍♂️': 6, + '🤽🏾‍♀️': 6, + '🤽🏾‍♂️': 6, + '🤽🏿‍♀️': 6, + '🤽🏿‍♂️': 6, + '🤾‍♀️': 4, + '🤾‍♂️': 4, + '🤾🏻‍♀️': 6, + '🤾🏻‍♂️': 6, + '🤾🏼‍♀️': 6, + '🤾🏼‍♂️': 6, + '🤾🏽‍♀️': 6, + '🤾🏽‍♂️': 6, + '🤾🏾‍♀️': 6, + '🤾🏾‍♂️': 6, + '🤾🏿‍♀️': 6, + '🤾🏿‍♂️': 6, + '🦸‍♀️': 4, + '🦸‍♂️': 4, + '🦸🏻‍♀️': 6, + '🦸🏻‍♂️': 6, + '🦸🏼‍♀️': 6, + '🦸🏼‍♂️': 6, + '🦸🏽‍♀️': 6, + '🦸🏽‍♂️': 6, + '🦸🏾‍♀️': 6, + '🦸🏾‍♂️': 6, + '🦸🏿‍♀️': 6, + '🦸🏿‍♂️': 6, + '🦹‍♀️': 4, + '🦹‍♂️': 4, + '🦹🏻‍♀️': 6, + '🦹🏻‍♂️': 6, + '🦹🏼‍♀️': 6, + '🦹🏼‍♂️': 6, + '🦹🏽‍♀️': 6, + '🦹🏽‍♂️': 6, + '🦹🏾‍♀️': 6, + '🦹🏾‍♂️': 6, + '🦹🏿‍♀️': 6, + '🦹🏿‍♂️': 6, + '🧍‍♀️': 4, + '🧍‍♂️': 4, + '🧍🏻‍♀️': 6, + '🧍🏻‍♂️': 6, + '🧍🏼‍♀️': 6, + '🧍🏼‍♂️': 6, + '🧍🏽‍♀️': 6, + '🧍🏽‍♂️': 6, + '🧍🏾‍♀️': 6, + '🧍🏾‍♂️': 6, + '🧍🏿‍♀️': 6, + '🧍🏿‍♂️': 6, + '🧎‍♀️': 4, + '🧎‍♀️‍➡️': 6, + '🧎‍♂️': 4, + '🧎‍♂️‍➡️': 6, + '🧎‍➡️': 4, + '🧎🏻‍♀️': 6, + '🧎🏻‍♀️‍➡️': 8, + '🧎🏻‍♂️': 6, + '🧎🏻‍♂️‍➡️': 8, + '🧎🏻‍➡️': 6, + '🧎🏼‍♀️': 6, + '🧎🏼‍♀️‍➡️': 8, + '🧎🏼‍♂️': 6, + '🧎🏼‍♂️‍➡️': 8, + '🧎🏼‍➡️': 6, + '🧎🏽‍♀️': 6, + '🧎🏽‍♀️‍➡️': 8, + '🧎🏽‍♂️': 6, + '🧎🏽‍♂️‍➡️': 8, + '🧎🏽‍➡️': 6, + '🧎🏾‍♀️': 6, + '🧎🏾‍♀️‍➡️': 8, + '🧎🏾‍♂️': 6, + '🧎🏾‍♂️‍➡️': 8, + '🧎🏾‍➡️': 6, + '🧎🏿‍♀️': 6, + '🧎🏿‍♀️‍➡️': 8, + '🧎🏿‍♂️': 6, + '🧎🏿‍♂️‍➡️': 8, + '🧎🏿‍➡️': 6, + '🧏‍♀️': 4, + '🧏‍♂️': 4, + '🧏🏻‍♀️': 6, + '🧏🏻‍♂️': 6, + '🧏🏼‍♀️': 6, + '🧏🏼‍♂️': 6, + '🧏🏽‍♀️': 6, + '🧏🏽‍♂️': 6, + '🧏🏾‍♀️': 6, + '🧏🏾‍♂️': 6, + '🧏🏿‍♀️': 6, + '🧏🏿‍♂️': 6, + '🧑‍⚕️': 4, + '🧑‍⚖️': 4, + '🧑‍✈️': 4, + '🧑‍🌾': 5, + '🧑‍🍳': 5, + '🧑‍🍼': 5, + '🧑‍🎄': 5, + '🧑‍🎓': 5, + '🧑‍🎤': 5, + '🧑‍🎨': 5, + '🧑‍🏫': 5, + '🧑‍🏭': 5, + '🧑‍💻': 5, + '🧑‍💼': 5, + '🧑‍🔧': 5, + '🧑‍🔬': 5, + '🧑‍🚀': 5, + '🧑‍🚒': 5, + '🧑‍🤝‍🧑': 8, + '🧑‍🦯': 5, + '🧑‍🦯‍➡️': 7, + '🧑‍🦰': 5, + '🧑‍🦱': 5, + '🧑‍🦲': 5, + '🧑‍🦳': 5, + '🧑‍🦼': 5, + '🧑‍🦼‍➡️': 7, + '🧑‍🦽': 5, + '🧑‍🦽‍➡️': 7, + '🧑‍🧑‍🧒': 8, + '🧑‍🧑‍🧒‍🧒': 11, + '🧑‍🧒': 5, + '🧑‍🧒‍🧒': 8, + '🧑‍🩰': 5, + '🧑🏻‍⚕️': 6, + '🧑🏻‍⚖️': 6, + '🧑🏻‍✈️': 6, + '🧑🏻‍❤️‍💋‍🧑🏿': 14, + '🧑🏻‍🌾': 7, + '🧑🏻‍🍳': 7, + '🧑🏻‍🍼': 7, + '🧑🏻‍🎄': 7, + '🧑🏻‍🎓': 7, + '🧑🏻‍🎤': 7, + '🧑🏻‍🎨': 7, + '🧑🏻‍🏫': 7, + '🧑🏻‍🏭': 7, + '🧑🏻‍🐰‍🧑🏽': 12, + '🧑🏻‍🐰‍🧑🏾': 12, + '🧑🏻‍🐰‍🧑🏿': 12, + '🧑🏻‍💻': 7, + '🧑🏻‍💼': 7, + '🧑🏻‍🔧': 7, + '🧑🏻‍🔬': 7, + '🧑🏻‍🚀': 7, + '🧑🏻‍🚒': 7, + '🧑🏻‍🤝‍🧑🏻': 12, + '🧑🏻‍🤝‍🧑🏼': 12, + '🧑🏻‍🤝‍🧑🏽': 12, + '🧑🏻‍🤝‍🧑🏾': 12, + '🧑🏻‍🤝‍🧑🏿': 12, + '🧑🏻‍🦯': 7, + '🧑🏻‍🦯‍➡️': 9, + '🧑🏻‍🦰': 7, + '🧑🏻‍🦱': 7, + '🧑🏻‍🦲': 7, + '🧑🏻‍🦳': 7, + '🧑🏻‍🦼': 7, + '🧑🏻‍🦼‍➡️': 9, + '🧑🏻‍🦽': 7, + '🧑🏻‍🦽‍➡️': 9, + '🧑🏻‍🩰': 7, + '🧑🏻‍🫯‍🧑🏼': 12, + '🧑🏻‍🫯‍🧑🏽': 12, + '🧑🏻‍🫯‍🧑🏾': 12, + '🧑🏻‍🫯‍🧑🏿': 12, + '🧑🏼‍⚕️': 6, + '🧑🏼‍⚖️': 6, + '🧑🏼‍✈️': 6, + '🧑🏼‍🌾': 7, + '🧑🏼‍🍳': 7, + '🧑🏼‍🍼': 7, + '🧑🏼‍🎄': 7, + '🧑🏼‍🎓': 7, + '🧑🏼‍🎤': 7, + '🧑🏼‍🎨': 7, + '🧑🏼‍🏫': 7, + '🧑🏼‍🏭': 7, + '🧑🏼‍🐰‍🧑🏿': 12, + '🧑🏼‍💻': 7, + '🧑🏼‍💼': 7, + '🧑🏼‍🔧': 7, + '🧑🏼‍🔬': 7, + '🧑🏼‍🚀': 7, + '🧑🏼‍🚒': 7, + '🧑🏼‍🤝‍🧑🏻': 12, + '🧑🏼‍🤝‍🧑🏼': 12, + '🧑🏼‍🤝‍🧑🏽': 12, + '🧑🏼‍🤝‍🧑🏿': 12, + '🧑🏼‍🦯': 7, + '🧑🏼‍🦯‍➡️': 9, + '🧑🏼‍🦰': 7, + '🧑🏼‍🦱': 7, + '🧑🏼‍🦲': 7, + '🧑🏼‍🦳': 7, + '🧑🏼‍🦼': 7, + '🧑🏼‍🦽': 7, + '🧑🏼‍🩰': 7, + '🧑🏼‍🫯‍🧑🏻': 12, + '🧑🏼‍🫯‍🧑🏽': 12, + '🧑🏼‍🫯‍🧑🏿': 12, + '🧑🏽‍⚕️': 6, + '🧑🏽‍⚖️': 6, + '🧑🏽‍✈️': 6, + '🧑🏽‍❤️‍💋‍🧑🏿': 14, + '🧑🏽‍🌾': 7, + '🧑🏽‍🍳': 7, + '🧑🏽‍🍼': 7, + '🧑🏽‍🎄': 7, + '🧑🏽‍🎓': 7, + '🧑🏽‍🎤': 7, + '🧑🏽‍🎨': 7, + '🧑🏽‍🏫': 7, + '🧑🏽‍🏭': 7, + '🧑🏽‍🐰‍🧑🏻': 12, + '🧑🏽‍🐰‍🧑🏿': 12, + '🧑🏽‍💻': 7, + '🧑🏽‍💼': 7, + '🧑🏽‍🔧': 7, + '🧑🏽‍🔬': 7, + '🧑🏽‍🚀': 7, + '🧑🏽‍🚒': 7, + '🧑🏽‍🤝‍🧑🏻': 12, + '🧑🏽‍🤝‍🧑🏼': 12, + '🧑🏽‍🤝‍🧑🏽': 12, + '🧑🏽‍🤝‍🧑🏾': 12, + '🧑🏽‍🤝‍🧑🏿': 12, + '🧑🏽‍🦯': 7, + '🧑🏽‍🦯‍➡️': 9, + '🧑🏽‍🦰': 7, + '🧑🏽‍🦱': 7, + '🧑🏽‍🦲': 7, + '🧑🏽‍🦳': 7, + '🧑🏽‍🦼': 7, + '🧑🏽‍🦼‍➡️': 9, + '🧑🏽‍🦽': 7, + '🧑🏽‍🦽‍➡️': 9, + '🧑🏽‍🩰': 7, + '🧑🏽‍🫯‍🧑🏻': 12, + '🧑🏽‍🫯‍🧑🏼': 12, + '🧑🏽‍🫯‍🧑🏾': 12, + '🧑🏽‍🫯‍🧑🏿': 12, + '🧑🏾‍⚕️': 6, + '🧑🏾‍⚖️': 6, + '🧑🏾‍✈️': 6, + '🧑🏾‍🌾': 7, + '🧑🏾‍🍳': 7, + '🧑🏾‍🍼': 7, + '🧑🏾‍🎄': 7, + '🧑🏾‍🎓': 7, + '🧑🏾‍🎤': 7, + '🧑🏾‍🎨': 7, + '🧑🏾‍🏫': 7, + '🧑🏾‍🏭': 7, + '🧑🏾‍🐰‍🧑🏻': 12, + '🧑🏾‍🐰‍🧑🏿': 12, + '🧑🏾‍💻': 7, + '🧑🏾‍💼': 7, + '🧑🏾‍🔧': 7, + '🧑🏾‍🔬': 7, + '🧑🏾‍🚀': 7, + '🧑🏾‍🚒': 7, + '🧑🏾‍🤝‍🧑🏻': 12, + '🧑🏾‍🤝‍🧑🏽': 12, + '🧑🏾‍🤝‍🧑🏾': 12, + '🧑🏾‍🤝‍🧑🏿': 12, + '🧑🏾‍🦯': 7, + '🧑🏾‍🦯‍➡️': 9, + '🧑🏾‍🦰': 7, + '🧑🏾‍🦱': 7, + '🧑🏾‍🦲': 7, + '🧑🏾‍🦳': 7, + '🧑🏾‍🦼': 7, + '🧑🏾‍🦽': 7, + '🧑🏾‍🩰': 7, + '🧑🏾‍🫯‍🧑🏻': 12, + '🧑🏾‍🫯‍🧑🏽': 12, + '🧑🏾‍🫯‍🧑🏿': 12, + '🧑🏿‍⚕️': 6, + '🧑🏿‍⚖️': 6, + '🧑🏿‍✈️': 6, + '🧑🏿‍❤️‍💋‍🧑🏻': 14, + '🧑🏿‍❤️‍💋‍🧑🏽': 14, + '🧑🏿‍🌾': 7, + '🧑🏿‍🍳': 7, + '🧑🏿‍🍼': 7, + '🧑🏿‍🎄': 7, + '🧑🏿‍🎓': 7, + '🧑🏿‍🎤': 7, + '🧑🏿‍🎨': 7, + '🧑🏿‍🏫': 7, + '🧑🏿‍🏭': 7, + '🧑🏿‍🐰‍🧑🏻': 12, + '🧑🏿‍🐰‍🧑🏼': 12, + '🧑🏿‍🐰‍🧑🏽': 12, + '🧑🏿‍🐰‍🧑🏾': 12, + '🧑🏿‍💻': 7, + '🧑🏿‍💼': 7, + '🧑🏿‍🔧': 7, + '🧑🏿‍🔬': 7, + '🧑🏿‍🚀': 7, + '🧑🏿‍🚒': 7, + '🧑🏿‍🤝‍🧑🏻': 12, + '🧑🏿‍🤝‍🧑🏼': 12, + '🧑🏿‍🤝‍🧑🏽': 12, + '🧑🏿‍🤝‍🧑🏾': 12, + '🧑🏿‍🤝‍🧑🏿': 12, + '🧑🏿‍🦯': 7, + '🧑🏿‍🦯‍➡️': 9, + '🧑🏿‍🦰': 7, + '🧑🏿‍🦱': 7, + '🧑🏿‍🦲': 7, + '🧑🏿‍🦳': 7, + '🧑🏿‍🦼': 7, + '🧑🏿‍🦼‍➡️': 9, + '🧑🏿‍🦽': 7, + '🧑🏿‍🦽‍➡️': 9, + '🧑🏿‍🩰': 7, + '🧑🏿‍🫯‍🧑🏻': 12, + '🧑🏿‍🫯‍🧑🏼': 12, + '🧑🏿‍🫯‍🧑🏽': 12, + '🧑🏿‍🫯‍🧑🏾': 12, + '🧔‍♀️': 4, + '🧔‍♂️': 4, + '🧔🏻‍♀️': 6, + '🧔🏻‍♂️': 6, + '🧔🏼‍♀️': 6, + '🧔🏼‍♂️': 6, + '🧔🏽‍♀️': 6, + '🧔🏽‍♂️': 6, + '🧔🏾‍♀️': 6, + '🧔🏾‍♂️': 6, + '🧔🏿‍♀️': 6, + '🧔🏿‍♂️': 6, + '🧖‍♀️': 4, + '🧖‍♂️': 4, + '🧖🏻‍♀️': 6, + '🧖🏻‍♂️': 6, + '🧖🏼‍♀️': 6, + '🧖🏼‍♂️': 6, + '🧖🏽‍♀️': 6, + '🧖🏽‍♂️': 6, + '🧖🏾‍♀️': 6, + '🧖🏾‍♂️': 6, + '🧖🏿‍♀️': 6, + '🧖🏿‍♂️': 6, + '🧗‍♀️': 4, + '🧗‍♂️': 4, + '🧗🏻‍♀️': 6, + '🧗🏻‍♂️': 6, + '🧗🏼‍♀️': 6, + '🧗🏼‍♂️': 6, + '🧗🏽‍♀️': 6, + '🧗🏽‍♂️': 6, + '🧗🏾‍♀️': 6, + '🧗🏾‍♂️': 6, + '🧗🏿‍♀️': 6, + '🧗🏿‍♂️': 6, + '🧘‍♀️': 4, + '🧘‍♂️': 4, + '🧘🏻‍♀️': 6, + '🧘🏻‍♂️': 6, + '🧘🏼‍♀️': 6, + '🧘🏼‍♂️': 6, + '🧘🏽‍♀️': 6, + '🧘🏽‍♂️': 6, + '🧘🏾‍♀️': 6, + '🧘🏾‍♂️': 6, + '🧘🏿‍♀️': 6, + '🧘🏿‍♂️': 6, + '🧙‍♀️': 4, + '🧙‍♂️': 4, + '🧙🏻‍♀️': 6, + '🧙🏻‍♂️': 6, + '🧙🏼‍♀️': 6, + '🧙🏼‍♂️': 6, + '🧙🏽‍♀️': 6, + '🧙🏽‍♂️': 6, + '🧙🏾‍♀️': 6, + '🧙🏾‍♂️': 6, + '🧙🏿‍♀️': 6, + '🧙🏿‍♂️': 6, + '🧚‍♀️': 4, + '🧚‍♂️': 4, + '🧚🏻‍♀️': 6, + '🧚🏻‍♂️': 6, + '🧚🏼‍♀️': 6, + '🧚🏼‍♂️': 6, + '🧚🏽‍♀️': 6, + '🧚🏽‍♂️': 6, + '🧚🏾‍♀️': 6, + '🧚🏾‍♂️': 6, + '🧚🏿‍♀️': 6, + '🧚🏿‍♂️': 6, + '🧛‍♀️': 4, + '🧛‍♂️': 4, + '🧛🏻‍♀️': 6, + '🧛🏻‍♂️': 6, + '🧛🏼‍♀️': 6, + '🧛🏼‍♂️': 6, + '🧛🏽‍♀️': 6, + '🧛🏽‍♂️': 6, + '🧛🏾‍♀️': 6, + '🧛🏾‍♂️': 6, + '🧛🏿‍♀️': 6, + '🧛🏿‍♂️': 6, + '🧜‍♀️': 4, + '🧜‍♂️': 4, + '🧜🏻‍♀️': 6, + '🧜🏻‍♂️': 6, + '🧜🏼‍♀️': 6, + '🧜🏼‍♂️': 6, + '🧜🏽‍♀️': 6, + '🧜🏽‍♂️': 6, + '🧜🏾‍♀️': 6, + '🧜🏾‍♂️': 6, + '🧜🏿‍♀️': 6, + '🧜🏿‍♂️': 6, + '🧝‍♀️': 4, + '🧝‍♂️': 4, + '🧝🏻‍♀️': 6, + '🧝🏻‍♂️': 6, + '🧝🏼‍♀️': 6, + '🧝🏼‍♂️': 6, + '🧝🏽‍♀️': 6, + '🧝🏽‍♂️': 6, + '🧝🏾‍♀️': 6, + '🧝🏾‍♂️': 6, + '🧝🏿‍♀️': 6, + '🧝🏿‍♂️': 6, + '🧞‍♀️': 4, + '🧞‍♂️': 4, + '🧟‍♀️': 4, + '🧟‍♂️': 4, + '🫱🏻‍🫲🏼': 9, + '🫱🏻‍🫲🏽': 9, + '🫱🏻‍🫲🏾': 9, + '🫱🏻‍🫲🏿': 9, + '🫱🏼‍🫲🏻': 9, + '🫱🏼‍🫲🏽': 9, + '🫱🏼‍🫲🏾': 9, + '🫱🏼‍🫲🏿': 9, + '🫱🏽‍🫲🏻': 9, + '🫱🏽‍🫲🏼': 9, + '🫱🏽‍🫲🏾': 9, + '🫱🏽‍🫲🏿': 9, + '🫱🏾‍🫲🏻': 9, + '🫱🏾‍🫲🏼': 9, + '🫱🏾‍🫲🏽': 9, + '🫱🏾‍🫲🏿': 9, + '🫱🏿‍🫲🏻': 9, + '🫱🏿‍🫲🏼': 9, + '🫱🏿‍🫲🏽': 9, + '🫱🏿‍🫲🏾': 9, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_e22030f3.py b/libs/wcwidth/table_grapheme_overrides/_known_e22030f3.py new file mode 100644 index 000000000..e58fa075e --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_e22030f3.py @@ -0,0 +1,6396 @@ +""" +Grapheme overrides for _known_e22030f3. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: terminology +""" +# pylint: skip-file +GRAPHEMES = { + ' ོ': 2, + 'אַ': 2, + 'אָ': 2, + 'בֿ': 2, + 'וּ': 2, + 'יִ': 2, + 'כּ': 2, + 'פּ': 2, + 'פֿ': 2, + 'תּ': 2, + 'ײַ': 2, + 'اً': 2, + 'اُ': 2, + 'اّ': 2, + 'أ': 2, + 'إ': 2, + 'بَ': 2, + 'تُ': 2, + 'ثّ': 2, + 'جُ': 2, + 'دّ': 2, + 'رً': 2, + 'رِ': 2, + 'رّ': 2, + 'صّ': 2, + 'ضً': 2, + 'فّ': 2, + 'قِ': 2, + 'لِ': 2, + 'مً': 2, + 'مِ': 2, + 'مّ': 2, + 'نِ': 2, + 'نّ': 2, + 'هٔ': 2, + 'وَ': 2, + 'وّ': 2, + 'ؤ': 2, + 'يُ': 2, + 'پُ': 2, + 'چّ': 2, + 'کُ': 2, + 'کِ': 2, + 'ھُ': 2, + 'ھٔ': 2, + 'ہِ': 2, + 'ہٰ': 2, + 'یِ': 2, + 'یّ': 2, + 'یٔ': 2, + 'یٰ': 2, + 'ܒ݂': 2, + 'ܓ݂': 2, + 'ܝܼ': 2, + 'ܟ݂': 2, + 'ܡܹ': 2, + 'ܬܵ': 2, + 'ހަ': 2, + 'ހާ': 2, + 'ހި': 2, + 'ހީ': 2, + 'ހު': 2, + 'ހެ': 2, + 'ހޭ': 2, + 'ހޮ': 2, + 'ހޯ': 2, + 'ށަ': 2, + 'ށާ': 2, + 'ށި': 2, + 'ށު': 2, + 'ށެ': 2, + 'ށޭ': 2, + 'ށް': 2, + 'ނަ': 2, + 'ނާ': 2, + 'ނި': 2, + 'ނީ': 2, + 'ނު': 2, + 'ނޫ': 2, + 'ނެ': 2, + 'ނޭ': 2, + 'ން': 2, + 'ރަ': 2, + 'ރާ': 2, + 'ރި': 2, + 'ރީ': 2, + 'ރު': 2, + 'ރޫ': 2, + 'ރެ': 2, + 'ރޭ': 2, + 'ރޮ': 2, + 'ބަ': 2, + 'ބާ': 2, + 'ބި': 2, + 'ބީ': 2, + 'ބު': 2, + 'ބޫ': 2, + 'ބެ': 2, + 'ބޭ': 2, + 'ބޮ': 2, + 'ބް': 2, + 'ޅަ': 2, + 'ޅާ': 2, + 'ޅި': 2, + 'ޅު': 2, + 'ޅެ': 2, + 'ޅޭ': 2, + 'ކަ': 2, + 'ކާ': 2, + 'ކި': 2, + 'ކީ': 2, + 'ކު': 2, + 'ކެ': 2, + 'ކޮ': 2, + 'ކް': 2, + 'އަ': 2, + 'އާ': 2, + 'އި': 2, + 'އީ': 2, + 'އު': 2, + 'އޫ': 2, + 'އެ': 2, + 'އޭ': 2, + 'އޮ': 2, + 'އޯ': 2, + 'އް': 2, + 'ވަ': 2, + 'ވާ': 2, + 'ވި': 2, + 'ވީ': 2, + 'ވު': 2, + 'ވެ': 2, + 'ވޭ': 2, + 'ވޮ': 2, + 'ވޯ': 2, + 'މަ': 2, + 'މާ': 2, + 'މި': 2, + 'މީ': 2, + 'މު': 2, + 'މުި': 3, + 'މެ': 2, + 'މޭ': 2, + 'މް': 2, + 'ފަ': 2, + 'ފާ': 2, + 'ފި': 2, + 'ފީ': 2, + 'ފު': 2, + 'ފޫ': 2, + 'ފެ': 2, + 'ފޯ': 2, + 'ފް': 2, + 'ދަ': 2, + 'ދާ': 2, + 'ދި': 2, + 'ދީ': 2, + 'ދު': 2, + 'ދޫ': 2, + 'ދެ': 2, + 'ދޭ': 2, + 'ދޮ': 2, + 'ތަ': 2, + 'ތާ': 2, + 'ތި': 2, + 'ތީ': 2, + 'ތު': 2, + 'ތެ': 2, + 'ތް': 2, + 'ލަ': 2, + 'ލާ': 2, + 'ލި': 2, + 'ލީ': 2, + 'ލު': 2, + 'ލޫ': 2, + 'ލެ': 2, + 'ލޭ': 2, + 'ލް': 2, + 'ގަ': 2, + 'ގާ': 2, + 'ގި': 2, + 'ގު': 2, + 'ގެ': 2, + 'ގޮ': 2, + 'ޏަ': 2, + 'ޏެ': 2, + 'ސަ': 2, + 'ސާ': 2, + 'ސި': 2, + 'ސީ': 2, + 'ސު': 2, + 'ސޫ': 2, + 'ސެ': 2, + 'ސް': 2, + 'ޑަ': 2, + 'ޑާ': 2, + 'ޑި': 2, + 'ޑު': 2, + 'ޑެ': 2, + 'ޒަ': 2, + 'ޒާ': 2, + 'ޒި': 2, + 'ޒީ': 2, + 'ޒު': 2, + 'ޒެ': 2, + 'ޓަ': 2, + 'ޓި': 2, + 'ޓީ': 2, + 'ޓު': 2, + 'ޓެ': 2, + 'ޔަ': 2, + 'ޔާ': 2, + 'ޔި': 2, + 'ޔު': 2, + 'ޔެ': 2, + 'ޔޭ': 2, + 'ޔޮ': 2, + 'ޕް': 2, + 'ޖަ': 2, + 'ޖާ': 2, + 'ޖި': 2, + 'ޖު': 2, + 'ޖޫ': 2, + 'ޖެ': 2, + 'ޖޭ': 2, + 'ޖް': 2, + 'ޗަ': 2, + 'ޗު': 2, + 'ޗެ': 2, + 'ޘަ': 2, + 'ޙަ': 2, + 'ޙާ': 2, + 'ޙި': 2, + 'ޙީ': 2, + 'ޙު': 2, + 'ޙެ': 2, + 'ޙް': 2, + 'ޚާ': 2, + 'ޚި': 2, + 'ޚު': 2, + 'ޚް': 2, + 'ޛަ': 2, + 'ޝަ': 2, + 'ޞަ': 2, + 'ޞާ': 2, + 'ޞި': 2, + 'ޞު': 2, + 'ޞޫ': 2, + 'ޞް': 2, + 'ޟު': 2, + 'ޠަ': 2, + 'ޠީ': 2, + 'ޢަ': 2, + 'ޢާ': 2, + 'ޢީ': 2, + 'ޢު': 2, + 'ޢެ': 2, + 'ޢް': 2, + 'ޣާ': 2, + 'ޤަ': 2, + 'ޤާ': 2, + 'ޤި': 2, + 'ޤީ': 2, + 'ޤު': 2, + 'ޤެ': 2, + 'ޤް': 2, + 'अं': 2, + 'आं': 2, + 'इँ': 2, + 'इं': 2, + 'ऊं': 2, + 'एं': 2, + 'ओं': 2, + 'कं': 2, + 'कां': 3, + 'किं': 3, + 'कु': 2, + 'कू': 2, + 'कृ': 2, + 'के': 2, + 'केँ': 3, + 'कें': 3, + 'केा': 3, + 'कै': 2, + 'क्': 2, + 'क्क': 3, + 'क्का': 4, + 'क्कां': 5, + 'क्को': 4, + 'क्घ्घि्': 7, + 'क्छ': 3, + 'क्त': 3, + 'क्ता': 4, + 'क्ति': 4, + 'क्ती': 4, + 'क्तीं': 5, + 'क्तुं': 5, + 'क्तेः': 5, + 'क्त्योः': 7, + 'क्ना': 4, + 'क्नि': 4, + 'क्य': 3, + 'क्या': 4, + 'क्र': 3, + 'क्रा': 4, + 'क्रि': 4, + 'क्रू': 4, + 'क्ष': 3, + 'क्षं': 4, + 'क्षा': 4, + 'क्षां': 5, + 'क्षाः': 5, + 'क्षि': 4, + 'क्षे': 4, + 'क्षै': 4, + 'क्षो': 4, + 'क्ष्ण': 5, + 'क्ष्य': 5, + 'क्ष्या': 6, + 'क्सा': 4, + 'खु': 2, + 'खे': 2, + 'खै': 2, + 'ख्त': 3, + 'ख्ने': 4, + 'ख्य': 3, + 'ख्यः': 4, + 'ख्या': 4, + 'ख्‍ने': 4, + 'गं': 2, + 'ग़': 2, + 'ग़ा': 3, + 'ग़ु': 3, + 'गाँ': 3, + 'गां': 3, + 'गु': 2, + 'गू': 2, + 'गृ': 2, + 'गे': 2, + 'गै': 2, + 'गों': 3, + 'ग्ण': 3, + 'ग्न': 3, + 'ग्ने': 4, + 'ग्भे': 4, + 'ग्य': 3, + 'ग्या': 4, + 'ग्ये': 4, + 'ग्यैः': 5, + 'ग्र': 3, + 'ग्रे': 4, + 'ग्र्य': 5, + 'घं': 2, + 'घू': 2, + 'घृ': 2, + 'घे': 2, + 'घेि': 3, + 'घ्': 2, + 'घ्ंत': 4, + 'घ्का': 4, + 'घ्त': 3, + 'घ्र': 3, + 'घ्र्व्य': 7, + 'घ्लि': 4, + 'घ्व': 3, + 'घ्श्य': 5, + 'घ्स्ट': 5, + 'ङ्ग': 3, + 'ङ्घ': 3, + 'चां': 3, + 'चु': 2, + 'चूं': 3, + 'चे': 2, + 'चै': 2, + 'च्च': 3, + 'च्चा': 4, + 'च्चे': 4, + 'च्चों': 5, + 'च्छ': 3, + 'च्छं': 4, + 'च्छा': 4, + 'च्छि': 4, + 'च्छु': 4, + 'च्छे': 4, + 'च्या': 4, + 'च्यां': 5, + 'च्यो': 4, + 'छु': 2, + 'छुी': 3, + 'छै': 2, + 'जँ': 2, + 'जिं': 3, + 'जु': 2, + 'जे': 2, + 'जेँ': 3, + 'ज्ज': 3, + 'ज्ञा': 4, + 'ज्ञे': 4, + 'ज्ने': 4, + 'ज्य': 3, + 'ज्या': 4, + 'ज्या‌': 4, + 'ज्रा': 4, + 'झे': 2, + 'ञ्च': 3, + 'ञ्चा': 4, + 'ञ्चि': 4, + 'टां': 3, + 'टुँ': 3, + 'टुं': 3, + 'टू': 2, + 'टे': 2, + 'टै': 2, + 'टों': 3, + 'ट्ज़': 4, + 'ट्टि': 4, + 'ट्टी': 4, + 'ट्या': 4, + 'ट्र': 3, + 'ट्रा': 4, + 'ट्रे': 4, + 'ठे': 2, + 'ठैः': 3, + 'ड़': 2, + 'ड़ा': 3, + 'ड़े': 3, + 'डू': 2, + 'डे़': 3, + 'ढं': 2, + 'ढ़': 2, + 'ढ़ा': 3, + 'ढ़ि': 3, + 'ढ़ी': 3, + 'ढ़ो': 3, + 'ढाँ': 3, + 'ढे': 2, + 'णं': 2, + 'णां': 3, + 'णु': 2, + 'णू': 2, + 'णे': 2, + 'णेँ': 3, + 'णों': 3, + 'ण्ड': 3, + 'ण्डं': 4, + 'ण्डः': 4, + 'ण्डि': 4, + 'ण्य': 3, + 'ण्या': 4, + 'ण्यां': 5, + 'तँ': 2, + 'तं': 2, + 'तां': 3, + 'ताः': 3, + 'तिं': 3, + 'तु': 2, + 'तुं': 3, + 'तू': 2, + 'तृ': 2, + 'ते': 2, + 'तेँ': 3, + 'तै': 2, + 'तों': 3, + 'त्': 2, + 'त्का': 4, + 'त्त': 3, + 'त्ता': 4, + 'त्ति': 4, + 'त्ते': 4, + 'त्त्थि': 6, + 'त्त्व': 5, + 'त्त्वा': 6, + 'त्त्वे': 6, + 'त्न': 3, + 'त्नी': 4, + 'त्नों': 5, + 'त्प': 3, + 'त्पा': 4, + 'त्पी': 4, + 'त्पू': 4, + 'त्म': 3, + 'त्मा': 4, + 'त्य': 3, + 'त्यं': 4, + 'त्या': 4, + 'त्यां': 5, + 'त्यि': 4, + 'त्ये': 4, + 'त्यै': 4, + 'त्र': 3, + 'त्रा': 4, + 'त्राा': 5, + 'त्राि': 5, + 'त्रि': 4, + 'त्री': 4, + 'त्रे': 4, + 'त्र्त': 5, + 'त्र्य': 5, + 'त्र्या': 6, + 'त्र्ये': 6, + 'त्व': 3, + 'त्वं': 4, + 'त्वा': 4, + 'त्वां': 5, + 'त्वे': 4, + 'त्सं': 4, + 'त्सा': 4, + 'त्सो': 4, + 'त्स्वा': 6, + 'थु': 2, + 'थे': 2, + 'थै': 2, + 'थ्य': 3, + 'दं': 2, + 'दां': 3, + 'दु': 2, + 'दू': 2, + 'दृ': 2, + 'दे': 2, + 'दै': 2, + 'द्घ': 3, + 'द्घि': 4, + 'द्घो': 4, + 'द्द': 3, + 'द्दा': 4, + 'द्दि': 4, + 'द्दी': 4, + 'द्दे': 4, + 'द्ध': 3, + 'द्धः': 4, + 'द्धा': 4, + 'द्धां': 5, + 'द्धि': 4, + 'द्धिं': 5, + 'द्धी': 4, + 'द्धीं': 5, + 'द्धैः': 5, + 'द्भ': 3, + 'द्भा': 4, + 'द्य': 3, + 'द्या': 4, + 'द्र': 3, + 'द्रो': 4, + 'द्व': 3, + 'द्वा': 4, + 'द्वि': 4, + 'द्वे': 4, + 'द्स': 3, + 'द्‌': 2, + 'धां': 3, + 'धाः': 3, + 'धु': 2, + 'धृ': 2, + 'धे': 2, + 'धेः': 3, + 'धों': 3, + 'ध्य': 3, + 'ध्या': 4, + 'ध्ये': 4, + 'ध्व': 3, + 'नं': 2, + 'नां': 3, + 'नाः': 3, + 'निँ': 3, + 'निः': 3, + 'नु': 2, + 'नू': 2, + 'नृ': 2, + 'ने': 2, + 'नै': 2, + 'नैः': 3, + 'नों': 3, + 'न्': 2, + 'न्का': 4, + 'न्चि': 4, + 'न्छे': 4, + 'न्जे': 4, + 'न्ञ्या': 6, + 'न्त': 3, + 'न्तं': 4, + 'न्तः': 4, + 'न्ता': 4, + 'न्ति': 4, + 'न्तु': 4, + 'न्तुं': 5, + 'न्ते': 4, + 'न्तो': 4, + 'न्तों': 5, + 'न्त्र': 5, + 'न्त्रा': 6, + 'न्त्राः': 7, + 'न्त्रि': 6, + 'न्त्री': 6, + 'न्त्र्य': 7, + 'न्त्र्यं': 8, + 'न्त्र्या': 8, + 'न्त्र्ये': 8, + 'न्द': 3, + 'न्दा': 4, + 'न्दी': 4, + 'न्दो': 4, + 'न्ध': 3, + 'न्धा': 4, + 'न्धि': 4, + 'न्धी': 4, + 'न्धु': 4, + 'न्धों': 5, + 'न्न': 3, + 'न्ना': 4, + 'न्नाः': 5, + 'न्नि': 4, + 'न्ने': 4, + 'न्म': 3, + 'न्मा': 4, + 'न्मु': 4, + 'न्मे': 4, + 'न्य': 3, + 'न्या': 4, + 'न्ये': 4, + 'न्यै': 4, + 'न्यो': 4, + 'न्व': 3, + 'न्वे': 4, + 'न्स': 3, + 'न्हा': 4, + 'न्हें': 5, + 'न्ह्यां': 7, + 'पं': 2, + 'पाँ': 3, + 'पां': 3, + 'पु': 2, + 'पू': 2, + 'पृ': 2, + 'पे': 2, + 'पेँ': 3, + 'पै': 2, + 'पों': 3, + 'प्त': 3, + 'प्तं': 4, + 'प्ति': 4, + 'प्तुं': 5, + 'प्तौ': 4, + 'प्य': 3, + 'प्यं': 4, + 'प्या': 4, + 'प्र': 3, + 'प्रा': 4, + 'फ़': 2, + 'फाँ': 3, + 'फु': 2, + 'फे': 2, + 'फ्त': 3, + 'फ्ता': 4, + 'फ्ना': 4, + 'फ्नो': 4, + 'फ्रां': 5, + 'बँ': 2, + 'बं': 2, + 'बाँ': 3, + 'बु': 2, + 'बू': 2, + 'बृ': 2, + 'बे': 2, + 'बै': 2, + 'ब्द': 3, + 'ब्ध': 3, + 'ब्धि': 4, + 'ब्यू': 4, + 'भं': 2, + 'भिः': 3, + 'भु': 2, + 'भू': 2, + 'भे': 2, + 'भै': 2, + 'भ्य': 3, + 'भ्यः': 4, + 'भ्या': 4, + 'भ्यां': 5, + 'भ्यु': 4, + 'भ्यो': 4, + 'भ्र': 3, + 'मं': 2, + 'मां': 3, + 'मु': 2, + 'मू': 2, + 'मे': 2, + 'मेँ': 3, + 'में': 3, + 'मै': 2, + 'मों': 3, + 'म्': 2, + 'म्ति': 4, + 'म्न': 3, + 'म्नि': 4, + 'म्प': 3, + 'म्पा': 4, + 'म्प्र': 5, + 'म्ब': 3, + 'म्बु': 4, + 'म्भि': 4, + 'म्म': 3, + 'म्मा': 4, + 'म्मि': 4, + 'म्मे': 4, + 'म्या': 4, + 'म्से': 4, + 'म्ह': 3, + 'म्ही': 4, + 'म्हो': 4, + 'यं': 2, + 'यां': 3, + 'याः': 3, + 'यु': 2, + 'यू': 2, + 'ये': 2, + 'यै': 2, + 'यैः': 3, + 'यों': 3, + 'य्य': 3, + 'य्यो': 4, + 'रं': 2, + 'रां': 3, + 'राः': 3, + 'रिं': 3, + 'रु': 2, + 'रू': 2, + 'रे': 2, + 'रें': 3, + 'रै': 2, + 'रों': 3, + 'र्क': 3, + 'र्कि': 4, + 'र्ग': 3, + 'र्गी': 4, + 'र्गे': 4, + 'र्चि': 4, + 'र्छ': 3, + 'र्ज': 3, + 'र्जा': 4, + 'र्जि': 4, + 'र्जी': 4, + 'र्ट': 3, + 'र्ढ': 3, + 'र्ण': 3, + 'र्णं': 4, + 'र्णां': 5, + 'र्णी': 4, + 'र्त': 3, + 'र्ता': 4, + 'र्ति': 4, + 'र्तिः': 5, + 'र्ती': 4, + 'र्तुं': 5, + 'र्त्त': 5, + 'र्त्ति': 6, + 'र्थ': 3, + 'र्थं': 4, + 'र्थि': 4, + 'र्थी': 4, + 'र्द': 3, + 'र्दा': 4, + 'र्दि': 4, + 'र्दु': 4, + 'र्दे': 4, + 'र्दै': 4, + 'र्दो': 4, + 'र्द्ध': 5, + 'र्ध': 3, + 'र्धा': 4, + 'र्धि': 4, + 'र्न': 3, + 'र्नि': 4, + 'र्नु': 4, + 'र्नू': 4, + 'र्ने': 4, + 'र्फ': 3, + 'र्ब': 3, + 'र्बं': 4, + 'र्बा': 4, + 'र्भ': 3, + 'र्भा': 4, + 'र्म': 3, + 'र्मं': 4, + 'र्मा': 4, + 'र्मि': 4, + 'र्य': 3, + 'र्यं': 4, + 'र्या': 4, + 'र्यां': 5, + 'र्ये': 4, + 'र्यों': 5, + 'र्रा': 4, + 'र्व': 3, + 'र्वा': 4, + 'र्वां': 5, + 'र्वृ': 4, + 'र्वे': 4, + 'र्वो': 4, + 'र्श': 3, + 'र्ष': 3, + 'र्षे': 4, + 'र्स': 3, + 'र्सा': 4, + 'र्ह': 3, + 'र्हाः': 5, + 'र्हि': 4, + 'र्हो': 4, + 'र्‍या': 4, + 'लां': 3, + 'लाः': 3, + 'लिं': 3, + 'लीं': 3, + 'लु': 2, + 'लू': 2, + 'ले': 2, + 'लै': 2, + 'लों': 3, + 'ल्क': 3, + 'ल्का': 4, + 'ल्ने': 4, + 'ल्प': 3, + 'ल्पि': 4, + 'ल्म': 3, + 'ल्य': 3, + 'ल्यं': 4, + 'ल्या': 4, + 'ल्यां': 5, + 'ल्याः': 5, + 'ल्ये': 4, + 'ल्ल': 3, + 'ल्लं': 4, + 'ल्ला': 4, + 'ल्लि': 4, + 'ल्ले': 4, + 'ल्ह': 3, + 'ळे': 2, + 'वं': 2, + 'वां': 3, + 'वाः': 3, + 'वू': 2, + 'वृ': 2, + 'वे': 2, + 'वै': 2, + 'वों': 3, + 'व्य': 3, + 'व्या': 4, + 'व्ये': 4, + 'व्यों': 5, + 'शं': 2, + 'शां': 3, + 'शाः': 3, + 'शु': 2, + 'शू': 2, + 'शे': 2, + 'शों': 3, + 'श्च': 3, + 'श्चि': 4, + 'श्य': 3, + 'श्या': 4, + 'श्यि': 4, + 'श्यों': 5, + 'श्र': 3, + 'श्रा': 4, + 'श्रां': 5, + 'श्रु': 4, + 'श्व': 3, + 'श्वा': 4, + 'श्वि': 4, + 'षां': 3, + 'षाः': 3, + 'षु': 2, + 'षे': 2, + 'षों': 3, + 'ष्का': 4, + 'ष्ट': 3, + 'ष्टः': 4, + 'ष्टा': 4, + 'ष्टां': 5, + 'ष्टाः': 5, + 'ष्टि': 4, + 'ष्टी': 4, + 'ष्टीं': 5, + 'ष्ट्र': 5, + 'ष्ट्रा': 6, + 'ष्ट्रां': 7, + 'ष्ट्रि': 6, + 'ष्ट्री': 6, + 'ष्ट्रे': 6, + 'ष्ट्रो': 6, + 'ष्ट्रों': 7, + 'ष्ठा': 4, + 'ष्ठे': 4, + 'ष्ठों': 5, + 'ष्णु': 4, + 'ष्प': 3, + 'ष्प्र': 5, + 'ष्य': 3, + 'ष्यः': 4, + 'ष्यों': 5, + 'सँ': 2, + 'सं': 2, + 'साँ': 3, + 'सां': 3, + 'सु': 2, + 'सू': 2, + 'सृ': 2, + 'से': 2, + 'सै': 2, + 'स्क': 3, + 'स्का': 4, + 'स्काः': 5, + 'स्कु': 4, + 'स्कू': 4, + 'स्कृ': 4, + 'स्ट': 3, + 'स्ट्या': 6, + 'स्त': 3, + 'स्ता': 4, + 'स्ति': 4, + 'स्ती': 4, + 'स्तु': 4, + 'स्तुं': 5, + 'स्तै': 4, + 'स्तो': 4, + 'स्त्र': 5, + 'स्त्रा': 6, + 'स्त्रि': 6, + 'स्त्री': 6, + 'स्थ': 3, + 'स्था': 4, + 'स्थां': 5, + 'स्थि': 4, + 'स्थी': 4, + 'स्थे': 4, + 'स्थ्य': 5, + 'स्धी': 4, + 'स्प': 3, + 'स्पे': 4, + 'स्म': 3, + 'स्मि': 4, + 'स्मिं': 5, + 'स्मै': 4, + 'स्य': 3, + 'स्या': 4, + 'स्यां': 5, + 'स्याः': 5, + 'स्युः': 5, + 'स्यों': 5, + 'स्रो': 4, + 'स्ल': 3, + 'स्व': 3, + 'स्वं': 4, + 'स्वा': 4, + 'स्वी': 4, + 'स्वे': 4, + 'स्स': 3, + 'स्सा': 4, + 'हाँ': 3, + 'हां': 3, + 'हिं': 3, + 'हीं': 3, + 'हु': 2, + 'हुँ': 3, + 'हू': 2, + 'हे': 2, + 'है': 2, + 'हैं': 3, + 'हैः': 3, + 'हों': 3, + 'ह्या': 4, + 'ह्रा': 4, + 'फ़्ता': 4, + 'কিং': 3, + 'কু': 2, + 'কূ': 2, + 'কৃ': 2, + 'ক্ত': 3, + 'ক্তি': 4, + 'ক্য': 3, + 'ক্র': 3, + 'ক্রী': 4, + 'ক্ষ': 3, + 'ক্ষা': 4, + 'ক্ষি': 4, + 'ক্ষে': 4, + 'ক্ষ্য': 5, + 'ক্ষ্যে': 6, + 'খু': 2, + 'খ্যা': 4, + 'গু': 2, + 'গৃ': 2, + 'গ্য': 3, + 'গ্র': 3, + 'গ্রা': 4, + 'গ্রে': 4, + 'ঘৃ': 2, + 'ঘো': 3, + 'ঙ্খা': 4, + 'ঙ্গ': 3, + 'ঙ্গী': 4, + 'ঙ্গে': 4, + 'ঙ্ঘ': 3, + 'চ্চ': 3, + 'চ্ছ': 3, + 'চ্ছা': 4, + 'চ্ছা‌': 4, + 'চ্ছু': 4, + 'চ্ছে': 4, + 'ছু': 2, + 'জ্ঞ': 3, + 'জ্ঞা': 4, + 'জ্য': 3, + 'ঞ্চি': 4, + 'ঞ্ছি': 4, + 'ট্রে': 4, + 'ড়': 2, + 'ড়া': 3, + 'ঢ়': 2, + 'ণ্ড': 3, + 'ণ্ডে': 4, + 'ণ্য': 3, + 'তাঁ': 3, + 'তু': 2, + 'তৃ': 2, + 'ত্ত': 3, + 'ত্তা': 4, + 'ত্তি': 4, + 'ত্ন': 3, + 'ত্ব': 3, + 'ত্বে': 4, + 'ত্ম': 3, + 'ত্য': 3, + 'ত্যা': 4, + 'ত্যি': 4, + 'ত্যে': 4, + 'ত্র': 3, + 'ত্রা': 4, + 'ত্রে': 4, + 'ত্‍': 2, + 'ত্‍প': 3, + 'ত্‍পী': 4, + 'ত্‍সা': 4, + 'থ্য': 3, + 'দৃ': 2, + 'দ্দি': 4, + 'দ্দে': 4, + 'দ্ধ': 3, + 'দ্ধা': 4, + 'দ্ধি': 4, + 'দ্ধে': 4, + 'দ্বা': 4, + 'দ্ভ': 3, + 'দ্ভূ': 4, + 'দ্য': 3, + 'দ্রো': 4, + 'ধ্য': 3, + 'ধ্যে': 4, + 'নু': 2, + 'ন্ড': 3, + 'ন্ত': 3, + 'ন্তা': 4, + 'ন্তি': 4, + 'ন্তে': 4, + 'ন্ত্রি': 6, + 'ন্ত্রে': 6, + 'ন্থী': 4, + 'ন্ধ': 3, + 'ন্ধা': 4, + 'ন্ধু': 4, + 'ন্ন': 3, + 'ন্ম': 3, + 'ন্মা': 4, + 'ন্মু': 4, + 'ন্য': 3, + 'ন্যা': 4, + 'ন্যে': 4, + 'পু': 2, + 'পূ': 2, + 'পৃ': 2, + 'প্ত': 3, + 'প্তা': 4, + 'প্র': 3, + 'প্রা': 4, + 'বু': 2, + 'বৃ': 2, + 'ব্ধি': 4, + 'ব্য': 3, + 'ব্যা': 4, + 'ব্র': 3, + 'ভূ': 2, + 'ভ্য': 3, + 'ভ্রা': 4, + 'মু': 2, + 'মূ': 2, + 'ম্প': 3, + 'ম্পা': 4, + 'ম্ব': 3, + 'ম্ভ': 3, + 'ম্ম': 3, + 'ম্মা': 4, + 'ম্য': 3, + 'য়': 2, + 'য়া': 3, + 'য়ি': 3, + 'য়ে': 3, + 'য়ো': 3, + 'যু': 2, + 'য্য': 3, + 'রাং': 3, + 'রু': 2, + 'রূ': 2, + 'র্ক': 3, + 'র্জ': 3, + 'র্জা': 4, + 'র্ণ': 3, + 'র্ত': 3, + 'র্থ': 3, + 'র্থে': 4, + 'র্দো': 4, + 'র্ধ': 3, + 'র্ধা': 4, + 'র্নী': 4, + 'র্ব': 3, + 'র্বা': 4, + 'র্বি': 4, + 'র্বো': 4, + 'র্ব্য': 5, + 'র্ভূ': 4, + 'র্ম': 3, + 'র্মে': 4, + 'র্য': 3, + 'র্যা': 4, + 'ল্প': 3, + 'ল্য': 3, + 'ল্যা': 4, + 'ল্যে': 4, + 'ল্লে': 4, + 'শু': 2, + 'শৃং': 3, + 'শ্চ': 3, + 'শ্চি': 4, + 'শ্বা': 4, + 'শ্বে': 4, + 'শ্য': 3, + 'শ্যে': 4, + 'শ্র': 3, + 'শ্রা': 4, + 'ষ্ট': 3, + 'ষ্টা': 4, + 'ষ্টি': 4, + 'ষ্ট্র': 5, + 'ষ্ট্রী': 6, + 'ষ্ট্রে': 6, + 'ষ্ঠা': 4, + 'ষ্ঠি': 4, + 'ষ্ঠী': 4, + 'ষ্ঠু': 4, + 'ষ্ণু': 4, + 'সাং': 3, + 'সু': 2, + 'সৃ': 2, + 'স্ক': 3, + 'স্কৃ': 4, + 'স্ত': 3, + 'স্তি': 4, + 'স্তি‌': 4, + 'স্ত্র': 5, + 'স্থ': 3, + 'স্থা': 4, + 'স্থ্য': 5, + 'স্ব': 3, + 'স্বা': 4, + 'স্বী': 4, + 'স্য': 3, + 'স্যা': 4, + 'স্যে': 4, + 'হ্য': 3, + 'ਅੰ': 2, + 'ਅੱ': 2, + 'ਆਂ': 2, + 'ਇੰ': 2, + 'ਉਂ': 2, + 'ਉੱ': 2, + 'ਕਾਂ': 3, + 'ਕਿ੍': 3, + 'ਕਿੱ': 3, + 'ਕੁ': 2, + 'ਕੂ': 2, + 'ਕੇ': 2, + 'ਕੋ': 2, + 'ਕੌ': 2, + 'ਕੰ': 2, + 'ਕੱ': 2, + 'ਖਾਂ': 3, + 'ਖਿ਼': 3, + 'ਖੁ': 2, + 'ਖੁੱ': 3, + 'ਖੇ': 2, + 'ਖੋ': 2, + 'ਖੋਂ': 3, + 'ਗਿ੍': 3, + 'ਗੁ': 2, + 'ਗੇ': 2, + 'ਗੈ': 2, + 'ਗੱ': 2, + 'ਘੁੰ': 3, + 'ਘੰ': 2, + 'ਚਿੱ': 3, + 'ਚੁ': 2, + 'ਚੇ': 2, + 'ਚੋ': 2, + 'ਚੋਂ': 3, + 'ਚੰ': 2, + 'ਛੁ': 2, + 'ਛੋਂ': 3, + 'ਜਾਂ': 3, + 'ਜਿ਼': 3, + 'ਜਿ਼ੰ': 4, + 'ਜੁ': 2, + 'ਜੇ': 2, + 'ਜੋ': 2, + 'ਜੋਂ': 3, + 'ਜੰ': 2, + 'ਜੱ': 2, + 'ਝੰ': 2, + 'ਟਿ੍': 3, + 'ਟੇ': 2, + 'ਢੇ': 2, + 'ਢੰ': 2, + 'ਣਾਂ': 3, + 'ਣੇ': 2, + 'ਤਾਂ': 3, + 'ਤੇ': 2, + 'ਤੋਂ': 3, + 'ਤੰ': 2, + 'ਤੱ': 2, + 'ਥੇ': 2, + 'ਥੋਂ': 3, + 'ਦਾਂ': 3, + 'ਦਿੱ': 3, + 'ਦੁ': 2, + 'ਦੂ': 2, + 'ਦੇ': 2, + 'ਦੋ': 2, + 'ਦੋਂ': 3, + 'ਦੰ': 2, + 'ਧਾਂ': 3, + 'ਧੁ': 2, + 'ਧੇ': 2, + 'ਧੱ': 2, + 'ਨਾਂ': 3, + 'ਨੀਂ': 3, + 'ਨੁ': 2, + 'ਨੁੱ': 3, + 'ਨੂੰ': 3, + 'ਨੇ': 2, + 'ਨੋ': 2, + 'ਨੌ': 2, + 'ਨ੍': 2, + 'ਨੰ': 2, + 'ਪੁ': 2, + 'ਪੂ': 2, + 'ਪੈ': 2, + 'ਪ੍': 2, + 'ਪੰ': 2, + 'ਪੱ': 2, + 'ਫੌ': 2, + 'ਬੁ': 2, + 'ਬੂ': 2, + 'ਬੇ': 2, + 'ਬੋ': 2, + 'ਬੰ': 2, + 'ਬੱ': 2, + 'ਭੂ': 2, + 'ਭੇ': 2, + 'ਮਾਂ': 3, + 'ਮੁ': 2, + 'ਮੁੱ': 3, + 'ਮੂ': 2, + 'ਮੇ': 2, + 'ਮੇਂ': 3, + 'ਮੈਂ': 3, + 'ਮੌ': 2, + 'ਮੰ': 2, + 'ਯੁ': 2, + 'ਯੂ': 2, + 'ਯੋ': 2, + 'ਰਾਂ': 3, + 'ਰੁ': 2, + 'ਰੁੱ': 3, + 'ਰੂ': 2, + 'ਰੇ': 2, + 'ਰੋ': 2, + 'ਰ੍': 2, + 'ਰੰ': 2, + 'ਰੱ': 2, + 'ਲਾਂ': 3, + 'ਲਿੰ': 3, + 'ਲੇ': 2, + 'ਲੈ': 2, + 'ਲੋ': 2, + 'ਲ੍': 2, + 'ਲੰ': 2, + 'ਵਾਂ': 3, + 'ਵਿੱ': 3, + 'ਵੇ': 2, + 'ਵੇਂ': 3, + 'ਵੈ': 2, + 'ਵੰ': 2, + 'ਵੱ': 2, + 'ਸ਼ਾਂ': 3, + 'ਸ਼ੇ': 2, + 'ਸ਼ੋ': 2, + 'ਸ਼ੌ': 2, + 'ਸਾਂ': 3, + 'ਸਿੱ': 3, + 'ਸੁ': 2, + 'ਸੁੰ': 3, + 'ਸੂ': 2, + 'ਸੇ': 2, + 'ਸੇੇ': 3, + 'ਸੈਂ': 3, + 'ਸੌ': 2, + 'ਸੰ': 2, + 'ਸੱ': 2, + 'ਹਾਂ': 3, + 'ਹਿੰ': 3, + 'ਹਿੱ': 3, + 'ਹੀਂ': 3, + 'ਹੁ': 2, + 'ਹੁੰ': 3, + 'ਹੂ': 2, + 'ਹੇ': 2, + 'ਹੈ': 2, + 'ਹੋ': 2, + 'ਹੋਂ': 3, + 'ਹੱ': 2, + 'ਜ਼ੁ': 2, + 'ੜੀਂ': 3, + 'ੜੇ': 2, + 'ੜ੍': 2, + 'ਫ਼ੁ': 2, + 'અં': 2, + 'આં': 2, + 'ઊં': 2, + 'કાં': 3, + 'કુ': 2, + 'કૃ': 2, + 'કે': 2, + 'ક્ક': 3, + 'ક્કા': 4, + 'ક્કો': 4, + 'ક્તિ': 4, + 'ક્ર': 3, + 'ક્ષ': 3, + 'ક્ષા': 4, + 'ખુ': 2, + 'ખૂ': 2, + 'ખ્ત': 3, + 'ગુ': 2, + 'ગે': 2, + 'ગ્ન': 3, + 'ગ્ય': 3, + 'ચૂં': 3, + 'ચ્ચ': 3, + 'ચ્ચા': 4, + 'ચ્ચે': 4, + 'ચ્છા': 4, + 'ચ્છે': 4, + 'છું': 3, + 'છૂ': 2, + 'છે': 2, + 'જં': 2, + 'જાં': 3, + 'જુ': 2, + 'જૂ': 2, + 'જે': 2, + 'જ્ઞા': 4, + 'જ્ય': 3, + 'જ્યો': 4, + 'ઝૂં': 3, + 'ટું': 3, + 'ટે': 2, + 'ટ્ર': 3, + 'ટ્રે': 4, + 'ડાં': 3, + 'ડે': 2, + 'ણુ': 2, + 'ણે': 2, + 'તં': 2, + 'તાં': 3, + 'તુ': 2, + 'તૃ': 2, + 'તે': 2, + 'ત્તા': 4, + 'ત્તિ': 4, + 'ત્ત્વ': 5, + 'ત્ત્વા': 6, + 'ત્ન': 3, + 'ત્મ': 3, + 'ત્ય': 3, + 'ત્યા': 4, + 'ત્યાં': 5, + 'ત્યિ': 4, + 'ત્યે': 4, + 'ત્યો': 4, + 'ત્ર': 3, + 'ત્રી': 4, + 'ત્ર્ય': 5, + 'ત્વ': 3, + 'થે': 2, + 'દુ': 2, + 'દૃ': 2, + 'દે': 2, + 'દ્ધ': 3, + 'દ્ધા': 4, + 'દ્ધાં': 5, + 'દ્ધિ': 4, + 'દ્ભ': 3, + 'દ્વા': 4, + 'ધાં': 3, + 'ધુ': 2, + 'ધ્ય': 3, + 'ધ્યા': 4, + 'નં': 2, + 'નાં': 3, + 'નુ': 2, + 'નું': 3, + 'ને': 2, + 'નૈ': 2, + 'ન્ત': 3, + 'ન્તો': 4, + 'ન્મ': 3, + 'ન્મા': 4, + 'ન્મે': 4, + 'ન્ય': 3, + 'ન્યા': 4, + 'ન્યૂં': 5, + 'પુ': 2, + 'પૂ': 2, + 'પે': 2, + 'પ્ત': 3, + 'પ્ર': 3, + 'પ્રા': 4, + 'ફે': 2, + 'બં': 2, + 'બાં': 3, + 'બે': 2, + 'બ્ધ': 3, + 'ભં': 2, + 'ભાં': 3, + 'ભૂ': 2, + 'ભે': 2, + 'ભ્ય': 3, + 'મં': 2, + 'માં': 3, + 'મુ': 2, + 'મૂ': 2, + 'મૃ': 2, + 'મે': 2, + 'મૈ': 2, + 'મ્ય': 3, + 'મ્યાં': 5, + 'યુ': 2, + 'રં': 2, + 'રુ': 2, + 'રૂ': 2, + 'રે': 2, + 'ર્જ': 3, + 'ર્ણ': 3, + 'ર્ત': 3, + 'ર્તા': 4, + 'ર્થ': 3, + 'ર્થિ': 4, + 'ર્દો': 4, + 'ર્મ': 3, + 'ર્મિ': 4, + 'ર્મે': 4, + 'ર્ય': 3, + 'ર્યા': 4, + 'ર્યો': 4, + 'ર્વ': 3, + 'લાં': 3, + 'લિં': 3, + 'લે': 2, + 'લ્મ': 3, + 'લ્ય': 3, + 'ળે': 2, + 'વં': 2, + 'વાં': 3, + 'વું': 3, + 'વૃ': 2, + 'વે': 2, + 'વૈ': 2, + 'વ્ય': 3, + 'વ્યા': 4, + 'વ્યો': 4, + 'શાં': 3, + 'શુ': 2, + 'શે': 2, + 'શ્કે': 4, + 'શ્ચ': 3, + 'શ્ય': 3, + 'શ્ર': 3, + 'શ્વ': 3, + 'ષ્ટિ': 4, + 'ષ્ટ્ર': 5, + 'ષ્ટ્રી': 6, + 'ષ્ટ્રો': 6, + 'ષ્ઠા': 4, + 'ષ્ણુ': 4, + 'ષ્પ': 3, + 'સં': 2, + 'સાં': 3, + 'સુ': 2, + 'સું': 3, + 'સૂ': 2, + 'સે': 2, + 'સ્કૃ': 4, + 'સ્ટ': 3, + 'સ્તિ': 4, + 'સ્તી': 4, + 'સ્ત્રી': 6, + 'સ્થા': 4, + 'સ્થિ': 4, + 'સ્પ': 3, + 'સ્રી': 4, + 'સ્વ': 3, + 'સ્વા': 4, + 'સ્વી': 4, + 'સ્સા': 4, + 'હુ': 2, + 'હે': 2, + 'கீ': 2, + 'க்': 2, + 'ங்': 2, + 'சீ': 2, + 'ச்': 2, + 'ஞ்': 2, + 'ட்': 2, + 'ண்': 2, + 'தீ': 2, + 'த்': 2, + 'நீ': 2, + 'ந்': 2, + 'ன்': 2, + 'ப்': 2, + 'மீ': 2, + 'ம்': 2, + 'யீ': 2, + 'ய்': 2, + 'ர்': 2, + 'ற்': 2, + 'ல்': 2, + 'ள்': 2, + 'ழ்': 2, + 'வீ': 2, + 'வ்': 2, + 'ஸ்': 2, + 'కా': 2, + 'కాం': 3, + 'కి': 2, + 'కీ': 2, + 'కుం': 3, + 'కె': 2, + 'కెం': 3, + 'కై': 2, + 'కొ': 2, + 'క్క': 3, + 'క్కు': 4, + 'క్త': 3, + 'క్తి': 4, + 'క్తీ': 4, + 'క్తు': 4, + 'క్య': 3, + 'క్ర': 3, + 'క్రాం': 5, + 'క్రి': 4, + 'క్రూ': 4, + 'క్ష': 3, + 'క్షా': 4, + 'క్షి': 4, + 'క్షిం': 5, + 'క్షే': 4, + 'క్షో': 4, + 'క్స్వా': 6, + 'ఖా': 2, + 'ఖ్య': 3, + 'ఖ్యా': 4, + 'గా': 2, + 'గి': 2, + 'గిం': 3, + 'గీ': 2, + 'గౌ': 2, + 'గ్భా': 4, + 'గ్మ': 3, + 'గ్య': 3, + 'గ్ర': 3, + 'గ్రి': 4, + 'ఘి': 2, + 'ఘో': 2, + 'చా': 2, + 'చి': 2, + 'చెం': 3, + 'చే': 2, + 'చో': 2, + 'చ్చు': 4, + 'చ్ఛ': 3, + 'చ్ఛా': 4, + 'చ్ఛే': 4, + 'ఛ్ఛా': 4, + 'జా': 2, + 'జి': 2, + 'జీ': 2, + 'జే': 2, + 'జో': 2, + 'జ్ఞ': 3, + 'జ్ఞా': 4, + 'జ్య': 3, + 'టి': 2, + 'టిం': 3, + 'టుం': 3, + 'టే': 2, + 'ట్టి': 4, + 'ట్లు': 4, + 'డి': 2, + 'డ్డ': 3, + 'డ్రు': 4, + 'ణా': 2, + 'ణిం': 3, + 'ణీ': 2, + 'తా': 2, + 'తి': 2, + 'తీ': 2, + 'తె': 2, + 'తొ': 2, + 'తో': 2, + 'త్కా': 4, + 'త్త': 3, + 'త్తి': 4, + 'త్తు': 4, + 'త్న': 3, + 'త్నిం': 5, + 'త్ప': 3, + 'త్ఫ': 3, + 'త్య': 3, + 'త్యం': 4, + 'త్యా': 4, + 'త్ర': 3, + 'త్రు': 4, + 'త్ర్య': 5, + 'త్వ': 3, + 'త్వా': 4, + 'త్స': 3, + 'థా': 2, + 'దా': 2, + 'ది': 2, + 'దిం': 3, + 'దుం': 3, + 'దే': 2, + 'దై': 2, + 'దో': 2, + 'ద్ఘో': 4, + 'ద్ది': 4, + 'ద్దు': 4, + 'ద్దే': 4, + 'ద్ధ': 3, + 'ద్ధి': 4, + 'ద్ధిం': 5, + 'ద్య': 3, + 'ద్యా': 4, + 'ద్యో': 4, + 'ద్ర్య': 5, + 'ద్వా': 4, + 'ధా': 2, + 'ధి': 2, + 'ధిం': 3, + 'ధీ': 2, + 'ధ్య': 3, + 'నా': 2, + 'నాం': 3, + 'ని': 2, + 'నిం': 3, + 'నీ': 2, + 'నుం': 3, + 'నె': 2, + 'నే': 2, + 'నై': 2, + 'నొ': 2, + 'నొం': 3, + 'న్ధ': 3, + 'న్న': 3, + 'న్నా': 4, + 'న్ని': 4, + 'న్ను': 4, + 'న్మ': 3, + 'న్య': 3, + 'న్యా': 4, + 'న్వే': 4, + 'పా': 2, + 'పిం': 3, + 'పీ': 2, + 'పె': 2, + 'పెం': 3, + 'పే': 2, + 'పై': 2, + 'పొం': 3, + 'ప్త': 3, + 'ప్ర': 3, + 'ప్రా': 4, + 'ప్రే': 4, + 'బా': 2, + 'బి': 2, + 'బో': 2, + 'బ్ర': 3, + 'భా': 2, + 'భి': 2, + 'భే': 2, + 'భ్య': 3, + 'భ్యు': 4, + 'భ్ర': 3, + 'భ్రా': 4, + 'మా': 2, + 'మి': 2, + 'మిం': 3, + 'మే': 2, + 'మై': 2, + 'మో': 2, + 'మ్య': 3, + 'యా': 2, + 'యి': 2, + 'యిం': 3, + 'యుం': 3, + 'యె': 2, + 'యే': 2, + 'యొ': 2, + 'యో': 2, + 'రా': 2, + 'రి': 2, + 'రిం': 3, + 'రై': 2, + 'రో': 2, + 'ర్': 2, + 'ర్గ': 3, + 'ర్చి': 4, + 'ర్చు': 4, + 'ర్ణ': 3, + 'ర్ణాం': 5, + 'ర్ణు': 4, + 'ర్త': 3, + 'ర్తి': 4, + 'ర్తిం': 5, + 'ర్తృ': 4, + 'ర్థ': 3, + 'ర్థి': 4, + 'ర్థిం': 5, + 'ర్ద': 3, + 'ర్దో': 4, + 'ర్ధ': 3, + 'ర్ప': 3, + 'ర్బం': 4, + 'ర్భ': 3, + 'ర్భా': 4, + 'ర్య': 3, + 'ర్యా': 4, + 'ర్రా': 4, + 'ర్వ': 3, + 'ర్వా': 4, + 'ర్హ': 3, + 'ర్హాం': 5, + 'ర్హు': 4, + 'లా': 2, + 'లి': 2, + 'లిం': 3, + 'లీ': 2, + 'లె': 2, + 'లే': 2, + 'లో': 2, + 'లౌ': 2, + 'ల్క': 3, + 'ల్గొ': 4, + 'ల్ప': 3, + 'ల్ల': 3, + 'ల్లి': 4, + 'ళా': 2, + 'వా': 2, + 'వి': 2, + 'విం': 3, + 'వీ': 2, + 'వే': 2, + 'వై': 2, + 'వ్య': 3, + 'వ్యా': 4, + 'వ్వ': 3, + 'శా': 2, + 'శాం': 3, + 'శి': 2, + 'శిం': 3, + 'శై': 2, + 'శ్చ': 3, + 'శ్చి': 4, + 'శ్య': 3, + 'శ్ర': 3, + 'శ్రాం': 5, + 'శ్లే': 4, + 'శ్వా': 4, + 'షా': 2, + 'షి': 2, + 'షిం': 3, + 'షే': 2, + 'ష్ట': 3, + 'ష్టు': 4, + 'ష్ట్ర': 5, + 'ష్ట్రీ': 6, + 'ష్ఠ': 3, + 'ష్ఠా': 4, + 'ష్ఠు': 4, + 'ష్థ': 3, + 'ష్ప': 3, + 'సా': 2, + 'సాం': 3, + 'సి': 2, + 'సీ': 2, + 'సె': 2, + 'సే': 2, + 'సౌ': 2, + 'స్కృ': 4, + 'స్తా': 4, + 'స్తి': 4, + 'స్త్ర': 5, + 'స్త్రీ': 6, + 'స్థ': 3, + 'స్థా': 4, + 'స్థి': 4, + 'స్థ్య': 5, + 'స్ప': 3, + 'స్య': 3, + 'స్వ': 3, + 'స్వా': 4, + 'స్వీ': 4, + 'స్వే': 4, + 'హా': 2, + 'హి': 2, + 'హిం': 3, + 'హో': 2, + 'ಕಾಂ': 3, + 'ಕಿಂ': 3, + 'ಕುಂ': 3, + 'ಕೆ': 2, + 'ಕೆಂ': 3, + 'ಕೊಂ': 3, + 'ಕೌ': 2, + 'ಕ್': 2, + 'ಕ್‌': 2, + 'ಖಾಂ': 3, + 'ಖ್': 2, + 'ಗಾಂ': 3, + 'ಗಿ': 2, + 'ಗೆ': 2, + 'ಗೊಂ': 3, + 'ಗೌ': 2, + 'ಗ್': 2, + 'ಘಿ': 2, + 'ಚಿ': 2, + 'ಚ್': 2, + 'ಛಿ': 2, + 'ಛೆ': 2, + 'ಜಿ': 2, + 'ಜೆ': 2, + 'ಜ್': 2, + 'ಞೆ': 2, + 'ಞೌ': 2, + 'ಟಿ': 2, + 'ಟುಂ': 3, + 'ಟ್': 2, + 'ಠಿ': 2, + 'ಡಿ': 2, + 'ಡೆ': 2, + 'ಡ್': 2, + 'ಢಿ': 2, + 'ಣಿ': 2, + 'ಣೆ': 2, + 'ತಿ': 2, + 'ತೆ': 2, + 'ತ್': 2, + 'ಥಿ': 2, + 'ಥೆ': 2, + 'ದಿ': 2, + 'ದಿಂ': 3, + 'ದೆ': 2, + 'ದೌ': 2, + 'ದ್': 2, + 'ಧಿ': 2, + 'ಧೆ': 2, + 'ಧ್': 2, + 'ನಾಂ': 3, + 'ನಿ': 2, + 'ನಿಂ': 3, + 'ನೆ': 2, + 'ನೌ': 2, + 'ನ್': 2, + 'ಪಿ': 2, + 'ಪ್': 2, + 'ಬೆ': 2, + 'ಬ್': 2, + 'ಭಿ': 2, + 'ಭೆ': 2, + 'ಭೌ': 2, + 'ಮಿ': 2, + 'ಮೆ': 2, + 'ಮ್': 2, + 'ಯಿ': 2, + 'ಯಿಂ': 3, + 'ಯುಂ': 3, + 'ಯೆ': 2, + 'ಯೆಂ': 3, + 'ಯ್': 2, + 'ರಾಂ': 3, + 'ರಿ': 2, + 'ರಿಂ': 3, + 'ರೆ': 2, + 'ರೆಂ': 3, + 'ರ್': 2, + 'ಲಿ': 2, + 'ಲೆ': 2, + 'ಲೆಂ': 3, + 'ಲ್': 2, + 'ಳಿ': 2, + 'ಳಿಂ': 3, + 'ಳೆ': 2, + 'ಳ್': 2, + 'ವಿ': 2, + 'ವುಂ': 3, + 'ವೆ': 2, + 'ವೆಂ': 3, + 'ವ್': 2, + 'ಶಾಂ': 3, + 'ಶಿ': 2, + 'ಶ್': 2, + 'ಷಿ': 2, + 'ಷೆ': 2, + 'ಷ್': 2, + 'ಸಾಂ': 3, + 'ಸಿ': 2, + 'ಸೆ': 2, + 'ಸ್': 2, + 'ಹಿ': 2, + 'ಹೆ': 2, + 'ಹೊಂ': 3, + 'കു': 2, + 'കൂ': 2, + 'കൃ': 2, + 'ക്ക': 3, + 'ക്കാ': 4, + 'ക്കി': 4, + 'ക്കു': 4, + 'ക്കും': 5, + 'ക്കെ': 4, + 'ക്കേ': 4, + 'ക്കൊ': 4, + 'ക്കോ': 4, + 'ക്ക്‌': 4, + 'ക്തി': 4, + 'ക്യ': 3, + 'ക്യം': 4, + 'ക്ര': 3, + 'ക്രൂ': 4, + 'ക്ഷ': 3, + 'ക്ഷാ': 4, + 'ക്ഷി': 4, + 'ക്ഷേ': 4, + 'ഖ്യാ': 4, + 'ഗു': 2, + 'ഗ്ഗം': 4, + 'ഗ്ദാ': 4, + 'ഗ്യ': 3, + 'ഗ്യം': 4, + 'ഗ്ര': 3, + 'ങ്കി': 4, + 'ങ്കെ': 4, + 'ങ്കേ': 4, + 'ങ്ങ': 3, + 'ങ്ങി': 4, + 'ച്ച': 3, + 'ച്ചി': 4, + 'ച്ചു': 4, + 'ച്ചും': 5, + 'ച്ചെ': 4, + 'ച്ചേ': 4, + 'ജ്യ': 3, + 'ജ്യാ': 4, + 'ഞ്ച': 3, + 'ഞ്ഞ': 3, + 'ഞ്ഞി': 4, + 'ഞ്ഞെ': 4, + 'ടു': 2, + 'ടും': 3, + 'ട്ട': 3, + 'ട്ടാ': 4, + 'ട്ടി': 4, + 'ട്ടു': 4, + 'ട്ടോ': 4, + 'ട്‌': 2, + 'ണ്ട': 3, + 'ണ്ടാ': 4, + 'ണ്ടി': 4, + 'ണ്ടു': 4, + 'ണ്ടും': 5, + 'ണ്ടെ': 4, + 'ണ്ട്‌': 4, + 'ണ്ഡ': 3, + 'ണ്ണ': 3, + 'ണ്‌': 2, + 'തു': 2, + 'തും': 3, + 'തൃ': 2, + 'ത്ത': 3, + 'ത്താ': 4, + 'ത്തി': 4, + 'ത്തു': 4, + 'ത്തും': 5, + 'ത്തെ': 4, + 'ത്തേ': 4, + 'ത്തോ': 4, + 'ത്ത്‌': 4, + 'ത്ന': 3, + 'ത്നം': 4, + 'ത്നി': 4, + 'ത്യ': 3, + 'ത്യാ': 4, + 'ത്യേ': 4, + 'ത്ര': 3, + 'ത്വ': 3, + 'ത്വം': 4, + 'ത്‌': 2, + 'ദൃ': 2, + 'ദ്ദ': 3, + 'ദ്ധ': 3, + 'ദ്ധി': 4, + 'ദ്യ': 3, + 'ദ്യാ': 4, + 'ദ്ര': 3, + 'ധൂ': 2, + 'ധ്യ': 3, + 'നു': 2, + 'നും': 3, + 'ന്ത': 3, + 'ന്തം': 4, + 'ന്തെ': 4, + 'ന്ത്ര': 5, + 'ന്ത്ര്യ': 7, + 'ന്ത്ര്യം': 8, + 'ന്ഥ': 3, + 'ന്ദ': 3, + 'ന്ധ': 3, + 'ന്ധി': 4, + 'ന്ന': 3, + 'ന്നാ': 4, + 'ന്നി': 4, + 'ന്നീ': 4, + 'ന്നു': 4, + 'ന്നും': 5, + 'ന്നെ': 4, + 'ന്നേ': 4, + 'ന്നോ': 4, + 'ന്ന്': 4, + 'ന്മ': 3, + 'ന്മാ': 4, + 'ന്മു': 4, + 'ന്മൂ': 4, + 'ന്യ': 3, + 'ന്യം': 4, + 'ന്യാ': 4, + 'ന്യേ': 4, + 'ന്യോ': 4, + 'ന്റെ': 4, + 'ന്‌': 2, + 'ന്‍': 2, + 'ന്‍കൂ': 4, + 'ന്‍പി': 4, + 'പു': 2, + 'പൂ': 2, + 'പ്പ': 3, + 'പ്പി': 4, + 'പ്പു': 4, + 'പ്പെ': 4, + 'പ്പോ': 4, + 'പ്പ്‌': 4, + 'പ്ര': 3, + 'പ്രാ': 4, + 'ബു': 2, + 'ബ്ലി': 4, + 'ഭ്യാ': 4, + 'ഭ്രാ': 4, + 'മു': 2, + 'മൃ': 2, + 'മ്പ': 3, + 'മ്പാ': 4, + 'മ്പ്ര': 5, + 'മ്മ': 3, + 'മ്മി': 4, + 'യു': 2, + 'യും': 3, + 'യ്ക്കു': 6, + 'യ്തി': 4, + 'യ്തു': 4, + 'യ്മ': 3, + 'യ്യാ': 4, + 'യ്യി': 4, + 'രു': 2, + 'രും': 3, + 'രൂ': 2, + 'ര്യ': 3, + 'ര്യം': 4, + 'ര്‍ക്കു': 6, + 'ര്‍ക്കും': 7, + 'ര്‍ക്ക്‌': 6, + 'ര്‍ഗ്ഗം': 6, + 'ര്‍ച്ച': 5, + 'ര്‍ണ്ണ': 5, + 'ര്‍ത്ത': 5, + 'ര്‍ത്തി': 6, + 'ര്‍ത്തു': 6, + 'ര്‍ത്തേ': 6, + 'ര്‍ദേ': 4, + 'ര്‍ദ്ദ': 5, + 'ര്‍പ്പെ': 6, + 'ര്‍ബ': 3, + 'ര്‍ഭ': 3, + 'ര്‍മ്മി': 6, + 'ര്‍രാ': 4, + 'ര്‍വ്വ': 5, + 'ര്‍ഹ': 3, + 'ര്‍ഹി': 4, + 'റു': 2, + 'റ്റ': 3, + 'റ്റം': 4, + 'റ്റു': 4, + 'റ്റേ': 4, + 'ലു': 2, + 'ലും': 3, + 'ല്യ': 3, + 'ല്യാ': 4, + 'ല്ല': 3, + 'ല്ലാ': 4, + 'ല്ലാം': 5, + 'ല്ലെ': 4, + 'ല്‍': 2, + 'ല്‍കി': 4, + 'ല്‍കു': 4, + 'ല്‍കേ': 4, + 'ല്‍ത': 3, + 'ല്‍നി': 4, + 'ല്‍പ്പ': 5, + 'ളു': 2, + 'ളും': 3, + 'ള്ള': 3, + 'ള്ളാ': 4, + 'ള്ളി': 4, + 'ള്ളൂ': 4, + 'ള്‍': 2, + 'ള്‍കൊ': 4, + 'ള്‍ക്ക': 5, + 'ള്‍ക്കി': 6, + 'ള്‍ക്കു': 6, + 'ള്‍ക്കും': 7, + 'ള്‍ക്കെ': 6, + 'ള്‍ക്ക്‌': 6, + 'ഴു': 2, + 'വു': 2, + 'വും': 3, + 'വൃ': 2, + 'വ്യ': 3, + 'വ്യം': 4, + 'വ്യാ': 4, + 'വ്വ': 3, + 'ശു': 2, + 'ശ്യ': 3, + 'ശ്ര': 3, + 'ശ്വാ': 4, + 'ഷ്ട': 3, + 'ഷ്ട്യാ': 6, + 'ഷ്ട്ര': 5, + 'ഷ്ട്രീ': 6, + 'ഷ്ണു': 4, + 'ഷ്യ': 3, + 'ഷ്യാ': 4, + 'സാം': 3, + 'സു': 2, + 'സൃ': 2, + 'സ്കാ': 4, + 'സ്തു': 4, + 'സ്ത്രം': 6, + 'സ്ത്രീ': 6, + 'സ്ഥ': 3, + 'സ്ഥാ': 4, + 'സ്ഥി': 4, + 'സ്യ': 3, + 'സ്റ്റ്‌': 6, + 'സ്വ': 3, + 'സ്വാ': 4, + 'സ്വീ': 4, + 'സ്സ': 3, + 'സ്സാ': 4, + 'സ്സി': 4, + 'സ്സും': 5, + 'സ്സോ': 4, + 'ഹിം': 3, + 'ഹു': 2, + 'ഹൃ': 2, + 'ൻ്': 2, + 'ක්': 2, + 'ක්‍': 2, + 'කි': 2, + 'කී': 2, + 'කු': 2, + 'කූ': 2, + 'ඛ්‍': 2, + 'ගි': 2, + 'ගී': 2, + 'ගු': 2, + 'ඟි': 2, + 'ඟී': 2, + 'ච්': 2, + 'චි': 2, + 'ජ්‍': 2, + 'ජි': 2, + 'ජී': 2, + 'ටි': 2, + 'ටී': 2, + 'ටු': 2, + 'ඩි': 2, + 'ඩු': 2, + 'ණ්': 2, + 'ණි': 2, + 'ණී': 2, + 'ණු': 2, + 'ඬු': 2, + 'ත්': 2, + 'ත්‍': 2, + 'ති': 2, + 'තී': 2, + 'තු': 2, + 'ථි': 2, + 'ද්': 2, + 'ද්‍': 2, + 'දි': 2, + 'දිං': 3, + 'දී': 2, + 'දු': 2, + 'ධ්‍': 2, + 'ධි': 2, + 'ධී': 2, + 'න්': 2, + 'න්‍': 2, + 'නි': 2, + 'නී': 2, + 'නු': 2, + 'ඳි': 2, + 'ඳී': 2, + 'ඳු': 2, + 'ප්': 2, + 'ප්‍': 2, + 'පි': 2, + 'පී': 2, + 'පු': 2, + 'පූ': 2, + 'බි': 2, + 'බී': 2, + 'භු': 2, + 'ම්': 2, + 'මි': 2, + 'මී': 2, + 'මු': 2, + 'මූ': 2, + 'ඹී': 2, + 'ඹු': 2, + 'යි': 2, + 'යු': 2, + 'ර්': 2, + 'රි': 2, + 'රී': 2, + 'රු': 2, + 'රූ': 2, + 'ල්': 2, + 'ලි': 2, + 'ලී': 2, + 'ලු': 2, + 'ව්': 2, + 'ව්‍': 2, + 'වි': 2, + 'වී': 2, + 'වු': 2, + 'වූ': 2, + 'ශ්': 2, + 'ශ්‍': 2, + 'ශී': 2, + 'ෂ්': 2, + 'ෂ්‍': 2, + 'ෂි': 2, + 'ස්': 2, + 'සි': 2, + 'සී': 2, + 'සු': 2, + 'හි': 2, + 'හිං': 3, + 'හී': 2, + 'හු': 2, + 'ළි': 2, + 'ළු': 2, + 'กั': 2, + 'กิ': 2, + 'กี': 2, + 'กุ': 2, + 'ก็': 2, + 'ก่': 2, + 'ก้': 2, + 'ขั': 2, + 'ขั้': 3, + 'ขี่': 3, + 'ขึ้': 3, + 'ขื': 2, + 'ข็': 2, + 'ข่': 2, + 'ข้': 2, + 'คั': 2, + 'คิ': 2, + 'คุ': 2, + 'คุ้': 3, + 'คู่': 3, + 'ค่': 2, + 'ค้': 2, + 'ค์': 2, + 'งิ': 2, + 'งื่': 3, + 'จั': 2, + 'จิ': 2, + 'จึ': 2, + 'จุ': 2, + 'จ็': 2, + 'จ่': 2, + 'จ้': 2, + 'ฉั': 2, + 'ชั้': 3, + 'ชิ': 2, + 'ชี': 2, + 'ชื่': 3, + 'ชื้': 3, + 'ชุ': 2, + 'ช่': 2, + 'ช้': 2, + 'ซี่': 3, + 'ซึ่': 3, + 'ญิ': 2, + 'ฎิ': 2, + 'ฏิ': 2, + 'ฒิ': 2, + 'ณิ': 2, + 'ณี': 2, + 'ณ์': 2, + 'ดั': 2, + 'ดั่': 3, + 'ดิ': 2, + 'ดิ์': 3, + 'ดี': 2, + 'ดู': 2, + 'ด็': 2, + 'ด้': 2, + 'ตั': 2, + 'ตั้': 3, + 'ติ': 2, + 'ตี': 2, + 'ตุ': 2, + 'ต็': 2, + 'ต่': 2, + 'ต่ำ': 3, + 'ต้': 2, + 'ถิ่': 3, + 'ถี': 2, + 'ถึ': 2, + 'ถื': 2, + 'ถื่': 3, + 'ถุ': 2, + 'ถู': 2, + 'ถ้': 2, + 'ทั': 2, + 'ทั่': 3, + 'ทั้': 3, + 'ทิ': 2, + 'ที': 2, + 'ที่': 3, + 'ทุ': 2, + 'ท่': 2, + 'ท้': 2, + 'ธั': 2, + 'ธิ': 2, + 'ธิ์': 3, + 'ธี': 2, + 'ธุ์': 3, + 'ธ์': 2, + 'นั': 2, + 'นั้': 3, + 'นิ': 2, + 'นี้': 3, + 'นึ': 2, + 'นึ่': 3, + 'นื': 2, + 'นื่': 3, + 'นุ': 2, + 'นุ่': 3, + 'นู': 2, + 'น่': 2, + 'น้': 2, + 'น์': 2, + 'บั': 2, + 'บั่': 3, + 'บิ': 2, + 'บี': 2, + 'บื้': 3, + 'บุ': 2, + 'บู': 2, + 'บ่': 2, + 'ปั': 2, + 'ปิ': 2, + 'ป็': 2, + 'ป่': 2, + 'ผิ': 2, + 'ผู้': 3, + 'ผ่': 2, + 'พั': 2, + 'พิ': 2, + 'พิ่': 3, + 'พี': 2, + 'พื่': 3, + 'พื้': 3, + 'พู': 2, + 'พ้': 2, + 'ภั': 2, + 'มั': 2, + 'มั่': 3, + 'มิ': 2, + 'มิ่': 3, + 'มี': 2, + 'มื': 2, + 'มุ่': 3, + 'มู': 2, + 'มู่': 3, + 'ม่': 2, + 'ม้': 2, + 'ยั': 2, + 'ยิ': 2, + 'ยิ่': 3, + 'ยี': 2, + 'ยึ': 2, + 'ยื': 2, + 'ยุ': 2, + 'ยู่': 3, + 'ย่': 2, + 'ย่ำ': 3, + 'ย้': 2, + 'ย์': 2, + 'รั': 2, + 'รั้': 3, + 'ริ': 2, + 'รี': 2, + 'รื': 2, + 'รื่': 3, + 'รุ': 2, + 'รู': 2, + 'ร็': 2, + 'ร่': 2, + 'ร้': 2, + 'ร์': 2, + 'ลั': 2, + 'ลิ': 2, + 'ลี่': 3, + 'ลี้': 3, + 'ลึ': 2, + 'ลื': 2, + 'ลื่': 3, + 'ลุ': 2, + 'ลุ่': 3, + 'ลู่': 3, + 'ล่': 2, + 'ล้': 2, + 'วั': 2, + 'วิ': 2, + 'วุ': 2, + 'ว่': 2, + 'ว้': 2, + 'ศั': 2, + 'ศิ': 2, + 'ศี': 2, + 'ศึ': 2, + 'ษ์': 2, + 'สั': 2, + 'สิ': 2, + 'สิ่': 3, + 'สิ้': 3, + 'สี': 2, + 'สื': 2, + 'สื่': 3, + 'สุ': 2, + 'สู': 2, + 'สู่': 3, + 'สู้': 3, + 'ส่': 2, + 'หั': 2, + 'ห็': 2, + 'ห่': 2, + 'ห้': 2, + 'อั': 2, + 'อิ': 2, + 'อี': 2, + 'อื่': 3, + 'อื้': 3, + 'อ้': 2, + 'ກັ': 2, + 'ກິ': 2, + 'ກີ': 2, + 'ກຸ': 2, + 'ກຸ່': 3, + 'ກົ': 2, + 'ກ່': 2, + 'ກ້': 2, + 'ກໍ': 2, + 'ກໍ່': 3, + 'ຂັ': 2, + 'ຂັ້': 3, + 'ຂີ່': 3, + 'ຂື້': 3, + 'ຂົ': 2, + 'ຂົ່': 3, + 'ຂົ້': 3, + 'ຂ່': 2, + 'ຂ້': 2, + 'ຂໍ': 2, + 'ຂໍ້': 3, + 'ຄັ': 2, + 'ຄິ': 2, + 'ຄື': 2, + 'ຄື່': 3, + 'ຄຸ': 2, + 'ຄຸ້': 3, + 'ຄູ່': 3, + 'ຄົ': 2, + 'ຄົ້': 3, + 'ຄ່': 2, + 'ຄ້': 2, + 'ງົ': 2, + 'ຈັ': 2, + 'ຈັ້': 3, + 'ຈິ': 2, + 'ຈື່': 3, + 'ຈຸ': 2, + 'ຈົ': 2, + 'ຈົ່': 3, + 'ຈົ້': 3, + 'ຈ່': 2, + 'ຈ້': 2, + 'ຊັ': 2, + 'ຊັ່': 3, + 'ຊັ້': 3, + 'ຊິ': 2, + 'ຊີ': 2, + 'ຊີ້': 3, + 'ຊື່': 3, + 'ຊື້': 3, + 'ຊຸ': 2, + 'ຊູ': 2, + 'ຊົ': 2, + 'ຊົ່': 3, + 'ຊ່': 2, + 'ຊ້': 2, + 'ຍິ': 2, + 'ຍີ່': 3, + 'ຍຸ': 2, + 'ຍົ': 2, + 'ຍ່': 2, + 'ຍ້': 2, + 'ດັ': 2, + 'ດັ່': 3, + 'ດິ': 2, + 'ດີ': 2, + 'ດົ': 2, + 'ດ້': 2, + 'ຕັ': 2, + 'ຕັ້': 3, + 'ຕິ': 2, + 'ຕີ': 2, + 'ຕົ': 2, + 'ຕົ້': 3, + 'ຕ່': 2, + 'ຕ້': 2, + 'ຕໍ່': 3, + 'ຖິ': 2, + 'ຖື': 2, + 'ຖື່': 3, + 'ຖຸ': 2, + 'ຖົ້': 3, + 'ຖ້': 2, + 'ທັ': 2, + 'ທິ': 2, + 'ທີ': 2, + 'ທີ່': 3, + 'ທຸ': 2, + 'ທົ່': 3, + 'ທ່': 2, + 'ທ້': 2, + 'ທໍ': 2, + 'ນັ': 2, + 'ນັ້': 3, + 'ນິ': 2, + 'ນີ': 2, + 'ນີ້': 3, + 'ນື': 2, + 'ນື່': 3, + 'ນື້': 3, + 'ນຸ': 2, + 'ນຸ່': 3, + 'ນູ': 2, + 'ນົ': 2, + 'ນ້': 2, + 'ບັ': 2, + 'ບີ': 2, + 'ບຸ': 2, + 'ບູ': 2, + 'ບົ': 2, + 'ບ່': 2, + 'ບໍ': 2, + 'ບໍ່': 3, + 'ປັ': 2, + 'ປີ': 2, + 'ປົ': 2, + 'ປ່': 2, + 'ປ້': 2, + 'ຜັ': 2, + 'ຜິ': 2, + 'ຜີ': 2, + 'ຜູ້': 3, + 'ຜົ': 2, + 'ຜ່': 2, + 'ພັ': 2, + 'ພິ': 2, + 'ພີ': 2, + 'ພີ່': 3, + 'ພຶ': 2, + 'ພື່': 3, + 'ພູ': 2, + 'ພົ': 2, + 'ພົ້': 3, + 'ພ້': 2, + 'ພໍ': 2, + 'ພໍ່': 3, + 'ຟັ': 2, + 'ຟ້': 2, + 'ມັ': 2, + 'ມິ': 2, + 'ມີ': 2, + 'ມື': 2, + 'ມື່': 3, + 'ມຸ້': 3, + 'ມູ': 2, + 'ມົ': 2, + 'ມ່': 2, + 'ຢັ': 2, + 'ຢື': 2, + 'ຢູ່': 3, + 'ຢູ້': 3, + 'ຢ່': 2, + 'ຢ້': 2, + 'ລັ': 2, + 'ລິ': 2, + 'ລີ': 2, + 'ລື': 2, + 'ລຸ': 2, + 'ລຸ້': 3, + 'ລູ': 2, + 'ລົ': 2, + 'ລ້': 2, + 'ວັ': 2, + 'ວັ່': 3, + 'ວິ': 2, + 'ວຸ': 2, + 'ວົ': 2, + 'ວົ້': 3, + 'ວ່': 2, + 'ວ້': 2, + 'ສັ': 2, + 'ສິ': 2, + 'ສີ່': 3, + 'ສີ້': 3, + 'ສຶ': 2, + 'ສື': 2, + 'ສຸ': 2, + 'ສູ້': 3, + 'ສົ': 2, + 'ສ່': 2, + 'ສ້': 2, + 'ຫັ': 2, + 'ຫຼ': 2, + 'ຫຼັ': 3, + 'ຫຼິ': 3, + 'ຫຼື': 3, + 'ຫຼົ': 3, + 'ຫຼົ່': 4, + 'ຫ່': 2, + 'ຫ້': 2, + 'ອັ': 2, + 'ອິ': 2, + 'ອີ': 2, + 'ອື່': 3, + 'ອຸ': 2, + 'ອົ': 2, + 'ອ່': 2, + 'ອ້': 2, + 'ຮັ': 2, + 'ຮີ': 2, + 'ຮູ້': 3, + 'ຮົ': 2, + 'ຮົ່': 3, + 'ຮ່': 2, + 'ຮ້': 2, + 'ໜັ': 2, + 'ໜີ່': 3, + 'ໜື່': 3, + 'ໜູ': 2, + 'ໜ່': 2, + 'ໜ້': 2, + 'ໝັ້': 3, + 'ໝີ': 2, + 'ໝູ່': 3, + 'ໝ່': 2, + 'ໝ້': 2, + 'ཀུ': 2, + 'ཀོ': 2, + 'ཀྱ': 2, + 'ཀྱི': 3, + 'ཀླ': 2, + 'ཀློ': 3, + 'ཁུ': 2, + 'ཁེ': 2, + 'ཁོ': 2, + 'ཁྱ': 2, + 'ཁྱི': 3, + 'ཁྲི': 3, + 'ཁྲོ': 3, + 'གི': 2, + 'གུ': 2, + 'གེ': 2, + 'གོ': 2, + 'གྱ': 2, + 'གྱི': 3, + 'གྱུ': 3, + 'གྱོ': 3, + 'གྲ': 2, + 'གྲི': 3, + 'གྲུ': 3, + 'གྲེ': 3, + 'གྲོ': 3, + 'གླ': 2, + 'གླི': 3, + 'གླེ': 3, + 'ངུ': 2, + 'ངེ': 2, + 'ངོ': 2, + 'ཅི': 2, + 'ཅུ': 2, + 'ཅོ': 2, + 'ཆུ': 2, + 'ཆེ': 2, + 'ཆོ': 2, + 'ཇི': 2, + 'ཇུ': 2, + 'ཇོ': 2, + 'ཉི': 2, + 'ཉེ': 2, + 'ཉོ': 2, + 'ཏུ': 2, + 'ཏེ': 2, + 'ཏོ': 2, + 'ཐི': 2, + 'ཐུ': 2, + 'ཐེ': 2, + 'ཐོ': 2, + 'དི': 2, + 'དུ': 2, + 'དེ': 2, + 'དོ': 2, + 'དྲ': 2, + 'དྲི': 3, + 'དྲུ': 3, + 'དྲོ': 3, + 'ནི': 2, + 'ནུ': 2, + 'ནོ': 2, + 'པེ': 2, + 'པོ': 2, + 'པྱ': 2, + 'པྱོ': 3, + 'ཕེ': 2, + 'ཕོ': 2, + 'ཕྱི': 3, + 'ཕྱེ': 3, + 'ཕྱོ': 3, + 'ཕྲུ': 3, + 'ཕྲོ': 3, + 'བུ': 2, + 'བེ': 2, + 'བོ': 2, + 'བྱ': 2, + 'བྱི': 3, + 'བྱུ': 3, + 'བྱེ': 3, + 'བྱོ': 3, + 'བྲ': 2, + 'བྲེ': 3, + 'བླ': 2, + 'བློ': 3, + 'མི': 2, + 'མེ': 2, + 'མོ': 2, + 'མྱོ': 3, + 'ཙུ': 2, + 'ཙོ': 2, + 'ཚུ': 2, + 'ཚེ': 2, + 'ཚོ': 2, + 'ཛི': 2, + 'ཛུ': 2, + 'ཛོ': 2, + 'ཞི': 2, + 'ཞུ': 2, + 'ཟི': 2, + 'ཟུ': 2, + 'ཟེ': 2, + 'ཟོ': 2, + 'ཟླ': 2, + 'ཟློ': 3, + 'འི': 2, + 'འུ': 2, + 'འོ': 2, + 'ཡི': 2, + 'ཡུ': 2, + 'ཡོ': 2, + 'རི': 2, + 'རུ': 2, + 'རེ': 2, + 'རོ': 2, + 'རྐྱ': 3, + 'རྐྱེ': 4, + 'རྒ': 2, + 'རྒོ': 3, + 'རྒྱ': 3, + 'རྒྱུ': 4, + 'རྗེ': 3, + 'རྗོ': 3, + 'རྟ': 2, + 'རྟེ': 3, + 'རྟོ': 3, + 'རྡ': 2, + 'རྣ': 2, + 'རྩ': 2, + 'རྩི': 3, + 'རྩུ': 3, + 'རྩོ': 3, + 'རྫ': 2, + 'ལཱ': 2, + 'ལུ': 2, + 'ལེ': 2, + 'ལོ': 2, + 'ལྔ': 2, + 'ལྕི': 3, + 'ལྕོ': 3, + 'ལྟ': 2, + 'ལྟེ': 3, + 'ལྡ': 2, + 'ལྡེ': 3, + 'ལྷ': 2, + 'ལྷུ': 3, + 'ལྷོ': 3, + 'ཤི': 2, + 'ཤུ': 2, + 'ཤེ': 2, + 'ཤོ': 2, + 'སུ': 2, + 'སེ': 2, + 'སོ': 2, + 'སྐ': 2, + 'སྐི': 3, + 'སྐུ': 3, + 'སྐོ': 3, + 'སྐྱ': 3, + 'སྐྱི': 4, + 'སྐྱུ': 4, + 'སྐྱེ': 4, + 'སྐྱོ': 4, + 'སྒེ': 3, + 'སྒོ': 3, + 'སྒྱུ': 4, + 'སྒྲ': 3, + 'སྒྲི': 4, + 'སྒྲུ': 4, + 'སྔ': 2, + 'སྔོ': 3, + 'སྙི': 3, + 'སྟ': 2, + 'སྟུ': 3, + 'སྟེ': 3, + 'སྟོ': 3, + 'སྡ': 2, + 'སྡི': 3, + 'སྡུ': 3, + 'སྡེ': 3, + 'སྡོ': 3, + 'སྣ': 2, + 'སྣོ': 3, + 'སྤུ': 3, + 'སྤེ': 3, + 'སྤྱ': 3, + 'སྤྱི': 4, + 'སྤྱོ': 4, + 'སྤྲ': 3, + 'སྤྲོ': 4, + 'སྦེ': 3, + 'སྦྱ': 3, + 'སྦྱི': 4, + 'སྦྱོ': 4, + 'སྦྲེ': 4, + 'སྨ': 2, + 'སྨྲ': 3, + 'སྲ': 2, + 'སྲི': 3, + 'སྲུ': 3, + 'སྲོ': 3, + 'སླ': 2, + 'སློ': 3, + 'ཧྲི': 3, + 'ཧྲེ': 3, + 'ཨི': 2, + 'ཨུ': 2, + 'ကိ': 2, + 'ကို': 3, + 'ကီု': 3, + 'ကု': 2, + 'ကူ': 2, + 'ကေု': 3, + 'ကဲ့': 3, + 'ကဵု': 3, + 'ကံ': 2, + 'က္က': 3, + 'က္ခ': 3, + 'က္ဍ': 3, + 'က္ဍို': 5, + 'က်': 2, + 'က်ျ': 3, + 'ကျို': 4, + 'ကျူ': 3, + 'ကျေ': 3, + 'ကျွ': 3, + 'ကြို': 4, + 'ကြီ': 3, + 'ကြု': 3, + 'ကြေ': 3, + 'ကြံ': 3, + 'ကြွေ': 4, + 'ကွ': 2, + 'ကၟ': 2, + 'ကၟိ': 3, + 'ကၟို': 4, + 'ကၟဲ': 3, + 'ကၠ': 2, + 'ကၠို': 4, + 'ကၠီု': 4, + 'ကၠု': 3, + 'ကၠေ': 3, + 'ကၠေံ': 4, + 'ကၠဳ': 3, + 'ခိ': 2, + 'ခို': 3, + 'ခု': 2, + 'ခဲ့': 3, + 'ခဳ': 2, + 'ခံ': 2, + 'ချိ': 3, + 'ချို': 4, + 'ချို့': 5, + 'ချီ': 3, + 'ချု': 3, + 'ချေ': 3, + 'ချဳ': 3, + 'ခြုံ': 4, + 'ခြေ': 3, + 'ခွ': 2, + 'ခွဲ': 3, + 'ခၞံ': 3, + 'ခၠ': 2, + 'ဂိ': 2, + 'ဂို': 3, + 'ဂီု': 3, + 'ဂု': 2, + 'ဂဵု': 3, + 'ဂ္ဂ': 3, + 'ဂ္ဂို': 5, + 'ဂျို': 4, + 'ဂွံ': 3, + 'ဂှ်': 3, + 'ဂၞ': 2, + 'ဂၞဴ': 3, + 'ဂၟံ': 3, + 'ဂၠ': 2, + 'ဂၠိ': 3, + 'ဂၠို': 4, + 'ဂၠေ': 3, + 'ဂၠံ': 3, + 'ငိ': 2, + 'ငို': 3, + 'ငဲ့': 3, + 'ငံ': 2, + 'င့်': 3, + 'င်': 2, + 'င်္ဂ': 4, + 'ငြိ': 3, + 'ငွေ': 3, + 'ငှ': 2, + 'စိ': 2, + 'စို': 3, + 'စီ': 2, + 'စု': 2, + 'စုံ': 3, + 'စဳ': 2, + 'စဴ': 2, + 'စဵု': 3, + 'စံ': 2, + 'စ္စ': 3, + 'စ်': 2, + 'စွ': 2, + 'စွဲ': 3, + 'စွံ': 3, + 'စှ်': 3, + 'စှ်ေ': 4, + 'စၞ': 2, + 'စၞေ': 3, + 'စၟ': 2, + 'စၠေ': 3, + 'ဆို': 3, + 'ဆီ': 2, + 'ဆုဲ': 3, + 'ဆုံ': 3, + 'ဆံ': 2, + 'ဆွဲ': 3, + 'ဇို': 3, + 'ဇူ': 2, + 'ဇြေ': 3, + 'ဇြဳ': 3, + 'ဇၞ': 2, + 'ဇၞေ': 3, + 'ဇၟ': 2, + 'ဉ်': 2, + 'ညိ': 2, + 'ညီ': 2, + 'ညဳ': 2, + 'ညံ': 2, + 'ည်': 2, + 'ည့်': 3, + 'ညှ': 2, + 'ဋိ': 2, + 'ဋ္ဌ': 3, + 'ဍိ': 2, + 'ဍို': 3, + 'ဍု': 2, + 'ဍဵု': 3, + 'ဏီ': 2, + 'ဏံ': 2, + 'ဏ်': 2, + 'ဏှ': 2, + 'တိ': 2, + 'တို': 3, + 'တို့': 4, + 'တိူ': 3, + 'တီ': 2, + 'တု': 2, + 'တုဲ': 3, + 'တူ': 2, + 'တေံ': 3, + 'တဲ': 2, + 'တဴ': 2, + 'တဵ': 2, + 'တံ': 2, + 'တံ့': 3, + 'တ္တ': 3, + 'တ္ၚဲ': 4, + 'တ်': 2, + 'တ်ို': 4, + 'တြုံ': 4, + 'တွ': 2, + 'တွေ': 3, + 'တွဵု': 4, + 'တှ်ေ': 4, + 'တၞ': 2, + 'တၟ': 2, + 'တၟိ': 3, + 'တၟို': 4, + 'တၟေ': 3, + 'တၟံ': 3, + 'တၠု': 3, + 'တႂ်': 3, + 'တႅ': 2, + 'ထိ': 2, + 'ထို': 3, + 'ထို့': 4, + 'ထီ': 2, + 'ထု': 2, + 'ထုဲ': 3, + 'ထူ': 2, + 'ထဵ': 2, + 'ထံ': 2, + 'ထ္ၜ': 3, + 'ထွ': 2, + 'ထွေ': 3, + 'ဒို': 3, + 'ဒီ': 2, + 'ဒီု': 3, + 'ဒု': 2, + 'ဒေံ': 3, + 'ဒဳ': 2, + 'ဒဵု': 3, + 'ဒ္ဂေ': 4, + 'ဒ်': 2, + 'ဒှ်': 3, + 'ဒၞ': 2, + 'ဒၟံ': 3, + 'ဓိ': 2, + 'ဓု': 2, + 'နိ': 2, + 'နို': 3, + 'နု': 2, + 'နူ': 2, + 'နေ့': 3, + 'န္ဒ': 3, + 'န္နိ': 4, + 'န်': 2, + 'န့်': 3, + 'နွ': 2, + 'နွံ': 3, + 'နှ': 2, + 'နှိ': 3, + 'နှီ': 3, + 'နှေ': 3, + 'ပိ': 2, + 'ပို': 3, + 'ပိုဲ': 4, + 'ပိူ': 3, + 'ပီ': 2, + 'ပု': 2, + 'ပုံ': 3, + 'ပူ': 2, + 'ပေဲ': 3, + 'ပဲ': 2, + 'ပဵ': 2, + 'ပံ': 2, + 'ပံ့': 3, + 'ပ္ဍဲ': 4, + 'ပ္တံ': 4, + 'ပ္ပ': 3, + 'ပ္ပံ': 4, + 'ပ်': 2, + 'ပျေ': 3, + 'ပျဲ': 3, + 'ပြီ': 3, + 'ပြု': 3, + 'ပြေ': 3, + 'ပြဳ': 3, + 'ပြံ': 3, + 'ပွ': 2, + 'ပွဲ': 3, + 'ပွႆ': 3, + 'ပၟိ': 3, + 'ပၠ': 2, + 'ပႂ်': 3, + 'ပႅ': 2, + 'ပႆ': 2, + 'ဖိ': 2, + 'ဖီ': 2, + 'ဖဵု': 3, + 'ဖံ': 2, + 'ဖျု': 3, + 'ဖျေံ': 4, + 'ဖြို': 4, + 'ဖွ': 2, + 'ဖွဲ့': 4, + 'ဖွံ့': 4, + 'ဗီု': 3, + 'ဗု': 2, + 'ဗ္တေ': 4, + 'ဗ္ၜေ': 4, + 'ဗျဵု': 4, + 'ဗြေ': 3, + 'ဗြဴ': 3, + 'ဗွို': 4, + 'ဗွဲ': 3, + 'ဗၠ': 2, + 'ဗၠေ': 3, + 'ဘူ': 2, + 'ဘဲ': 2, + 'ဘဴ': 2, + 'မိ': 2, + 'မို': 3, + 'မိူ': 3, + 'မီ': 2, + 'မု': 2, + 'မူ': 2, + 'မဲ': 2, + 'မဲ့': 3, + 'မဳ': 2, + 'မဵု': 3, + 'မံ': 2, + 'မ္ဂ': 3, + 'မ္တီ': 4, + 'မ္ဘ': 3, + 'မ္မ': 3, + 'မ္ၚဵု': 5, + 'မ်': 2, + 'မ့်': 3, + 'မျို': 4, + 'မျီု': 4, + 'မျှ': 3, + 'မြေ': 3, + 'မြှ': 3, + 'မွ': 2, + 'မွေ': 3, + 'မွေ့': 4, + 'မွဲ': 3, + 'မှ': 2, + 'မှီ': 3, + 'မှု': 3, + 'မှေ': 3, + 'မၞိ': 3, + 'မၞု': 3, + 'မၞုံ': 4, + 'မၠို': 4, + 'မၠေ': 3, + 'မႂ်': 3, + 'မႅ': 2, + 'မႆ': 2, + 'ယိ': 2, + 'ယို': 3, + 'ယိူ': 3, + 'ယု': 2, + 'ယုံ': 3, + 'ယူ': 2, + 'ယဲ': 2, + 'ယဵ': 2, + 'ယံ': 2, + 'ယ်': 2, + 'ယျဵု': 4, + 'ယြဴ': 3, + 'ယွ': 2, + 'ယှ': 2, + 'ယႂ်': 3, + 'ယႅ': 2, + 'ရို': 3, + 'ရီု': 3, + 'ရု': 2, + 'ရုဲ': 3, + 'ရုံ': 3, + 'ရူ': 2, + 'ရေံ': 3, + 'ရဲ': 2, + 'ရဴ': 2, + 'ရံ': 2, + 'ရ်': 2, + 'ရွ': 2, + 'ရွေ': 3, + 'ရွံ့': 4, + 'ရွှေ့': 5, + 'ရှ': 2, + 'ရှိ': 3, + 'ရှေ': 3, + 'ရှေ့': 4, + 'ရှ်ေ': 4, + 'လိ': 2, + 'လို': 3, + 'လို့': 4, + 'လိူ': 3, + 'လီ': 2, + 'လီု': 3, + 'လု': 2, + 'လုံ': 3, + 'လူ': 2, + 'လူ့': 3, + 'လဲ': 2, + 'လဵ': 2, + 'လဵု': 3, + 'လံ': 2, + 'လ္ပ': 3, + 'လ္ၚ': 3, + 'လ်': 2, + 'လျို': 4, + 'လျေ': 3, + 'လျှ': 3, + 'လျှို့': 6, + 'လျှေ': 4, + 'လွ': 2, + 'လွဳ': 3, + 'လွှ': 3, + 'လွှဲ': 4, + 'လှ': 2, + 'လှုံ': 4, + 'လၟေ': 3, + 'လႂ်': 3, + 'လႅ': 2, + 'လႆ': 2, + 'ဝူ': 2, + 'ဝဲ': 2, + 'ဝဳ': 2, + 'ဝဵု': 3, + 'ဝ်': 2, + 'ဝွံ': 3, + 'ဝှ': 2, + 'ဝႆ': 2, + 'သိ': 2, + 'သို': 3, + 'သို့': 4, + 'သိူ': 3, + 'သီ': 2, + 'သီု': 3, + 'သု': 2, + 'သုံ': 3, + 'သူ': 2, + 'သူ့': 3, + 'သဳ': 2, + 'သဵ': 2, + 'သဵု': 3, + 'သ္က': 3, + 'သ္ကံ': 4, + 'သ္ဂု': 4, + 'သ္ဂေ': 4, + 'သ္ဇို': 5, + 'သ္ဒ': 3, + 'သ္ပ': 3, + 'သ္ၚိ': 4, + 'သ်': 2, + 'သြို': 4, + 'သွ': 2, + 'သွံ': 3, + 'သှ်': 3, + 'သှ်ေ': 4, + 'သၞ': 2, + 'သၞေ': 3, + 'သၟ': 2, + 'သၠ': 2, + 'သၠု': 3, + 'သႂ်': 3, + 'သႅ': 2, + 'ဟို': 3, + 'ဟီု': 3, + 'ဟု': 2, + 'ဟူ': 2, + 'ဟ်': 2, + 'ဟွံ': 3, + 'အိ': 2, + 'အို': 3, + 'အု': 2, + 'အံ': 2, + 'အ်': 2, + 'ဢိ': 2, + 'ဢို': 3, + 'ဢွ': 2, + 'ဿီု': 3, + 'ၚု': 2, + 'ၚ်': 2, + 'ၜို': 3, + 'ၵိ': 2, + 'ၵို': 3, + 'ၵိူ': 3, + 'ၵု': 2, + 'ၵူ': 2, + 'ၵဵ': 2, + 'ၵ်': 2, + 'ၵွ': 2, + 'ၵႂ': 2, + 'ၵႅ': 2, + 'ၶိ': 2, + 'ၶို': 3, + 'ၶိူ': 3, + 'ၶီ': 2, + 'ၶု': 2, + 'ၶူ': 2, + 'ၶွ': 2, + 'ၶႂ': 2, + 'ၶႅ': 2, + 'ၶႆ': 2, + 'ၸိ': 2, + 'ၸို': 3, + 'ၸိူ': 3, + 'ၸု': 2, + 'ၸူ': 2, + 'ၸဵ': 2, + 'ၸွ': 2, + 'ၸွႆ': 3, + 'ၸႂ်': 3, + 'ၸႅ': 2, + 'ၺ်': 2, + 'ၺွ': 2, + 'ၼို': 3, + 'ၼိူ': 3, + 'ၼု': 2, + 'ၼဵ': 2, + 'ၼ်': 2, + 'ၼွ': 2, + 'ၼႂ်': 3, + 'ၼႆ': 2, + 'ၽိ': 2, + 'ၽို': 3, + 'ၽု': 2, + 'ၽူ': 2, + 'ၽဵ': 2, + 'ၽွ': 2, + 'ၽႂ်': 3, + 'ၾိ': 2, + 'ႁို': 3, + 'ႁိူ': 3, + 'ႁု': 2, + 'ႁူ': 2, + 'ႁဵ': 2, + 'ႁွ': 2, + 'ႁႂ်': 3, + 'ႁႅ': 2, + 'ᜄ᜔': 2, + 'ᜆᜒ': 2, + 'ᜆ᜔': 2, + 'ᜇᜒ': 2, + 'ᜇᜓ': 2, + 'ᜇ᜔': 2, + 'ᜈᜒ': 2, + 'ᜈ᜔': 2, + 'ᜉᜒ': 2, + 'ᜊᜓ': 2, + 'ᜋ᜔': 2, + 'ᜌ᜔': 2, + 'ᜎᜓ': 2, + 'ᜏᜒ': 2, + 'ᜐᜒ': 2, + 'ᜑᜒ': 2, + 'កិ': 2, + 'កុ': 2, + 'កុំ': 3, + 'កំ': 2, + 'ក់': 2, + 'ក៏': 2, + 'ក្ខ': 3, + 'ក្ដិ': 4, + 'ក្ដី': 4, + 'ក្ដៅ': 4, + 'ក្នុ': 4, + 'ក្រ': 3, + 'ក្រុ': 4, + 'ក្រោ': 4, + 'ក្ស': 3, + 'ក្សា': 4, + 'ខិ': 2, + 'ខុ': 2, + 'ខំ': 2, + 'ខ្ជា': 4, + 'ខ្ព': 3, + 'ខ្លា': 4, + 'ខ្លួ': 4, + 'គាំ': 3, + 'គិ': 2, + 'គឺ': 2, + 'គំ': 2, + 'គ៌': 2, + 'គ្គ': 3, + 'គ្នា': 4, + 'គ្មា': 4, + 'គ្រ': 3, + 'គ្រា': 4, + 'គ្រឹះ': 5, + 'គ្រួ': 4, + 'ឃុំ': 3, + 'ឃ្ន': 3, + 'ងឺ': 2, + 'ង់': 2, + 'ង្ក': 3, + 'ង្កើ': 4, + 'ង្ខិ': 4, + 'ង្ខំ': 4, + 'ង្គ': 3, + 'ង្រី': 4, + 'ង្រឹ': 4, + 'ង្រៀ': 4, + 'ង្ហា': 4, + 'ង្អ': 3, + 'ចាំ': 3, + 'ចិ': 2, + 'ចុ': 2, + 'ចូ': 2, + 'ចំ': 2, + 'ច់': 2, + 'ច្ច': 3, + 'ច្ចេ': 4, + 'ច្បា': 4, + 'ឆាំ': 3, + 'ឆ្នាំ': 5, + 'ឆ្នោ': 4, + 'ឆ្ល': 3, + 'ឆ្លើ': 4, + 'ជិ': 2, + 'ជី': 2, + 'ជុំ': 3, + 'ជំ': 2, + 'ជ្ជ': 3, + 'ជ្ជា': 4, + 'ជ្ញា': 4, + 'ជ្យ': 3, + 'ជ្រ': 3, + 'ជ្រើ': 4, + 'ជ្រៀ': 4, + 'ជ្រែ': 4, + 'ឈ្មោះ': 5, + 'ញុះ': 3, + 'ញ្ចូ': 4, + 'ញ្ជា': 4, + 'ញ្ញ': 3, + 'ញ្ញា': 4, + 'ញ្ញៈ': 4, + 'ដី': 2, + 'ដឹ': 2, + 'ដូ': 2, + 'ដំ': 2, + 'ដ៏': 2, + 'ដ្ឋ': 3, + 'ដ្ឋា': 4, + 'ឌ្ឍ': 3, + 'ណិ': 2, + 'ណី': 2, + 'ណោះ': 3, + 'ណ៌': 2, + 'ណ៍': 2, + 'ណ្ដា': 4, + 'ណ្ដុះ': 5, + 'ណ្ឌ': 3, + 'ណ្ណោះ': 5, + 'តិ': 2, + 'តី': 2, + 'តុ': 2, + 'តំ': 2, + 'ត់': 2, + 'ត់់': 3, + 'ត៌': 2, + 'ត្ដ': 3, + 'ត្ដិ': 4, + 'ត្ថ': 3, + 'ត្ថិ': 4, + 'ត្យ': 3, + 'ត្រ': 3, + 'ត្រា': 4, + 'ត្រឹ': 4, + 'ត្រូ': 4, + 'ត្ស': 3, + 'ថ្ងៃ': 4, + 'ថ្នា': 4, + 'ថ្នូ': 4, + 'ថ្មី': 4, + 'ថ្លៃ': 4, + 'ទាំ': 3, + 'ទិ': 2, + 'ទិ៍': 3, + 'ទី': 2, + 'ទឹ': 2, + 'ទុ': 2, + 'ទូ': 2, + 'ទួ': 2, + 'ទោះ': 3, + 'ទំ': 2, + 'ទ្ធ': 3, + 'ទ្ធិ': 4, + 'ទ្យា': 4, + 'ទ្រ': 3, + 'ធិ': 2, + 'ធី': 2, + 'ធ្ង': 3, + 'ធ្នា': 4, + 'ធ្នូ': 4, + 'ធ្យា': 4, + 'ធ្យោ': 4, + 'ធ្វើ': 4, + 'នាំ': 3, + 'និ': 2, + 'នី': 2, + 'នឹ': 2, + 'នុ': 2, + 'នូ': 2, + 'នួ': 2, + 'នេះ': 3, + 'នោះ': 3, + 'ន់': 2, + 'ន៍': 2, + 'ន្ដ': 3, + 'ន្ដិ': 4, + 'ន្ថែ': 4, + 'ន្ថោ': 4, + 'ន្ទា': 4, + 'ន្ទៈ': 4, + 'ន្ធ': 3, + 'ន្និ': 4, + 'ន្ម': 3, + 'ន្យា': 4, + 'ន្លែ': 4, + 'បិ': 2, + 'បី': 2, + 'បុ': 2, + 'បូ': 2, + 'បោះ': 3, + 'បំ': 2, + 'ប៉ុ': 3, + 'ប៉ះ': 3, + 'ប់': 2, + 'ប្ដី': 4, + 'ប្ដូ': 4, + 'ប្ដេ': 4, + 'ប្ប': 3, + 'ប្បី': 4, + 'ប្រ': 3, + 'ប្រា': 4, + 'ប្រឹ': 4, + 'ប្រុ': 4, + 'ប្រើ': 4, + 'ប្រែ': 4, + 'ប្អូ': 4, + 'ផុ': 2, + 'ផ្ដ': 3, + 'ផ្ដា': 4, + 'ផ្ទា': 4, + 'ផ្ទុ': 4, + 'ផ្ទេ': 4, + 'ផ្នែ': 4, + 'ផ្លា': 4, + 'ផ្លិ': 4, + 'ផ្ស': 3, + 'ផ្សា': 4, + 'ផ្សេ': 4, + 'ផ្សៃ': 4, + 'ពិ': 2, + 'ពី': 2, + 'ពុ': 2, + 'ពូ': 2, + 'ពោះ': 3, + 'ព័': 2, + 'ព្យ': 3, + 'ព្យា': 4, + 'ព្រ': 3, + 'ព្រឹ': 4, + 'ព្រៀ': 4, + 'ព្រៃ': 4, + 'ព្រំ': 4, + 'ព្វ': 3, + 'ភិ': 2, + 'ភ័': 2, + 'មិ': 2, + 'មុ': 2, + 'មូ': 2, + 'មួ': 2, + 'ម៉ា': 3, + 'ម៉ោ': 3, + 'ម៌': 2, + 'ម័': 2, + 'ម្ងា': 4, + 'ម្ចា': 4, + 'ម្ដា': 4, + 'ម្ដែ': 4, + 'ម្នា': 4, + 'ម្នេ': 4, + 'ម្ប': 3, + 'ម្បី': 4, + 'ម្បុ': 4, + 'ម្ភា': 4, + 'ម្ម': 3, + 'ម្រ': 3, + 'ម្រា': 4, + 'ម្រិ': 4, + 'ម្រូ': 4, + 'ម្រើ': 4, + 'ម្រេ': 4, + 'ម្លៀ': 4, + 'ម្លៃ': 4, + 'យុ': 2, + 'យ៉ា': 3, + 'យ្យ': 3, + 'រាំ': 3, + 'រិ': 2, + 'រី': 2, + 'រុ': 2, + 'រូ': 2, + 'រួ': 2, + 'រំ': 2, + 'រ្ដិ៍': 5, + 'លិ': 2, + 'លុះ': 3, + 'លូ': 2, + 'លំ': 2, + 'ល់': 2, + 'ល្បៈ': 4, + 'ល្មើ': 4, + 'វិ': 2, + 'សិ': 2, + 'សី': 2, + 'សុ': 2, + 'សុំ': 3, + 'សំ': 2, + 'ស់': 2, + 'ស័': 2, + 'ស្គា': 4, + 'ស្ដី': 4, + 'ស្ដ្រី': 6, + 'ស្ត្រ': 5, + 'ស្ថា': 4, + 'ស្ថិ': 4, + 'ស្ទួ': 4, + 'ស្នា': 4, + 'ស្មា': 4, + 'ស្មើ': 4, + 'ស្រ': 3, + 'ស្រា': 4, + 'ស្រី': 4, + 'ស្រ័': 4, + 'ស្លា': 4, + 'ស្វែ': 4, + 'ស្វ័': 4, + 'ស្ស': 3, + 'ហូ': 2, + 'ហ៍': 2, + 'ហ្ម': 3, + 'ឡុ': 2, + 'អំ': 2, + 'អ្ន': 3, + 'អ្វី': 4, + 'ឱ្យ': 3, + 'ᨠ᩠ᨠ': 3, + 'ᨠ᩠ᨦᩢᩢ': 5, + 'ᨠ᩠ᨦᩨ᩵': 5, + 'ᨠ᩠ᨭᩫ': 4, + 'ᨠ᩠ᨯᩢ': 4, + 'ᨠ᩠ᨶᩢ': 4, + 'ᨠ᩠ᨶᩥ': 4, + 'ᨠ᩠ᩅ': 3, + 'ᨠ᩠ᩅᩢ': 4, + 'ᨠ᩠ᩅᩫ': 4, + 'ᨠ᩠ᩋ᩵': 4, + 'ᨠᩢ': 2, + 'ᨠᩢ᩠ᨷ': 4, + 'ᨠᩥ': 2, + 'ᨠᩨ': 2, + 'ᨠᩩ': 2, + 'ᨠᩮ᩠': 3, + 'ᨠᩮᩢ᩵': 4, + 'ᨠᩮ᩵': 3, + 'ᨠᩯ᩠': 3, + 'ᨠᩯ᩵': 3, + 'ᨠᩳ᩵': 3, + 'ᨠ᩵': 2, + 'ᨠ᩵ᩴ': 3, + 'ᨡ᩠ᨦᩢ': 4, + 'ᨡ᩠ᨯᩢ': 4, + 'ᨡ᩠ᨶ': 3, + 'ᨡ᩠ᨶᩢ': 4, + 'ᨡ᩠ᨶᩨ᩶': 5, + 'ᨡ᩠ᩅ': 3, + 'ᨡ᩠ᩋ': 3, + 'ᨡᩢ': 2, + 'ᨡᩮ᩠': 3, + 'ᨡᩮᩢ': 3, + 'ᨡᩮᩢᩢ': 4, + 'ᨡᩯ᩠': 3, + 'ᨡᩱᩢ': 3, + 'ᨡᩳ᩶': 3, + 'ᨡ᩵': 2, + 'ᨣᩕ᩠': 3, + 'ᨣᩕᩮ᩠': 4, + 'ᨣ᩠ᨣ': 3, + 'ᨣ᩠ᨯᩧ': 4, + 'ᨣ᩠ᨶᩢ': 4, + 'ᨣ᩠ᨾᩫ': 4, + 'ᨣ᩠ᩅ': 3, + 'ᨣ᩠ᩋᩢ': 4, + 'ᨣᩢ': 2, + 'ᨣᩢ᩠ᨷ': 4, + 'ᨣᩢᩴ': 3, + 'ᨣᩩ': 2, + 'ᨣᩩᩢ': 3, + 'ᨣᩮᩢᩢ': 4, + 'ᨣᩯ᩠': 3, + 'ᨣᩳ᩶': 3, + 'ᨣᩴ': 2, + 'ᨤ᩠ᨯᩥ': 4, + 'ᨤ᩠ᨶᩨ': 4, + 'ᨤ᩠ᨶᩫ': 4, + 'ᨤ᩠ᩅᩫ': 4, + 'ᨤ᩠ᩋ': 3, + 'ᨤᩢ᩠ᨬ': 4, + 'ᨤᩮᩢ': 3, + 'ᨤᩱ᩵': 3, + 'ᨥᩢ': 2, + 'ᨦᩢ': 2, + 'ᨦᩢᩢ': 3, + 'ᨦᩨ': 2, + 'ᨦᩨ᩵': 3, + 'ᨦᩨ᩶': 3, + 'ᨦ᩵': 2, + 'ᨧ᩠ᨦᩥ᩵': 5, + 'ᨧ᩠ᨯᩢ': 4, + 'ᨧᩢ': 2, + 'ᨧᩮ᩠': 3, + 'ᨧᩮᩢᩢ': 4, + 'ᨧᩱ᩠': 3, + 'ᨧᩴ': 2, + 'ᨧ᩵ᩴ': 3, + 'ᨨ᩠ᨶᩢ': 4, + 'ᨩ᩠ᨶᩢᩢ': 5, + 'ᨩ᩠ᨶ᩼': 4, + 'ᨩᩢ': 2, + 'ᨩᩦ': 2, + 'ᨩᩨ᩵': 3, + 'ᨩᩩ': 2, + 'ᨩᩭ᩵': 3, + 'ᨩᩮ᩠': 3, + 'ᨩᩱᩢ': 3, + 'ᨩ᩵ᩭ': 3, + 'ᨪ᩠ᨦᩨ᩵': 5, + 'ᨪᩨ᩵': 3, + 'ᨪᩩ': 2, + 'ᨪᩮᩢ': 3, + 'ᨬ᩠ᨬ': 3, + 'ᨭᩛ': 2, + 'ᨭᩥ': 2, + 'ᨯ᩠ᨦᩢ᩵': 5, + 'ᨯ᩠ᨰ': 3, + 'ᨯ᩠ᨶᩥ': 4, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 6, + 'ᨯᩢ': 2, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 6, + 'ᨯᩦ': 2, + 'ᨯᩨ': 2, + 'ᨯᩪ': 2, + 'ᨯᩪᩯ': 3, + 'ᨯᩯ᩠': 3, + 'ᨯᩰ᩠': 3, + 'ᨯᩱᩢ': 3, + 'ᨯᩴ᩠ᨿ': 4, + 'ᨱ᩠ᨱ': 3, + 'ᨲᩕᩦ': 3, + 'ᨲᩕ᩼': 3, + 'ᨲ᩠ᨠᩫ': 4, + 'ᨲ᩠ᨦᩢᩢ': 5, + 'ᨲ᩠ᨯᩢ': 4, + 'ᨲ᩠ᨲᩥ': 4, + 'ᨲ᩠ᨳᩩ': 4, + 'ᨲ᩠ᨶᩫ': 4, + 'ᨲ᩠ᨶᩫᩢ': 5, + 'ᨲ᩠ᩅᩫ': 4, + 'ᨲ᩠ᩋᩢ': 4, + 'ᨲᩥ': 2, + 'ᨲᩩ': 2, + 'ᨲᩮ᩠': 3, + 'ᨲᩯ᩠': 3, + 'ᨲᩯ᩵': 3, + 'ᨲᩱ᩠': 3, + 'ᨲᩱᩢ': 3, + 'ᨲᩳ᩵': 3, + 'ᨲ᩵': 2, + 'ᨲ᩵ᩴ': 3, + 'ᨳ᩠ᨦᩨ': 4, + 'ᨳ᩠ᩅᩢ': 4, + 'ᨳᩨ': 2, + 'ᨳᩪ': 2, + 'ᨳᩫ᩠ᨷ': 4, + 'ᨳᩮ᩠': 3, + 'ᨳᩮᩢᩢ': 4, + 'ᨳᩯ᩠': 3, + 'ᨴ᩠ᨦᩢ': 4, + 'ᨴ᩠ᨦᩨ': 4, + 'ᨴ᩠ᨵᩥ': 4, + 'ᨴ᩠ᨿ': 3, + 'ᨴ᩠ᨿ᩵': 4, + 'ᨴ᩠ᩅᩢ': 4, + 'ᨴ᩠ᩅᩫ᩵': 5, + 'ᨴᩦ᩵': 3, + 'ᨴᩩ': 2, + 'ᨴᩮ᩠': 3, + 'ᨴᩮᩢᩢ': 4, + 'ᨴᩮᩢ᩵': 4, + 'ᨴᩯ᩠': 3, + 'ᨴᩯᩢ': 3, + 'ᨴᩰ᩠': 3, + 'ᨵᩥ': 2, + 'ᨵᩦ': 2, + 'ᨶ᩠ᨲᩥ': 4, + 'ᨶ᩠ᨶᩢᩢ': 5, + 'ᨶ᩠ᨶᩥ': 4, + 'ᨶ᩠ᩋ': 3, + 'ᨶ᩠ᩋᩢ': 4, + 'ᨶᩢ': 2, + 'ᨶᩢ᩠ᨷ': 4, + 'ᨶᩦ᩶': 3, + 'ᨶᩨ': 2, + 'ᨶᩨ᩵': 3, + 'ᨶᩩ': 2, + 'ᨶᩩ᩵': 3, + 'ᨶᩯ᩠': 3, + 'ᨶ᩵': 2, + 'ᨷ᩠ᨦᩢ': 4, + 'ᨷ᩠ᨯᩢ': 4, + 'ᨷ᩠ᨲᩢ': 4, + 'ᨷ᩠ᨲᩥ': 4, + 'ᨷ᩠ᨴᩫ': 4, + 'ᨷ᩠ᨿ': 3, + 'ᨷ᩠ᩅᩫ': 4, + 'ᨷ᩠ᩋ': 3, + 'ᨷᩢ': 2, + 'ᨷᩥ': 2, + 'ᨷᩩ': 2, + 'ᨷᩪ': 2, + 'ᨷᩮ᩠': 3, + 'ᨸ᩠ᨠᩫ': 4, + 'ᨸ᩠ᨯᩨ': 4, + 'ᨸ᩠ᨶᩢ': 4, + 'ᨸ᩠ᩋ': 3, + 'ᨸᩮ᩠': 3, + 'ᨸᩮᩢ᩵': 4, + 'ᨸ᩵': 2, + 'ᨸ᩠᩵ᨿ': 4, + 'ᨹ᩠ᨯᩥ': 4, + 'ᨹ᩠ᨿ᩵': 4, + 'ᨹ᩠ᩅᩥ': 4, + 'ᨹᩪᩢ': 3, + 'ᨹᩪᩢᩱ': 4, + 'ᨹᩫ᩠ᩃ': 4, + 'ᨹᩮᩨ᩠': 4, + 'ᨹ᩵': 2, + 'ᨻᩕ᩠': 3, + 'ᨻ᩠ᨦ': 3, + 'ᨻ᩠ᨶᩨ᩶': 5, + 'ᨻ᩠ᨶᩫ᩶': 5, + 'ᨻ᩠ᨿ': 3, + 'ᨻ᩠ᩋ': 3, + 'ᨻᩥ': 2, + 'ᨻᩦ᩵': 3, + 'ᨻᩮ᩠': 3, + 'ᨻᩳ': 2, + 'ᨽᩱ᩠': 3, + 'ᨾ᩠ᨲᩥ': 4, + 'ᨾ᩠ᨾ': 3, + 'ᨾ᩠ᨾ᩼': 4, + 'ᨾ᩠ᩅ': 3, + 'ᨾ᩠ᩅ᩵': 4, + 'ᨾᩢ': 2, + 'ᨾᩦ': 2, + 'ᨾᩨ': 2, + 'ᨾᩨᩨ': 3, + 'ᨾᩨ᩵': 3, + 'ᨾᩩ': 2, + 'ᨾᩪ': 2, + 'ᨾᩮ᩠': 3, + 'ᨾᩯ᩵': 3, + 'ᨾ᩵': 2, + 'ᨿ᩠ᨦᩢ': 4, + 'ᨿ᩠ᨦᩢᩢ': 5, + 'ᨿ᩠ᨦᩥ': 4, + 'ᨿ᩠ᨦᩥ᩵': 5, + 'ᨿ᩠ᩋ': 3, + 'ᨿᩢ᩠ᨷ': 4, + 'ᨿᩩ': 2, + 'ᨿᩫ᩠ᩈ': 4, + 'ᨿᩮ᩠': 3, + 'ᨿ᩵': 2, + 'ᩀᩪ᩵': 3, + 'ᩀ᩵': 2, + 'ᩁ᩠ᨠᩢ': 4, + 'ᩁ᩠ᨮᩢ': 4, + 'ᩁ᩠ᨾᩫ': 4, + 'ᩁ᩠ᨿ': 3, + 'ᩁ᩠ᩅ': 3, + 'ᩁ᩠ᩅ᩵': 4, + 'ᩁ᩠ᩋ': 3, + 'ᩁᩢ': 2, + 'ᩁᩢ᩠ᨷ': 4, + 'ᩁᩢᩭ': 3, + 'ᩁᩥ': 2, + 'ᩁᩦ': 2, + 'ᩁᩪᩢ': 3, + 'ᩁᩫ᩠ᨷ': 4, + 'ᩁᩮ᩠': 3, + 'ᩁᩮᩨ᩠': 4, + 'ᩁᩯ᩠': 3, + 'ᩃ᩠ᨦᩫ': 4, + 'ᩃ᩠ᩅ᩵': 4, + 'ᩃᩢ᩠ᨿ': 4, + 'ᩃᩢᩴ': 3, + 'ᩃᩥ': 2, + 'ᩃᩨ᩠ᨷ': 4, + 'ᩃᩩ': 2, + 'ᩃᩪ': 2, + 'ᩃᩮ᩠': 3, + 'ᩃᩰ᩠': 3, + 'ᩅ᩠ᨲᩥ': 4, + 'ᩅ᩠ᨶᩢ': 4, + 'ᩅ᩠ᨿ': 3, + 'ᩅ᩠ᩅ᩵': 4, + 'ᩅᩢ': 2, + 'ᩅᩥ': 2, + 'ᩅᩮ᩠': 3, + 'ᩅᩱᩢ': 3, + 'ᩈ᩠ᨠᩢ': 4, + 'ᩈ᩠ᨠᩧ': 4, + 'ᩈ᩠ᨦᩢ': 4, + 'ᩈ᩠ᨦᩥ᩵': 5, + 'ᩈ᩠ᨦᩫ': 4, + 'ᩈ᩠ᨶᩫ': 4, + 'ᩈ᩠ᨾᩫ': 4, + 'ᩈ᩠ᨿ': 3, + 'ᩈ᩠ᩅ᩵': 4, + 'ᩈ᩠ᩋ': 3, + 'ᩈᩢ': 2, + 'ᩈᩢ᩠ᨿ': 4, + 'ᩈᩥ': 2, + 'ᩈᩦ': 2, + 'ᩈᩨ᩵': 3, + 'ᩈᩩ': 2, + 'ᩈᩪ': 2, + 'ᩈᩪᩢ': 3, + 'ᩈᩫ᩠ᨾ': 4, + 'ᩈᩮ᩠': 3, + 'ᩈᩯ᩠': 3, + 'ᩈᩱ᩠': 3, + 'ᩈᩳ᩵': 3, + 'ᩈ᩵ᩴ': 3, + 'ᩉᩕᩨ': 3, + 'ᩉᩖ': 2, + 'ᩉᩖᩢ': 3, + 'ᩉ᩠ᨶᩢ': 4, + 'ᩉ᩠ᨶᩦ': 4, + 'ᩉ᩠ᨶᩧ': 4, + 'ᩉ᩠ᨶᩧ᩵': 5, + 'ᩉ᩠ᨶᩫ': 4, + 'ᩉ᩠ᨾ': 3, + 'ᩉ᩠ᨾᩢ': 4, + 'ᩉ᩠ᨾᩢᩢ': 5, + 'ᩉ᩠ᨾᩪ᩵': 5, + 'ᩉ᩠ᨾᩳ': 4, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 6, + 'ᩉ᩠ᨿᩩ': 4, + 'ᩉ᩠ᩅᩫ': 4, + 'ᩉ᩠ᩅ᩵': 4, + 'ᩉᩢ': 2, + 'ᩉᩨᩢ': 3, + 'ᩉᩨ᩶': 3, + 'ᩉᩮᩖᩨ': 4, + 'ᩉᩮ᩠': 3, + 'ᩉᩯ᩠': 3, + 'ᩉᩯ᩵': 3, + 'ᩉᩱ᩠': 3, + 'ᩉᩳ': 2, + 'ᩋ᩠ᨶᩢ': 4, + 'ᩋ᩠ᨶᩨ᩵': 5, + 'ᩋ᩠ᩋ': 3, + 'ᩋᩢ': 2, + 'ᩋᩨ': 2, + 'ᩋᩨ᩵': 3, + 'ᩋᩨ᩶': 3, + 'ᩋᩪᩢ': 3, + 'ᩋᩴ': 2, + 'ᩐᩢ': 2, + 'ᩓ᩠ᩅᩢ': 4, + 'ᩓᩢ': 2, + 'ᩔ᩼': 2, + '⛓️‍💥': 3, + '⛹🏻‍♀️': 4, + '⛹🏻‍♂️': 4, + '⛹🏼‍♀️': 4, + '⛹🏼‍♂️': 4, + '⛹🏽‍♀️': 4, + '⛹🏽‍♂️': 4, + '⛹🏾‍♀️': 4, + '⛹🏾‍♂️': 4, + '⛹🏿‍♀️': 4, + '⛹🏿‍♂️': 4, + '❤️‍🔥': 3, + '❤️‍🩹': 3, + 'ꦉꦁ': 2, + 'ꦏꦁ': 2, + 'ꦏꦂ': 2, + 'ꦏꦶ': 2, + 'ꦏꦶꦂ': 3, + 'ꦏꦸ': 2, + 'ꦏꦸꦂ': 3, + 'ꦏꦺꦴ': 3, + 'ꦏꦼ': 2, + 'ꦏꦿꦁ': 3, + 'ꦏꦿꦂ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 4, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 4, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 4, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 4, + 'ꦏ꧀ꦠꦸ': 4, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 4, + 'ꦏ꧀ꦢꦶ': 4, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 5, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 4, + 'ꦏ꧀ꦥꦿꦶ': 5, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 4, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 4, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 4, + 'ꦏ꧀ꦱꦶ': 4, + 'ꦏ꧀ꦱꦸ': 4, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 4, + 'ꦏ꧀ꦲꦶꦁ': 5, + 'ꦏ꧀ꦲꦸ': 4, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦁ': 2, + 'ꦒꦂ': 2, + 'ꦒꦸ': 2, + 'ꦒꦸꦁ': 3, + 'ꦒꦸꦂ': 3, + 'ꦒꦸꦃ': 3, + 'ꦒꦺꦴ': 3, + 'ꦒꦼ': 2, + 'ꦒꦼꦂ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦁ': 2, + 'ꦔꦶ': 2, + 'ꦔꦶꦁ': 3, + 'ꦔꦸ': 2, + 'ꦔꦺꦴ': 3, + 'ꦔꦼ': 2, + 'ꦔꦼꦂ': 3, + 'ꦔꦽ': 2, + 'ꦔ꧀ꦒꦼ': 4, + 'ꦔ꧀ꦒꦿꦼꦁ': 6, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 4, + 'ꦔ꧀ꦭꦸ': 4, + 'ꦔ꧀ꦭꦼ': 4, + 'ꦕꦶ': 2, + 'ꦕꦶꦁ': 3, + 'ꦕꦸ': 2, + 'ꦕꦺꦴ': 3, + 'ꦗꦶ': 2, + 'ꦗꦸ': 2, + 'ꦗꦼ': 2, + 'ꦗꦿꦺꦴ': 4, + 'ꦚꦁ': 2, + 'ꦚꦶꦁ': 3, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 4, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 4, + 'ꦚ꧀ꦗꦸꦁ': 5, + 'ꦚ꧀ꦗꦸꦂ': 5, + 'ꦝꦁ': 2, + 'ꦝꦂ': 2, + 'ꦝꦶ': 2, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 4, + 'ꦝꦼ': 2, + 'ꦝꦼꦁ': 3, + 'ꦠꦁ': 2, + 'ꦠꦶ': 2, + 'ꦠꦸ': 2, + 'ꦠꦸꦁ': 3, + 'ꦠꦸꦂ': 3, + 'ꦠꦸꦃ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠꦼ': 2, + 'ꦠꦼꦂ': 3, + 'ꦠꦽ': 2, + 'ꦠꦿꦶ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 4, + 'ꦠ꧀ꦏꦸ': 4, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 4, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 4, + 'ꦠ꧀ꦠꦶꦁ': 5, + 'ꦠ꧀ꦠꦸ': 4, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 4, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 4, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 5, + 'ꦠ꧀ꦱꦶꦃ': 5, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 5, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦂ': 2, + 'ꦢꦶ': 2, + 'ꦢꦸ': 2, + 'ꦢꦸꦂ': 3, + 'ꦢꦺꦴ': 3, + 'ꦢꦼ': 2, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 5, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦁ': 2, + 'ꦤꦂ': 2, + 'ꦤꦶ': 2, + 'ꦤꦶꦁ': 3, + 'ꦤꦸ': 2, + 'ꦤꦸꦁ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤꦼ': 2, + 'ꦤꦼꦁ': 3, + 'ꦤꦼꦂ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 4, + 'ꦤ꧀ꦏꦸ': 4, + 'ꦤ꧀ꦏꦼ': 4, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 4, + 'ꦤ꧀ꦔꦶ': 4, + 'ꦤ꧀ꦔꦸ': 4, + 'ꦤ꧀ꦔꦸꦂ': 5, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 4, + 'ꦤ꧀ꦔꦿꦸ': 5, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 4, + 'ꦤ꧀ꦗꦼ': 4, + 'ꦤ꧀ꦚꦶ': 4, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 4, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 4, + 'ꦤ꧀ꦝꦶ': 4, + 'ꦤ꧀ꦝꦸ': 4, + 'ꦤ꧀ꦝꦸꦁ': 5, + 'ꦤ꧀ꦝꦺꦁ': 5, + 'ꦤ꧀ꦝꦺꦴꦁ': 6, + 'ꦤ꧀ꦝꦼ': 4, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 4, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 4, + 'ꦤ꧀ꦠꦸ': 4, + 'ꦤ꧀ꦠꦸꦁ': 5, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 4, + 'ꦤ꧀ꦠꦼꦂ': 5, + 'ꦤ꧀ꦠꦽ': 4, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 4, + 'ꦤ꧀ꦢꦂ': 4, + 'ꦤ꧀ꦢꦶ': 4, + 'ꦤ꧀ꦢꦸ': 4, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 4, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 4, + 'ꦤ꧀ꦤꦶꦁ': 5, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 4, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 4, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 7, + 'ꦤ꧀ꦥꦶ': 4, + 'ꦤ꧀ꦥꦸ': 4, + 'ꦤ꧀ꦥꦼ': 4, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 5, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 4, + 'ꦤ꧀ꦧꦶ': 4, + 'ꦤ꧀ꦧꦸ': 4, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 4, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 4, + 'ꦤ꧀ꦩꦶ': 4, + 'ꦤ꧀ꦩꦸꦂ': 5, + 'ꦤ꧀ꦩꦼ': 4, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 4, + 'ꦤ꧀ꦭꦸ': 4, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 4, + 'ꦤ꧀ꦮꦶ': 4, + 'ꦤ꧀ꦮꦸ': 4, + 'ꦤ꧀ꦮꦼ': 4, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 4, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 4, + 'ꦤ꧀ꦱꦶꦁ': 5, + 'ꦤ꧀ꦱꦸ': 4, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 4, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 4, + 'ꦤ꧀ꦲꦶ': 4, + 'ꦤ꧀ꦲꦶꦁ': 5, + 'ꦤ꧀ꦲꦸ': 4, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥꦁ': 2, + 'ꦥ꦳ꦼ': 3, + 'ꦥ꦳꧀': 3, + 'ꦥꦶ': 2, + 'ꦥꦸ': 2, + 'ꦥꦸꦁ': 3, + 'ꦥꦺꦴ': 3, + 'ꦥꦼ': 2, + 'ꦥꦼꦂ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦶ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 4, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 4, + 'ꦥ꧀ꦢꦸ': 4, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 4, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 4, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 4, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 4, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 5, + 'ꦥ꧀ꦱꦼ': 4, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦁ': 2, + 'ꦧꦶ': 2, + 'ꦧꦸ': 2, + 'ꦧꦸꦂ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧꦼ': 2, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 4, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 4, + 'ꦩꦁ': 2, + 'ꦩꦂ': 2, + 'ꦩꦶ': 2, + 'ꦩꦸ': 2, + 'ꦩꦸꦁ': 3, + 'ꦩꦸꦂ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦼ': 2, + 'ꦩꦼꦂ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 4, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 4, + 'ꦩ꧀ꦔꦼ': 4, + 'ꦩ꧀ꦠꦸ': 4, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 5, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 4, + 'ꦩ꧀ꦥꦸ': 4, + 'ꦩ꧀ꦥꦼ': 4, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 4, + 'ꦩ꧀ꦧꦂ': 4, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 4, + 'ꦩ꧀ꦧꦸ': 4, + 'ꦩ꧀ꦧꦸꦁ': 5, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 5, + 'ꦩ꧀ꦧꦼꦂ': 5, + 'ꦩ꧀ꦧꦾꦸꦂ': 6, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 4, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 4, + 'ꦪꦁ': 2, + 'ꦪꦶ': 2, + 'ꦪꦸ': 2, + 'ꦪꦸꦁ': 3, + 'ꦪꦸꦃ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦁ': 2, + 'ꦫꦶ': 2, + 'ꦫꦶꦁ': 3, + 'ꦫꦶꦃ': 3, + 'ꦫꦸ': 2, + 'ꦫꦸꦁ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦁ': 2, + 'ꦭꦂ': 2, + 'ꦭꦶ': 2, + 'ꦭꦶꦁ': 3, + 'ꦭꦶꦃ': 3, + 'ꦭꦸ': 2, + 'ꦭꦸꦁ': 3, + 'ꦭꦸꦂ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 4, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 4, + 'ꦭ꧀ꦚꦼꦁ': 5, + 'ꦭ꧀ꦠꦸ': 4, + 'ꦭ꧀ꦧꦸ': 4, + 'ꦭ꧀ꦩꦸ': 4, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 4, + 'ꦭ꧀ꦭꦶꦁ': 5, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 4, + 'ꦭ꧀ꦱꦶꦁ': 5, + 'ꦭ꧀ꦱꦸ': 4, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 4, + 'ꦭ꧀ꦲꦶꦁ': 5, + 'ꦭ꧀ꦲꦸ': 4, + 'ꦮꦂ': 2, + 'ꦮ꦳ꦼꦂ': 4, + 'ꦮꦶ': 2, + 'ꦮꦶꦃ': 3, + 'ꦮꦸ': 2, + 'ꦮꦸꦁ': 3, + 'ꦮꦸꦃ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 4, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦼ': 2, + 'ꦮꦼꦁ': 3, + 'ꦮꦼꦂ': 3, + 'ꦮꦿꦸꦃ': 4, + 'ꦱꦂ': 2, + 'ꦱꦶ': 2, + 'ꦱꦶꦁ': 3, + 'ꦱꦶꦂ': 3, + 'ꦱꦶꦃ': 3, + 'ꦱꦸ': 2, + 'ꦱꦸꦁ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦼ': 2, + 'ꦱꦼꦁ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 4, + 'ꦱ꧀ꦏꦿꦶ': 5, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 4, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 4, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 4, + 'ꦱ꧀ꦢꦶ': 4, + 'ꦱ꧀ꦢꦸ': 4, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 4, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 4, + 'ꦱ꧀ꦩꦼ': 4, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 4, + 'ꦱ꧀ꦱꦶ': 4, + 'ꦱ꧀ꦱꦶꦁ': 5, + 'ꦱ꧀ꦱꦸ': 4, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 4, + 'ꦱ꧀ꦲꦶ': 4, + 'ꦱ꧀ꦲꦶꦁ': 5, + 'ꦱ꧀ꦲꦸ': 4, + 'ꦲꦁ': 2, + 'ꦲꦶ': 2, + 'ꦲꦶꦁ': 3, + 'ꦲꦶꦂ': 3, + 'ꦲꦸ': 2, + 'ꦲꦸꦂ': 3, + 'ꦲꦺꦴ': 3, + 'ꦲꦼ': 2, + 'ꪀꪰ': 2, + 'ꪀꪲ': 2, + 'ꪀꪳ': 2, + 'ꪀꪴ': 2, + 'ꪀꪷ': 2, + 'ꪀꪾ': 2, + 'ꪁꪴ': 2, + 'ꪁꪷ': 2, + 'ꪁꪸ': 2, + 'ꪁꪾ': 2, + 'ꪄꪰ': 2, + 'ꪄꪲ': 2, + 'ꪄꪳ': 2, + 'ꪄꪴ': 2, + 'ꪄꪷ': 2, + 'ꪅꪰ': 2, + 'ꪈꪲ': 2, + 'ꪈꪸ': 2, + 'ꪈꪾ': 2, + 'ꪉꪷ': 2, + 'ꪉꪸ': 2, + 'ꪊꪰ': 2, + 'ꪊꪲ': 2, + 'ꪊꪴ': 2, + 'ꪊꪸ': 2, + 'ꪋꪲ': 2, + 'ꪋꪳ': 2, + 'ꪋꪴ': 2, + 'ꪋꪷ': 2, + 'ꪋꪸ': 2, + 'ꪎꪰ': 2, + 'ꪎꪲ': 2, + 'ꪎꪳ': 2, + 'ꪎꪴ': 2, + 'ꪎꪷ': 2, + 'ꪎꪸ': 2, + 'ꪏꪴ': 2, + 'ꪏꪷ': 2, + 'ꪏꪸ': 2, + 'ꪑꪰ': 2, + 'ꪑꪲ': 2, + 'ꪑꪷ': 2, + 'ꪒꪰ': 2, + 'ꪒꪲ': 2, + 'ꪒꪴ': 2, + 'ꪒꪷ': 2, + 'ꪒꪸ': 2, + 'ꪔꪰ': 2, + 'ꪔꪲ': 2, + 'ꪔꪳ': 2, + 'ꪔꪷ': 2, + 'ꪔꪸ': 2, + 'ꪔꪾ': 2, + 'ꪕꪰ': 2, + 'ꪕꪲ': 2, + 'ꪕꪳ': 2, + 'ꪕꪴ': 2, + 'ꪕꪷ': 2, + 'ꪕꪸ': 2, + 'ꪕꪾ': 2, + 'ꪖꪸ': 2, + 'ꪘꪰ': 2, + 'ꪘꪳ': 2, + 'ꪙꪲ': 2, + 'ꪙꪴ': 2, + 'ꪙꪾ': 2, + 'ꪚꪰ': 2, + 'ꪚꪴ': 2, + 'ꪚꪷ': 2, + 'ꪜꪰ': 2, + 'ꪜꪲ': 2, + 'ꪜꪴ': 2, + 'ꪜꪸ': 2, + 'ꪝꪰ': 2, + 'ꪝꪲ': 2, + 'ꪝꪳ': 2, + 'ꪝꪷ': 2, + 'ꪝꪸ': 2, + 'ꪠꪲ': 2, + 'ꪠꪴ': 2, + 'ꪠꪸ': 2, + 'ꪢꪲ': 2, + 'ꪢꪴ': 2, + 'ꪢꪸ': 2, + 'ꪣꪰ': 2, + 'ꪣꪲ': 2, + 'ꪣꪳ': 2, + 'ꪣꪷ': 2, + 'ꪣꪸ': 2, + 'ꪤꪰ': 2, + 'ꪤꪴ': 2, + 'ꪤꪷ': 2, + 'ꪤꪸ': 2, + 'ꪤꪾ': 2, + 'ꪥꪰ': 2, + 'ꪥꪴ': 2, + 'ꪥꪸ': 2, + 'ꪨꪰ': 2, + 'ꪨꪷ': 2, + 'ꪨꪸ': 2, + 'ꪩꪰ': 2, + 'ꪩꪳ': 2, + 'ꪩꪴ': 2, + 'ꪩꪷ': 2, + 'ꪩꪾ': 2, + 'ꪪꪴ': 2, + 'ꪫꪸ': 2, + 'ꪬꪷ': 2, + 'ꪬꪸ': 2, + 'ꪭꪰ': 2, + 'ꪭꪲ': 2, + 'ꪭꪳ': 2, + 'ꪭꪴ': 2, + 'ꪭꪷ': 2, + 'ꪭꪸ': 2, + 'ꪮꪲ': 2, + 'ꪮꪳ': 2, + 'ꪮꪴ': 2, + 'ꪮꪸ': 2, + 'ꪯꪸ': 2, + '𑄃𑄧': 2, + '𑄃𑄧𑄁': 3, + '𑄃𑄨': 2, + '𑄃𑄪': 2, + '𑄃𑄬𑄭': 3, + '𑄃𑄮': 2, + '𑄃𑄰': 2, + '𑄃𑄳𑄃': 3, + '𑄃𑄳𑄦𑄧': 4, + '𑄃𑄳𑄦𑄨': 4, + '𑄅𑄪': 2, + '𑄅𑄫': 2, + '𑄇𑄧': 2, + '𑄇𑄧𑄧': 3, + '𑄇𑄧𑄧𑄱': 4, + '𑄇𑄨': 2, + '𑄇𑄨𑄁': 3, + '𑄇𑄪': 2, + '𑄇𑄭𑄭': 3, + '𑄇𑄳𑄠': 3, + '𑄇𑄳𑄠𑄬': 4, + '𑄇𑄳𑄠𑄴': 4, + '𑄇𑄳𑄢𑄨': 4, + '𑄇𑄳𑄦𑄧': 4, + '𑄇𑄳𑄦𑄴': 4, + '𑄇𑄴': 2, + '𑄈𑄧': 2, + '𑄈𑄪': 2, + '𑄈𑄳𑄠': 3, + '𑄈𑄳𑄠𑄬': 4, + '𑄉𑄧': 2, + '𑄉𑄨': 2, + '𑄉𑄩': 2, + '𑄉𑄪': 2, + '𑄉𑄬𑄭': 3, + '𑄉𑄭': 2, + '𑄉𑄮': 2, + '𑄉𑄳𑄠': 3, + '𑄉𑄳𑄢𑄧': 4, + '𑄊𑄧': 2, + '𑄊𑄨': 2, + '𑄋𑄧': 2, + '𑄋𑄨': 2, + '𑄌𑄧': 2, + '𑄌𑄨': 2, + '𑄌𑄮': 2, + '𑄌𑄯': 2, + '𑄌𑄳𑄠𑄬': 4, + '𑄌𑄳𑄠𑄴': 4, + '𑄌𑄴': 2, + '𑄍𑄨': 2, + '𑄍𑄪': 2, + '𑄎𑄧': 2, + '𑄎𑄨': 2, + '𑄎𑄨𑄁': 3, + '𑄎𑄩': 2, + '𑄎𑄩𑄁': 3, + '𑄎𑄪': 2, + '𑄎𑄪𑄁': 3, + '𑄎𑄬𑄭': 3, + '𑄎𑄮': 2, + '𑄎𑄳𑄠𑄧': 4, + '𑄎𑄳𑄠𑄬': 4, + '𑄏𑄨': 2, + '𑄏𑄮': 2, + '𑄑𑄨': 2, + '𑄑𑄳𑄢𑄬': 4, + '𑄒𑄨': 2, + '𑄓𑄧': 2, + '𑄓𑄨': 2, + '𑄓𑄮': 2, + '𑄕𑄧': 2, + '𑄖𑄧': 2, + '𑄖𑄧𑄧': 3, + '𑄖𑄧𑄧𑄱': 4, + '𑄖𑄨': 2, + '𑄖𑄨𑄨': 3, + '𑄖𑄨𑄨𑄪': 4, + '𑄖𑄩': 2, + '𑄖𑄪': 2, + '𑄖𑄳𑄠𑄧': 4, + '𑄖𑄳𑄠𑄬': 4, + '𑄖𑄳𑄠𑄴': 4, + '𑄖𑄳𑄢𑄨': 4, + '𑄖𑄳𑄢𑄬': 4, + '𑄖𑄳𑄦𑄴': 4, + '𑄖𑄴': 2, + '𑄗𑄧': 2, + '𑄗𑄪': 2, + '𑄗𑄭': 2, + '𑄘𑄁': 2, + '𑄘𑄧': 2, + '𑄘𑄨': 2, + '𑄘𑄪': 2, + '𑄘𑄮': 2, + '𑄘𑄳𑄠𑄧': 4, + '𑄘𑄳𑄠𑄬': 4, + '𑄙𑄧': 2, + '𑄙𑄨': 2, + '𑄙𑄩': 2, + '𑄙𑄪': 2, + '𑄙𑄮': 2, + '𑄙𑄳𑄠': 3, + '𑄙𑄳𑄠𑄧': 4, + '𑄚𑄧': 2, + '𑄚𑄧𑄧𑄱': 4, + '𑄚𑄨': 2, + '𑄚𑄩': 2, + '𑄚𑄪': 2, + '𑄚𑄬𑄭': 3, + '𑄚𑄮': 2, + '𑄚𑄳𑄠𑄧': 4, + '𑄚𑄳𑄠𑄬': 4, + '𑄚𑄳𑄠𑄴': 4, + '𑄚𑄳𑄢𑄧𑄧': 5, + '𑄚𑄳𑄦𑄧': 4, + '𑄚𑄳𑄦𑄨': 4, + '𑄚𑄳𑄦𑄴': 4, + '𑄚𑄴': 2, + '𑄛𑄧': 2, + '𑄛𑄨': 2, + '𑄛𑄪': 2, + '𑄛𑄭': 2, + '𑄛𑄮': 2, + '𑄛𑄱': 2, + '𑄛𑄳𑄢': 3, + '𑄛𑄳𑄢𑄧': 4, + '𑄛𑄳𑄦': 3, + '𑄛𑄴': 2, + '𑄜𑄧': 2, + '𑄜𑄨': 2, + '𑄜𑄪': 2, + '𑄜𑄯': 2, + '𑄝𑄧': 2, + '𑄝𑄧𑄧': 3, + '𑄝𑄨': 2, + '𑄝𑄪': 2, + '𑄝𑄮': 2, + '𑄝𑄰': 2, + '𑄝𑄳𑄠': 3, + '𑄝𑄳𑄢𑄨': 4, + '𑄝𑄳𑄢𑄬': 4, + '𑄞𑄧': 2, + '𑄞𑄨': 2, + '𑄞𑄩': 2, + '𑄞𑄪': 2, + '𑄞𑄬𑄭': 3, + '𑄞𑄮': 2, + '𑄞𑄯': 2, + '𑄟𑄧': 2, + '𑄟𑄨': 2, + '𑄟𑄪': 2, + '𑄟𑄫': 2, + '𑄟𑄮': 2, + '𑄟𑄱': 2, + '𑄟𑄳𑄠𑄬': 4, + '𑄟𑄴': 2, + '𑄠𑄧': 2, + '𑄠𑄨': 2, + '𑄠𑄬𑄭': 3, + '𑄠𑄮': 2, + '𑄠𑄴': 2, + '𑄡𑄧': 2, + '𑄡𑄨': 2, + '𑄡𑄪': 2, + '𑄡𑄬𑄭': 3, + '𑄡𑄳𑄠𑄬': 4, + '𑄢𑄁': 2, + '𑄢𑄧': 2, + '𑄢𑄨': 2, + '𑄢𑄩': 2, + '𑄢𑄪': 2, + '𑄢𑄬𑄭': 3, + '𑄢𑄮': 2, + '𑄢𑄳𑄦𑄧𑄱𑄱': 6, + '𑄢𑄴': 2, + '𑄣𑄧': 2, + '𑄣𑄨': 2, + '𑄣𑄩': 2, + '𑄣𑄪': 2, + '𑄣𑄬𑄭': 3, + '𑄣𑄮': 2, + '𑄣𑄰𑄱': 3, + '𑄣𑄳𑄦𑄬': 4, + '𑄣𑄳𑄦𑄴': 4, + '𑄣𑄴': 2, + '𑄥𑄁': 2, + '𑄥𑄧': 2, + '𑄥𑄧𑄁': 3, + '𑄥𑄨': 2, + '𑄥𑄩': 2, + '𑄥𑄪': 2, + '𑄥𑄳𑄠': 3, + '𑄥𑄳𑄠𑄧': 4, + '𑄥𑄳𑄢𑄧': 4, + '𑄥𑄳𑄢𑄨𑄁': 5, + '𑄦𑄧': 2, + '𑄦𑄨': 2, + '𑄦𑄮': 2, + '𑄦𑄰': 2, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍀': 2, + '𑌕𑍍': 2, + '𑌗𑍀': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑌼': 2, + '𑌜𑍀': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌢𑌼𑍀': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍀': 2, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍀': 2, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍀': 2, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍀': 2, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍀': 2, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍀': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍀': 2, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍀': 2, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍀': 2, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍀': 2, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍀': 2, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍀': 2, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '𑌹𑍀': 2, + '𞤀𞥄': 2, + '𞤢𞥄': 2, + '𞤣𞥆': 2, + '𞤂𞥆': 2, + '𞤤𞥆': 2, + '𞤥𞥆': 2, + '𞤦𞥆': 2, + '𞤨𞥆': 2, + '𞤩𞥆': 2, + '𞤫𞥅': 2, + '𞤭𞥅': 2, + '𞤌𞥅': 2, + '𞤮𞥅': 2, + '𞤍𞥆': 2, + '𞤯𞥆': 2, + '𞤰𞥆': 2, + '𞤲𞥆': 2, + '𞤳𞥆': 2, + '𞤵𞥅': 2, + '𞤔𞥆': 2, + '𞤶𞥆': 2, + '𞤷𞥆': 2, + '𞤺𞥆': 2, + '𞤼𞥆': 2, + '🍄‍🟫': 4, + '🍋‍🟩': 4, + '🏃‍♀️': 3, + '🏃‍♀️‍➡️': 4, + '🏃‍♂️': 3, + '🏃‍♂️‍➡️': 4, + '🏃‍➡️': 3, + '🏃🏻‍♀️': 5, + '🏃🏻‍♀️‍➡️': 6, + '🏃🏻‍♂️': 5, + '🏃🏻‍♂️‍➡️': 6, + '🏃🏻‍➡️': 5, + '🏃🏼‍♀️': 5, + '🏃🏼‍♀️‍➡️': 6, + '🏃🏼‍♂️': 5, + '🏃🏼‍♂️‍➡️': 6, + '🏃🏼‍➡️': 5, + '🏃🏽‍♀️': 5, + '🏃🏽‍♀️‍➡️': 6, + '🏃🏽‍♂️': 5, + '🏃🏽‍♂️‍➡️': 6, + '🏃🏽‍➡️': 5, + '🏃🏾‍♀️': 5, + '🏃🏾‍♀️‍➡️': 6, + '🏃🏾‍♂️': 5, + '🏃🏾‍♂️‍➡️': 6, + '🏃🏾‍➡️': 5, + '🏃🏿‍♀️': 5, + '🏃🏿‍♀️‍➡️': 6, + '🏃🏿‍♂️': 5, + '🏃🏿‍♂️‍➡️': 6, + '🏃🏿‍➡️': 5, + '🏄‍♀️': 3, + '🏄‍♂️': 3, + '🏄🏻‍♀️': 5, + '🏄🏻‍♂️': 5, + '🏄🏼‍♀️': 5, + '🏄🏼‍♂️': 5, + '🏄🏽‍♀️': 5, + '🏄🏽‍♂️': 5, + '🏄🏾‍♀️': 5, + '🏄🏾‍♂️': 5, + '🏄🏿‍♀️': 5, + '🏄🏿‍♂️': 5, + '🏊‍♀️': 3, + '🏊‍♂️': 3, + '🏊🏻‍♀️': 5, + '🏊🏻‍♂️': 5, + '🏊🏼‍♀️': 5, + '🏊🏼‍♂️': 5, + '🏊🏽‍♀️': 5, + '🏊🏽‍♂️': 5, + '🏊🏾‍♀️': 5, + '🏊🏾‍♂️': 5, + '🏊🏿‍♀️': 5, + '🏊🏿‍♂️': 5, + '🏋🏻‍♀️': 4, + '🏋🏻‍♂️': 4, + '🏋🏼‍♀️': 4, + '🏋🏼‍♂️': 4, + '🏋🏽‍♀️': 4, + '🏋🏽‍♂️': 4, + '🏋🏾‍♀️': 4, + '🏋🏾‍♂️': 4, + '🏋🏿‍♀️': 4, + '🏋🏿‍♂️': 4, + '🏌🏻‍♀️': 4, + '🏌🏻‍♂️': 4, + '🏌🏼‍♀️': 4, + '🏌🏼‍♂️': 4, + '🏌🏽‍♀️': 4, + '🏌🏽‍♂️': 4, + '🏌🏾‍♀️': 4, + '🏌🏾‍♂️': 4, + '🏌🏿‍♀️': 4, + '🏌🏿‍♂️': 4, + '🏳️‍🌈': 3, + '🏴‍☠️': 3, + '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 8, + '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 8, + '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 8, + '🐈‍⬛': 4, + '🐕‍🦺': 4, + '🐦‍⬛': 4, + '🐦‍🔥': 4, + '🐻‍❄️': 3, + '👨‍⚕️': 3, + '👨‍⚖️': 3, + '👨‍✈️': 3, + '👨‍❤️‍👨': 5, + '👨‍❤️‍💋‍👨': 7, + '👨‍🌾': 4, + '👨‍🍳': 4, + '👨‍🍼': 4, + '👨‍🎓': 4, + '👨‍🎤': 4, + '👨‍🎨': 4, + '👨‍🏫': 4, + '👨‍🏭': 4, + '👨‍👦': 4, + '👨‍👦‍👦': 6, + '👨‍👧': 4, + '👨‍👧‍👦': 6, + '👨‍👧‍👧': 6, + '👨‍👨‍👦': 6, + '👨‍👨‍👦‍👦': 8, + '👨‍👨‍👧': 6, + '👨‍👨‍👧‍👦': 8, + '👨‍👨‍👧‍👧': 8, + '👨‍👩‍👦': 6, + '👨‍👩‍👦‍👦': 8, + '👨‍👩‍👧': 6, + '👨‍👩‍👧‍👦': 8, + '👨‍👩‍👧‍👧': 8, + '👨‍💻': 4, + '👨‍💼': 4, + '👨‍🔧': 4, + '👨‍🔬': 4, + '👨‍🚀': 4, + '👨‍🚒': 4, + '👨‍🦯': 4, + '👨‍🦯‍➡️': 5, + '👨‍🦰': 4, + '👨‍🦱': 4, + '👨‍🦲': 4, + '👨‍🦳': 4, + '👨‍🦼': 4, + '👨‍🦼‍➡️': 5, + '👨‍🦽': 4, + '👨‍🦽‍➡️': 5, + '👨🏻‍⚕️': 5, + '👨🏻‍⚖️': 5, + '👨🏻‍✈️': 5, + '👨🏻‍❤️‍👨🏻': 9, + '👨🏻‍❤️‍👨🏽': 9, + '👨🏻‍❤️‍👨🏿': 9, + '👨🏻‍❤️‍💋‍👨🏻': 11, + '👨🏻‍❤️‍💋‍👨🏽': 11, + '👨🏻‍❤️‍💋‍👨🏾': 11, + '👨🏻‍❤️‍💋‍👨🏿': 11, + '👨🏻‍🌾': 6, + '👨🏻‍🍳': 6, + '👨🏻‍🍼': 6, + '👨🏻‍🎓': 6, + '👨🏻‍🎤': 6, + '👨🏻‍🎨': 6, + '👨🏻‍🏫': 6, + '👨🏻‍🏭': 6, + '👨🏻‍🐰‍👨🏼': 10, + '👨🏻‍🐰‍👨🏽': 10, + '👨🏻‍🐰‍👨🏾': 10, + '👨🏻‍🐰‍👨🏿': 10, + '👨🏻‍💻': 6, + '👨🏻‍💼': 6, + '👨🏻‍🔧': 6, + '👨🏻‍🔬': 6, + '👨🏻‍🚀': 6, + '👨🏻‍🚒': 6, + '👨🏻‍🤝‍👨🏼': 10, + '👨🏻‍🤝‍👨🏽': 10, + '👨🏻‍🤝‍👨🏾': 10, + '👨🏻‍🤝‍👨🏿': 10, + '👨🏻‍🦯': 6, + '👨🏻‍🦯‍➡️': 7, + '👨🏻‍🦰': 6, + '👨🏻‍🦱': 6, + '👨🏻‍🦲': 6, + '👨🏻‍🦳': 6, + '👨🏻‍🦼': 6, + '👨🏻‍🦼‍➡️': 7, + '👨🏻‍🦽': 6, + '👨🏻‍🦽‍➡️': 7, + '👨🏻‍🫯‍👨🏼': 9, + '👨🏻‍🫯‍👨🏽': 9, + '👨🏻‍🫯‍👨🏾': 9, + '👨🏻‍🫯‍👨🏿': 9, + '👨🏼‍⚕️': 5, + '👨🏼‍⚖️': 5, + '👨🏼‍✈️': 5, + '👨🏼‍❤️‍👨🏼': 9, + '👨🏼‍❤️‍💋‍👨🏼': 11, + '👨🏼‍❤️‍💋‍👨🏿': 11, + '👨🏼‍🌾': 6, + '👨🏼‍🍳': 6, + '👨🏼‍🍼': 6, + '👨🏼‍🎓': 6, + '👨🏼‍🎤': 6, + '👨🏼‍🎨': 6, + '👨🏼‍🏫': 6, + '👨🏼‍🏭': 6, + '👨🏼‍🐰‍👨🏻': 10, + '👨🏼‍🐰‍👨🏽': 10, + '👨🏼‍🐰‍👨🏿': 10, + '👨🏼‍💻': 6, + '👨🏼‍💼': 6, + '👨🏼‍🔧': 6, + '👨🏼‍🔬': 6, + '👨🏼‍🚀': 6, + '👨🏼‍🚒': 6, + '👨🏼‍🤝‍👨🏻': 10, + '👨🏼‍🤝‍👨🏽': 10, + '👨🏼‍🤝‍👨🏿': 10, + '👨🏼‍🦯': 6, + '👨🏼‍🦯‍➡️': 7, + '👨🏼‍🦰': 6, + '👨🏼‍🦱': 6, + '👨🏼‍🦲': 6, + '👨🏼‍🦳': 6, + '👨🏼‍🦼': 6, + '👨🏼‍🦽': 6, + '👨🏼‍🦽‍➡️': 7, + '👨🏼‍🫯‍👨🏻': 9, + '👨🏼‍🫯‍👨🏽': 9, + '👨🏼‍🫯‍👨🏾': 9, + '👨🏼‍🫯‍👨🏿': 9, + '👨🏽‍⚕️': 5, + '👨🏽‍⚖️': 5, + '👨🏽‍✈️': 5, + '👨🏽‍❤️‍👨🏻': 9, + '👨🏽‍❤️‍👨🏽': 9, + '👨🏽‍❤️‍👨🏿': 9, + '👨🏽‍❤️‍💋‍👨🏻': 11, + '👨🏽‍❤️‍💋‍👨🏽': 11, + '👨🏽‍❤️‍💋‍👨🏿': 11, + '👨🏽‍🌾': 6, + '👨🏽‍🍳': 6, + '👨🏽‍🍼': 6, + '👨🏽‍🎓': 6, + '👨🏽‍🎤': 6, + '👨🏽‍🎨': 6, + '👨🏽‍🏫': 6, + '👨🏽‍🏭': 6, + '👨🏽‍🐰‍👨🏻': 10, + '👨🏽‍🐰‍👨🏼': 10, + '👨🏽‍🐰‍👨🏾': 10, + '👨🏽‍🐰‍👨🏿': 10, + '👨🏽‍💻': 6, + '👨🏽‍💼': 6, + '👨🏽‍🔧': 6, + '👨🏽‍🔬': 6, + '👨🏽‍🚀': 6, + '👨🏽‍🚒': 6, + '👨🏽‍🤝‍👨🏻': 10, + '👨🏽‍🤝‍👨🏼': 10, + '👨🏽‍🤝‍👨🏾': 10, + '👨🏽‍🤝‍👨🏿': 10, + '👨🏽‍🦯': 6, + '👨🏽‍🦯‍➡️': 7, + '👨🏽‍🦰': 6, + '👨🏽‍🦱': 6, + '👨🏽‍🦲': 6, + '👨🏽‍🦳': 6, + '👨🏽‍🦼': 6, + '👨🏽‍🦼‍➡️': 7, + '👨🏽‍🦽': 6, + '👨🏽‍🦽‍➡️': 7, + '👨🏽‍🫯‍👨🏻': 9, + '👨🏽‍🫯‍👨🏼': 9, + '👨🏽‍🫯‍👨🏾': 9, + '👨🏽‍🫯‍👨🏿': 9, + '👨🏾‍⚕️': 5, + '👨🏾‍⚖️': 5, + '👨🏾‍✈️': 5, + '👨🏾‍❤️‍👨🏾': 9, + '👨🏾‍❤️‍💋‍👨🏻': 11, + '👨🏾‍❤️‍💋‍👨🏾': 11, + '👨🏾‍❤️‍💋‍👨🏿': 11, + '👨🏾‍🌾': 6, + '👨🏾‍🍳': 6, + '👨🏾‍🍼': 6, + '👨🏾‍🎓': 6, + '👨🏾‍🎤': 6, + '👨🏾‍🎨': 6, + '👨🏾‍🏫': 6, + '👨🏾‍🏭': 6, + '👨🏾‍🐰‍👨🏻': 10, + '👨🏾‍🐰‍👨🏽': 10, + '👨🏾‍🐰‍👨🏿': 10, + '👨🏾‍💻': 6, + '👨🏾‍💼': 6, + '👨🏾‍🔧': 6, + '👨🏾‍🔬': 6, + '👨🏾‍🚀': 6, + '👨🏾‍🚒': 6, + '👨🏾‍🤝‍👨🏻': 10, + '👨🏾‍🤝‍👨🏽': 10, + '👨🏾‍🤝‍👨🏿': 10, + '👨🏾‍🦯': 6, + '👨🏾‍🦯‍➡️': 7, + '👨🏾‍🦰': 6, + '👨🏾‍🦱': 6, + '👨🏾‍🦲': 6, + '👨🏾‍🦳': 6, + '👨🏾‍🦼': 6, + '👨🏾‍🦽': 6, + '👨🏾‍🦽‍➡️': 7, + '👨🏾‍🫯‍👨🏻': 9, + '👨🏾‍🫯‍👨🏼': 9, + '👨🏾‍🫯‍👨🏽': 9, + '👨🏾‍🫯‍👨🏿': 9, + '👨🏿‍⚕️': 5, + '👨🏿‍⚖️': 5, + '👨🏿‍✈️': 5, + '👨🏿‍❤️‍👨🏻': 9, + '👨🏿‍❤️‍👨🏽': 9, + '👨🏿‍❤️‍👨🏿': 9, + '👨🏿‍❤️‍💋‍👨🏻': 11, + '👨🏿‍❤️‍💋‍👨🏼': 11, + '👨🏿‍❤️‍💋‍👨🏽': 11, + '👨🏿‍❤️‍💋‍👨🏾': 11, + '👨🏿‍❤️‍💋‍👨🏿': 11, + '👨🏿‍🌾': 6, + '👨🏿‍🍳': 6, + '👨🏿‍🍼': 6, + '👨🏿‍🎓': 6, + '👨🏿‍🎤': 6, + '👨🏿‍🎨': 6, + '👨🏿‍🏫': 6, + '👨🏿‍🏭': 6, + '👨🏿‍🐰‍👨🏻': 10, + '👨🏿‍🐰‍👨🏼': 10, + '👨🏿‍🐰‍👨🏽': 10, + '👨🏿‍🐰‍👨🏾': 10, + '👨🏿‍💻': 6, + '👨🏿‍💼': 6, + '👨🏿‍🔧': 6, + '👨🏿‍🔬': 6, + '👨🏿‍🚀': 6, + '👨🏿‍🚒': 6, + '👨🏿‍🤝‍👨🏻': 10, + '👨🏿‍🤝‍👨🏼': 10, + '👨🏿‍🤝‍👨🏽': 10, + '👨🏿‍🤝‍👨🏾': 10, + '👨🏿‍🦯': 6, + '👨🏿‍🦯‍➡️': 7, + '👨🏿‍🦰': 6, + '👨🏿‍🦱': 6, + '👨🏿‍🦲': 6, + '👨🏿‍🦳': 6, + '👨🏿‍🦼': 6, + '👨🏿‍🦼‍➡️': 7, + '👨🏿‍🦽': 6, + '👨🏿‍🦽‍➡️': 7, + '👨🏿‍🫯‍👨🏻': 9, + '👨🏿‍🫯‍👨🏼': 9, + '👨🏿‍🫯‍👨🏽': 9, + '👨🏿‍🫯‍👨🏾': 9, + '👩‍⚕️': 3, + '👩‍⚖️': 3, + '👩‍✈️': 3, + '👩‍❤️‍👨': 5, + '👩‍❤️‍👩': 5, + '👩‍❤️‍💋‍👨': 7, + '👩‍❤️‍💋‍👩': 7, + '👩‍🌾': 4, + '👩‍🍳': 4, + '👩‍🍼': 4, + '👩‍🎓': 4, + '👩‍🎤': 4, + '👩‍🎨': 4, + '👩‍🏫': 4, + '👩‍🏭': 4, + '👩‍👦': 4, + '👩‍👦‍👦': 6, + '👩‍👧': 4, + '👩‍👧‍👦': 6, + '👩‍👧‍👧': 6, + '👩‍👩‍👦': 6, + '👩‍👩‍👦‍👦': 8, + '👩‍👩‍👧': 6, + '👩‍👩‍👧‍👦': 8, + '👩‍👩‍👧‍👧': 8, + '👩‍💻': 4, + '👩‍💼': 4, + '👩‍🔧': 4, + '👩‍🔬': 4, + '👩‍🚀': 4, + '👩‍🚒': 4, + '👩‍🦯': 4, + '👩‍🦯‍➡️': 5, + '👩‍🦰': 4, + '👩‍🦱': 4, + '👩‍🦲': 4, + '👩‍🦳': 4, + '👩‍🦼': 4, + '👩‍🦼‍➡️': 5, + '👩‍🦽': 4, + '👩‍🦽‍➡️': 5, + '👩🏻‍⚕️': 5, + '👩🏻‍⚖️': 5, + '👩🏻‍✈️': 5, + '👩🏻‍❤️‍👨🏻': 9, + '👩🏻‍❤️‍👨🏿': 9, + '👩🏻‍❤️‍👩🏻': 9, + '👩🏻‍❤️‍💋‍👨🏻': 11, + '👩🏻‍❤️‍💋‍👨🏽': 11, + '👩🏻‍❤️‍💋‍👨🏿': 11, + '👩🏻‍❤️‍💋‍👩🏻': 11, + '👩🏻‍❤️‍💋‍👩🏽': 11, + '👩🏻‍❤️‍💋‍👩🏿': 11, + '👩🏻‍🌾': 6, + '👩🏻‍🍳': 6, + '👩🏻‍🍼': 6, + '👩🏻‍🎓': 6, + '👩🏻‍🎤': 6, + '👩🏻‍🎨': 6, + '👩🏻‍🏫': 6, + '👩🏻‍🏭': 6, + '👩🏻‍🐰‍👩🏼': 10, + '👩🏻‍🐰‍👩🏽': 10, + '👩🏻‍🐰‍👩🏾': 10, + '👩🏻‍🐰‍👩🏿': 10, + '👩🏻‍💻': 6, + '👩🏻‍💼': 6, + '👩🏻‍🔧': 6, + '👩🏻‍🔬': 6, + '👩🏻‍🚀': 6, + '👩🏻‍🚒': 6, + '👩🏻‍🤝‍👨🏽': 10, + '👩🏻‍🤝‍👨🏿': 10, + '👩🏻‍🤝‍👩🏼': 10, + '👩🏻‍🤝‍👩🏽': 10, + '👩🏻‍🤝‍👩🏾': 10, + '👩🏻‍🤝‍👩🏿': 10, + '👩🏻‍🦯': 6, + '👩🏻‍🦯‍➡️': 7, + '👩🏻‍🦰': 6, + '👩🏻‍🦱': 6, + '👩🏻‍🦲': 6, + '👩🏻‍🦳': 6, + '👩🏻‍🦼': 6, + '👩🏻‍🦼‍➡️': 7, + '👩🏻‍🦽': 6, + '👩🏻‍🦽‍➡️': 7, + '👩🏻‍🫯‍👩🏼': 9, + '👩🏻‍🫯‍👩🏽': 9, + '👩🏻‍🫯‍👩🏾': 9, + '👩🏻‍🫯‍👩🏿': 9, + '👩🏼‍⚕️': 5, + '👩🏼‍⚖️': 5, + '👩🏼‍✈️': 5, + '👩🏼‍❤️‍👨🏼': 9, + '👩🏼‍❤️‍👩🏼': 9, + '👩🏼‍❤️‍💋‍👨🏼': 11, + '👩🏼‍❤️‍💋‍👩🏼': 11, + '👩🏼‍🌾': 6, + '👩🏼‍🍳': 6, + '👩🏼‍🍼': 6, + '👩🏼‍🎓': 6, + '👩🏼‍🎤': 6, + '👩🏼‍🎨': 6, + '👩🏼‍🏫': 6, + '👩🏼‍🏭': 6, + '👩🏼‍🐰‍👩🏻': 10, + '👩🏼‍🐰‍👩🏿': 10, + '👩🏼‍💻': 6, + '👩🏼‍💼': 6, + '👩🏼‍🔧': 6, + '👩🏼‍🔬': 6, + '👩🏼‍🚀': 6, + '👩🏼‍🚒': 6, + '👩🏼‍🤝‍👩🏻': 10, + '👩🏼‍🤝‍👩🏽': 10, + '👩🏼‍🤝‍👩🏿': 10, + '👩🏼‍🦯': 6, + '👩🏼‍🦯‍➡️': 7, + '👩🏼‍🦰': 6, + '👩🏼‍🦱': 6, + '👩🏼‍🦲': 6, + '👩🏼‍🦳': 6, + '👩🏼‍🦼': 6, + '👩🏼‍🦽': 6, + '👩🏼‍🫯‍👩🏻': 9, + '👩🏼‍🫯‍👩🏽': 9, + '👩🏼‍🫯‍👩🏾': 9, + '👩🏼‍🫯‍👩🏿': 9, + '👩🏽‍⚕️': 5, + '👩🏽‍⚖️': 5, + '👩🏽‍✈️': 5, + '👩🏽‍❤️‍👨🏽': 9, + '👩🏽‍❤️‍👩🏽': 9, + '👩🏽‍❤️‍💋‍👨🏻': 11, + '👩🏽‍❤️‍💋‍👨🏽': 11, + '👩🏽‍❤️‍💋‍👨🏿': 11, + '👩🏽‍❤️‍💋‍👩🏻': 11, + '👩🏽‍❤️‍💋‍👩🏽': 11, + '👩🏽‍❤️‍💋‍👩🏿': 11, + '👩🏽‍🌾': 6, + '👩🏽‍🍳': 6, + '👩🏽‍🍼': 6, + '👩🏽‍🎓': 6, + '👩🏽‍🎤': 6, + '👩🏽‍🎨': 6, + '👩🏽‍🏫': 6, + '👩🏽‍🏭': 6, + '👩🏽‍🐰‍👩🏻': 10, + '👩🏽‍🐰‍👩🏾': 10, + '👩🏽‍🐰‍👩🏿': 10, + '👩🏽‍💻': 6, + '👩🏽‍💼': 6, + '👩🏽‍🔧': 6, + '👩🏽‍🔬': 6, + '👩🏽‍🚀': 6, + '👩🏽‍🚒': 6, + '👩🏽‍🤝‍👨🏻': 10, + '👩🏽‍🤝‍👨🏿': 10, + '👩🏽‍🤝‍👩🏻': 10, + '👩🏽‍🤝‍👩🏼': 10, + '👩🏽‍🤝‍👩🏾': 10, + '👩🏽‍🤝‍👩🏿': 10, + '👩🏽‍🦯': 6, + '👩🏽‍🦯‍➡️': 7, + '👩🏽‍🦰': 6, + '👩🏽‍🦱': 6, + '👩🏽‍🦲': 6, + '👩🏽‍🦳': 6, + '👩🏽‍🦼': 6, + '👩🏽‍🦼‍➡️': 7, + '👩🏽‍🦽': 6, + '👩🏽‍🦽‍➡️': 7, + '👩🏽‍🫯‍👩🏻': 9, + '👩🏽‍🫯‍👩🏼': 9, + '👩🏽‍🫯‍👩🏾': 9, + '👩🏽‍🫯‍👩🏿': 9, + '👩🏾‍⚕️': 5, + '👩🏾‍⚖️': 5, + '👩🏾‍✈️': 5, + '👩🏾‍❤️‍👨🏾': 9, + '👩🏾‍❤️‍👩🏾': 9, + '👩🏾‍❤️‍💋‍👨🏾': 11, + '👩🏾‍❤️‍💋‍👩🏾': 11, + '👩🏾‍🌾': 6, + '👩🏾‍🍳': 6, + '👩🏾‍🍼': 6, + '👩🏾‍🎓': 6, + '👩🏾‍🎤': 6, + '👩🏾‍🎨': 6, + '👩🏾‍🏫': 6, + '👩🏾‍🏭': 6, + '👩🏾‍🐰‍👩🏻': 10, + '👩🏾‍🐰‍👩🏽': 10, + '👩🏾‍🐰‍👩🏿': 10, + '👩🏾‍💻': 6, + '👩🏾‍💼': 6, + '👩🏾‍🔧': 6, + '👩🏾‍🔬': 6, + '👩🏾‍🚀': 6, + '👩🏾‍🚒': 6, + '👩🏾‍🤝‍👩🏻': 10, + '👩🏾‍🤝‍👩🏽': 10, + '👩🏾‍🤝‍👩🏿': 10, + '👩🏾‍🦯': 6, + '👩🏾‍🦯‍➡️': 7, + '👩🏾‍🦰': 6, + '👩🏾‍🦱': 6, + '👩🏾‍🦲': 6, + '👩🏾‍🦳': 6, + '👩🏾‍🦼': 6, + '👩🏾‍🦽': 6, + '👩🏾‍🦽‍➡️': 7, + '👩🏾‍🫯‍👩🏻': 9, + '👩🏾‍🫯‍👩🏼': 9, + '👩🏾‍🫯‍👩🏽': 9, + '👩🏾‍🫯‍👩🏿': 9, + '👩🏿‍⚕️': 5, + '👩🏿‍⚖️': 5, + '👩🏿‍✈️': 5, + '👩🏿‍❤️‍👨🏻': 9, + '👩🏿‍❤️‍👨🏿': 9, + '👩🏿‍❤️‍👩🏿': 9, + '👩🏿‍❤️‍💋‍👨🏻': 11, + '👩🏿‍❤️‍💋‍👨🏽': 11, + '👩🏿‍❤️‍💋‍👨🏿': 11, + '👩🏿‍❤️‍💋‍👩🏻': 11, + '👩🏿‍❤️‍💋‍👩🏽': 11, + '👩🏿‍❤️‍💋‍👩🏿': 11, + '👩🏿‍🌾': 6, + '👩🏿‍🍳': 6, + '👩🏿‍🍼': 6, + '👩🏿‍🎓': 6, + '👩🏿‍🎤': 6, + '👩🏿‍🎨': 6, + '👩🏿‍🏫': 6, + '👩🏿‍🏭': 6, + '👩🏿‍🐰‍👩🏻': 10, + '👩🏿‍🐰‍👩🏼': 10, + '👩🏿‍🐰‍👩🏽': 10, + '👩🏿‍🐰‍👩🏾': 10, + '👩🏿‍💻': 6, + '👩🏿‍💼': 6, + '👩🏿‍🔧': 6, + '👩🏿‍🔬': 6, + '👩🏿‍🚀': 6, + '👩🏿‍🚒': 6, + '👩🏿‍🤝‍👨🏻': 10, + '👩🏿‍🤝‍👨🏽': 10, + '👩🏿‍🤝‍👩🏻': 10, + '👩🏿‍🤝‍👩🏼': 10, + '👩🏿‍🤝‍👩🏽': 10, + '👩🏿‍🤝‍👩🏾': 10, + '👩🏿‍🦯': 6, + '👩🏿‍🦯‍➡️': 7, + '👩🏿‍🦰': 6, + '👩🏿‍🦱': 6, + '👩🏿‍🦲': 6, + '👩🏿‍🦳': 6, + '👩🏿‍🦼': 6, + '👩🏿‍🦼‍➡️': 7, + '👩🏿‍🦽': 6, + '👩🏿‍🦽‍➡️': 7, + '👩🏿‍🫯‍👩🏻': 9, + '👩🏿‍🫯‍👩🏼': 9, + '👩🏿‍🫯‍👩🏽': 9, + '👩🏿‍🫯‍👩🏾': 9, + '👮‍♀️': 3, + '👮‍♂️': 3, + '👮🏻‍♀️': 5, + '👮🏻‍♂️': 5, + '👮🏼‍♀️': 5, + '👮🏼‍♂️': 5, + '👮🏽‍♀️': 5, + '👮🏽‍♂️': 5, + '👮🏾‍♀️': 5, + '👮🏾‍♂️': 5, + '👮🏿‍♀️': 5, + '👮🏿‍♂️': 5, + '👯‍♀️': 3, + '👯‍♂️': 3, + '👯🏻‍♀️': 5, + '👯🏻‍♂️': 5, + '👯🏼‍♀️': 5, + '👯🏼‍♂️': 5, + '👯🏽‍♀️': 5, + '👯🏽‍♂️': 5, + '👯🏾‍♀️': 5, + '👯🏾‍♂️': 5, + '👯🏿‍♀️': 5, + '👯🏿‍♂️': 5, + '👰‍♀️': 3, + '👰‍♂️': 3, + '👰🏻‍♀️': 5, + '👰🏻‍♂️': 5, + '👰🏼‍♀️': 5, + '👰🏼‍♂️': 5, + '👰🏽‍♀️': 5, + '👰🏽‍♂️': 5, + '👰🏾‍♀️': 5, + '👰🏾‍♂️': 5, + '👰🏿‍♀️': 5, + '👰🏿‍♂️': 5, + '👱‍♀️': 3, + '👱‍♂️': 3, + '👱🏻‍♀️': 5, + '👱🏻‍♂️': 5, + '👱🏼‍♀️': 5, + '👱🏼‍♂️': 5, + '👱🏽‍♀️': 5, + '👱🏽‍♂️': 5, + '👱🏾‍♀️': 5, + '👱🏾‍♂️': 5, + '👱🏿‍♀️': 5, + '👱🏿‍♂️': 5, + '👳‍♀️': 3, + '👳‍♂️': 3, + '👳🏻‍♀️': 5, + '👳🏻‍♂️': 5, + '👳🏼‍♀️': 5, + '👳🏼‍♂️': 5, + '👳🏽‍♀️': 5, + '👳🏽‍♂️': 5, + '👳🏾‍♀️': 5, + '👳🏾‍♂️': 5, + '👳🏿‍♀️': 5, + '👳🏿‍♂️': 5, + '👷‍♀️': 3, + '👷‍♂️': 3, + '👷🏻‍♀️': 5, + '👷🏻‍♂️': 5, + '👷🏼‍♀️': 5, + '👷🏼‍♂️': 5, + '👷🏽‍♀️': 5, + '👷🏽‍♂️': 5, + '👷🏾‍♀️': 5, + '👷🏾‍♂️': 5, + '👷🏿‍♀️': 5, + '👷🏿‍♂️': 5, + '💁‍♀️': 3, + '💁‍♂️': 3, + '💁🏻‍♀️': 5, + '💁🏻‍♂️': 5, + '💁🏼‍♀️': 5, + '💁🏼‍♂️': 5, + '💁🏽‍♀️': 5, + '💁🏽‍♂️': 5, + '💁🏾‍♀️': 5, + '💁🏾‍♂️': 5, + '💁🏿‍♀️': 5, + '💁🏿‍♂️': 5, + '💂‍♀️': 3, + '💂‍♂️': 3, + '💂🏻‍♀️': 5, + '💂🏻‍♂️': 5, + '💂🏼‍♀️': 5, + '💂🏼‍♂️': 5, + '💂🏽‍♀️': 5, + '💂🏽‍♂️': 5, + '💂🏾‍♀️': 5, + '💂🏾‍♂️': 5, + '💂🏿‍♀️': 5, + '💂🏿‍♂️': 5, + '💆‍♀️': 3, + '💆‍♂️': 3, + '💆🏻‍♀️': 5, + '💆🏻‍♂️': 5, + '💆🏼‍♀️': 5, + '💆🏼‍♂️': 5, + '💆🏽‍♀️': 5, + '💆🏽‍♂️': 5, + '💆🏾‍♀️': 5, + '💆🏾‍♂️': 5, + '💆🏿‍♀️': 5, + '💆🏿‍♂️': 5, + '💇‍♀️': 3, + '💇‍♂️': 3, + '💇🏻‍♀️': 5, + '💇🏻‍♂️': 5, + '💇🏼‍♀️': 5, + '💇🏼‍♂️': 5, + '💇🏽‍♀️': 5, + '💇🏽‍♂️': 5, + '💇🏾‍♀️': 5, + '💇🏾‍♂️': 5, + '💇🏿‍♀️': 5, + '💇🏿‍♂️': 5, + '🕵🏻‍♀️': 4, + '🕵🏻‍♂️': 4, + '🕵🏼‍♀️': 4, + '🕵🏼‍♂️': 4, + '🕵🏽‍♀️': 4, + '🕵🏽‍♂️': 4, + '🕵🏾‍♀️': 4, + '🕵🏾‍♂️': 4, + '🕵🏿‍♀️': 4, + '🕵🏿‍♂️': 4, + '😮‍💨': 4, + '😵‍💫': 4, + '😶‍🌫️': 3, + '🙂‍↔️': 3, + '🙂‍↕️': 3, + '🙅‍♀️': 3, + '🙅‍♂️': 3, + '🙅🏻‍♀️': 5, + '🙅🏻‍♂️': 5, + '🙅🏼‍♀️': 5, + '🙅🏼‍♂️': 5, + '🙅🏽‍♀️': 5, + '🙅🏽‍♂️': 5, + '🙅🏾‍♀️': 5, + '🙅🏾‍♂️': 5, + '🙅🏿‍♀️': 5, + '🙅🏿‍♂️': 5, + '🙆‍♀️': 3, + '🙆‍♂️': 3, + '🙆🏻‍♀️': 5, + '🙆🏻‍♂️': 5, + '🙆🏼‍♀️': 5, + '🙆🏼‍♂️': 5, + '🙆🏽‍♀️': 5, + '🙆🏽‍♂️': 5, + '🙆🏾‍♀️': 5, + '🙆🏾‍♂️': 5, + '🙆🏿‍♀️': 5, + '🙆🏿‍♂️': 5, + '🙇‍♀️': 3, + '🙇‍♂️': 3, + '🙇🏻‍♀️': 5, + '🙇🏻‍♂️': 5, + '🙇🏼‍♀️': 5, + '🙇🏼‍♂️': 5, + '🙇🏽‍♀️': 5, + '🙇🏽‍♂️': 5, + '🙇🏾‍♀️': 5, + '🙇🏾‍♂️': 5, + '🙇🏿‍♀️': 5, + '🙇🏿‍♂️': 5, + '🙋‍♀️': 3, + '🙋‍♂️': 3, + '🙋🏻‍♀️': 5, + '🙋🏻‍♂️': 5, + '🙋🏼‍♀️': 5, + '🙋🏼‍♂️': 5, + '🙋🏽‍♀️': 5, + '🙋🏽‍♂️': 5, + '🙋🏾‍♀️': 5, + '🙋🏾‍♂️': 5, + '🙋🏿‍♀️': 5, + '🙋🏿‍♂️': 5, + '🙍‍♀️': 3, + '🙍‍♂️': 3, + '🙍🏻‍♀️': 5, + '🙍🏻‍♂️': 5, + '🙍🏼‍♀️': 5, + '🙍🏼‍♂️': 5, + '🙍🏽‍♀️': 5, + '🙍🏽‍♂️': 5, + '🙍🏾‍♀️': 5, + '🙍🏾‍♂️': 5, + '🙍🏿‍♀️': 5, + '🙍🏿‍♂️': 5, + '🙎‍♀️': 3, + '🙎‍♂️': 3, + '🙎🏻‍♀️': 5, + '🙎🏻‍♂️': 5, + '🙎🏼‍♀️': 5, + '🙎🏼‍♂️': 5, + '🙎🏽‍♀️': 5, + '🙎🏽‍♂️': 5, + '🙎🏾‍♀️': 5, + '🙎🏾‍♂️': 5, + '🙎🏿‍♀️': 5, + '🙎🏿‍♂️': 5, + '🚣‍♀️': 3, + '🚣‍♂️': 3, + '🚣🏻‍♀️': 5, + '🚣🏻‍♂️': 5, + '🚣🏼‍♀️': 5, + '🚣🏼‍♂️': 5, + '🚣🏽‍♀️': 5, + '🚣🏽‍♂️': 5, + '🚣🏾‍♀️': 5, + '🚣🏾‍♂️': 5, + '🚣🏿‍♀️': 5, + '🚣🏿‍♂️': 5, + '🚴‍♀️': 3, + '🚴‍♂️': 3, + '🚴🏻‍♀️': 5, + '🚴🏻‍♂️': 5, + '🚴🏼‍♀️': 5, + '🚴🏼‍♂️': 5, + '🚴🏽‍♀️': 5, + '🚴🏽‍♂️': 5, + '🚴🏾‍♀️': 5, + '🚴🏾‍♂️': 5, + '🚴🏿‍♀️': 5, + '🚴🏿‍♂️': 5, + '🚵‍♀️': 3, + '🚵‍♂️': 3, + '🚵🏻‍♀️': 5, + '🚵🏻‍♂️': 5, + '🚵🏼‍♀️': 5, + '🚵🏼‍♂️': 5, + '🚵🏽‍♀️': 5, + '🚵🏽‍♂️': 5, + '🚵🏾‍♀️': 5, + '🚵🏾‍♂️': 5, + '🚵🏿‍♀️': 5, + '🚵🏿‍♂️': 5, + '🚶‍♀️': 3, + '🚶‍♀️‍➡️': 4, + '🚶‍♂️': 3, + '🚶‍♂️‍➡️': 4, + '🚶‍➡️': 3, + '🚶🏻‍♀️': 5, + '🚶🏻‍♀️‍➡️': 6, + '🚶🏻‍♂️': 5, + '🚶🏻‍♂️‍➡️': 6, + '🚶🏻‍➡️': 5, + '🚶🏼‍♀️': 5, + '🚶🏼‍♀️‍➡️': 6, + '🚶🏼‍♂️': 5, + '🚶🏼‍♂️‍➡️': 6, + '🚶🏼‍➡️': 5, + '🚶🏽‍♀️': 5, + '🚶🏽‍♀️‍➡️': 6, + '🚶🏽‍♂️': 5, + '🚶🏽‍♂️‍➡️': 6, + '🚶🏽‍➡️': 5, + '🚶🏾‍♀️': 5, + '🚶🏾‍♀️‍➡️': 6, + '🚶🏾‍♂️': 5, + '🚶🏾‍♂️‍➡️': 6, + '🚶🏾‍➡️': 5, + '🚶🏿‍♀️': 5, + '🚶🏿‍♀️‍➡️': 6, + '🚶🏿‍♂️': 5, + '🚶🏿‍♂️‍➡️': 6, + '🚶🏿‍➡️': 5, + '🤦‍♀️': 3, + '🤦‍♂️': 3, + '🤦🏻‍♀️': 5, + '🤦🏻‍♂️': 5, + '🤦🏼‍♀️': 5, + '🤦🏼‍♂️': 5, + '🤦🏽‍♀️': 5, + '🤦🏽‍♂️': 5, + '🤦🏾‍♀️': 5, + '🤦🏾‍♂️': 5, + '🤦🏿‍♀️': 5, + '🤦🏿‍♂️': 5, + '🤵‍♀️': 3, + '🤵‍♂️': 3, + '🤵🏻‍♀️': 5, + '🤵🏻‍♂️': 5, + '🤵🏼‍♀️': 5, + '🤵🏼‍♂️': 5, + '🤵🏽‍♀️': 5, + '🤵🏽‍♂️': 5, + '🤵🏾‍♀️': 5, + '🤵🏾‍♂️': 5, + '🤵🏿‍♀️': 5, + '🤵🏿‍♂️': 5, + '🤷‍♀️': 3, + '🤷‍♂️': 3, + '🤷🏻‍♀️': 5, + '🤷🏻‍♂️': 5, + '🤷🏼‍♀️': 5, + '🤷🏼‍♂️': 5, + '🤷🏽‍♀️': 5, + '🤷🏽‍♂️': 5, + '🤷🏾‍♀️': 5, + '🤷🏾‍♂️': 5, + '🤷🏿‍♀️': 5, + '🤷🏿‍♂️': 5, + '🤸‍♀️': 3, + '🤸‍♂️': 3, + '🤸🏻‍♀️': 5, + '🤸🏻‍♂️': 5, + '🤸🏼‍♀️': 5, + '🤸🏼‍♂️': 5, + '🤸🏽‍♀️': 5, + '🤸🏽‍♂️': 5, + '🤸🏾‍♀️': 5, + '🤸🏾‍♂️': 5, + '🤸🏿‍♀️': 5, + '🤸🏿‍♂️': 5, + '🤹‍♀️': 3, + '🤹‍♂️': 3, + '🤹🏻‍♀️': 5, + '🤹🏻‍♂️': 5, + '🤹🏼‍♀️': 5, + '🤹🏼‍♂️': 5, + '🤹🏽‍♀️': 5, + '🤹🏽‍♂️': 5, + '🤹🏾‍♀️': 5, + '🤹🏾‍♂️': 5, + '🤹🏿‍♀️': 5, + '🤹🏿‍♂️': 5, + '🤼‍♀️': 3, + '🤼‍♂️': 3, + '🤼🏻‍♀️': 5, + '🤼🏻‍♂️': 5, + '🤼🏼‍♀️': 5, + '🤼🏼‍♂️': 5, + '🤼🏽‍♀️': 5, + '🤼🏽‍♂️': 5, + '🤼🏾‍♀️': 5, + '🤼🏾‍♂️': 5, + '🤼🏿‍♀️': 5, + '🤼🏿‍♂️': 5, + '🤽‍♀️': 3, + '🤽‍♂️': 3, + '🤽🏻‍♀️': 5, + '🤽🏻‍♂️': 5, + '🤽🏼‍♀️': 5, + '🤽🏼‍♂️': 5, + '🤽🏽‍♀️': 5, + '🤽🏽‍♂️': 5, + '🤽🏾‍♀️': 5, + '🤽🏾‍♂️': 5, + '🤽🏿‍♀️': 5, + '🤽🏿‍♂️': 5, + '🤾‍♀️': 3, + '🤾‍♂️': 3, + '🤾🏻‍♀️': 5, + '🤾🏻‍♂️': 5, + '🤾🏼‍♀️': 5, + '🤾🏼‍♂️': 5, + '🤾🏽‍♀️': 5, + '🤾🏽‍♂️': 5, + '🤾🏾‍♀️': 5, + '🤾🏾‍♂️': 5, + '🤾🏿‍♀️': 5, + '🤾🏿‍♂️': 5, + '🦸‍♀️': 3, + '🦸‍♂️': 3, + '🦸🏻‍♀️': 5, + '🦸🏻‍♂️': 5, + '🦸🏼‍♀️': 5, + '🦸🏼‍♂️': 5, + '🦸🏽‍♀️': 5, + '🦸🏽‍♂️': 5, + '🦸🏾‍♀️': 5, + '🦸🏾‍♂️': 5, + '🦸🏿‍♀️': 5, + '🦸🏿‍♂️': 5, + '🦹‍♀️': 3, + '🦹‍♂️': 3, + '🦹🏻‍♀️': 5, + '🦹🏻‍♂️': 5, + '🦹🏼‍♀️': 5, + '🦹🏼‍♂️': 5, + '🦹🏽‍♀️': 5, + '🦹🏽‍♂️': 5, + '🦹🏾‍♀️': 5, + '🦹🏾‍♂️': 5, + '🦹🏿‍♀️': 5, + '🦹🏿‍♂️': 5, + '🧍‍♀️': 3, + '🧍‍♂️': 3, + '🧍🏻‍♀️': 5, + '🧍🏻‍♂️': 5, + '🧍🏼‍♀️': 5, + '🧍🏼‍♂️': 5, + '🧍🏽‍♀️': 5, + '🧍🏽‍♂️': 5, + '🧍🏾‍♀️': 5, + '🧍🏾‍♂️': 5, + '🧍🏿‍♀️': 5, + '🧍🏿‍♂️': 5, + '🧎‍♀️': 3, + '🧎‍♀️‍➡️': 4, + '🧎‍♂️': 3, + '🧎‍♂️‍➡️': 4, + '🧎‍➡️': 3, + '🧎🏻‍♀️': 5, + '🧎🏻‍♀️‍➡️': 6, + '🧎🏻‍♂️': 5, + '🧎🏻‍♂️‍➡️': 6, + '🧎🏻‍➡️': 5, + '🧎🏼‍♀️': 5, + '🧎🏼‍♀️‍➡️': 6, + '🧎🏼‍♂️': 5, + '🧎🏼‍♂️‍➡️': 6, + '🧎🏼‍➡️': 5, + '🧎🏽‍♀️': 5, + '🧎🏽‍♀️‍➡️': 6, + '🧎🏽‍♂️': 5, + '🧎🏽‍♂️‍➡️': 6, + '🧎🏽‍➡️': 5, + '🧎🏾‍♀️': 5, + '🧎🏾‍♀️‍➡️': 6, + '🧎🏾‍♂️': 5, + '🧎🏾‍♂️‍➡️': 6, + '🧎🏾‍➡️': 5, + '🧎🏿‍♀️': 5, + '🧎🏿‍♀️‍➡️': 6, + '🧎🏿‍♂️': 5, + '🧎🏿‍♂️‍➡️': 6, + '🧎🏿‍➡️': 5, + '🧏‍♀️': 3, + '🧏‍♂️': 3, + '🧏🏻‍♀️': 5, + '🧏🏻‍♂️': 5, + '🧏🏼‍♀️': 5, + '🧏🏼‍♂️': 5, + '🧏🏽‍♀️': 5, + '🧏🏽‍♂️': 5, + '🧏🏾‍♀️': 5, + '🧏🏾‍♂️': 5, + '🧏🏿‍♀️': 5, + '🧏🏿‍♂️': 5, + '🧑‍⚕️': 3, + '🧑‍⚖️': 3, + '🧑‍✈️': 3, + '🧑‍🌾': 4, + '🧑‍🍳': 4, + '🧑‍🍼': 4, + '🧑‍🎄': 4, + '🧑‍🎓': 4, + '🧑‍🎤': 4, + '🧑‍🎨': 4, + '🧑‍🏫': 4, + '🧑‍🏭': 4, + '🧑‍💻': 4, + '🧑‍💼': 4, + '🧑‍🔧': 4, + '🧑‍🔬': 4, + '🧑‍🚀': 4, + '🧑‍🚒': 4, + '🧑‍🤝‍🧑': 6, + '🧑‍🦯': 4, + '🧑‍🦯‍➡️': 5, + '🧑‍🦰': 4, + '🧑‍🦱': 4, + '🧑‍🦲': 4, + '🧑‍🦳': 4, + '🧑‍🦼': 4, + '🧑‍🦼‍➡️': 5, + '🧑‍🦽': 4, + '🧑‍🦽‍➡️': 5, + '🧑‍🧑‍🧒': 6, + '🧑‍🧑‍🧒‍🧒': 8, + '🧑‍🧒': 4, + '🧑‍🧒‍🧒': 6, + '🧑‍🩰': 4, + '🧑🏻‍⚕️': 5, + '🧑🏻‍⚖️': 5, + '🧑🏻‍✈️': 5, + '🧑🏻‍❤️‍💋‍🧑🏿': 11, + '🧑🏻‍🌾': 6, + '🧑🏻‍🍳': 6, + '🧑🏻‍🍼': 6, + '🧑🏻‍🎄': 6, + '🧑🏻‍🎓': 6, + '🧑🏻‍🎤': 6, + '🧑🏻‍🎨': 6, + '🧑🏻‍🏫': 6, + '🧑🏻‍🏭': 6, + '🧑🏻‍🐰‍🧑🏽': 10, + '🧑🏻‍🐰‍🧑🏾': 10, + '🧑🏻‍🐰‍🧑🏿': 10, + '🧑🏻‍💻': 6, + '🧑🏻‍💼': 6, + '🧑🏻‍🔧': 6, + '🧑🏻‍🔬': 6, + '🧑🏻‍🚀': 6, + '🧑🏻‍🚒': 6, + '🧑🏻‍🤝‍🧑🏻': 10, + '🧑🏻‍🤝‍🧑🏼': 10, + '🧑🏻‍🤝‍🧑🏽': 10, + '🧑🏻‍🤝‍🧑🏾': 10, + '🧑🏻‍🤝‍🧑🏿': 10, + '🧑🏻‍🦯': 6, + '🧑🏻‍🦯‍➡️': 7, + '🧑🏻‍🦰': 6, + '🧑🏻‍🦱': 6, + '🧑🏻‍🦲': 6, + '🧑🏻‍🦳': 6, + '🧑🏻‍🦼': 6, + '🧑🏻‍🦼‍➡️': 7, + '🧑🏻‍🦽': 6, + '🧑🏻‍🦽‍➡️': 7, + '🧑🏻‍🩰': 6, + '🧑🏻‍🫯‍🧑🏼': 9, + '🧑🏻‍🫯‍🧑🏽': 9, + '🧑🏻‍🫯‍🧑🏾': 9, + '🧑🏻‍🫯‍🧑🏿': 9, + '🧑🏼‍⚕️': 5, + '🧑🏼‍⚖️': 5, + '🧑🏼‍✈️': 5, + '🧑🏼‍🌾': 6, + '🧑🏼‍🍳': 6, + '🧑🏼‍🍼': 6, + '🧑🏼‍🎄': 6, + '🧑🏼‍🎓': 6, + '🧑🏼‍🎤': 6, + '🧑🏼‍🎨': 6, + '🧑🏼‍🏫': 6, + '🧑🏼‍🏭': 6, + '🧑🏼‍🐰‍🧑🏿': 10, + '🧑🏼‍💻': 6, + '🧑🏼‍💼': 6, + '🧑🏼‍🔧': 6, + '🧑🏼‍🔬': 6, + '🧑🏼‍🚀': 6, + '🧑🏼‍🚒': 6, + '🧑🏼‍🤝‍🧑🏻': 10, + '🧑🏼‍🤝‍🧑🏼': 10, + '🧑🏼‍🤝‍🧑🏽': 10, + '🧑🏼‍🤝‍🧑🏿': 10, + '🧑🏼‍🦯': 6, + '🧑🏼‍🦯‍➡️': 7, + '🧑🏼‍🦰': 6, + '🧑🏼‍🦱': 6, + '🧑🏼‍🦲': 6, + '🧑🏼‍🦳': 6, + '🧑🏼‍🦼': 6, + '🧑🏼‍🦽': 6, + '🧑🏼‍🩰': 6, + '🧑🏼‍🫯‍🧑🏻': 9, + '🧑🏼‍🫯‍🧑🏽': 9, + '🧑🏼‍🫯‍🧑🏿': 9, + '🧑🏽‍⚕️': 5, + '🧑🏽‍⚖️': 5, + '🧑🏽‍✈️': 5, + '🧑🏽‍❤️‍💋‍🧑🏿': 11, + '🧑🏽‍🌾': 6, + '🧑🏽‍🍳': 6, + '🧑🏽‍🍼': 6, + '🧑🏽‍🎄': 6, + '🧑🏽‍🎓': 6, + '🧑🏽‍🎤': 6, + '🧑🏽‍🎨': 6, + '🧑🏽‍🏫': 6, + '🧑🏽‍🏭': 6, + '🧑🏽‍🐰‍🧑🏻': 10, + '🧑🏽‍🐰‍🧑🏿': 10, + '🧑🏽‍💻': 6, + '🧑🏽‍💼': 6, + '🧑🏽‍🔧': 6, + '🧑🏽‍🔬': 6, + '🧑🏽‍🚀': 6, + '🧑🏽‍🚒': 6, + '🧑🏽‍🤝‍🧑🏻': 10, + '🧑🏽‍🤝‍🧑🏼': 10, + '🧑🏽‍🤝‍🧑🏽': 10, + '🧑🏽‍🤝‍🧑🏾': 10, + '🧑🏽‍🤝‍🧑🏿': 10, + '🧑🏽‍🦯': 6, + '🧑🏽‍🦯‍➡️': 7, + '🧑🏽‍🦰': 6, + '🧑🏽‍🦱': 6, + '🧑🏽‍🦲': 6, + '🧑🏽‍🦳': 6, + '🧑🏽‍🦼': 6, + '🧑🏽‍🦼‍➡️': 7, + '🧑🏽‍🦽': 6, + '🧑🏽‍🦽‍➡️': 7, + '🧑🏽‍🩰': 6, + '🧑🏽‍🫯‍🧑🏻': 9, + '🧑🏽‍🫯‍🧑🏼': 9, + '🧑🏽‍🫯‍🧑🏾': 9, + '🧑🏽‍🫯‍🧑🏿': 9, + '🧑🏾‍⚕️': 5, + '🧑🏾‍⚖️': 5, + '🧑🏾‍✈️': 5, + '🧑🏾‍🌾': 6, + '🧑🏾‍🍳': 6, + '🧑🏾‍🍼': 6, + '🧑🏾‍🎄': 6, + '🧑🏾‍🎓': 6, + '🧑🏾‍🎤': 6, + '🧑🏾‍🎨': 6, + '🧑🏾‍🏫': 6, + '🧑🏾‍🏭': 6, + '🧑🏾‍🐰‍🧑🏻': 10, + '🧑🏾‍🐰‍🧑🏿': 10, + '🧑🏾‍💻': 6, + '🧑🏾‍💼': 6, + '🧑🏾‍🔧': 6, + '🧑🏾‍🔬': 6, + '🧑🏾‍🚀': 6, + '🧑🏾‍🚒': 6, + '🧑🏾‍🤝‍🧑🏻': 10, + '🧑🏾‍🤝‍🧑🏽': 10, + '🧑🏾‍🤝‍🧑🏾': 10, + '🧑🏾‍🤝‍🧑🏿': 10, + '🧑🏾‍🦯': 6, + '🧑🏾‍🦯‍➡️': 7, + '🧑🏾‍🦰': 6, + '🧑🏾‍🦱': 6, + '🧑🏾‍🦲': 6, + '🧑🏾‍🦳': 6, + '🧑🏾‍🦼': 6, + '🧑🏾‍🦽': 6, + '🧑🏾‍🩰': 6, + '🧑🏾‍🫯‍🧑🏻': 9, + '🧑🏾‍🫯‍🧑🏽': 9, + '🧑🏾‍🫯‍🧑🏿': 9, + '🧑🏿‍⚕️': 5, + '🧑🏿‍⚖️': 5, + '🧑🏿‍✈️': 5, + '🧑🏿‍❤️‍💋‍🧑🏻': 11, + '🧑🏿‍❤️‍💋‍🧑🏽': 11, + '🧑🏿‍🌾': 6, + '🧑🏿‍🍳': 6, + '🧑🏿‍🍼': 6, + '🧑🏿‍🎄': 6, + '🧑🏿‍🎓': 6, + '🧑🏿‍🎤': 6, + '🧑🏿‍🎨': 6, + '🧑🏿‍🏫': 6, + '🧑🏿‍🏭': 6, + '🧑🏿‍🐰‍🧑🏻': 10, + '🧑🏿‍🐰‍🧑🏼': 10, + '🧑🏿‍🐰‍🧑🏽': 10, + '🧑🏿‍🐰‍🧑🏾': 10, + '🧑🏿‍💻': 6, + '🧑🏿‍💼': 6, + '🧑🏿‍🔧': 6, + '🧑🏿‍🔬': 6, + '🧑🏿‍🚀': 6, + '🧑🏿‍🚒': 6, + '🧑🏿‍🤝‍🧑🏻': 10, + '🧑🏿‍🤝‍🧑🏼': 10, + '🧑🏿‍🤝‍🧑🏽': 10, + '🧑🏿‍🤝‍🧑🏾': 10, + '🧑🏿‍🤝‍🧑🏿': 10, + '🧑🏿‍🦯': 6, + '🧑🏿‍🦯‍➡️': 7, + '🧑🏿‍🦰': 6, + '🧑🏿‍🦱': 6, + '🧑🏿‍🦲': 6, + '🧑🏿‍🦳': 6, + '🧑🏿‍🦼': 6, + '🧑🏿‍🦼‍➡️': 7, + '🧑🏿‍🦽': 6, + '🧑🏿‍🦽‍➡️': 7, + '🧑🏿‍🩰': 6, + '🧑🏿‍🫯‍🧑🏻': 9, + '🧑🏿‍🫯‍🧑🏼': 9, + '🧑🏿‍🫯‍🧑🏽': 9, + '🧑🏿‍🫯‍🧑🏾': 9, + '🧔‍♀️': 3, + '🧔‍♂️': 3, + '🧔🏻‍♀️': 5, + '🧔🏻‍♂️': 5, + '🧔🏼‍♀️': 5, + '🧔🏼‍♂️': 5, + '🧔🏽‍♀️': 5, + '🧔🏽‍♂️': 5, + '🧔🏾‍♀️': 5, + '🧔🏾‍♂️': 5, + '🧔🏿‍♀️': 5, + '🧔🏿‍♂️': 5, + '🧖‍♀️': 3, + '🧖‍♂️': 3, + '🧖🏻‍♀️': 5, + '🧖🏻‍♂️': 5, + '🧖🏼‍♀️': 5, + '🧖🏼‍♂️': 5, + '🧖🏽‍♀️': 5, + '🧖🏽‍♂️': 5, + '🧖🏾‍♀️': 5, + '🧖🏾‍♂️': 5, + '🧖🏿‍♀️': 5, + '🧖🏿‍♂️': 5, + '🧗‍♀️': 3, + '🧗‍♂️': 3, + '🧗🏻‍♀️': 5, + '🧗🏻‍♂️': 5, + '🧗🏼‍♀️': 5, + '🧗🏼‍♂️': 5, + '🧗🏽‍♀️': 5, + '🧗🏽‍♂️': 5, + '🧗🏾‍♀️': 5, + '🧗🏾‍♂️': 5, + '🧗🏿‍♀️': 5, + '🧗🏿‍♂️': 5, + '🧘‍♀️': 3, + '🧘‍♂️': 3, + '🧘🏻‍♀️': 5, + '🧘🏻‍♂️': 5, + '🧘🏼‍♀️': 5, + '🧘🏼‍♂️': 5, + '🧘🏽‍♀️': 5, + '🧘🏽‍♂️': 5, + '🧘🏾‍♀️': 5, + '🧘🏾‍♂️': 5, + '🧘🏿‍♀️': 5, + '🧘🏿‍♂️': 5, + '🧙‍♀️': 3, + '🧙‍♂️': 3, + '🧙🏻‍♀️': 5, + '🧙🏻‍♂️': 5, + '🧙🏼‍♀️': 5, + '🧙🏼‍♂️': 5, + '🧙🏽‍♀️': 5, + '🧙🏽‍♂️': 5, + '🧙🏾‍♀️': 5, + '🧙🏾‍♂️': 5, + '🧙🏿‍♀️': 5, + '🧙🏿‍♂️': 5, + '🧚‍♀️': 3, + '🧚‍♂️': 3, + '🧚🏻‍♀️': 5, + '🧚🏻‍♂️': 5, + '🧚🏼‍♀️': 5, + '🧚🏼‍♂️': 5, + '🧚🏽‍♀️': 5, + '🧚🏽‍♂️': 5, + '🧚🏾‍♀️': 5, + '🧚🏾‍♂️': 5, + '🧚🏿‍♀️': 5, + '🧚🏿‍♂️': 5, + '🧛‍♀️': 3, + '🧛‍♂️': 3, + '🧛🏻‍♀️': 5, + '🧛🏻‍♂️': 5, + '🧛🏼‍♀️': 5, + '🧛🏼‍♂️': 5, + '🧛🏽‍♀️': 5, + '🧛🏽‍♂️': 5, + '🧛🏾‍♀️': 5, + '🧛🏾‍♂️': 5, + '🧛🏿‍♀️': 5, + '🧛🏿‍♂️': 5, + '🧜‍♀️': 3, + '🧜‍♂️': 3, + '🧜🏻‍♀️': 5, + '🧜🏻‍♂️': 5, + '🧜🏼‍♀️': 5, + '🧜🏼‍♂️': 5, + '🧜🏽‍♀️': 5, + '🧜🏽‍♂️': 5, + '🧜🏾‍♀️': 5, + '🧜🏾‍♂️': 5, + '🧜🏿‍♀️': 5, + '🧜🏿‍♂️': 5, + '🧝‍♀️': 3, + '🧝‍♂️': 3, + '🧝🏻‍♀️': 5, + '🧝🏻‍♂️': 5, + '🧝🏼‍♀️': 5, + '🧝🏼‍♂️': 5, + '🧝🏽‍♀️': 5, + '🧝🏽‍♂️': 5, + '🧝🏾‍♀️': 5, + '🧝🏾‍♂️': 5, + '🧝🏿‍♀️': 5, + '🧝🏿‍♂️': 5, + '🧞‍♀️': 3, + '🧞‍♂️': 3, + '🧟‍♀️': 3, + '🧟‍♂️': 3, + '🫱🏻‍🫲🏼': 6, + '🫱🏻‍🫲🏽': 6, + '🫱🏻‍🫲🏾': 6, + '🫱🏻‍🫲🏿': 6, + '🫱🏼‍🫲🏻': 6, + '🫱🏼‍🫲🏽': 6, + '🫱🏼‍🫲🏾': 6, + '🫱🏼‍🫲🏿': 6, + '🫱🏽‍🫲🏻': 6, + '🫱🏽‍🫲🏼': 6, + '🫱🏽‍🫲🏾': 6, + '🫱🏽‍🫲🏿': 6, + '🫱🏾‍🫲🏻': 6, + '🫱🏾‍🫲🏼': 6, + '🫱🏾‍🫲🏽': 6, + '🫱🏾‍🫲🏿': 6, + '🫱🏿‍🫲🏻': 6, + '🫱🏿‍🫲🏼': 6, + '🫱🏿‍🫲🏽': 6, + '🫱🏿‍🫲🏾': 6, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_fcc05a0f.py b/libs/wcwidth/table_grapheme_overrides/_known_fcc05a0f.py new file mode 100644 index 000000000..fbcb7b45a --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_fcc05a0f.py @@ -0,0 +1,6648 @@ +""" +Grapheme overrides for _known_fcc05a0f. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: extraterm +""" +# pylint: skip-file +GRAPHEMES = { + ' ̃': 2, + ' ̣': 2, + ' ̱́': 3, + ' ོ': 2, + '-́': 2, + '.̃': 2, + 'à': 2, + 'À': 2, + 'á': 2, + 'â': 2, + 'ã': 2, + 'Ā': 2, + 'ā': 2, + 'ä': 2, + 'ả': 2, + 'ǎ': 2, + 'ạ': 2, + 'a̱': 2, + 'A̱': 2, + 'A̸': 2, + 'b̃': 2, + 'c̈': 2, + 'ç': 2, + 'Ç': 2, + 'c̱': 2, + 'ḍ': 2, + 'ḏ': 2, + 'è': 2, + 'È': 2, + 'É': 2, + 'é': 2, + 'Ê': 2, + 'ê': 2, + 'ẽ': 2, + 'ē': 2, + 'ẻ': 2, + 'e̊': 2, + 'ě': 2, + 'ẹ': 2, + 'e̱': 2, + 'E̱': 2, + 'ğ': 2, + 'ḥ': 2, + 'Ḥ': 2, + 'ì': 2, + 'í': 2, + 'Í': 2, + 'î': 2, + 'ĩ': 2, + 'ī': 2, + 'İ': 2, + 'ï': 2, + 'ỉ': 2, + 'ǐ': 2, + 'ị': 2, + 'I̱': 2, + 'i̱': 2, + 'k̀': 2, + 'm̃': 2, + 'ǹ': 2, + 'ñ': 2, + 'n̄': 2, + 'ṅ': 2, + 'ṉ': 2, + 'ò': 2, + 'ó': 2, + 'ô': 2, + 'õ': 2, + 'ō': 2, + 'Ö': 2, + 'ö': 2, + 'ỏ': 2, + 'ǒ': 2, + 'ọ': 2, + 'O̱': 2, + 'o̱': 2, + 'p̃': 2, + 'p̱': 2, + 'p̱̱': 3, + 'r̃': 2, + 'ṛ': 2, + 'S̈': 2, + 's̈': 2, + 'Ṣ': 2, + 'ṣ': 2, + 'Ş': 2, + 'ş': 2, + 'T̃': 2, + 't̃': 2, + 'ṭ': 2, + 'ṯ': 2, + 'ù': 2, + 'Ú': 2, + 'ú': 2, + 'û': 2, + 'ũ': 2, + 'ū': 2, + 'Ü': 2, + 'ü': 2, + 'ủ': 2, + 'ụ': 2, + 'u̱': 2, + 'U̱': 2, + 'ỳ': 2, + 'Ý': 2, + 'ý': 2, + 'ỹ': 2, + 'ỷ': 2, + 'ỵ': 2, + 'ẓ': 2, + 'á̱': 2, + 'ầ': 2, + 'ấ': 2, + 'ẫ': 2, + 'ẩ': 2, + 'ậ': 2, + 'â̱': 2, + 'ã̱': 2, + 'ä́': 2, + 'ä̱': 2, + 'ề': 2, + 'ế': 2, + 'ễ': 2, + 'ể': 2, + 'ệ': 2, + 'ë́': 2, + 'ë̱': 2, + 'í̱': 2, + 'ï̱': 2, + 'ó̱': 2, + 'ồ': 2, + 'ố': 2, + 'ỗ': 2, + 'ổ': 2, + 'ộ': 2, + 'ö́': 2, + 'ö̱': 2, + 'Ü̃': 2, + 'ü̃': 2, + 'ü̃́': 3, + 'ü̱̃': 3, + 'ü̱': 2, + 'ằ': 2, + 'ắ': 2, + 'ẳ': 2, + 'ặ': 2, + 'ą́': 2, + 'ę́': 2, + 'į́': 2, + 'ờ': 2, + 'ớ': 2, + 'ỡ': 2, + 'ở': 2, + 'ợ': 2, + 'ừ': 2, + 'ứ': 2, + 'ữ': 2, + 'ử': 2, + 'ự': 2, + 'ǎ̱': 2, + 'ǫ́': 2, + 'ɔ̀': 2, + 'ɔ́': 2, + 'Ɔ́': 2, + 'ɔ̂': 2, + 'ɔ̃': 2, + 'ɔ̈': 2, + 'ɔ̌': 2, + 'ə̀': 2, + 'ə́': 2, + 'ɛ̀': 2, + 'ɛ́': 2, + 'ɛ̂': 2, + 'ɛ̃': 2, + 'ɛ̈': 2, + 'ɛ̌': 2, + 'Ɨ́': 2, + 'ɨ́': 2, + 'ɨ̂': 2, + 'ɨ̈': 2, + 'ɨ̌': 2, + 'ɨ̱': 2, + 'ɨ̱́': 3, + 'ɨ̱̈': 3, + 'ɩ́': 2, + 'ʉ́': 2, + 'ʉ̌': 2, + 'ʊ́': 2, + 'а̄': 2, + 'е̄': 2, + 'ё': 2, + 'ӣ': 2, + 'ӣ̄': 3, + 'й': 2, + 'о̄': 2, + 'о̄̄': 3, + 'Р̌': 2, + 'р̌': 2, + 'ӯ': 2, + 'ы̄': 2, + 'ы̄̄': 3, + 'э̄': 2, + 'э̄̄': 3, + 'ю̄': 2, + 'я̄': 2, + 'ө̄': 2, + 'אַ': 2, + 'אָ': 2, + 'בֿ': 2, + 'וּ': 2, + 'יִ': 2, + 'כּ': 2, + 'פּ': 2, + 'פֿ': 2, + 'תּ': 2, + 'ײַ': 2, + 'اً': 2, + 'اُ': 2, + 'اّ': 2, + 'أ': 2, + 'إ': 2, + 'بَ': 2, + 'تُ': 2, + 'ت‌': 2, + 'ثّ': 2, + 'جُ': 2, + 'دّ': 2, + 'رً': 2, + 'رِ': 2, + 'رّ': 2, + 'صّ': 2, + 'ضً': 2, + 'فّ': 2, + 'قِ': 2, + 'لِ': 2, + 'مً': 2, + 'مِ': 2, + 'مّ': 2, + 'نِ': 2, + 'نّ': 2, + 'ن‌': 2, + 'هٔ': 2, + 'ه‌': 2, + 'وَ': 2, + 'وّ': 2, + 'ؤ': 2, + 'يُ': 2, + 'پُ': 2, + 'چّ': 2, + 'کُ': 2, + 'کِ': 2, + 'ھُ': 2, + 'ھٔ': 2, + 'ہِ': 2, + 'ہٰ': 2, + 'یِ': 2, + 'یّ': 2, + 'یٔ': 2, + 'یٰ': 2, + 'ی‌': 2, + 'ے‌': 2, + 'ܒ݂': 2, + 'ܓ̰': 2, + 'ܓ݂': 2, + 'ܝܼ': 2, + 'ܟ̰': 2, + 'ܟ݂': 2, + 'ܡܹ': 2, + 'ܬܵ': 2, + 'ހަ': 2, + 'ހާ': 2, + 'ހި': 2, + 'ހީ': 2, + 'ހު': 2, + 'ހެ': 2, + 'ހޭ': 2, + 'ހޮ': 2, + 'ހޯ': 2, + 'ށަ': 2, + 'ށާ': 2, + 'ށި': 2, + 'ށު': 2, + 'ށެ': 2, + 'ށޭ': 2, + 'ށް': 2, + 'ނަ': 2, + 'ނާ': 2, + 'ނި': 2, + 'ނީ': 2, + 'ނު': 2, + 'ނޫ': 2, + 'ނެ': 2, + 'ނޭ': 2, + 'ން': 2, + 'ރަ': 2, + 'ރާ': 2, + 'ރި': 2, + 'ރީ': 2, + 'ރު': 2, + 'ރޫ': 2, + 'ރެ': 2, + 'ރޭ': 2, + 'ރޮ': 2, + 'ބަ': 2, + 'ބާ': 2, + 'ބި': 2, + 'ބީ': 2, + 'ބު': 2, + 'ބޫ': 2, + 'ބެ': 2, + 'ބޭ': 2, + 'ބޮ': 2, + 'ބް': 2, + 'ޅަ': 2, + 'ޅާ': 2, + 'ޅި': 2, + 'ޅު': 2, + 'ޅެ': 2, + 'ޅޭ': 2, + 'ކަ': 2, + 'ކާ': 2, + 'ކި': 2, + 'ކީ': 2, + 'ކު': 2, + 'ކެ': 2, + 'ކޮ': 2, + 'ކް': 2, + 'އަ': 2, + 'އާ': 2, + 'އި': 2, + 'އީ': 2, + 'އު': 2, + 'އޫ': 2, + 'އެ': 2, + 'އޭ': 2, + 'އޮ': 2, + 'އޯ': 2, + 'އް': 2, + 'ވަ': 2, + 'ވާ': 2, + 'ވި': 2, + 'ވީ': 2, + 'ވު': 2, + 'ވެ': 2, + 'ވޭ': 2, + 'ވޮ': 2, + 'ވޯ': 2, + 'މަ': 2, + 'މާ': 2, + 'މި': 2, + 'މީ': 2, + 'މު': 2, + 'މުި': 3, + 'މެ': 2, + 'މޭ': 2, + 'މް': 2, + 'ފަ': 2, + 'ފާ': 2, + 'ފި': 2, + 'ފީ': 2, + 'ފު': 2, + 'ފޫ': 2, + 'ފެ': 2, + 'ފޯ': 2, + 'ފް': 2, + 'ދަ': 2, + 'ދާ': 2, + 'ދި': 2, + 'ދީ': 2, + 'ދު': 2, + 'ދޫ': 2, + 'ދެ': 2, + 'ދޭ': 2, + 'ދޮ': 2, + 'ތަ': 2, + 'ތާ': 2, + 'ތި': 2, + 'ތީ': 2, + 'ތު': 2, + 'ތެ': 2, + 'ތް': 2, + 'ލަ': 2, + 'ލާ': 2, + 'ލި': 2, + 'ލީ': 2, + 'ލު': 2, + 'ލޫ': 2, + 'ލެ': 2, + 'ލޭ': 2, + 'ލް': 2, + 'ގަ': 2, + 'ގާ': 2, + 'ގި': 2, + 'ގު': 2, + 'ގެ': 2, + 'ގޮ': 2, + 'ޏަ': 2, + 'ޏެ': 2, + 'ސަ': 2, + 'ސާ': 2, + 'ސި': 2, + 'ސީ': 2, + 'ސު': 2, + 'ސޫ': 2, + 'ސެ': 2, + 'ސް': 2, + 'ޑަ': 2, + 'ޑާ': 2, + 'ޑި': 2, + 'ޑު': 2, + 'ޑެ': 2, + 'ޒަ': 2, + 'ޒާ': 2, + 'ޒި': 2, + 'ޒީ': 2, + 'ޒު': 2, + 'ޒެ': 2, + 'ޓަ': 2, + 'ޓި': 2, + 'ޓީ': 2, + 'ޓު': 2, + 'ޓެ': 2, + 'ޔަ': 2, + 'ޔާ': 2, + 'ޔި': 2, + 'ޔު': 2, + 'ޔެ': 2, + 'ޔޭ': 2, + 'ޔޮ': 2, + 'ޕް': 2, + 'ޖަ': 2, + 'ޖާ': 2, + 'ޖި': 2, + 'ޖު': 2, + 'ޖޫ': 2, + 'ޖެ': 2, + 'ޖޭ': 2, + 'ޖް': 2, + 'ޗަ': 2, + 'ޗު': 2, + 'ޗެ': 2, + 'ޘަ': 2, + 'ޙަ': 2, + 'ޙާ': 2, + 'ޙި': 2, + 'ޙީ': 2, + 'ޙު': 2, + 'ޙެ': 2, + 'ޙް': 2, + 'ޚާ': 2, + 'ޚި': 2, + 'ޚު': 2, + 'ޚް': 2, + 'ޛަ': 2, + 'ޝަ': 2, + 'ޞަ': 2, + 'ޞާ': 2, + 'ޞި': 2, + 'ޞު': 2, + 'ޞޫ': 2, + 'ޞް': 2, + 'ޟު': 2, + 'ޠަ': 2, + 'ޠީ': 2, + 'ޢަ': 2, + 'ޢާ': 2, + 'ޢީ': 2, + 'ޢު': 2, + 'ޢެ': 2, + 'ޢް': 2, + 'ޣާ': 2, + 'ޤަ': 2, + 'ޤާ': 2, + 'ޤި': 2, + 'ޤީ': 2, + 'ޤު': 2, + 'ޤެ': 2, + 'ޤް': 2, + 'अं': 2, + 'आं': 2, + 'इँ': 2, + 'इं': 2, + 'ऊं': 2, + 'एं': 2, + 'ओं': 2, + 'कं': 2, + 'कां': 3, + 'किं': 3, + 'कु': 2, + 'कू': 2, + 'कृ': 2, + 'के': 2, + 'केँ': 3, + 'कें': 3, + 'केा': 3, + 'कै': 2, + 'क्': 2, + 'क्क': 3, + 'क्का': 4, + 'क्कां': 5, + 'क्को': 4, + 'क्घ्घि्': 7, + 'क्छ': 3, + 'क्त': 3, + 'क्ता': 4, + 'क्ति': 4, + 'क्ती': 4, + 'क्तीं': 5, + 'क्तुं': 5, + 'क्तेः': 5, + 'क्त्योः': 7, + 'क्ना': 4, + 'क्नि': 4, + 'क्य': 3, + 'क्या': 4, + 'क्र': 3, + 'क्रा': 4, + 'क्रि': 4, + 'क्रू': 4, + 'क्ष': 3, + 'क्षं': 4, + 'क्षा': 4, + 'क्षां': 5, + 'क्षाः': 5, + 'क्षि': 4, + 'क्षे': 4, + 'क्षै': 4, + 'क्षो': 4, + 'क्ष्ण': 5, + 'क्ष्य': 5, + 'क्ष्या': 6, + 'क्सा': 4, + 'खु': 2, + 'खे': 2, + 'खै': 2, + 'ख्त': 3, + 'ख्ने': 4, + 'ख्य': 3, + 'ख्यः': 4, + 'ख्या': 4, + 'ख्‍ने': 5, + 'गं': 2, + 'ग़': 2, + 'ग़ा': 3, + 'ग़ु': 3, + 'गाँ': 3, + 'गां': 3, + 'गु': 2, + 'गू': 2, + 'गृ': 2, + 'गे': 2, + 'गै': 2, + 'गों': 3, + 'ग्ण': 3, + 'ग्न': 3, + 'ग्ने': 4, + 'ग्भे': 4, + 'ग्य': 3, + 'ग्या': 4, + 'ग्ये': 4, + 'ग्यैः': 5, + 'ग्र': 3, + 'ग्रे': 4, + 'ग्र्य': 5, + 'घं': 2, + 'घू': 2, + 'घृ': 2, + 'घे': 2, + 'घेि': 3, + 'घ्': 2, + 'घ्ंत': 4, + 'घ्का': 4, + 'घ्त': 3, + 'घ्र': 3, + 'घ्र्व्य': 7, + 'घ्लि': 4, + 'घ्व': 3, + 'घ्श्य': 5, + 'घ्स्ट': 5, + 'ङ्ग': 3, + 'ङ्घ': 3, + 'चां': 3, + 'चु': 2, + 'चूं': 3, + 'चे': 2, + 'चै': 2, + 'च्च': 3, + 'च्चा': 4, + 'च्चे': 4, + 'च्चों': 5, + 'च्छ': 3, + 'च्छं': 4, + 'च्छा': 4, + 'च्छि': 4, + 'च्छु': 4, + 'च्छे': 4, + 'च्या': 4, + 'च्यां': 5, + 'च्यो': 4, + 'छु': 2, + 'छुी': 3, + 'छै': 2, + 'जँ': 2, + 'जिं': 3, + 'जु': 2, + 'जे': 2, + 'जेँ': 3, + 'ज्ज': 3, + 'ज्ञा': 4, + 'ज्ञे': 4, + 'ज्ने': 4, + 'ज्य': 3, + 'ज्या': 4, + 'ज्या‌': 5, + 'ज्रा': 4, + 'झे': 2, + 'ञ्च': 3, + 'ञ्चा': 4, + 'ञ्चि': 4, + 'टां': 3, + 'टुँ': 3, + 'टुं': 3, + 'टू': 2, + 'टे': 2, + 'टै': 2, + 'टों': 3, + 'ट्ज़': 4, + 'ट्टि': 4, + 'ट्टी': 4, + 'ट्या': 4, + 'ट्र': 3, + 'ट्रा': 4, + 'ट्रे': 4, + 'ठे': 2, + 'ठैः': 3, + 'ड़': 2, + 'ड़ा': 3, + 'ड़े': 3, + 'डू': 2, + 'डे़': 3, + 'ढं': 2, + 'ढ़': 2, + 'ढ़ा': 3, + 'ढ़ि': 3, + 'ढ़ी': 3, + 'ढ़ो': 3, + 'ढाँ': 3, + 'ढे': 2, + 'णं': 2, + 'णां': 3, + 'णु': 2, + 'णू': 2, + 'णे': 2, + 'णेँ': 3, + 'णों': 3, + 'ण्ड': 3, + 'ण्डं': 4, + 'ण्डः': 4, + 'ण्डि': 4, + 'ण्य': 3, + 'ण्या': 4, + 'ण्यां': 5, + 'तँ': 2, + 'तं': 2, + 'तां': 3, + 'ताः': 3, + 'तिं': 3, + 'तु': 2, + 'तुं': 3, + 'तू': 2, + 'तृ': 2, + 'ते': 2, + 'तेँ': 3, + 'तै': 2, + 'तों': 3, + 'त्': 2, + 'त्का': 4, + 'त्त': 3, + 'त्ता': 4, + 'त्ति': 4, + 'त्ते': 4, + 'त्त्थि': 6, + 'त्त्व': 5, + 'त्त्वा': 6, + 'त्त्वे': 6, + 'त्न': 3, + 'त्नी': 4, + 'त्नों': 5, + 'त्प': 3, + 'त्पा': 4, + 'त्पी': 4, + 'त्पू': 4, + 'त्म': 3, + 'त्मा': 4, + 'त्य': 3, + 'त्यं': 4, + 'त्या': 4, + 'त्यां': 5, + 'त्यि': 4, + 'त्ये': 4, + 'त्यै': 4, + 'त्र': 3, + 'त्रा': 4, + 'त्राा': 5, + 'त्राि': 5, + 'त्रि': 4, + 'त्री': 4, + 'त्रे': 4, + 'त्र्त': 5, + 'त्र्य': 5, + 'त्र्या': 6, + 'त्र्ये': 6, + 'त्व': 3, + 'त्वं': 4, + 'त्वा': 4, + 'त्वां': 5, + 'त्वे': 4, + 'त्सं': 4, + 'त्सा': 4, + 'त्सो': 4, + 'त्स्वा': 6, + 'थु': 2, + 'थे': 2, + 'थै': 2, + 'थ्य': 3, + 'दं': 2, + 'दां': 3, + 'दु': 2, + 'दू': 2, + 'दृ': 2, + 'दे': 2, + 'दै': 2, + 'द्घ': 3, + 'द्घि': 4, + 'द्घो': 4, + 'द्द': 3, + 'द्दा': 4, + 'द्दि': 4, + 'द्दी': 4, + 'द्दे': 4, + 'द्ध': 3, + 'द्धः': 4, + 'द्धा': 4, + 'द्धां': 5, + 'द्धि': 4, + 'द्धिं': 5, + 'द्धी': 4, + 'द्धीं': 5, + 'द्धैः': 5, + 'द्भ': 3, + 'द्भा': 4, + 'द्य': 3, + 'द्या': 4, + 'द्र': 3, + 'द्रो': 4, + 'द्व': 3, + 'द्वा': 4, + 'द्वि': 4, + 'द्वे': 4, + 'द्स': 3, + 'द्‌': 3, + 'धां': 3, + 'धाः': 3, + 'धु': 2, + 'धृ': 2, + 'धे': 2, + 'धेः': 3, + 'धों': 3, + 'ध्य': 3, + 'ध्या': 4, + 'ध्ये': 4, + 'ध्व': 3, + 'नं': 2, + 'नां': 3, + 'नाः': 3, + 'ना‌': 3, + 'निँ': 3, + 'निः': 3, + 'नु': 2, + 'नू': 2, + 'नृ': 2, + 'ने': 2, + 'नै': 2, + 'नैः': 3, + 'नों': 3, + 'न्': 2, + 'न्का': 4, + 'न्चि': 4, + 'न्छे': 4, + 'न्जे': 4, + 'न्ञ्या': 6, + 'न्त': 3, + 'न्तं': 4, + 'न्तः': 4, + 'न्ता': 4, + 'न्ति': 4, + 'न्तु': 4, + 'न्तुं': 5, + 'न्ते': 4, + 'न्तो': 4, + 'न्तों': 5, + 'न्त्र': 5, + 'न्त्रा': 6, + 'न्त्राः': 7, + 'न्त्रि': 6, + 'न्त्री': 6, + 'न्त्र्य': 7, + 'न्त्र्यं': 8, + 'न्त्र्या': 8, + 'न्त्र्ये': 8, + 'न्द': 3, + 'न्दा': 4, + 'न्दी': 4, + 'न्दो': 4, + 'न्ध': 3, + 'न्धा': 4, + 'न्धि': 4, + 'न्धी': 4, + 'न्धु': 4, + 'न्धों': 5, + 'न्न': 3, + 'न्ना': 4, + 'न्नाः': 5, + 'न्नि': 4, + 'न्ने': 4, + 'न्म': 3, + 'न्मा': 4, + 'न्मु': 4, + 'न्मे': 4, + 'न्य': 3, + 'न्या': 4, + 'न्ये': 4, + 'न्यै': 4, + 'न्यो': 4, + 'न्व': 3, + 'न्वे': 4, + 'न्स': 3, + 'न्हा': 4, + 'न्हें': 5, + 'न्ह्यां': 7, + 'पं': 2, + 'पाँ': 3, + 'पां': 3, + 'पु': 2, + 'पू': 2, + 'पृ': 2, + 'पे': 2, + 'पेँ': 3, + 'पै': 2, + 'पों': 3, + 'प्त': 3, + 'प्तं': 4, + 'प्ति': 4, + 'प्तुं': 5, + 'प्तौ': 4, + 'प्य': 3, + 'प्यं': 4, + 'प्या': 4, + 'प्र': 3, + 'प्रा': 4, + 'फ़': 2, + 'फाँ': 3, + 'फु': 2, + 'फे': 2, + 'फ्त': 3, + 'फ्ता': 4, + 'फ्ना': 4, + 'फ्नो': 4, + 'फ्रां': 5, + 'बँ': 2, + 'बं': 2, + 'बाँ': 3, + 'बु': 2, + 'बू': 2, + 'बृ': 2, + 'बे': 2, + 'बै': 2, + 'ब्द': 3, + 'ब्ध': 3, + 'ब्धि': 4, + 'ब्यू': 4, + 'भं': 2, + 'भिः': 3, + 'भु': 2, + 'भू': 2, + 'भे': 2, + 'भै': 2, + 'भ्य': 3, + 'भ्यः': 4, + 'भ्या': 4, + 'भ्यां': 5, + 'भ्यु': 4, + 'भ्यो': 4, + 'भ्र': 3, + 'मं': 2, + 'मां': 3, + 'मु': 2, + 'मू': 2, + 'मे': 2, + 'मेँ': 3, + 'में': 3, + 'मै': 2, + 'मों': 3, + 'म्': 2, + 'म्ति': 4, + 'म्न': 3, + 'म्नि': 4, + 'म्प': 3, + 'म्पा': 4, + 'म्प्र': 5, + 'म्ब': 3, + 'म्बु': 4, + 'म्भि': 4, + 'म्म': 3, + 'म्मा': 4, + 'म्मि': 4, + 'म्मे': 4, + 'म्या': 4, + 'म्से': 4, + 'म्ह': 3, + 'म्ही': 4, + 'म्हो': 4, + 'यं': 2, + 'यां': 3, + 'याः': 3, + 'यु': 2, + 'यू': 2, + 'ये': 2, + 'यै': 2, + 'यैः': 3, + 'यों': 3, + 'य्य': 3, + 'य्यो': 4, + 'रं': 2, + 'रां': 3, + 'राः': 3, + 'रिं': 3, + 'रु': 2, + 'रू': 2, + 'रे': 2, + 'रें': 3, + 'रै': 2, + 'रों': 3, + 'र्क': 3, + 'र्कि': 4, + 'र्ग': 3, + 'र्गी': 4, + 'र्गे': 4, + 'र्चि': 4, + 'र्छ': 3, + 'र्ज': 3, + 'र्जा': 4, + 'र्जि': 4, + 'र्जी': 4, + 'र्ट': 3, + 'र्ढ': 3, + 'र्ण': 3, + 'र्णं': 4, + 'र्णां': 5, + 'र्णी': 4, + 'र्त': 3, + 'र्ता': 4, + 'र्ति': 4, + 'र्तिः': 5, + 'र्ती': 4, + 'र्तुं': 5, + 'र्त्त': 5, + 'र्त्ति': 6, + 'र्थ': 3, + 'र्थं': 4, + 'र्थि': 4, + 'र्थी': 4, + 'र्द': 3, + 'र्दा': 4, + 'र्दि': 4, + 'र्दु': 4, + 'र्दे': 4, + 'र्दै': 4, + 'र्दो': 4, + 'र्द्ध': 5, + 'र्ध': 3, + 'र्धा': 4, + 'र्धि': 4, + 'र्न': 3, + 'र्नि': 4, + 'र्नु': 4, + 'र्नू': 4, + 'र्ने': 4, + 'र्फ': 3, + 'र्ब': 3, + 'र्बं': 4, + 'र्बा': 4, + 'र्भ': 3, + 'र्भा': 4, + 'र्म': 3, + 'र्मं': 4, + 'र्मा': 4, + 'र्मि': 4, + 'र्य': 3, + 'र्यं': 4, + 'र्या': 4, + 'र्यां': 5, + 'र्ये': 4, + 'र्यों': 5, + 'र्रा': 4, + 'र्व': 3, + 'र्वा': 4, + 'र्वां': 5, + 'र्वृ': 4, + 'र्वे': 4, + 'र्वो': 4, + 'र्श': 3, + 'र्ष': 3, + 'र्षे': 4, + 'र्स': 3, + 'र्सा': 4, + 'र्ह': 3, + 'र्हाः': 5, + 'र्हि': 4, + 'र्हो': 4, + 'र्‍या': 5, + 'लां': 3, + 'लाः': 3, + 'लिं': 3, + 'लीं': 3, + 'लु': 2, + 'लू': 2, + 'ले': 2, + 'लै': 2, + 'लों': 3, + 'ल्क': 3, + 'ल्का': 4, + 'ल्ने': 4, + 'ल्प': 3, + 'ल्पि': 4, + 'ल्म': 3, + 'ल्य': 3, + 'ल्यं': 4, + 'ल्या': 4, + 'ल्यां': 5, + 'ल्याः': 5, + 'ल्ये': 4, + 'ल्ल': 3, + 'ल्लं': 4, + 'ल्ला': 4, + 'ल्लि': 4, + 'ल्ले': 4, + 'ल्ह': 3, + 'ळे': 2, + 'वं': 2, + 'वां': 3, + 'वाः': 3, + 'वू': 2, + 'वृ': 2, + 'वे': 2, + 'वै': 2, + 'वों': 3, + 'व्य': 3, + 'व्या': 4, + 'व्ये': 4, + 'व्यों': 5, + 'शं': 2, + 'शां': 3, + 'शाः': 3, + 'शु': 2, + 'शू': 2, + 'शे': 2, + 'शों': 3, + 'श्च': 3, + 'श्चि': 4, + 'श्य': 3, + 'श्या': 4, + 'श्यि': 4, + 'श्यों': 5, + 'श्र': 3, + 'श्रा': 4, + 'श्रां': 5, + 'श्रु': 4, + 'श्व': 3, + 'श्वा': 4, + 'श्वि': 4, + 'षां': 3, + 'षाः': 3, + 'षु': 2, + 'षे': 2, + 'षों': 3, + 'ष्का': 4, + 'ष्ट': 3, + 'ष्टः': 4, + 'ष्टा': 4, + 'ष्टां': 5, + 'ष्टाः': 5, + 'ष्टि': 4, + 'ष्टी': 4, + 'ष्टीं': 5, + 'ष्ट्र': 5, + 'ष्ट्रा': 6, + 'ष्ट्रां': 7, + 'ष्ट्रि': 6, + 'ष्ट्री': 6, + 'ष्ट्रे': 6, + 'ष्ट्रो': 6, + 'ष्ट्रों': 7, + 'ष्ठा': 4, + 'ष्ठे': 4, + 'ष्ठों': 5, + 'ष्णु': 4, + 'ष्प': 3, + 'ष्प्र': 5, + 'ष्य': 3, + 'ष्यः': 4, + 'ष्यों': 5, + 'सँ': 2, + 'सं': 2, + 'साँ': 3, + 'सां': 3, + 'सु': 2, + 'सू': 2, + 'सृ': 2, + 'से': 2, + 'सै': 2, + 'स्क': 3, + 'स्का': 4, + 'स्काः': 5, + 'स्कु': 4, + 'स्कू': 4, + 'स्कृ': 4, + 'स्ट': 3, + 'स्ट्या': 6, + 'स्त': 3, + 'स्ता': 4, + 'स्ति': 4, + 'स्ती': 4, + 'स्तु': 4, + 'स्तुं': 5, + 'स्तै': 4, + 'स्तो': 4, + 'स्त्र': 5, + 'स्त्रा': 6, + 'स्त्रि': 6, + 'स्त्री': 6, + 'स्थ': 3, + 'स्था': 4, + 'स्थां': 5, + 'स्थि': 4, + 'स्थी': 4, + 'स्थे': 4, + 'स्थ्य': 5, + 'स्धी': 4, + 'स्प': 3, + 'स्पे': 4, + 'स्म': 3, + 'स्मि': 4, + 'स्मिं': 5, + 'स्मै': 4, + 'स्य': 3, + 'स्या': 4, + 'स्यां': 5, + 'स्याः': 5, + 'स्युः': 5, + 'स्यों': 5, + 'स्रो': 4, + 'स्ल': 3, + 'स्व': 3, + 'स्वं': 4, + 'स्वा': 4, + 'स्वी': 4, + 'स्वे': 4, + 'स्स': 3, + 'स्सा': 4, + 'हाँ': 3, + 'हां': 3, + 'हिं': 3, + 'हीं': 3, + 'हु': 2, + 'हुँ': 3, + 'हू': 2, + 'हे': 2, + 'है': 2, + 'हैं': 3, + 'हैः': 3, + 'हों': 3, + 'ह्या': 4, + 'ह्रा': 4, + 'फ़्ता': 4, + 'আ‌': 2, + 'ই‌': 2, + 'এ‌': 2, + 'কা‌': 3, + 'কিং': 3, + 'কু': 2, + 'কূ': 2, + 'কৃ': 2, + 'কে‌': 3, + 'ক্ত': 3, + 'ক্তি': 4, + 'ক্য': 3, + 'ক্র': 3, + 'ক্রী': 4, + 'ক্ষ': 3, + 'ক্ষা': 4, + 'ক্ষি': 4, + 'ক্ষে': 4, + 'ক্ষ্য': 5, + 'ক্ষ্যে': 6, + 'খু': 2, + 'খ্যা': 4, + 'গু': 2, + 'গৃ': 2, + 'গ্য': 3, + 'গ্র': 3, + 'গ্রা': 4, + 'গ্রে': 4, + 'ঘৃ': 2, + 'ঘো': 3, + 'ঙ্খা': 4, + 'ঙ্গ': 3, + 'ঙ্গী': 4, + 'ঙ্গে': 4, + 'ঙ্ঘ': 3, + 'চ্চ': 3, + 'চ্ছ': 3, + 'চ্ছা': 4, + 'চ্ছা‌': 5, + 'চ্ছু': 4, + 'চ্ছে': 4, + 'ছু': 2, + 'জে‌': 3, + 'জ্ঞ': 3, + 'জ্ঞা': 4, + 'জ্য': 3, + 'ঞ্চি': 4, + 'ঞ্ছি': 4, + 'ট্রে': 4, + 'ড়': 2, + 'ড়া': 3, + 'ঢ়': 2, + 'ণ্ড': 3, + 'ণ্ডে': 4, + 'ণ্য': 3, + 'তাঁ': 3, + 'তা‌': 3, + 'তি‌': 3, + 'তু': 2, + 'তৃ': 2, + 'তে‌': 3, + 'ত্ত': 3, + 'ত্তা': 4, + 'ত্তি': 4, + 'ত্ন': 3, + 'ত্ব': 3, + 'ত্বে': 4, + 'ত্ম': 3, + 'ত্য': 3, + 'ত্যা': 4, + 'ত্যি': 4, + 'ত্যে': 4, + 'ত্র': 3, + 'ত্রা': 4, + 'ত্রে': 4, + 'ত্‍': 3, + 'ত্‍প': 4, + 'ত্‍পী': 5, + 'ত্‍সা': 5, + 'থ্য': 3, + 'দৃ': 2, + 'দে‌': 3, + 'দ্দি': 4, + 'দ্দে': 4, + 'দ্ধ': 3, + 'দ্ধা': 4, + 'দ্ধি': 4, + 'দ্ধে': 4, + 'দ্বা': 4, + 'দ্ভ': 3, + 'দ্ভূ': 4, + 'দ্য': 3, + 'দ্রো': 4, + 'ধ্য': 3, + 'ধ্যে': 4, + 'না‌': 3, + 'নু': 2, + 'ন্ড': 3, + 'ন্ত': 3, + 'ন্তা': 4, + 'ন্তি': 4, + 'ন্তে': 4, + 'ন্ত্রি': 6, + 'ন্ত্রে': 6, + 'ন্থী': 4, + 'ন্ধ': 3, + 'ন্ধা': 4, + 'ন্ধু': 4, + 'ন্ন': 3, + 'ন্ম': 3, + 'ন্মা': 4, + 'ন্মু': 4, + 'ন্য': 3, + 'ন্যা': 4, + 'ন্যে': 4, + 'ন‌': 2, + 'পা‌': 3, + 'পু': 2, + 'পূ': 2, + 'পৃ': 2, + 'প্ত': 3, + 'প্তা': 4, + 'প্র': 3, + 'প্রা': 4, + 'বা‌': 3, + 'বু': 2, + 'বৃ': 2, + 'ব্ধি': 4, + 'ব্য': 3, + 'ব্যা': 4, + 'ব্র': 3, + 'ভূ': 2, + 'ভ্য': 3, + 'ভ্রা': 4, + 'মা‌': 3, + 'মু': 2, + 'মূ': 2, + 'ম্প': 3, + 'ম্পা': 4, + 'ম্ব': 3, + 'ম্ভ': 3, + 'ম্ম': 3, + 'ম্মা': 4, + 'ম্য': 3, + 'য়': 2, + 'য়া': 3, + 'য়ি': 3, + 'য়ে': 3, + 'য়ো': 3, + 'যু': 2, + 'য্য': 3, + 'রাং': 3, + 'রু': 2, + 'রূ': 2, + 'র্ক': 3, + 'র্জ': 3, + 'র্জা': 4, + 'র্ণ': 3, + 'র্ত': 3, + 'র্থ': 3, + 'র্থে': 4, + 'র্দো': 4, + 'র্ধ': 3, + 'র্ধা': 4, + 'র্নী': 4, + 'র্ব': 3, + 'র্বা': 4, + 'র্বি': 4, + 'র্বো': 4, + 'র্ব্য': 5, + 'র্ভূ': 4, + 'র্ম': 3, + 'র্মে': 4, + 'র্য': 3, + 'র্যা': 4, + 'র‌': 2, + 'লে‌': 3, + 'ল্প': 3, + 'ল্য': 3, + 'ল্যা': 4, + 'ল্যে': 4, + 'ল্লে': 4, + 'শু': 2, + 'শৃং': 3, + 'শ্চ': 3, + 'শ্চি': 4, + 'শ্বা': 4, + 'শ্বে': 4, + 'শ্য': 3, + 'শ্যে': 4, + 'শ্র': 3, + 'শ্রা': 4, + 'ষ্ট': 3, + 'ষ্টা': 4, + 'ষ্টি': 4, + 'ষ্ট্র': 5, + 'ষ্ট্রী': 6, + 'ষ্ট্রে': 6, + 'ষ্ঠা': 4, + 'ষ্ঠি': 4, + 'ষ্ঠী': 4, + 'ষ্ঠু': 4, + 'ষ্ণু': 4, + 'সাং': 3, + 'সু': 2, + 'সৃ': 2, + 'সে‌': 3, + 'স্ক': 3, + 'স্কৃ': 4, + 'স্ত': 3, + 'স্তি': 4, + 'স্তি‌': 5, + 'স্ত্র': 5, + 'স্থ': 3, + 'স্থা': 4, + 'স্থ্য': 5, + 'স্ব': 3, + 'স্বা': 4, + 'স্বী': 4, + 'স্য': 3, + 'স্যা': 4, + 'স্যে': 4, + 'হ্য': 3, + 'হ‌': 2, + 'ਅੰ': 2, + 'ਅੱ': 2, + 'ਆਂ': 2, + 'ਇੰ': 2, + 'ਉਂ': 2, + 'ਉੱ': 2, + 'ਕਾਂ': 3, + 'ਕਿ੍': 3, + 'ਕਿੱ': 3, + 'ਕੁ': 2, + 'ਕੂ': 2, + 'ਕੇ': 2, + 'ਕੋ': 2, + 'ਕੌ': 2, + 'ਕੰ': 2, + 'ਕੱ': 2, + 'ਖਾਂ': 3, + 'ਖਿ਼': 3, + 'ਖੁ': 2, + 'ਖੁੱ': 3, + 'ਖੇ': 2, + 'ਖੋ': 2, + 'ਖੋਂ': 3, + 'ਗਿ੍': 3, + 'ਗੁ': 2, + 'ਗੇ': 2, + 'ਗੈ': 2, + 'ਗੱ': 2, + 'ਘੁੰ': 3, + 'ਘੰ': 2, + 'ਚਿੱ': 3, + 'ਚੁ': 2, + 'ਚੇ': 2, + 'ਚੋ': 2, + 'ਚੋਂ': 3, + 'ਚੰ': 2, + 'ਛੁ': 2, + 'ਛੋਂ': 3, + 'ਜਾਂ': 3, + 'ਜਿ਼': 3, + 'ਜਿ਼ੰ': 4, + 'ਜੁ': 2, + 'ਜੇ': 2, + 'ਜੋ': 2, + 'ਜੋਂ': 3, + 'ਜੰ': 2, + 'ਜੱ': 2, + 'ਝੰ': 2, + 'ਟਿ੍': 3, + 'ਟੇ': 2, + 'ਢੇ': 2, + 'ਢੰ': 2, + 'ਣਾਂ': 3, + 'ਣੇ': 2, + 'ਤਾਂ': 3, + 'ਤੇ': 2, + 'ਤੋਂ': 3, + 'ਤੰ': 2, + 'ਤੱ': 2, + 'ਥੇ': 2, + 'ਥੋਂ': 3, + 'ਦਾਂ': 3, + 'ਦਿੱ': 3, + 'ਦੁ': 2, + 'ਦੂ': 2, + 'ਦੇ': 2, + 'ਦੋ': 2, + 'ਦੋਂ': 3, + 'ਦੰ': 2, + 'ਧਾਂ': 3, + 'ਧੁ': 2, + 'ਧੇ': 2, + 'ਧੱ': 2, + 'ਨਾਂ': 3, + 'ਨੀਂ': 3, + 'ਨੁ': 2, + 'ਨੁੱ': 3, + 'ਨੂੰ': 3, + 'ਨੇ': 2, + 'ਨੋ': 2, + 'ਨੌ': 2, + 'ਨ੍': 2, + 'ਨੰ': 2, + 'ਪੁ': 2, + 'ਪੂ': 2, + 'ਪੈ': 2, + 'ਪ੍': 2, + 'ਪੰ': 2, + 'ਪੱ': 2, + 'ਫੌ': 2, + 'ਬੁ': 2, + 'ਬੂ': 2, + 'ਬੇ': 2, + 'ਬੋ': 2, + 'ਬੰ': 2, + 'ਬੱ': 2, + 'ਭੂ': 2, + 'ਭੇ': 2, + 'ਮਾਂ': 3, + 'ਮੁ': 2, + 'ਮੁੱ': 3, + 'ਮੂ': 2, + 'ਮੇ': 2, + 'ਮੇਂ': 3, + 'ਮੈਂ': 3, + 'ਮੌ': 2, + 'ਮੰ': 2, + 'ਯੁ': 2, + 'ਯੂ': 2, + 'ਯੋ': 2, + 'ਰਾਂ': 3, + 'ਰੁ': 2, + 'ਰੁੱ': 3, + 'ਰੂ': 2, + 'ਰੇ': 2, + 'ਰੋ': 2, + 'ਰ੍': 2, + 'ਰੰ': 2, + 'ਰੱ': 2, + 'ਲਾਂ': 3, + 'ਲਿੰ': 3, + 'ਲੇ': 2, + 'ਲੈ': 2, + 'ਲੋ': 2, + 'ਲ੍': 2, + 'ਲੰ': 2, + 'ਵਾਂ': 3, + 'ਵਿੱ': 3, + 'ਵੇ': 2, + 'ਵੇਂ': 3, + 'ਵੈ': 2, + 'ਵੰ': 2, + 'ਵੱ': 2, + 'ਸ਼ਾਂ': 3, + 'ਸ਼ੇ': 2, + 'ਸ਼ੋ': 2, + 'ਸ਼ੌ': 2, + 'ਸਾਂ': 3, + 'ਸਿੱ': 3, + 'ਸੁ': 2, + 'ਸੁੰ': 3, + 'ਸੂ': 2, + 'ਸੇ': 2, + 'ਸੇੇ': 3, + 'ਸੈਂ': 3, + 'ਸੌ': 2, + 'ਸੰ': 2, + 'ਸੱ': 2, + 'ਹਾਂ': 3, + 'ਹਿੰ': 3, + 'ਹਿੱ': 3, + 'ਹੀਂ': 3, + 'ਹੁ': 2, + 'ਹੁੰ': 3, + 'ਹੂ': 2, + 'ਹੇ': 2, + 'ਹੈ': 2, + 'ਹੋ': 2, + 'ਹੋਂ': 3, + 'ਹੱ': 2, + 'ਜ਼ੁ': 2, + 'ੜੀਂ': 3, + 'ੜੇ': 2, + 'ੜ੍': 2, + 'ਫ਼ੁ': 2, + 'અં': 2, + 'આં': 2, + 'ઊં': 2, + 'કાં': 3, + 'કુ': 2, + 'કૃ': 2, + 'કે': 2, + 'ક્ક': 3, + 'ક્કા': 4, + 'ક્કો': 4, + 'ક્તિ': 4, + 'ક્ર': 3, + 'ક્ષ': 3, + 'ક્ષા': 4, + 'ખુ': 2, + 'ખૂ': 2, + 'ખ્ત': 3, + 'ગુ': 2, + 'ગે': 2, + 'ગ્ન': 3, + 'ગ્ય': 3, + 'ચૂં': 3, + 'ચ્ચ': 3, + 'ચ્ચા': 4, + 'ચ્ચે': 4, + 'ચ્છા': 4, + 'ચ્છે': 4, + 'છું': 3, + 'છૂ': 2, + 'છે': 2, + 'જં': 2, + 'જાં': 3, + 'જુ': 2, + 'જૂ': 2, + 'જે': 2, + 'જ્ઞા': 4, + 'જ્ય': 3, + 'જ્યો': 4, + 'ઝૂં': 3, + 'ટું': 3, + 'ટે': 2, + 'ટ્ર': 3, + 'ટ્રે': 4, + 'ડાં': 3, + 'ડે': 2, + 'ણુ': 2, + 'ણે': 2, + 'તં': 2, + 'તાં': 3, + 'તુ': 2, + 'તૃ': 2, + 'તે': 2, + 'ત્તા': 4, + 'ત્તિ': 4, + 'ત્ત્વ': 5, + 'ત્ત્વા': 6, + 'ત્ન': 3, + 'ત્મ': 3, + 'ત્ય': 3, + 'ત્યા': 4, + 'ત્યાં': 5, + 'ત્યિ': 4, + 'ત્યે': 4, + 'ત્યો': 4, + 'ત્ર': 3, + 'ત્રી': 4, + 'ત્ર્ય': 5, + 'ત્વ': 3, + 'થે': 2, + 'દુ': 2, + 'દૃ': 2, + 'દે': 2, + 'દ્ધ': 3, + 'દ્ધા': 4, + 'દ્ધાં': 5, + 'દ્ધિ': 4, + 'દ્ભ': 3, + 'દ્વા': 4, + 'ધાં': 3, + 'ધુ': 2, + 'ધ્ય': 3, + 'ધ્યા': 4, + 'નં': 2, + 'નાં': 3, + 'નુ': 2, + 'નું': 3, + 'ને': 2, + 'નૈ': 2, + 'ન્ત': 3, + 'ન્તો': 4, + 'ન્મ': 3, + 'ન્મા': 4, + 'ન્મે': 4, + 'ન્ય': 3, + 'ન્યા': 4, + 'ન્યૂં': 5, + 'પુ': 2, + 'પૂ': 2, + 'પે': 2, + 'પ્ત': 3, + 'પ્ર': 3, + 'પ્રા': 4, + 'ફે': 2, + 'બં': 2, + 'બાં': 3, + 'બે': 2, + 'બ્ધ': 3, + 'ભં': 2, + 'ભાં': 3, + 'ભૂ': 2, + 'ભે': 2, + 'ભ્ય': 3, + 'મં': 2, + 'માં': 3, + 'મુ': 2, + 'મૂ': 2, + 'મૃ': 2, + 'મે': 2, + 'મૈ': 2, + 'મ્ય': 3, + 'મ્યાં': 5, + 'યુ': 2, + 'રં': 2, + 'રુ': 2, + 'રૂ': 2, + 'રે': 2, + 'ર્જ': 3, + 'ર્ણ': 3, + 'ર્ત': 3, + 'ર્તા': 4, + 'ર્થ': 3, + 'ર્થિ': 4, + 'ર્દો': 4, + 'ર્મ': 3, + 'ર્મિ': 4, + 'ર્મે': 4, + 'ર્ય': 3, + 'ર્યા': 4, + 'ર્યો': 4, + 'ર્વ': 3, + 'લાં': 3, + 'લિં': 3, + 'લે': 2, + 'લ્મ': 3, + 'લ્ય': 3, + 'ળે': 2, + 'વં': 2, + 'વાં': 3, + 'વું': 3, + 'વૃ': 2, + 'વે': 2, + 'વૈ': 2, + 'વ્ય': 3, + 'વ્યા': 4, + 'વ્યો': 4, + 'શાં': 3, + 'શુ': 2, + 'શે': 2, + 'શ્કે': 4, + 'શ્ચ': 3, + 'શ્ય': 3, + 'શ્ર': 3, + 'શ્વ': 3, + 'ષ્ટિ': 4, + 'ષ્ટ્ર': 5, + 'ષ્ટ્રી': 6, + 'ષ્ટ્રો': 6, + 'ષ્ઠા': 4, + 'ષ્ણુ': 4, + 'ષ્પ': 3, + 'સં': 2, + 'સાં': 3, + 'સુ': 2, + 'સું': 3, + 'સૂ': 2, + 'સે': 2, + 'સ્કૃ': 4, + 'સ્ટ': 3, + 'સ્તિ': 4, + 'સ્તી': 4, + 'સ્ત્રી': 6, + 'સ્થા': 4, + 'સ્થિ': 4, + 'સ્પ': 3, + 'સ્રી': 4, + 'સ્વ': 3, + 'સ્વા': 4, + 'સ્વી': 4, + 'સ્સા': 4, + 'હુ': 2, + 'હે': 2, + 'கீ': 2, + 'க்': 2, + 'ங்': 2, + 'சீ': 2, + 'ச்': 2, + 'ஞ்': 2, + 'ட்': 2, + 'ண்': 2, + 'தீ': 2, + 'த்': 2, + 'நீ': 2, + 'ந்': 2, + 'ன்': 2, + 'ப்': 2, + 'மீ': 2, + 'ம்': 2, + 'யீ': 2, + 'ய்': 2, + 'ர்': 2, + 'ற்': 2, + 'ல்': 2, + 'ள்': 2, + 'ழ்': 2, + 'வீ': 2, + 'வ்': 2, + 'ஸ்': 2, + 'కా': 2, + 'కాం': 3, + 'కి': 2, + 'కీ': 2, + 'కుం': 3, + 'కె': 2, + 'కెం': 3, + 'కై': 2, + 'కొ': 2, + 'క్క': 3, + 'క్కు': 4, + 'క్త': 3, + 'క్తి': 4, + 'క్తీ': 4, + 'క్తు': 4, + 'క్య': 3, + 'క్ర': 3, + 'క్రాం': 5, + 'క్రి': 4, + 'క్రూ': 4, + 'క్ష': 3, + 'క్షా': 4, + 'క్షి': 4, + 'క్షిం': 5, + 'క్షే': 4, + 'క్షో': 4, + 'క్స్వా': 6, + 'ఖా': 2, + 'ఖ్య': 3, + 'ఖ్యా': 4, + 'గా': 2, + 'గి': 2, + 'గిం': 3, + 'గీ': 2, + 'గౌ': 2, + 'గ్భా': 4, + 'గ్మ': 3, + 'గ్య': 3, + 'గ్ర': 3, + 'గ్రి': 4, + 'ఘి': 2, + 'ఘో': 2, + 'చా': 2, + 'చి': 2, + 'చెం': 3, + 'చే': 2, + 'చో': 2, + 'చ్చు': 4, + 'చ్ఛ': 3, + 'చ్ఛా': 4, + 'చ్ఛే': 4, + 'ఛ్ఛా': 4, + 'జా': 2, + 'జి': 2, + 'జీ': 2, + 'జే': 2, + 'జో': 2, + 'జ్ఞ': 3, + 'జ్ఞా': 4, + 'జ్య': 3, + 'టి': 2, + 'టిం': 3, + 'టుం': 3, + 'టే': 2, + 'ట్టి': 4, + 'ట్లు': 4, + 'డి': 2, + 'డ్డ': 3, + 'డ్రు': 4, + 'ణా': 2, + 'ణిం': 3, + 'ణీ': 2, + 'తా': 2, + 'తి': 2, + 'తీ': 2, + 'తె': 2, + 'తొ': 2, + 'తో': 2, + 'త్కా': 4, + 'త్త': 3, + 'త్తి': 4, + 'త్తు': 4, + 'త్న': 3, + 'త్నిం': 5, + 'త్ప': 3, + 'త్ఫ': 3, + 'త్య': 3, + 'త్యం': 4, + 'త్యా': 4, + 'త్ర': 3, + 'త్రు': 4, + 'త్ర్య': 5, + 'త్వ': 3, + 'త్వా': 4, + 'త్స': 3, + 'థా': 2, + 'దా': 2, + 'ది': 2, + 'దిం': 3, + 'దుం': 3, + 'దే': 2, + 'దై': 2, + 'దో': 2, + 'ద్ఘో': 4, + 'ద్ది': 4, + 'ద్దు': 4, + 'ద్దే': 4, + 'ద్ధ': 3, + 'ద్ధి': 4, + 'ద్ధిం': 5, + 'ద్య': 3, + 'ద్యా': 4, + 'ద్యో': 4, + 'ద్ర్య': 5, + 'ద్వా': 4, + 'ధా': 2, + 'ధి': 2, + 'ధిం': 3, + 'ధీ': 2, + 'ధ్య': 3, + 'నా': 2, + 'నాం': 3, + 'ని': 2, + 'నిం': 3, + 'నీ': 2, + 'నుం': 3, + 'నె': 2, + 'నే': 2, + 'నై': 2, + 'నొ': 2, + 'నొం': 3, + 'న్ధ': 3, + 'న్న': 3, + 'న్నా': 4, + 'న్ని': 4, + 'న్ను': 4, + 'న్మ': 3, + 'న్య': 3, + 'న్యా': 4, + 'న్వే': 4, + 'పా': 2, + 'పిం': 3, + 'పీ': 2, + 'పె': 2, + 'పెం': 3, + 'పే': 2, + 'పై': 2, + 'పొం': 3, + 'ప్త': 3, + 'ప్ర': 3, + 'ప్రా': 4, + 'ప్రే': 4, + 'బా': 2, + 'బి': 2, + 'బో': 2, + 'బ్ర': 3, + 'భా': 2, + 'భి': 2, + 'భే': 2, + 'భ్య': 3, + 'భ్యు': 4, + 'భ్ర': 3, + 'భ్రా': 4, + 'మా': 2, + 'మి': 2, + 'మిం': 3, + 'మే': 2, + 'మై': 2, + 'మో': 2, + 'మ్య': 3, + 'యా': 2, + 'యి': 2, + 'యిం': 3, + 'యుం': 3, + 'యె': 2, + 'యే': 2, + 'యొ': 2, + 'యో': 2, + 'రా': 2, + 'రి': 2, + 'రిం': 3, + 'రై': 2, + 'రో': 2, + 'ర్': 2, + 'ర్గ': 3, + 'ర్చి': 4, + 'ర్చు': 4, + 'ర్ణ': 3, + 'ర్ణాం': 5, + 'ర్ణు': 4, + 'ర్త': 3, + 'ర్తి': 4, + 'ర్తిం': 5, + 'ర్తృ': 4, + 'ర్థ': 3, + 'ర్థి': 4, + 'ర్థిం': 5, + 'ర్ద': 3, + 'ర్దో': 4, + 'ర్ధ': 3, + 'ర్ప': 3, + 'ర్బం': 4, + 'ర్భ': 3, + 'ర్భా': 4, + 'ర్య': 3, + 'ర్యా': 4, + 'ర్రా': 4, + 'ర్వ': 3, + 'ర్వా': 4, + 'ర్హ': 3, + 'ర్హాం': 5, + 'ర్హు': 4, + 'లా': 2, + 'లి': 2, + 'లిం': 3, + 'లీ': 2, + 'లె': 2, + 'లే': 2, + 'లో': 2, + 'లౌ': 2, + 'ల్క': 3, + 'ల్గొ': 4, + 'ల్ప': 3, + 'ల్ల': 3, + 'ల్లి': 4, + 'ళా': 2, + 'వా': 2, + 'వి': 2, + 'విం': 3, + 'వీ': 2, + 'వే': 2, + 'వై': 2, + 'వ్య': 3, + 'వ్యా': 4, + 'వ్వ': 3, + 'శా': 2, + 'శాం': 3, + 'శి': 2, + 'శిం': 3, + 'శై': 2, + 'శ్చ': 3, + 'శ్చి': 4, + 'శ్య': 3, + 'శ్ర': 3, + 'శ్రాం': 5, + 'శ్లే': 4, + 'శ్వా': 4, + 'షా': 2, + 'షి': 2, + 'షిం': 3, + 'షే': 2, + 'ష్ట': 3, + 'ష్టు': 4, + 'ష్ట్ర': 5, + 'ష్ట్రీ': 6, + 'ష్ఠ': 3, + 'ష్ఠా': 4, + 'ష్ఠు': 4, + 'ష్థ': 3, + 'ష్ప': 3, + 'సా': 2, + 'సాం': 3, + 'సి': 2, + 'సీ': 2, + 'సె': 2, + 'సే': 2, + 'సౌ': 2, + 'స్కృ': 4, + 'స్తా': 4, + 'స్తి': 4, + 'స్త్ర': 5, + 'స్త్రీ': 6, + 'స్థ': 3, + 'స్థా': 4, + 'స్థి': 4, + 'స్థ్య': 5, + 'స్ప': 3, + 'స్య': 3, + 'స్వ': 3, + 'స్వా': 4, + 'స్వీ': 4, + 'స్వే': 4, + 'హా': 2, + 'హి': 2, + 'హిం': 3, + 'హో': 2, + 'ಕಾಂ': 3, + 'ಕಿಂ': 3, + 'ಕುಂ': 3, + 'ಕೆ': 2, + 'ಕೆಂ': 3, + 'ಕೊಂ': 3, + 'ಕೌ': 2, + 'ಕ್': 2, + 'ಕ್‌': 3, + 'ಖಾಂ': 3, + 'ಖ್': 2, + 'ಗಾಂ': 3, + 'ಗಿ': 2, + 'ಗೆ': 2, + 'ಗೊಂ': 3, + 'ಗೌ': 2, + 'ಗ್': 2, + 'ಘಿ': 2, + 'ಚಿ': 2, + 'ಚ್': 2, + 'ಛಿ': 2, + 'ಛೆ': 2, + 'ಜಿ': 2, + 'ಜೆ': 2, + 'ಜ್': 2, + 'ಞೆ': 2, + 'ಞೌ': 2, + 'ಟಿ': 2, + 'ಟುಂ': 3, + 'ಟ್': 2, + 'ಠಿ': 2, + 'ಡಿ': 2, + 'ಡೆ': 2, + 'ಡ್': 2, + 'ಢಿ': 2, + 'ಣಿ': 2, + 'ಣೆ': 2, + 'ತಿ': 2, + 'ತೆ': 2, + 'ತ್': 2, + 'ಥಿ': 2, + 'ಥೆ': 2, + 'ದಿ': 2, + 'ದಿಂ': 3, + 'ದೆ': 2, + 'ದೌ': 2, + 'ದ್': 2, + 'ಧಿ': 2, + 'ಧೆ': 2, + 'ಧ್': 2, + 'ನಾಂ': 3, + 'ನಿ': 2, + 'ನಿಂ': 3, + 'ನೆ': 2, + 'ನೌ': 2, + 'ನ್': 2, + 'ಪಿ': 2, + 'ಪ್': 2, + 'ಬೆ': 2, + 'ಬ್': 2, + 'ಭಿ': 2, + 'ಭೆ': 2, + 'ಭೌ': 2, + 'ಮಿ': 2, + 'ಮೆ': 2, + 'ಮ್': 2, + 'ಯಿ': 2, + 'ಯಿಂ': 3, + 'ಯುಂ': 3, + 'ಯೆ': 2, + 'ಯೆಂ': 3, + 'ಯ್': 2, + 'ರಾಂ': 3, + 'ರಿ': 2, + 'ರಿಂ': 3, + 'ರೆ': 2, + 'ರೆಂ': 3, + 'ರ್': 2, + 'ಲಿ': 2, + 'ಲೆ': 2, + 'ಲೆಂ': 3, + 'ಲ್': 2, + 'ಳಿ': 2, + 'ಳಿಂ': 3, + 'ಳೆ': 2, + 'ಳ್': 2, + 'ವಿ': 2, + 'ವುಂ': 3, + 'ವೆ': 2, + 'ವೆಂ': 3, + 'ವ್': 2, + 'ಶಾಂ': 3, + 'ಶಿ': 2, + 'ಶ್': 2, + 'ಷಿ': 2, + 'ಷೆ': 2, + 'ಷ್': 2, + 'ಸಾಂ': 3, + 'ಸಿ': 2, + 'ಸೆ': 2, + 'ಸ್': 2, + 'ಹಿ': 2, + 'ಹೆ': 2, + 'ಹೊಂ': 3, + 'കു': 2, + 'കൂ': 2, + 'കൃ': 2, + 'ക്ക': 3, + 'ക്കാ': 4, + 'ക്കി': 4, + 'ക്കു': 4, + 'ക്കും': 5, + 'ക്കെ': 4, + 'ക്കേ': 4, + 'ക്കൊ': 4, + 'ക്കോ': 4, + 'ക്ക്‌': 5, + 'ക്തി': 4, + 'ക്യ': 3, + 'ക്യം': 4, + 'ക്ര': 3, + 'ക്രൂ': 4, + 'ക്ഷ': 3, + 'ക്ഷാ': 4, + 'ക്ഷി': 4, + 'ക്ഷേ': 4, + 'ഖ്യാ': 4, + 'ഗു': 2, + 'ഗ്ഗം': 4, + 'ഗ്ദാ': 4, + 'ഗ്യ': 3, + 'ഗ്യം': 4, + 'ഗ്ര': 3, + 'ങ്കി': 4, + 'ങ്കെ': 4, + 'ങ്കേ': 4, + 'ങ്ങ': 3, + 'ങ്ങി': 4, + 'ച്ച': 3, + 'ച്ചി': 4, + 'ച്ചു': 4, + 'ച്ചും': 5, + 'ച്ചെ': 4, + 'ച്ചേ': 4, + 'ജ്യ': 3, + 'ജ്യാ': 4, + 'ഞ്ച': 3, + 'ഞ്ഞ': 3, + 'ഞ്ഞി': 4, + 'ഞ്ഞെ': 4, + 'ടു': 2, + 'ടും': 3, + 'ട്ട': 3, + 'ട്ടാ': 4, + 'ട്ടി': 4, + 'ട്ടു': 4, + 'ട്ടോ': 4, + 'ട്‌': 3, + 'ണ്ട': 3, + 'ണ്ടാ': 4, + 'ണ്ടി': 4, + 'ണ്ടു': 4, + 'ണ്ടും': 5, + 'ണ്ടെ': 4, + 'ണ്ട്‌': 5, + 'ണ്ഡ': 3, + 'ണ്ണ': 3, + 'ണ്‌': 3, + 'തു': 2, + 'തും': 3, + 'തൃ': 2, + 'ത്ത': 3, + 'ത്താ': 4, + 'ത്തി': 4, + 'ത്തു': 4, + 'ത്തും': 5, + 'ത്തെ': 4, + 'ത്തേ': 4, + 'ത്തോ': 4, + 'ത്ത്‌': 5, + 'ത്ന': 3, + 'ത്നം': 4, + 'ത്നി': 4, + 'ത്യ': 3, + 'ത്യാ': 4, + 'ത്യേ': 4, + 'ത്ര': 3, + 'ത്വ': 3, + 'ത്വം': 4, + 'ത്‌': 3, + 'ദൃ': 2, + 'ദ്ദ': 3, + 'ദ്ധ': 3, + 'ദ്ധി': 4, + 'ദ്യ': 3, + 'ദ്യാ': 4, + 'ദ്ര': 3, + 'ധൂ': 2, + 'ധ്യ': 3, + 'നു': 2, + 'നും': 3, + 'ന്ത': 3, + 'ന്തം': 4, + 'ന്തെ': 4, + 'ന്ത്ര': 5, + 'ന്ത്ര്യ': 7, + 'ന്ത്ര്യം': 8, + 'ന്ഥ': 3, + 'ന്ദ': 3, + 'ന്ധ': 3, + 'ന്ധി': 4, + 'ന്ന': 3, + 'ന്നാ': 4, + 'ന്നി': 4, + 'ന്നീ': 4, + 'ന്നു': 4, + 'ന്നും': 5, + 'ന്നെ': 4, + 'ന്നേ': 4, + 'ന്നോ': 4, + 'ന്ന്': 4, + 'ന്മ': 3, + 'ന്മാ': 4, + 'ന്മു': 4, + 'ന്മൂ': 4, + 'ന്യ': 3, + 'ന്യം': 4, + 'ന്യാ': 4, + 'ന്യേ': 4, + 'ന്യോ': 4, + 'ന്റെ': 4, + 'ന്‌': 3, + 'ന്‍': 3, + 'ന്‍കൂ': 5, + 'ന്‍പി': 5, + 'പു': 2, + 'പൂ': 2, + 'പ്പ': 3, + 'പ്പി': 4, + 'പ്പു': 4, + 'പ്പെ': 4, + 'പ്പോ': 4, + 'പ്പ്‌': 5, + 'പ്ര': 3, + 'പ്രാ': 4, + 'ബു': 2, + 'ബ്ലി': 4, + 'ഭ്യാ': 4, + 'ഭ്രാ': 4, + 'മു': 2, + 'മൃ': 2, + 'മ്പ': 3, + 'മ്പാ': 4, + 'മ്പ്ര': 5, + 'മ്മ': 3, + 'മ്മി': 4, + 'യു': 2, + 'യും': 3, + 'യ്ക്കു': 6, + 'യ്തി': 4, + 'യ്തു': 4, + 'യ്മ': 3, + 'യ്യാ': 4, + 'യ്യി': 4, + 'രു': 2, + 'രും': 3, + 'രൂ': 2, + 'ര്യ': 3, + 'ര്യം': 4, + 'ര്‍ക്കു': 7, + 'ര്‍ക്കും': 8, + 'ര്‍ക്ക്‌': 8, + 'ര്‍ഗ്ഗം': 7, + 'ര്‍ച്ച': 6, + 'ര്‍ണ്ണ': 6, + 'ര്‍ത്ത': 6, + 'ര്‍ത്തി': 7, + 'ര്‍ത്തു': 7, + 'ര്‍ത്തേ': 7, + 'ര്‍ദേ': 5, + 'ര്‍ദ്ദ': 6, + 'ര്‍പ്പെ': 7, + 'ര്‍ബ': 4, + 'ര്‍ഭ': 4, + 'ര്‍മ്മി': 7, + 'ര്‍രാ': 5, + 'ര്‍വ്വ': 6, + 'ര്‍ഹ': 4, + 'ര്‍ഹി': 5, + 'റു': 2, + 'റ്റ': 3, + 'റ്റം': 4, + 'റ്റു': 4, + 'റ്റേ': 4, + 'ലു': 2, + 'ലും': 3, + 'ല്യ': 3, + 'ല്യാ': 4, + 'ല്ല': 3, + 'ല്ലാ': 4, + 'ല്ലാം': 5, + 'ല്ലെ': 4, + 'ല്‍': 3, + 'ല്‍കി': 5, + 'ല്‍കു': 5, + 'ല്‍കേ': 5, + 'ല്‍ത': 4, + 'ല്‍നി': 5, + 'ല്‍പ്പ': 6, + 'ളു': 2, + 'ളും': 3, + 'ള്ള': 3, + 'ള്ളാ': 4, + 'ള്ളി': 4, + 'ള്ളൂ': 4, + 'ള്‍': 3, + 'ള്‍കൊ': 5, + 'ള്‍ക്ക': 6, + 'ള്‍ക്കി': 7, + 'ള്‍ക്കു': 7, + 'ള്‍ക്കും': 8, + 'ള്‍ക്കെ': 7, + 'ള്‍ക്ക്‌': 8, + 'ഴു': 2, + 'വു': 2, + 'വും': 3, + 'വൃ': 2, + 'വ്യ': 3, + 'വ്യം': 4, + 'വ്യാ': 4, + 'വ്വ': 3, + 'ശു': 2, + 'ശ്യ': 3, + 'ശ്ര': 3, + 'ശ്വാ': 4, + 'ഷ്ട': 3, + 'ഷ്ട്യാ': 6, + 'ഷ്ട്ര': 5, + 'ഷ്ട്രീ': 6, + 'ഷ്ണു': 4, + 'ഷ്യ': 3, + 'ഷ്യാ': 4, + 'സാം': 3, + 'സു': 2, + 'സൃ': 2, + 'സ്കാ': 4, + 'സ്തു': 4, + 'സ്ത്രം': 6, + 'സ്ത്രീ': 6, + 'സ്ഥ': 3, + 'സ്ഥാ': 4, + 'സ്ഥി': 4, + 'സ്യ': 3, + 'സ്റ്റ്‌': 7, + 'സ്വ': 3, + 'സ്വാ': 4, + 'സ്വീ': 4, + 'സ്സ': 3, + 'സ്സാ': 4, + 'സ്സി': 4, + 'സ്സും': 5, + 'സ്സോ': 4, + 'ഹിം': 3, + 'ഹു': 2, + 'ഹൃ': 2, + 'ൻ്': 2, + 'ක්': 2, + 'ක්‍': 3, + 'කි': 2, + 'කී': 2, + 'කු': 2, + 'කූ': 2, + 'ඛ්‍': 3, + 'ගි': 2, + 'ගී': 2, + 'ගු': 2, + 'ඟි': 2, + 'ඟී': 2, + 'ච්': 2, + 'චි': 2, + 'ජ්‍': 3, + 'ජි': 2, + 'ජී': 2, + 'ටි': 2, + 'ටී': 2, + 'ටු': 2, + 'ඩි': 2, + 'ඩු': 2, + 'ණ්': 2, + 'ණි': 2, + 'ණී': 2, + 'ණු': 2, + 'ඬු': 2, + 'ත්': 2, + 'ත්‍': 3, + 'ති': 2, + 'තී': 2, + 'තු': 2, + 'ථි': 2, + 'ද්': 2, + 'ද්‍': 3, + 'දි': 2, + 'දිං': 3, + 'දී': 2, + 'දු': 2, + 'ධ්‍': 3, + 'ධි': 2, + 'ධී': 2, + 'න්': 2, + 'න්‍': 3, + 'නි': 2, + 'නී': 2, + 'නු': 2, + 'ඳි': 2, + 'ඳී': 2, + 'ඳු': 2, + 'ප්': 2, + 'ප්‍': 3, + 'පි': 2, + 'පී': 2, + 'පු': 2, + 'පූ': 2, + 'බි': 2, + 'බී': 2, + 'භු': 2, + 'ම්': 2, + 'මි': 2, + 'මී': 2, + 'මු': 2, + 'මූ': 2, + 'ඹී': 2, + 'ඹු': 2, + 'යි': 2, + 'යු': 2, + 'ර්': 2, + 'රි': 2, + 'රී': 2, + 'රු': 2, + 'රූ': 2, + 'ල්': 2, + 'ලි': 2, + 'ලී': 2, + 'ලු': 2, + 'ව්': 2, + 'ව්‍': 3, + 'වි': 2, + 'වී': 2, + 'වු': 2, + 'වූ': 2, + 'ශ්': 2, + 'ශ්‍': 3, + 'ශී': 2, + 'ෂ්': 2, + 'ෂ්‍': 3, + 'ෂි': 2, + 'ස්': 2, + 'සි': 2, + 'සී': 2, + 'සු': 2, + 'හි': 2, + 'හිං': 3, + 'හී': 2, + 'හු': 2, + 'ළි': 2, + 'ළු': 2, + 'กั': 2, + 'กิ': 2, + 'กี': 2, + 'กุ': 2, + 'ก็': 2, + 'ก่': 2, + 'ก้': 2, + 'ขั': 2, + 'ขั้': 3, + 'ขี่': 3, + 'ขึ้': 3, + 'ขื': 2, + 'ข็': 2, + 'ข่': 2, + 'ข้': 2, + 'คั': 2, + 'คิ': 2, + 'คุ': 2, + 'คุ้': 3, + 'คู่': 3, + 'ค่': 2, + 'ค้': 2, + 'ค์': 2, + 'งิ': 2, + 'งื่': 3, + 'จั': 2, + 'จิ': 2, + 'จึ': 2, + 'จุ': 2, + 'จ็': 2, + 'จ่': 2, + 'จ้': 2, + 'ฉั': 2, + 'ชั้': 3, + 'ชิ': 2, + 'ชี': 2, + 'ชื่': 3, + 'ชื้': 3, + 'ชุ': 2, + 'ช่': 2, + 'ช้': 2, + 'ซี่': 3, + 'ซึ่': 3, + 'ญิ': 2, + 'ฎิ': 2, + 'ฏิ': 2, + 'ฒิ': 2, + 'ณิ': 2, + 'ณี': 2, + 'ณ์': 2, + 'ดั': 2, + 'ดั่': 3, + 'ดิ': 2, + 'ดิ์': 3, + 'ดี': 2, + 'ดู': 2, + 'ด็': 2, + 'ด้': 2, + 'ตั': 2, + 'ตั้': 3, + 'ติ': 2, + 'ตี': 2, + 'ตุ': 2, + 'ต็': 2, + 'ต่': 2, + 'ต่ำ': 3, + 'ต้': 2, + 'ถิ่': 3, + 'ถี': 2, + 'ถึ': 2, + 'ถื': 2, + 'ถื่': 3, + 'ถุ': 2, + 'ถู': 2, + 'ถ้': 2, + 'ทั': 2, + 'ทั่': 3, + 'ทั้': 3, + 'ทิ': 2, + 'ที': 2, + 'ที่': 3, + 'ทุ': 2, + 'ท่': 2, + 'ท้': 2, + 'ธั': 2, + 'ธิ': 2, + 'ธิ์': 3, + 'ธี': 2, + 'ธุ์': 3, + 'ธ์': 2, + 'นั': 2, + 'นั้': 3, + 'นิ': 2, + 'นี้': 3, + 'นึ': 2, + 'นึ่': 3, + 'นื': 2, + 'นื่': 3, + 'นุ': 2, + 'นุ่': 3, + 'นู': 2, + 'น่': 2, + 'น้': 2, + 'น์': 2, + 'บั': 2, + 'บั่': 3, + 'บิ': 2, + 'บี': 2, + 'บื้': 3, + 'บุ': 2, + 'บู': 2, + 'บ่': 2, + 'ปั': 2, + 'ปิ': 2, + 'ป็': 2, + 'ป่': 2, + 'ผิ': 2, + 'ผู้': 3, + 'ผ่': 2, + 'พั': 2, + 'พิ': 2, + 'พิ่': 3, + 'พี': 2, + 'พื่': 3, + 'พื้': 3, + 'พู': 2, + 'พ้': 2, + 'ภั': 2, + 'มั': 2, + 'มั่': 3, + 'มิ': 2, + 'มิ่': 3, + 'มี': 2, + 'มื': 2, + 'มุ่': 3, + 'มู': 2, + 'มู่': 3, + 'ม่': 2, + 'ม้': 2, + 'ยั': 2, + 'ยิ': 2, + 'ยิ่': 3, + 'ยี': 2, + 'ยึ': 2, + 'ยื': 2, + 'ยุ': 2, + 'ยู่': 3, + 'ย่': 2, + 'ย่ำ': 3, + 'ย้': 2, + 'ย์': 2, + 'รั': 2, + 'รั้': 3, + 'ริ': 2, + 'รี': 2, + 'รื': 2, + 'รื่': 3, + 'รุ': 2, + 'รู': 2, + 'ร็': 2, + 'ร่': 2, + 'ร้': 2, + 'ร์': 2, + 'ลั': 2, + 'ลิ': 2, + 'ลี่': 3, + 'ลี้': 3, + 'ลึ': 2, + 'ลื': 2, + 'ลื่': 3, + 'ลุ': 2, + 'ลุ่': 3, + 'ลู่': 3, + 'ล่': 2, + 'ล้': 2, + 'วั': 2, + 'วิ': 2, + 'วุ': 2, + 'ว่': 2, + 'ว้': 2, + 'ศั': 2, + 'ศิ': 2, + 'ศี': 2, + 'ศึ': 2, + 'ษ์': 2, + 'สั': 2, + 'สิ': 2, + 'สิ่': 3, + 'สิ้': 3, + 'สี': 2, + 'สื': 2, + 'สื่': 3, + 'สุ': 2, + 'สู': 2, + 'สู่': 3, + 'สู้': 3, + 'ส่': 2, + 'หั': 2, + 'ห็': 2, + 'ห่': 2, + 'ห้': 2, + 'อั': 2, + 'อิ': 2, + 'อี': 2, + 'อื่': 3, + 'อื้': 3, + 'อ้': 2, + 'ກັ': 2, + 'ກິ': 2, + 'ກີ': 2, + 'ກຸ': 2, + 'ກຸ່': 3, + 'ກົ': 2, + 'ກ່': 2, + 'ກ້': 2, + 'ກໍ': 2, + 'ກໍ່': 3, + 'ຂັ': 2, + 'ຂັ້': 3, + 'ຂີ່': 3, + 'ຂື້': 3, + 'ຂົ': 2, + 'ຂົ່': 3, + 'ຂົ້': 3, + 'ຂ່': 2, + 'ຂ້': 2, + 'ຂໍ': 2, + 'ຂໍ້': 3, + 'ຄັ': 2, + 'ຄິ': 2, + 'ຄື': 2, + 'ຄື່': 3, + 'ຄຸ': 2, + 'ຄຸ້': 3, + 'ຄູ່': 3, + 'ຄົ': 2, + 'ຄົ້': 3, + 'ຄ່': 2, + 'ຄ້': 2, + 'ງົ': 2, + 'ຈັ': 2, + 'ຈັ້': 3, + 'ຈິ': 2, + 'ຈື່': 3, + 'ຈຸ': 2, + 'ຈົ': 2, + 'ຈົ່': 3, + 'ຈົ້': 3, + 'ຈ່': 2, + 'ຈ້': 2, + 'ຊັ': 2, + 'ຊັ່': 3, + 'ຊັ້': 3, + 'ຊິ': 2, + 'ຊີ': 2, + 'ຊີ້': 3, + 'ຊື່': 3, + 'ຊື້': 3, + 'ຊຸ': 2, + 'ຊູ': 2, + 'ຊົ': 2, + 'ຊົ່': 3, + 'ຊ່': 2, + 'ຊ້': 2, + 'ຍິ': 2, + 'ຍີ່': 3, + 'ຍຸ': 2, + 'ຍົ': 2, + 'ຍ່': 2, + 'ຍ້': 2, + 'ດັ': 2, + 'ດັ່': 3, + 'ດິ': 2, + 'ດີ': 2, + 'ດົ': 2, + 'ດ້': 2, + 'ຕັ': 2, + 'ຕັ້': 3, + 'ຕິ': 2, + 'ຕີ': 2, + 'ຕົ': 2, + 'ຕົ້': 3, + 'ຕ່': 2, + 'ຕ້': 2, + 'ຕໍ່': 3, + 'ຖິ': 2, + 'ຖື': 2, + 'ຖື່': 3, + 'ຖຸ': 2, + 'ຖົ້': 3, + 'ຖ້': 2, + 'ທັ': 2, + 'ທິ': 2, + 'ທີ': 2, + 'ທີ່': 3, + 'ທຸ': 2, + 'ທົ່': 3, + 'ທ່': 2, + 'ທ້': 2, + 'ທໍ': 2, + 'ນັ': 2, + 'ນັ້': 3, + 'ນິ': 2, + 'ນີ': 2, + 'ນີ້': 3, + 'ນື': 2, + 'ນື່': 3, + 'ນື້': 3, + 'ນຸ': 2, + 'ນຸ່': 3, + 'ນູ': 2, + 'ນົ': 2, + 'ນ້': 2, + 'ບັ': 2, + 'ບີ': 2, + 'ບຸ': 2, + 'ບູ': 2, + 'ບົ': 2, + 'ບ່': 2, + 'ບໍ': 2, + 'ບໍ່': 3, + 'ປັ': 2, + 'ປີ': 2, + 'ປົ': 2, + 'ປ່': 2, + 'ປ້': 2, + 'ຜັ': 2, + 'ຜິ': 2, + 'ຜີ': 2, + 'ຜູ້': 3, + 'ຜົ': 2, + 'ຜ່': 2, + 'ພັ': 2, + 'ພິ': 2, + 'ພີ': 2, + 'ພີ່': 3, + 'ພຶ': 2, + 'ພື່': 3, + 'ພູ': 2, + 'ພົ': 2, + 'ພົ້': 3, + 'ພ້': 2, + 'ພໍ': 2, + 'ພໍ່': 3, + 'ຟັ': 2, + 'ຟ້': 2, + 'ມັ': 2, + 'ມິ': 2, + 'ມີ': 2, + 'ມື': 2, + 'ມື່': 3, + 'ມຸ້': 3, + 'ມູ': 2, + 'ມົ': 2, + 'ມ່': 2, + 'ຢັ': 2, + 'ຢື': 2, + 'ຢູ່': 3, + 'ຢູ້': 3, + 'ຢ່': 2, + 'ຢ້': 2, + 'ລັ': 2, + 'ລິ': 2, + 'ລີ': 2, + 'ລື': 2, + 'ລຸ': 2, + 'ລຸ້': 3, + 'ລູ': 2, + 'ລົ': 2, + 'ລ້': 2, + 'ວັ': 2, + 'ວັ່': 3, + 'ວິ': 2, + 'ວຸ': 2, + 'ວົ': 2, + 'ວົ້': 3, + 'ວ່': 2, + 'ວ້': 2, + 'ສັ': 2, + 'ສິ': 2, + 'ສີ່': 3, + 'ສີ້': 3, + 'ສຶ': 2, + 'ສື': 2, + 'ສຸ': 2, + 'ສູ້': 3, + 'ສົ': 2, + 'ສ່': 2, + 'ສ້': 2, + 'ຫັ': 2, + 'ຫຼ': 2, + 'ຫຼັ': 3, + 'ຫຼິ': 3, + 'ຫຼື': 3, + 'ຫຼົ': 3, + 'ຫຼົ່': 4, + 'ຫ່': 2, + 'ຫ້': 2, + 'ອັ': 2, + 'ອິ': 2, + 'ອີ': 2, + 'ອື່': 3, + 'ອຸ': 2, + 'ອົ': 2, + 'ອ່': 2, + 'ອ້': 2, + 'ຮັ': 2, + 'ຮີ': 2, + 'ຮູ້': 3, + 'ຮົ': 2, + 'ຮົ່': 3, + 'ຮ່': 2, + 'ຮ້': 2, + 'ໜັ': 2, + 'ໜີ່': 3, + 'ໜື່': 3, + 'ໜູ': 2, + 'ໜ່': 2, + 'ໜ້': 2, + 'ໝັ້': 3, + 'ໝີ': 2, + 'ໝູ່': 3, + 'ໝ່': 2, + 'ໝ້': 2, + 'ཀུ': 2, + 'ཀོ': 2, + 'ཀྱ': 2, + 'ཀྱི': 3, + 'ཀླ': 2, + 'ཀློ': 3, + 'ཁུ': 2, + 'ཁེ': 2, + 'ཁོ': 2, + 'ཁྱ': 2, + 'ཁྱི': 3, + 'ཁྲི': 3, + 'ཁྲོ': 3, + 'གི': 2, + 'གུ': 2, + 'གེ': 2, + 'གོ': 2, + 'གྱ': 2, + 'གྱི': 3, + 'གྱུ': 3, + 'གྱོ': 3, + 'གྲ': 2, + 'གྲི': 3, + 'གྲུ': 3, + 'གྲེ': 3, + 'གྲོ': 3, + 'གླ': 2, + 'གླི': 3, + 'གླེ': 3, + 'ངུ': 2, + 'ངེ': 2, + 'ངོ': 2, + 'ཅི': 2, + 'ཅུ': 2, + 'ཅོ': 2, + 'ཆུ': 2, + 'ཆེ': 2, + 'ཆོ': 2, + 'ཇི': 2, + 'ཇུ': 2, + 'ཇོ': 2, + 'ཉི': 2, + 'ཉེ': 2, + 'ཉོ': 2, + 'ཏུ': 2, + 'ཏེ': 2, + 'ཏོ': 2, + 'ཐི': 2, + 'ཐུ': 2, + 'ཐེ': 2, + 'ཐོ': 2, + 'དི': 2, + 'དུ': 2, + 'དེ': 2, + 'དོ': 2, + 'དྲ': 2, + 'དྲི': 3, + 'དྲུ': 3, + 'དྲོ': 3, + 'ནི': 2, + 'ནུ': 2, + 'ནོ': 2, + 'པེ': 2, + 'པོ': 2, + 'པྱ': 2, + 'པྱོ': 3, + 'ཕེ': 2, + 'ཕོ': 2, + 'ཕྱི': 3, + 'ཕྱེ': 3, + 'ཕྱོ': 3, + 'ཕྲུ': 3, + 'ཕྲོ': 3, + 'བུ': 2, + 'བེ': 2, + 'བོ': 2, + 'བྱ': 2, + 'བྱི': 3, + 'བྱུ': 3, + 'བྱེ': 3, + 'བྱོ': 3, + 'བྲ': 2, + 'བྲེ': 3, + 'བླ': 2, + 'བློ': 3, + 'མི': 2, + 'མེ': 2, + 'མོ': 2, + 'མྱོ': 3, + 'ཙུ': 2, + 'ཙོ': 2, + 'ཚུ': 2, + 'ཚེ': 2, + 'ཚོ': 2, + 'ཛི': 2, + 'ཛུ': 2, + 'ཛོ': 2, + 'ཞི': 2, + 'ཞུ': 2, + 'ཟི': 2, + 'ཟུ': 2, + 'ཟེ': 2, + 'ཟོ': 2, + 'ཟླ': 2, + 'ཟློ': 3, + 'འི': 2, + 'འུ': 2, + 'འོ': 2, + 'ཡི': 2, + 'ཡུ': 2, + 'ཡོ': 2, + 'རི': 2, + 'རུ': 2, + 'རེ': 2, + 'རོ': 2, + 'རྐྱ': 3, + 'རྐྱེ': 4, + 'རྒ': 2, + 'རྒོ': 3, + 'རྒྱ': 3, + 'རྒྱུ': 4, + 'རྗེ': 3, + 'རྗོ': 3, + 'རྟ': 2, + 'རྟེ': 3, + 'རྟོ': 3, + 'རྡ': 2, + 'རྣ': 2, + 'རྩ': 2, + 'རྩི': 3, + 'རྩུ': 3, + 'རྩོ': 3, + 'རྫ': 2, + 'ལཱ': 2, + 'ལུ': 2, + 'ལེ': 2, + 'ལོ': 2, + 'ལྔ': 2, + 'ལྕི': 3, + 'ལྕོ': 3, + 'ལྟ': 2, + 'ལྟེ': 3, + 'ལྡ': 2, + 'ལྡེ': 3, + 'ལྷ': 2, + 'ལྷུ': 3, + 'ལྷོ': 3, + 'ཤི': 2, + 'ཤུ': 2, + 'ཤེ': 2, + 'ཤོ': 2, + 'སུ': 2, + 'སེ': 2, + 'སོ': 2, + 'སྐ': 2, + 'སྐི': 3, + 'སྐུ': 3, + 'སྐོ': 3, + 'སྐྱ': 3, + 'སྐྱི': 4, + 'སྐྱུ': 4, + 'སྐྱེ': 4, + 'སྐྱོ': 4, + 'སྒེ': 3, + 'སྒོ': 3, + 'སྒྱུ': 4, + 'སྒྲ': 3, + 'སྒྲི': 4, + 'སྒྲུ': 4, + 'སྔ': 2, + 'སྔོ': 3, + 'སྙི': 3, + 'སྟ': 2, + 'སྟུ': 3, + 'སྟེ': 3, + 'སྟོ': 3, + 'སྡ': 2, + 'སྡི': 3, + 'སྡུ': 3, + 'སྡེ': 3, + 'སྡོ': 3, + 'སྣ': 2, + 'སྣོ': 3, + 'སྤུ': 3, + 'སྤེ': 3, + 'སྤྱ': 3, + 'སྤྱི': 4, + 'སྤྱོ': 4, + 'སྤྲ': 3, + 'སྤྲོ': 4, + 'སྦེ': 3, + 'སྦྱ': 3, + 'སྦྱི': 4, + 'སྦྱོ': 4, + 'སྦྲེ': 4, + 'སྨ': 2, + 'སྨྲ': 3, + 'སྲ': 2, + 'སྲི': 3, + 'སྲུ': 3, + 'སྲོ': 3, + 'སླ': 2, + 'སློ': 3, + 'ཧྲི': 3, + 'ཧྲེ': 3, + 'ཨི': 2, + 'ཨུ': 2, + 'ကိ': 2, + 'ကို': 3, + 'ကီု': 3, + 'ကု': 2, + 'ကူ': 2, + 'ကေု': 3, + 'ကဲ့': 3, + 'ကဵု': 3, + 'ကံ': 2, + 'က္က': 3, + 'က္ခ': 3, + 'က္ဍ': 3, + 'က္ဍို': 5, + 'က်': 2, + 'က်ျ': 3, + 'ကျို': 4, + 'ကျူ': 3, + 'ကျေ': 3, + 'ကျွ': 3, + 'ကြို': 4, + 'ကြီ': 3, + 'ကြု': 3, + 'ကြေ': 3, + 'ကြံ': 3, + 'ကြွေ': 4, + 'ကွ': 2, + 'ကၟ': 2, + 'ကၟိ': 3, + 'ကၟို': 4, + 'ကၟဲ': 3, + 'ကၠ': 2, + 'ကၠို': 4, + 'ကၠီု': 4, + 'ကၠု': 3, + 'ကၠေ': 3, + 'ကၠေံ': 4, + 'ကၠဳ': 3, + 'ခိ': 2, + 'ခို': 3, + 'ခု': 2, + 'ခဲ့': 3, + 'ခဳ': 2, + 'ခံ': 2, + 'ချိ': 3, + 'ချို': 4, + 'ချို့': 5, + 'ချီ': 3, + 'ချု': 3, + 'ချေ': 3, + 'ချဳ': 3, + 'ခြုံ': 4, + 'ခြေ': 3, + 'ခွ': 2, + 'ခွဲ': 3, + 'ခၞံ': 3, + 'ခၠ': 2, + 'ဂိ': 2, + 'ဂို': 3, + 'ဂီု': 3, + 'ဂု': 2, + 'ဂဵု': 3, + 'ဂ္ဂ': 3, + 'ဂ္ဂို': 5, + 'ဂျို': 4, + 'ဂွံ': 3, + 'ဂှ်': 3, + 'ဂၞ': 2, + 'ဂၞဴ': 3, + 'ဂၟံ': 3, + 'ဂၠ': 2, + 'ဂၠိ': 3, + 'ဂၠို': 4, + 'ဂၠေ': 3, + 'ဂၠံ': 3, + 'ငိ': 2, + 'ငို': 3, + 'ငဲ့': 3, + 'ငံ': 2, + 'င့်': 3, + 'င်': 2, + 'င်္ဂ': 4, + 'ငြိ': 3, + 'ငွေ': 3, + 'ငှ': 2, + 'စိ': 2, + 'စို': 3, + 'စီ': 2, + 'စု': 2, + 'စုံ': 3, + 'စဳ': 2, + 'စဴ': 2, + 'စဵု': 3, + 'စံ': 2, + 'စ္စ': 3, + 'စ်': 2, + 'စွ': 2, + 'စွဲ': 3, + 'စွံ': 3, + 'စှ်': 3, + 'စှ်ေ': 4, + 'စၞ': 2, + 'စၞေ': 3, + 'စၟ': 2, + 'စၠေ': 3, + 'ဆို': 3, + 'ဆီ': 2, + 'ဆုဲ': 3, + 'ဆုံ': 3, + 'ဆံ': 2, + 'ဆွဲ': 3, + 'ဇို': 3, + 'ဇူ': 2, + 'ဇြေ': 3, + 'ဇြဳ': 3, + 'ဇၞ': 2, + 'ဇၞေ': 3, + 'ဇၟ': 2, + 'ဉ်': 2, + 'ညိ': 2, + 'ညီ': 2, + 'ညဳ': 2, + 'ညံ': 2, + 'ည်': 2, + 'ည့်': 3, + 'ညှ': 2, + 'ဋိ': 2, + 'ဋ္ဌ': 3, + 'ဍိ': 2, + 'ဍို': 3, + 'ဍု': 2, + 'ဍဵု': 3, + 'ဏီ': 2, + 'ဏံ': 2, + 'ဏ်': 2, + 'ဏှ': 2, + 'တိ': 2, + 'တို': 3, + 'တို့': 4, + 'တိူ': 3, + 'တီ': 2, + 'တု': 2, + 'တုဲ': 3, + 'တူ': 2, + 'တေံ': 3, + 'တဲ': 2, + 'တဴ': 2, + 'တဵ': 2, + 'တံ': 2, + 'တံ့': 3, + 'တ္တ': 3, + 'တ္ၚဲ': 4, + 'တ်': 2, + 'တ်ို': 4, + 'တြုံ': 4, + 'တွ': 2, + 'တွေ': 3, + 'တွဵု': 4, + 'တှ်ေ': 4, + 'တၞ': 2, + 'တၟ': 2, + 'တၟိ': 3, + 'တၟို': 4, + 'တၟေ': 3, + 'တၟံ': 3, + 'တၠု': 3, + 'တႂ်': 3, + 'တႅ': 2, + 'ထိ': 2, + 'ထို': 3, + 'ထို့': 4, + 'ထီ': 2, + 'ထု': 2, + 'ထုဲ': 3, + 'ထူ': 2, + 'ထဵ': 2, + 'ထံ': 2, + 'ထ္ၜ': 3, + 'ထွ': 2, + 'ထွေ': 3, + 'ဒို': 3, + 'ဒီ': 2, + 'ဒီု': 3, + 'ဒု': 2, + 'ဒေံ': 3, + 'ဒဳ': 2, + 'ဒဵု': 3, + 'ဒ္ဂေ': 4, + 'ဒ်': 2, + 'ဒှ်': 3, + 'ဒၞ': 2, + 'ဒၟံ': 3, + 'ဓိ': 2, + 'ဓု': 2, + 'နိ': 2, + 'နို': 3, + 'နု': 2, + 'နူ': 2, + 'နေ့': 3, + 'န္ဒ': 3, + 'န္နိ': 4, + 'န်': 2, + 'န့်': 3, + 'နွ': 2, + 'နွံ': 3, + 'နှ': 2, + 'နှိ': 3, + 'နှီ': 3, + 'နှေ': 3, + 'ပိ': 2, + 'ပို': 3, + 'ပိုဲ': 4, + 'ပိူ': 3, + 'ပီ': 2, + 'ပု': 2, + 'ပုံ': 3, + 'ပူ': 2, + 'ပေဲ': 3, + 'ပဲ': 2, + 'ပဵ': 2, + 'ပံ': 2, + 'ပံ့': 3, + 'ပ္ဍဲ': 4, + 'ပ္တံ': 4, + 'ပ္ပ': 3, + 'ပ္ပံ': 4, + 'ပ်': 2, + 'ပျေ': 3, + 'ပျဲ': 3, + 'ပြီ': 3, + 'ပြု': 3, + 'ပြေ': 3, + 'ပြဳ': 3, + 'ပြံ': 3, + 'ပွ': 2, + 'ပွဲ': 3, + 'ပွႆ': 3, + 'ပၟိ': 3, + 'ပၠ': 2, + 'ပႂ်': 3, + 'ပႅ': 2, + 'ပႆ': 2, + 'ဖိ': 2, + 'ဖီ': 2, + 'ဖဵု': 3, + 'ဖံ': 2, + 'ဖျု': 3, + 'ဖျေံ': 4, + 'ဖြို': 4, + 'ဖွ': 2, + 'ဖွဲ့': 4, + 'ဖွံ့': 4, + 'ဗီု': 3, + 'ဗု': 2, + 'ဗ္တေ': 4, + 'ဗ္ၜေ': 4, + 'ဗျဵု': 4, + 'ဗြေ': 3, + 'ဗြဴ': 3, + 'ဗွို': 4, + 'ဗွဲ': 3, + 'ဗၠ': 2, + 'ဗၠေ': 3, + 'ဘူ': 2, + 'ဘဲ': 2, + 'ဘဴ': 2, + 'မိ': 2, + 'မို': 3, + 'မိူ': 3, + 'မီ': 2, + 'မု': 2, + 'မူ': 2, + 'မဲ': 2, + 'မဲ့': 3, + 'မဳ': 2, + 'မဵု': 3, + 'မံ': 2, + 'မ္ဂ': 3, + 'မ္တီ': 4, + 'မ္ဘ': 3, + 'မ္မ': 3, + 'မ္ၚဵု': 5, + 'မ်': 2, + 'မ့်': 3, + 'မျို': 4, + 'မျီု': 4, + 'မျှ': 3, + 'မြေ': 3, + 'မြှ': 3, + 'မွ': 2, + 'မွေ': 3, + 'မွေ့': 4, + 'မွဲ': 3, + 'မှ': 2, + 'မှီ': 3, + 'မှု': 3, + 'မှေ': 3, + 'မၞိ': 3, + 'မၞု': 3, + 'မၞုံ': 4, + 'မၠို': 4, + 'မၠေ': 3, + 'မႂ်': 3, + 'မႅ': 2, + 'မႆ': 2, + 'ယိ': 2, + 'ယို': 3, + 'ယိူ': 3, + 'ယု': 2, + 'ယုံ': 3, + 'ယူ': 2, + 'ယဲ': 2, + 'ယဵ': 2, + 'ယံ': 2, + 'ယ်': 2, + 'ယျဵု': 4, + 'ယြဴ': 3, + 'ယွ': 2, + 'ယှ': 2, + 'ယႂ်': 3, + 'ယႅ': 2, + 'ရို': 3, + 'ရီု': 3, + 'ရု': 2, + 'ရုဲ': 3, + 'ရုံ': 3, + 'ရူ': 2, + 'ရေံ': 3, + 'ရဲ': 2, + 'ရဴ': 2, + 'ရံ': 2, + 'ရ်': 2, + 'ရွ': 2, + 'ရွေ': 3, + 'ရွံ့': 4, + 'ရွှေ့': 5, + 'ရှ': 2, + 'ရှိ': 3, + 'ရှေ': 3, + 'ရှေ့': 4, + 'ရှ်ေ': 4, + 'လိ': 2, + 'လို': 3, + 'လို့': 4, + 'လိူ': 3, + 'လီ': 2, + 'လီု': 3, + 'လု': 2, + 'လုံ': 3, + 'လူ': 2, + 'လူ့': 3, + 'လဲ': 2, + 'လဵ': 2, + 'လဵု': 3, + 'လံ': 2, + 'လ္ပ': 3, + 'လ္ၚ': 3, + 'လ်': 2, + 'လျို': 4, + 'လျေ': 3, + 'လျှ': 3, + 'လျှို့': 6, + 'လျှေ': 4, + 'လွ': 2, + 'လွဳ': 3, + 'လွှ': 3, + 'လွှဲ': 4, + 'လှ': 2, + 'လှုံ': 4, + 'လၟေ': 3, + 'လႂ်': 3, + 'လႅ': 2, + 'လႆ': 2, + 'ဝူ': 2, + 'ဝဲ': 2, + 'ဝဳ': 2, + 'ဝဵု': 3, + 'ဝ်': 2, + 'ဝွံ': 3, + 'ဝှ': 2, + 'ဝႆ': 2, + 'သိ': 2, + 'သို': 3, + 'သို့': 4, + 'သိူ': 3, + 'သီ': 2, + 'သီု': 3, + 'သု': 2, + 'သုံ': 3, + 'သူ': 2, + 'သူ့': 3, + 'သဳ': 2, + 'သဵ': 2, + 'သဵု': 3, + 'သ္က': 3, + 'သ္ကံ': 4, + 'သ္ဂု': 4, + 'သ္ဂေ': 4, + 'သ္ဇို': 5, + 'သ္ဒ': 3, + 'သ္ပ': 3, + 'သ္ၚိ': 4, + 'သ်': 2, + 'သြို': 4, + 'သွ': 2, + 'သွံ': 3, + 'သှ်': 3, + 'သှ်ေ': 4, + 'သၞ': 2, + 'သၞေ': 3, + 'သၟ': 2, + 'သၠ': 2, + 'သၠု': 3, + 'သႂ်': 3, + 'သႅ': 2, + 'ဟို': 3, + 'ဟီု': 3, + 'ဟု': 2, + 'ဟူ': 2, + 'ဟ်': 2, + 'ဟွံ': 3, + 'အိ': 2, + 'အို': 3, + 'အု': 2, + 'အံ': 2, + 'အ်': 2, + 'ဢိ': 2, + 'ဢို': 3, + 'ဢွ': 2, + 'ဿီု': 3, + 'ၚု': 2, + 'ၚ်': 2, + 'ၜို': 3, + 'ၵိ': 2, + 'ၵို': 3, + 'ၵိူ': 3, + 'ၵု': 2, + 'ၵူ': 2, + 'ၵဵ': 2, + 'ၵ်': 2, + 'ၵွ': 2, + 'ၵႂ': 2, + 'ၵႅ': 2, + 'ၶိ': 2, + 'ၶို': 3, + 'ၶိူ': 3, + 'ၶီ': 2, + 'ၶု': 2, + 'ၶူ': 2, + 'ၶွ': 2, + 'ၶႂ': 2, + 'ၶႅ': 2, + 'ၶႆ': 2, + 'ၸိ': 2, + 'ၸို': 3, + 'ၸိူ': 3, + 'ၸု': 2, + 'ၸူ': 2, + 'ၸဵ': 2, + 'ၸွ': 2, + 'ၸွႆ': 3, + 'ၸႂ်': 3, + 'ၸႅ': 2, + 'ၺ်': 2, + 'ၺွ': 2, + 'ၼို': 3, + 'ၼိူ': 3, + 'ၼု': 2, + 'ၼဵ': 2, + 'ၼ်': 2, + 'ၼွ': 2, + 'ၼႂ်': 3, + 'ၼႆ': 2, + 'ၽိ': 2, + 'ၽို': 3, + 'ၽု': 2, + 'ၽူ': 2, + 'ၽဵ': 2, + 'ၽွ': 2, + 'ၽႂ်': 3, + 'ၾိ': 2, + 'ႁို': 3, + 'ႁိူ': 3, + 'ႁု': 2, + 'ႁူ': 2, + 'ႁဵ': 2, + 'ႁွ': 2, + 'ႁႂ်': 3, + 'ႁႅ': 2, + 'ᜄ᜔': 2, + 'ᜆᜒ': 2, + 'ᜆ᜔': 2, + 'ᜇᜒ': 2, + 'ᜇᜓ': 2, + 'ᜇ᜔': 2, + 'ᜈᜒ': 2, + 'ᜈ᜔': 2, + 'ᜉᜒ': 2, + 'ᜊᜓ': 2, + 'ᜋ᜔': 2, + 'ᜌ᜔': 2, + 'ᜎᜓ': 2, + 'ᜏᜒ': 2, + 'ᜐᜒ': 2, + 'ᜑᜒ': 2, + 'កិ': 2, + 'កុ': 2, + 'កុំ': 3, + 'កំ': 2, + 'ក់': 2, + 'ក៏': 2, + 'ក្ខ': 3, + 'ក្ដិ': 4, + 'ក្ដី': 4, + 'ក្ដៅ': 4, + 'ក្នុ': 4, + 'ក្រ': 3, + 'ក្រុ': 4, + 'ក្រោ': 4, + 'ក្ស': 3, + 'ក្សា': 4, + 'ខិ': 2, + 'ខុ': 2, + 'ខំ': 2, + 'ខ្ជា': 4, + 'ខ្ព': 3, + 'ខ្លា': 4, + 'ខ្លួ': 4, + 'គាំ': 3, + 'គិ': 2, + 'គឺ': 2, + 'គំ': 2, + 'គ៌': 2, + 'គ្គ': 3, + 'គ្នា': 4, + 'គ្មា': 4, + 'គ្រ': 3, + 'គ្រា': 4, + 'គ្រឹះ': 5, + 'គ្រួ': 4, + 'ឃុំ': 3, + 'ឃ្ន': 3, + 'ងឺ': 2, + 'ង់': 2, + 'ង្ក': 3, + 'ង្កើ': 4, + 'ង្ខិ': 4, + 'ង្ខំ': 4, + 'ង្គ': 3, + 'ង្រី': 4, + 'ង្រឹ': 4, + 'ង្រៀ': 4, + 'ង្ហា': 4, + 'ង្អ': 3, + 'ចាំ': 3, + 'ចិ': 2, + 'ចុ': 2, + 'ចូ': 2, + 'ចំ': 2, + 'ច់': 2, + 'ច្ច': 3, + 'ច្ចេ': 4, + 'ច្បា': 4, + 'ឆាំ': 3, + 'ឆ្នាំ': 5, + 'ឆ្នោ': 4, + 'ឆ្ល': 3, + 'ឆ្លើ': 4, + 'ជិ': 2, + 'ជី': 2, + 'ជុំ': 3, + 'ជំ': 2, + 'ជ្ជ': 3, + 'ជ្ជា': 4, + 'ជ្ញា': 4, + 'ជ្យ': 3, + 'ជ្រ': 3, + 'ជ្រើ': 4, + 'ជ្រៀ': 4, + 'ជ្រែ': 4, + 'ឈ្មោះ': 5, + 'ញុះ': 3, + 'ញ្ចូ': 4, + 'ញ្ជា': 4, + 'ញ្ញ': 3, + 'ញ្ញា': 4, + 'ញ្ញៈ': 4, + 'ដី': 2, + 'ដឹ': 2, + 'ដូ': 2, + 'ដំ': 2, + 'ដ៏': 2, + 'ដ្ឋ': 3, + 'ដ្ឋា': 4, + 'ឌ្ឍ': 3, + 'ណិ': 2, + 'ណី': 2, + 'ណោះ': 3, + 'ណ៌': 2, + 'ណ៍': 2, + 'ណ្ដា': 4, + 'ណ្ដុះ': 5, + 'ណ្ឌ': 3, + 'ណ្ណោះ': 5, + 'តិ': 2, + 'តី': 2, + 'តុ': 2, + 'តំ': 2, + 'ត់': 2, + 'ត់់': 3, + 'ត៌': 2, + 'ត្ដ': 3, + 'ត្ដិ': 4, + 'ត្ថ': 3, + 'ត្ថិ': 4, + 'ត្យ': 3, + 'ត្រ': 3, + 'ត្រា': 4, + 'ត្រឹ': 4, + 'ត្រូ': 4, + 'ត្ស': 3, + 'ថ្ងៃ': 4, + 'ថ្នា': 4, + 'ថ្នូ': 4, + 'ថ្មី': 4, + 'ថ្លៃ': 4, + 'ទាំ': 3, + 'ទិ': 2, + 'ទិ៍': 3, + 'ទី': 2, + 'ទឹ': 2, + 'ទុ': 2, + 'ទូ': 2, + 'ទួ': 2, + 'ទោះ': 3, + 'ទំ': 2, + 'ទ្ធ': 3, + 'ទ្ធិ': 4, + 'ទ្យា': 4, + 'ទ្រ': 3, + 'ធិ': 2, + 'ធី': 2, + 'ធ្ង': 3, + 'ធ្នា': 4, + 'ធ្នូ': 4, + 'ធ្យា': 4, + 'ធ្យោ': 4, + 'ធ្វើ': 4, + 'នាំ': 3, + 'និ': 2, + 'នី': 2, + 'នឹ': 2, + 'នុ': 2, + 'នូ': 2, + 'នួ': 2, + 'នេះ': 3, + 'នោះ': 3, + 'ន់': 2, + 'ន៍': 2, + 'ន្ដ': 3, + 'ន្ដិ': 4, + 'ន្ថែ': 4, + 'ន្ថោ': 4, + 'ន្ទា': 4, + 'ន្ទៈ': 4, + 'ន្ធ': 3, + 'ន្និ': 4, + 'ន្ម': 3, + 'ន្យា': 4, + 'ន្លែ': 4, + 'បិ': 2, + 'បី': 2, + 'បុ': 2, + 'បូ': 2, + 'បោះ': 3, + 'បំ': 2, + 'ប៉ុ': 3, + 'ប៉ះ': 3, + 'ប់': 2, + 'ប្ដី': 4, + 'ប្ដូ': 4, + 'ប្ដេ': 4, + 'ប្ប': 3, + 'ប្បី': 4, + 'ប្រ': 3, + 'ប្រា': 4, + 'ប្រឹ': 4, + 'ប្រុ': 4, + 'ប្រើ': 4, + 'ប្រែ': 4, + 'ប្អូ': 4, + 'ផុ': 2, + 'ផ្ដ': 3, + 'ផ្ដា': 4, + 'ផ្ទា': 4, + 'ផ្ទុ': 4, + 'ផ្ទេ': 4, + 'ផ្នែ': 4, + 'ផ្លា': 4, + 'ផ្លិ': 4, + 'ផ្ស': 3, + 'ផ្សា': 4, + 'ផ្សេ': 4, + 'ផ្សៃ': 4, + 'ពិ': 2, + 'ពី': 2, + 'ពុ': 2, + 'ពូ': 2, + 'ពោះ': 3, + 'ព័': 2, + 'ព្យ': 3, + 'ព្យា': 4, + 'ព្រ': 3, + 'ព្រឹ': 4, + 'ព្រៀ': 4, + 'ព្រៃ': 4, + 'ព្រំ': 4, + 'ព្វ': 3, + 'ភិ': 2, + 'ភ័': 2, + 'មិ': 2, + 'មុ': 2, + 'មូ': 2, + 'មួ': 2, + 'ម៉ា': 3, + 'ម៉ោ': 3, + 'ម៌': 2, + 'ម័': 2, + 'ម្ងា': 4, + 'ម្ចា': 4, + 'ម្ដា': 4, + 'ម្ដែ': 4, + 'ម្នា': 4, + 'ម្នេ': 4, + 'ម្ប': 3, + 'ម្បី': 4, + 'ម្បុ': 4, + 'ម្ភា': 4, + 'ម្ម': 3, + 'ម្រ': 3, + 'ម្រា': 4, + 'ម្រិ': 4, + 'ម្រូ': 4, + 'ម្រើ': 4, + 'ម្រេ': 4, + 'ម្លៀ': 4, + 'ម្លៃ': 4, + 'យុ': 2, + 'យ៉ា': 3, + 'យ្យ': 3, + 'រាំ': 3, + 'រិ': 2, + 'រី': 2, + 'រុ': 2, + 'រូ': 2, + 'រួ': 2, + 'រំ': 2, + 'រ្ដិ៍': 5, + 'លិ': 2, + 'លុះ': 3, + 'លូ': 2, + 'លំ': 2, + 'ល់': 2, + 'ល្បៈ': 4, + 'ល្មើ': 4, + 'វិ': 2, + 'សិ': 2, + 'សី': 2, + 'សុ': 2, + 'សុំ': 3, + 'សំ': 2, + 'ស់': 2, + 'ស័': 2, + 'ស្គា': 4, + 'ស្ដី': 4, + 'ស្ដ្រី': 6, + 'ស្ត្រ': 5, + 'ស្ថា': 4, + 'ស្ថិ': 4, + 'ស្ទួ': 4, + 'ស្នា': 4, + 'ស្មា': 4, + 'ស្មើ': 4, + 'ស្រ': 3, + 'ស្រា': 4, + 'ស្រី': 4, + 'ស្រ័': 4, + 'ស្លា': 4, + 'ស្វែ': 4, + 'ស្វ័': 4, + 'ស្ស': 3, + 'ហូ': 2, + 'ហ៍': 2, + 'ហ្ម': 3, + 'ឡុ': 2, + 'អំ': 2, + 'អ្ន': 3, + 'អ្វី': 4, + 'ឱ្យ': 3, + 'ᨠ᩠ᨠ': 3, + 'ᨠ᩠ᨦᩢᩢ': 5, + 'ᨠ᩠ᨦᩨ᩵': 5, + 'ᨠ᩠ᨭᩫ': 4, + 'ᨠ᩠ᨯᩢ': 4, + 'ᨠ᩠ᨶᩢ': 4, + 'ᨠ᩠ᨶᩥ': 4, + 'ᨠ᩠ᩅ': 3, + 'ᨠ᩠ᩅᩢ': 4, + 'ᨠ᩠ᩅᩫ': 4, + 'ᨠ᩠ᩋ᩵': 4, + 'ᨠᩢ': 2, + 'ᨠᩢ᩠ᨷ': 4, + 'ᨠᩥ': 2, + 'ᨠᩨ': 2, + 'ᨠᩩ': 2, + 'ᨠᩮ᩠': 3, + 'ᨠᩮᩢ᩵': 4, + 'ᨠᩮ᩵': 3, + 'ᨠᩯ᩠': 3, + 'ᨠᩯ᩵': 3, + 'ᨠᩳ᩵': 3, + 'ᨠ᩵': 2, + 'ᨠ᩵ᩴ': 3, + 'ᨡ᩠ᨦᩢ': 4, + 'ᨡ᩠ᨯᩢ': 4, + 'ᨡ᩠ᨶ': 3, + 'ᨡ᩠ᨶᩢ': 4, + 'ᨡ᩠ᨶᩨ᩶': 5, + 'ᨡ᩠ᩅ': 3, + 'ᨡ᩠ᩋ': 3, + 'ᨡᩢ': 2, + 'ᨡᩮ᩠': 3, + 'ᨡᩮᩢ': 3, + 'ᨡᩮᩢᩢ': 4, + 'ᨡᩯ᩠': 3, + 'ᨡᩱᩢ': 3, + 'ᨡᩳ᩶': 3, + 'ᨡ᩵': 2, + 'ᨣᩕ᩠': 3, + 'ᨣᩕᩮ᩠': 4, + 'ᨣ᩠ᨣ': 3, + 'ᨣ᩠ᨯᩧ': 4, + 'ᨣ᩠ᨶᩢ': 4, + 'ᨣ᩠ᨾᩫ': 4, + 'ᨣ᩠ᩅ': 3, + 'ᨣ᩠ᩋᩢ': 4, + 'ᨣᩢ': 2, + 'ᨣᩢ᩠ᨷ': 4, + 'ᨣᩢᩴ': 3, + 'ᨣᩩ': 2, + 'ᨣᩩᩢ': 3, + 'ᨣᩮᩢᩢ': 4, + 'ᨣᩯ᩠': 3, + 'ᨣᩳ᩶': 3, + 'ᨣᩴ': 2, + 'ᨤ᩠ᨯᩥ': 4, + 'ᨤ᩠ᨶᩨ': 4, + 'ᨤ᩠ᨶᩫ': 4, + 'ᨤ᩠ᩅᩫ': 4, + 'ᨤ᩠ᩋ': 3, + 'ᨤᩢ᩠ᨬ': 4, + 'ᨤᩮᩢ': 3, + 'ᨤᩱ᩵': 3, + 'ᨥᩢ': 2, + 'ᨦᩢ': 2, + 'ᨦᩢᩢ': 3, + 'ᨦᩨ': 2, + 'ᨦᩨ᩵': 3, + 'ᨦᩨ᩶': 3, + 'ᨦ᩵': 2, + 'ᨧ᩠ᨦᩥ᩵': 5, + 'ᨧ᩠ᨯᩢ': 4, + 'ᨧᩢ': 2, + 'ᨧᩮ᩠': 3, + 'ᨧᩮᩢᩢ': 4, + 'ᨧᩱ᩠': 3, + 'ᨧᩴ': 2, + 'ᨧ᩵ᩴ': 3, + 'ᨨ᩠ᨶᩢ': 4, + 'ᨩ᩠ᨶᩢᩢ': 5, + 'ᨩ᩠ᨶ᩼': 4, + 'ᨩᩢ': 2, + 'ᨩᩦ': 2, + 'ᨩᩨ᩵': 3, + 'ᨩᩩ': 2, + 'ᨩᩭ᩵': 3, + 'ᨩᩮ᩠': 3, + 'ᨩᩱᩢ': 3, + 'ᨩ᩵ᩭ': 3, + 'ᨪ᩠ᨦᩨ᩵': 5, + 'ᨪᩨ᩵': 3, + 'ᨪᩩ': 2, + 'ᨪᩮᩢ': 3, + 'ᨬ᩠ᨬ': 3, + 'ᨭᩛ': 2, + 'ᨭᩥ': 2, + 'ᨯ᩠ᨦᩢ᩵': 5, + 'ᨯ᩠ᨰ': 3, + 'ᨯ᩠ᨶᩥ': 4, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 6, + 'ᨯᩢ': 2, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 6, + 'ᨯᩦ': 2, + 'ᨯᩨ': 2, + 'ᨯᩪ': 2, + 'ᨯᩪᩯ': 3, + 'ᨯᩯ᩠': 3, + 'ᨯᩰ᩠': 3, + 'ᨯᩱᩢ': 3, + 'ᨯᩴ᩠ᨿ': 4, + 'ᨱ᩠ᨱ': 3, + 'ᨲᩕᩦ': 3, + 'ᨲᩕ᩼': 3, + 'ᨲ᩠ᨠᩫ': 4, + 'ᨲ᩠ᨦᩢᩢ': 5, + 'ᨲ᩠ᨯᩢ': 4, + 'ᨲ᩠ᨲᩥ': 4, + 'ᨲ᩠ᨳᩩ': 4, + 'ᨲ᩠ᨶᩫ': 4, + 'ᨲ᩠ᨶᩫᩢ': 5, + 'ᨲ᩠ᩅᩫ': 4, + 'ᨲ᩠ᩋᩢ': 4, + 'ᨲᩥ': 2, + 'ᨲᩩ': 2, + 'ᨲᩮ᩠': 3, + 'ᨲᩯ᩠': 3, + 'ᨲᩯ᩵': 3, + 'ᨲᩱ᩠': 3, + 'ᨲᩱᩢ': 3, + 'ᨲᩳ᩵': 3, + 'ᨲ᩵': 2, + 'ᨲ᩵ᩴ': 3, + 'ᨳ᩠ᨦᩨ': 4, + 'ᨳ᩠ᩅᩢ': 4, + 'ᨳᩨ': 2, + 'ᨳᩪ': 2, + 'ᨳᩫ᩠ᨷ': 4, + 'ᨳᩮ᩠': 3, + 'ᨳᩮᩢᩢ': 4, + 'ᨳᩯ᩠': 3, + 'ᨴ᩠ᨦᩢ': 4, + 'ᨴ᩠ᨦᩨ': 4, + 'ᨴ᩠ᨵᩥ': 4, + 'ᨴ᩠ᨿ': 3, + 'ᨴ᩠ᨿ᩵': 4, + 'ᨴ᩠ᩅᩢ': 4, + 'ᨴ᩠ᩅᩫ᩵': 5, + 'ᨴᩦ᩵': 3, + 'ᨴᩩ': 2, + 'ᨴᩮ᩠': 3, + 'ᨴᩮᩢᩢ': 4, + 'ᨴᩮᩢ᩵': 4, + 'ᨴᩯ᩠': 3, + 'ᨴᩯᩢ': 3, + 'ᨴᩰ᩠': 3, + 'ᨵᩥ': 2, + 'ᨵᩦ': 2, + 'ᨶ᩠ᨲᩥ': 4, + 'ᨶ᩠ᨶᩢᩢ': 5, + 'ᨶ᩠ᨶᩥ': 4, + 'ᨶ᩠ᩋ': 3, + 'ᨶ᩠ᩋᩢ': 4, + 'ᨶᩢ': 2, + 'ᨶᩢ᩠ᨷ': 4, + 'ᨶᩦ᩶': 3, + 'ᨶᩨ': 2, + 'ᨶᩨ᩵': 3, + 'ᨶᩩ': 2, + 'ᨶᩩ᩵': 3, + 'ᨶᩯ᩠': 3, + 'ᨶ᩵': 2, + 'ᨷ᩠ᨦᩢ': 4, + 'ᨷ᩠ᨯᩢ': 4, + 'ᨷ᩠ᨲᩢ': 4, + 'ᨷ᩠ᨲᩥ': 4, + 'ᨷ᩠ᨴᩫ': 4, + 'ᨷ᩠ᨿ': 3, + 'ᨷ᩠ᩅᩫ': 4, + 'ᨷ᩠ᩋ': 3, + 'ᨷᩢ': 2, + 'ᨷᩥ': 2, + 'ᨷᩩ': 2, + 'ᨷᩪ': 2, + 'ᨷᩮ᩠': 3, + 'ᨸ᩠ᨠᩫ': 4, + 'ᨸ᩠ᨯᩨ': 4, + 'ᨸ᩠ᨶᩢ': 4, + 'ᨸ᩠ᩋ': 3, + 'ᨸᩮ᩠': 3, + 'ᨸᩮᩢ᩵': 4, + 'ᨸ᩵': 2, + 'ᨸ᩠᩵ᨿ': 4, + 'ᨹ᩠ᨯᩥ': 4, + 'ᨹ᩠ᨿ᩵': 4, + 'ᨹ᩠ᩅᩥ': 4, + 'ᨹᩪᩢ': 3, + 'ᨹᩪᩢᩱ': 4, + 'ᨹᩫ᩠ᩃ': 4, + 'ᨹᩮᩨ᩠': 4, + 'ᨹ᩵': 2, + 'ᨻᩕ᩠': 3, + 'ᨻ᩠ᨦ': 3, + 'ᨻ᩠ᨶᩨ᩶': 5, + 'ᨻ᩠ᨶᩫ᩶': 5, + 'ᨻ᩠ᨿ': 3, + 'ᨻ᩠ᩋ': 3, + 'ᨻᩥ': 2, + 'ᨻᩦ᩵': 3, + 'ᨻᩮ᩠': 3, + 'ᨻᩳ': 2, + 'ᨽᩱ᩠': 3, + 'ᨾ᩠ᨲᩥ': 4, + 'ᨾ᩠ᨾ': 3, + 'ᨾ᩠ᨾ᩼': 4, + 'ᨾ᩠ᩅ': 3, + 'ᨾ᩠ᩅ᩵': 4, + 'ᨾᩢ': 2, + 'ᨾᩦ': 2, + 'ᨾᩨ': 2, + 'ᨾᩨᩨ': 3, + 'ᨾᩨ᩵': 3, + 'ᨾᩩ': 2, + 'ᨾᩪ': 2, + 'ᨾᩮ᩠': 3, + 'ᨾᩯ᩵': 3, + 'ᨾ᩵': 2, + 'ᨿ᩠ᨦᩢ': 4, + 'ᨿ᩠ᨦᩢᩢ': 5, + 'ᨿ᩠ᨦᩥ': 4, + 'ᨿ᩠ᨦᩥ᩵': 5, + 'ᨿ᩠ᩋ': 3, + 'ᨿᩢ᩠ᨷ': 4, + 'ᨿᩩ': 2, + 'ᨿᩫ᩠ᩈ': 4, + 'ᨿᩮ᩠': 3, + 'ᨿ᩵': 2, + 'ᩀᩪ᩵': 3, + 'ᩀ᩵': 2, + 'ᩁ᩠ᨠᩢ': 4, + 'ᩁ᩠ᨮᩢ': 4, + 'ᩁ᩠ᨾᩫ': 4, + 'ᩁ᩠ᨿ': 3, + 'ᩁ᩠ᩅ': 3, + 'ᩁ᩠ᩅ᩵': 4, + 'ᩁ᩠ᩋ': 3, + 'ᩁᩢ': 2, + 'ᩁᩢ᩠ᨷ': 4, + 'ᩁᩢᩭ': 3, + 'ᩁᩥ': 2, + 'ᩁᩦ': 2, + 'ᩁᩪᩢ': 3, + 'ᩁᩫ᩠ᨷ': 4, + 'ᩁᩮ᩠': 3, + 'ᩁᩮᩨ᩠': 4, + 'ᩁᩯ᩠': 3, + 'ᩃ᩠ᨦᩫ': 4, + 'ᩃ᩠ᩅ᩵': 4, + 'ᩃᩢ᩠ᨿ': 4, + 'ᩃᩢᩴ': 3, + 'ᩃᩥ': 2, + 'ᩃᩨ᩠ᨷ': 4, + 'ᩃᩩ': 2, + 'ᩃᩪ': 2, + 'ᩃᩮ᩠': 3, + 'ᩃᩰ᩠': 3, + 'ᩅ᩠ᨲᩥ': 4, + 'ᩅ᩠ᨶᩢ': 4, + 'ᩅ᩠ᨿ': 3, + 'ᩅ᩠ᩅ᩵': 4, + 'ᩅᩢ': 2, + 'ᩅᩥ': 2, + 'ᩅᩮ᩠': 3, + 'ᩅᩱᩢ': 3, + 'ᩈ᩠ᨠᩢ': 4, + 'ᩈ᩠ᨠᩧ': 4, + 'ᩈ᩠ᨦᩢ': 4, + 'ᩈ᩠ᨦᩥ᩵': 5, + 'ᩈ᩠ᨦᩫ': 4, + 'ᩈ᩠ᨶᩫ': 4, + 'ᩈ᩠ᨾᩫ': 4, + 'ᩈ᩠ᨿ': 3, + 'ᩈ᩠ᩅ᩵': 4, + 'ᩈ᩠ᩋ': 3, + 'ᩈᩢ': 2, + 'ᩈᩢ᩠ᨿ': 4, + 'ᩈᩥ': 2, + 'ᩈᩦ': 2, + 'ᩈᩨ᩵': 3, + 'ᩈᩩ': 2, + 'ᩈᩪ': 2, + 'ᩈᩪᩢ': 3, + 'ᩈᩫ᩠ᨾ': 4, + 'ᩈᩮ᩠': 3, + 'ᩈᩯ᩠': 3, + 'ᩈᩱ᩠': 3, + 'ᩈᩳ᩵': 3, + 'ᩈ᩵ᩴ': 3, + 'ᩉᩕᩨ': 3, + 'ᩉᩖ': 2, + 'ᩉᩖᩢ': 3, + 'ᩉ᩠ᨶᩢ': 4, + 'ᩉ᩠ᨶᩦ': 4, + 'ᩉ᩠ᨶᩧ': 4, + 'ᩉ᩠ᨶᩧ᩵': 5, + 'ᩉ᩠ᨶᩫ': 4, + 'ᩉ᩠ᨾ': 3, + 'ᩉ᩠ᨾᩢ': 4, + 'ᩉ᩠ᨾᩢᩢ': 5, + 'ᩉ᩠ᨾᩪ᩵': 5, + 'ᩉ᩠ᨾᩳ': 4, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 6, + 'ᩉ᩠ᨿᩩ': 4, + 'ᩉ᩠ᩅᩫ': 4, + 'ᩉ᩠ᩅ᩵': 4, + 'ᩉᩢ': 2, + 'ᩉᩨᩢ': 3, + 'ᩉᩨ᩶': 3, + 'ᩉᩮᩖᩨ': 4, + 'ᩉᩮ᩠': 3, + 'ᩉᩯ᩠': 3, + 'ᩉᩯ᩵': 3, + 'ᩉᩱ᩠': 3, + 'ᩉᩳ': 2, + 'ᩋ᩠ᨶᩢ': 4, + 'ᩋ᩠ᨶᩨ᩵': 5, + 'ᩋ᩠ᩋ': 3, + 'ᩋᩢ': 2, + 'ᩋᩨ': 2, + 'ᩋᩨ᩵': 3, + 'ᩋᩨ᩶': 3, + 'ᩋᩪᩢ': 3, + 'ᩋᩴ': 2, + 'ᩐᩢ': 2, + 'ᩓ᩠ᩅᩢ': 4, + 'ᩓᩢ': 2, + 'ᩔ᩼': 2, + 'ᵾ́': 2, + 'Ẹ̀': 2, + 'ẹ̀': 2, + 'ẹ́': 2, + 'ẽ́': 2, + 'ẽ̱': 2, + 'Ọ̀': 2, + 'ọ̀': 2, + 'Ọ́': 2, + 'ọ́': 2, + '⛓️‍💥': 5, + '⛹️‍♀️': 5, + '⛹️‍♂️': 5, + '⛹🏻‍♀️': 6, + '⛹🏻‍♂️': 6, + '⛹🏼‍♀️': 6, + '⛹🏼‍♂️': 6, + '⛹🏽‍♀️': 6, + '⛹🏽‍♂️': 6, + '⛹🏾‍♀️': 6, + '⛹🏾‍♂️': 6, + '⛹🏿‍♀️': 6, + '⛹🏿‍♂️': 6, + '❤️‍🔥': 5, + '❤️‍🩹': 5, + 'ꦉꦁ': 2, + 'ꦏꦁ': 2, + 'ꦏꦂ': 2, + 'ꦏꦶ': 2, + 'ꦏꦶꦂ': 3, + 'ꦏꦸ': 2, + 'ꦏꦸꦂ': 3, + 'ꦏꦺꦴ': 3, + 'ꦏꦼ': 2, + 'ꦏꦿꦁ': 3, + 'ꦏꦿꦂ': 3, + 'ꦏ꧀': 2, + 'ꦏ꧀ꦏ': 3, + 'ꦏ꧀ꦏꦁ': 4, + 'ꦏ꧀ꦏꦺ': 4, + 'ꦏ꧀ꦏꦼ': 4, + 'ꦏ꧀ꦔ': 3, + 'ꦏ꧀ꦔꦸ': 4, + 'ꦏ꧀ꦔꦿ': 4, + 'ꦏ꧀ꦕ': 3, + 'ꦏ꧀ꦠ': 3, + 'ꦏ꧀ꦠꦶ': 4, + 'ꦏ꧀ꦠꦸ': 4, + 'ꦏ꧀ꦠꦺ': 4, + 'ꦏ꧀ꦢ': 3, + 'ꦏ꧀ꦢꦂ': 4, + 'ꦏ꧀ꦢꦶ': 4, + 'ꦏ꧀ꦤ': 3, + 'ꦏ꧀ꦤꦶꦁ': 5, + 'ꦏ꧀ꦥ': 3, + 'ꦏ꧀ꦥꦶ': 4, + 'ꦏ꧀ꦥꦿꦶ': 5, + 'ꦏ꧀ꦧ': 3, + 'ꦏ꧀ꦧꦶ': 4, + 'ꦏ꧀ꦧꦺ': 4, + 'ꦏ꧀ꦩ': 3, + 'ꦏ꧀ꦩꦸ': 4, + 'ꦏ꧀ꦪ': 3, + 'ꦏ꧀ꦭ': 3, + 'ꦏ꧀ꦮ': 3, + 'ꦏ꧀ꦱ': 3, + 'ꦏ꧀ꦱꦂ': 4, + 'ꦏ꧀ꦱꦶ': 4, + 'ꦏ꧀ꦱꦸ': 4, + 'ꦏ꧀ꦱꦺ': 4, + 'ꦏ꧀ꦲ': 3, + 'ꦏ꧀ꦲꦁ': 4, + 'ꦏ꧀ꦲꦶꦁ': 5, + 'ꦏ꧀ꦲꦸ': 4, + 'ꦏ꧀ꦲꦺ': 4, + 'ꦏ꧀ꦲꦺꦴ': 5, + 'ꦒꦁ': 2, + 'ꦒꦂ': 2, + 'ꦒꦸ': 2, + 'ꦒꦸꦁ': 3, + 'ꦒꦸꦂ': 3, + 'ꦒꦸꦃ': 3, + 'ꦒꦺꦴ': 3, + 'ꦒꦼ': 2, + 'ꦒꦼꦂ': 3, + 'ꦒꦿꦺ': 3, + 'ꦒ꧀ꦒ': 3, + 'ꦔꦁ': 2, + 'ꦔꦶ': 2, + 'ꦔꦶꦁ': 3, + 'ꦔꦸ': 2, + 'ꦔꦺꦴ': 3, + 'ꦔꦼ': 2, + 'ꦔꦼꦂ': 3, + 'ꦔꦽ': 2, + 'ꦔ꧀ꦒꦼ': 4, + 'ꦔ꧀ꦒꦿꦼꦁ': 6, + 'ꦔ꧀ꦭ': 3, + 'ꦔ꧀ꦭꦁ': 4, + 'ꦔ꧀ꦭꦸ': 4, + 'ꦔ꧀ꦭꦼ': 4, + 'ꦕꦶ': 2, + 'ꦕꦶꦁ': 3, + 'ꦕꦸ': 2, + 'ꦕꦺꦴ': 3, + 'ꦗꦶ': 2, + 'ꦗꦸ': 2, + 'ꦗꦼ': 2, + 'ꦗꦿꦺꦴ': 4, + 'ꦚꦁ': 2, + 'ꦚꦶꦁ': 3, + 'ꦚ꧀ꦕ': 3, + 'ꦚ꧀ꦕꦶ': 4, + 'ꦚ꧀ꦗ': 3, + 'ꦚ꧀ꦗꦶ': 4, + 'ꦚ꧀ꦗꦸꦁ': 5, + 'ꦚ꧀ꦗꦸꦂ': 5, + 'ꦝꦁ': 2, + 'ꦝꦂ': 2, + 'ꦝꦶ': 2, + 'ꦝꦺꦴ': 3, + 'ꦝꦺꦴꦂ': 4, + 'ꦝꦼ': 2, + 'ꦝꦼꦁ': 3, + 'ꦠꦁ': 2, + 'ꦠꦶ': 2, + 'ꦠꦸ': 2, + 'ꦠꦸꦁ': 3, + 'ꦠꦸꦂ': 3, + 'ꦠꦸꦃ': 3, + 'ꦠꦺꦃ': 3, + 'ꦠꦺꦴ': 3, + 'ꦠꦼ': 2, + 'ꦠꦼꦂ': 3, + 'ꦠꦽ': 2, + 'ꦠꦿꦶ': 3, + 'ꦠ꧀': 2, + 'ꦠ꧀ꦏꦁ': 4, + 'ꦠ꧀ꦏꦸ': 4, + 'ꦠ꧀ꦒ': 3, + 'ꦠ꧀ꦔꦸ': 4, + 'ꦠ꧀ꦚ': 3, + 'ꦠ꧀ꦠ': 3, + 'ꦠ꧀ꦠꦶ': 4, + 'ꦠ꧀ꦠꦶꦁ': 5, + 'ꦠ꧀ꦠꦸ': 4, + 'ꦠ꧀ꦠꦺ': 4, + 'ꦠ꧀ꦤ': 3, + 'ꦠ꧀ꦤꦶ': 4, + 'ꦠ꧀ꦩ': 3, + 'ꦠ꧀ꦩꦼ': 4, + 'ꦠ꧀ꦭ': 3, + 'ꦠ꧀ꦭꦺ': 4, + 'ꦠ꧀ꦱ': 3, + 'ꦠ꧀ꦱꦶꦁ': 5, + 'ꦠ꧀ꦱꦶꦃ': 5, + 'ꦠ꧀ꦱꦾ': 4, + 'ꦠ꧀ꦱ꧀ꦮ': 5, + 'ꦠ꧀ꦲ': 3, + 'ꦠ꧀ꦲꦶꦁ': 5, + 'ꦠ꧀ꦲꦺꦴ': 5, + 'ꦢꦂ': 2, + 'ꦢꦶ': 2, + 'ꦢꦸ': 2, + 'ꦢꦸꦂ': 3, + 'ꦢꦺꦴ': 3, + 'ꦢꦼ': 2, + 'ꦢ꧀': 2, + 'ꦢ꧀ꦏ': 3, + 'ꦢ꧀ꦠ': 3, + 'ꦢ꧀ꦠꦶꦁ': 5, + 'ꦢ꧀ꦢ': 3, + 'ꦢ꧀ꦥ': 3, + 'ꦢ꧀ꦩꦺꦴ': 5, + 'ꦢ꧀ꦲ': 3, + 'ꦤꦁ': 2, + 'ꦤꦂ': 2, + 'ꦤꦶ': 2, + 'ꦤꦶꦁ': 3, + 'ꦤꦸ': 2, + 'ꦤꦸꦁ': 3, + 'ꦤꦺꦃ': 3, + 'ꦤꦺꦴ': 3, + 'ꦤꦼ': 2, + 'ꦤꦼꦁ': 3, + 'ꦤꦼꦂ': 3, + 'ꦤ꧀': 2, + 'ꦤ꧀ꦏ': 3, + 'ꦤ꧀ꦏꦁ': 4, + 'ꦤ꧀ꦏꦸ': 4, + 'ꦤ꧀ꦏꦼ': 4, + 'ꦤ꧀ꦒ': 3, + 'ꦤ꧀ꦔ': 3, + 'ꦤ꧀ꦔꦁ': 4, + 'ꦤ꧀ꦔꦶ': 4, + 'ꦤ꧀ꦔꦸ': 4, + 'ꦤ꧀ꦔꦸꦂ': 5, + 'ꦤ꧀ꦔꦺ': 4, + 'ꦤ꧀ꦔꦼ': 4, + 'ꦤ꧀ꦔꦿꦸ': 5, + 'ꦤ꧀ꦗ': 3, + 'ꦤ꧀ꦗꦸ': 4, + 'ꦤ꧀ꦗꦼ': 4, + 'ꦤ꧀ꦚꦶ': 4, + 'ꦤ꧀ꦛꦃ': 4, + 'ꦤ꧀ꦛꦶ': 4, + 'ꦤ꧀ꦝ': 3, + 'ꦤ꧀ꦝꦁ': 4, + 'ꦤ꧀ꦝꦶ': 4, + 'ꦤ꧀ꦝꦸ': 4, + 'ꦤ꧀ꦝꦸꦁ': 5, + 'ꦤ꧀ꦝꦺꦁ': 5, + 'ꦤ꧀ꦝꦺꦴꦁ': 6, + 'ꦤ꧀ꦝꦼ': 4, + 'ꦤ꧀ꦠ': 3, + 'ꦤ꧀ꦠꦂ': 4, + 'ꦤ꧀ꦠꦃ': 4, + 'ꦤ꧀ꦠꦶ': 4, + 'ꦤ꧀ꦠꦸ': 4, + 'ꦤ꧀ꦠꦸꦁ': 5, + 'ꦤ꧀ꦠꦺ': 4, + 'ꦤ꧀ꦠꦼ': 4, + 'ꦤ꧀ꦠꦼꦂ': 5, + 'ꦤ꧀ꦠꦽ': 4, + 'ꦤ꧀ꦢ': 3, + 'ꦤ꧀ꦢꦁ': 4, + 'ꦤ꧀ꦢꦂ': 4, + 'ꦤ꧀ꦢꦶ': 4, + 'ꦤ꧀ꦢꦸ': 4, + 'ꦤ꧀ꦢꦺ': 4, + 'ꦤ꧀ꦢꦼ': 4, + 'ꦤ꧀ꦤ': 3, + 'ꦤ꧀ꦤꦶ': 4, + 'ꦤ꧀ꦤꦶꦁ': 5, + 'ꦤ꧀ꦤꦺ': 4, + 'ꦤ꧀ꦤꦼ': 4, + 'ꦤ꧀ꦥ': 3, + 'ꦤ꧀ꦥꦁ': 4, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 7, + 'ꦤ꧀ꦥꦶ': 4, + 'ꦤ꧀ꦥꦸ': 4, + 'ꦤ꧀ꦥꦼ': 4, + 'ꦤ꧀ꦥꦿ': 4, + 'ꦤ꧀ꦥꦿꦶ': 5, + 'ꦤ꧀ꦧ': 3, + 'ꦤ꧀ꦧꦁ': 4, + 'ꦤ꧀ꦧꦶ': 4, + 'ꦤ꧀ꦧꦸ': 4, + 'ꦤ꧀ꦧꦺ': 4, + 'ꦤ꧀ꦧꦺꦴ': 5, + 'ꦤ꧀ꦧꦼ': 4, + 'ꦤ꧀ꦩ': 3, + 'ꦤ꧀ꦩꦂ': 4, + 'ꦤ꧀ꦩꦶ': 4, + 'ꦤ꧀ꦩꦸꦂ': 5, + 'ꦤ꧀ꦩꦼ': 4, + 'ꦤ꧀ꦭ': 3, + 'ꦤ꧀ꦭꦶ': 4, + 'ꦤ꧀ꦭꦸ': 4, + 'ꦤ꧀ꦮ': 3, + 'ꦤ꧀ꦮꦂ': 4, + 'ꦤ꧀ꦮꦶ': 4, + 'ꦤ꧀ꦮꦸ': 4, + 'ꦤ꧀ꦮꦼ': 4, + 'ꦤ꧀ꦱ': 3, + 'ꦤ꧀ꦱꦂ': 4, + 'ꦤ꧀ꦱꦃ': 4, + 'ꦤ꧀ꦱꦶ': 4, + 'ꦤ꧀ꦱꦶꦁ': 5, + 'ꦤ꧀ꦱꦸ': 4, + 'ꦤ꧀ꦱꦺꦴ': 5, + 'ꦤ꧀ꦱꦼ': 4, + 'ꦤ꧀ꦲ': 3, + 'ꦤ꧀ꦲꦁ': 4, + 'ꦤ꧀ꦲꦶ': 4, + 'ꦤ꧀ꦲꦶꦁ': 5, + 'ꦤ꧀ꦲꦸ': 4, + 'ꦤ꧀ꦲꦺꦴ': 5, + 'ꦥꦁ': 2, + 'ꦥ꦳ꦼ': 3, + 'ꦥ꦳꧀': 3, + 'ꦥꦶ': 2, + 'ꦥꦸ': 2, + 'ꦥꦸꦁ': 3, + 'ꦥꦺꦴ': 3, + 'ꦥꦼ': 2, + 'ꦥꦼꦂ': 3, + 'ꦥꦿꦃ': 3, + 'ꦥꦿꦶ': 3, + 'ꦥꦿꦺꦴ': 4, + 'ꦥ꧀': 2, + 'ꦥ꧀ꦏ': 3, + 'ꦥ꧀ꦏꦁ': 4, + 'ꦥ꧀ꦠ': 3, + 'ꦥ꧀ꦠꦶ': 4, + 'ꦥ꧀ꦢꦸ': 4, + 'ꦥ꧀ꦤ': 3, + 'ꦥ꧀ꦤꦶ': 4, + 'ꦥ꧀ꦥ': 3, + 'ꦥ꧀ꦥꦶ': 4, + 'ꦥ꧀ꦥꦺ': 4, + 'ꦥ꧀ꦧꦺ': 4, + 'ꦥ꧀ꦩ': 3, + 'ꦥ꧀ꦩꦂ': 4, + 'ꦥ꧀ꦫꦺ': 4, + 'ꦥ꧀ꦭ': 3, + 'ꦥ꧀ꦭꦸ': 4, + 'ꦥ꧀ꦱ': 3, + 'ꦥ꧀ꦱꦶꦂ': 5, + 'ꦥ꧀ꦱꦼ': 4, + 'ꦥ꧀ꦲ': 3, + 'ꦧꦁ': 2, + 'ꦧꦶ': 2, + 'ꦧꦸ': 2, + 'ꦧꦸꦂ': 3, + 'ꦧꦺꦃ': 3, + 'ꦧꦺꦴ': 3, + 'ꦧꦼ': 2, + 'ꦧ꧀': 2, + 'ꦧ꧀ꦏ': 3, + 'ꦧ꧀ꦠꦸ': 4, + 'ꦧ꧀ꦥ': 3, + 'ꦧ꧀ꦧ': 3, + 'ꦧ꧀ꦧꦺ': 4, + 'ꦧ꧀ꦲ': 3, + 'ꦧ꧀ꦲꦶ': 4, + 'ꦩꦁ': 2, + 'ꦩꦂ': 2, + 'ꦩꦶ': 2, + 'ꦩꦸ': 2, + 'ꦩꦸꦁ': 3, + 'ꦩꦸꦂ': 3, + 'ꦩꦺꦃ': 3, + 'ꦩꦺꦴ': 3, + 'ꦩꦼ': 2, + 'ꦩꦼꦂ': 3, + 'ꦩꦿꦃ': 3, + 'ꦩꦿꦶꦃ': 4, + 'ꦩ꧀': 2, + 'ꦩ꧀ꦏꦁ': 4, + 'ꦩ꧀ꦔꦼ': 4, + 'ꦩ꧀ꦠꦸ': 4, + 'ꦩ꧀ꦠꦺꦴ': 5, + 'ꦩ꧀ꦤꦶꦁ': 5, + 'ꦩ꧀ꦥ': 3, + 'ꦩ꧀ꦥꦶ': 4, + 'ꦩ꧀ꦥꦸ': 4, + 'ꦩ꧀ꦥꦼ': 4, + 'ꦩ꧀ꦧ': 3, + 'ꦩ꧀ꦧꦁ': 4, + 'ꦩ꧀ꦧꦂ': 4, + 'ꦩ꧀ꦧꦃ': 4, + 'ꦩ꧀ꦧꦶ': 4, + 'ꦩ꧀ꦧꦸ': 4, + 'ꦩ꧀ꦧꦸꦁ': 5, + 'ꦩ꧀ꦧꦺ': 4, + 'ꦩ꧀ꦧꦺꦂ': 5, + 'ꦩ꧀ꦧꦼꦂ': 5, + 'ꦩ꧀ꦧꦾꦸꦂ': 6, + 'ꦩ꧀ꦩ': 3, + 'ꦩ꧀ꦩꦶ': 4, + 'ꦩ꧀ꦫ': 3, + 'ꦩ꧀ꦫꦃ': 4, + 'ꦩ꧀ꦭ': 3, + 'ꦩ꧀ꦲꦶ': 4, + 'ꦪꦁ': 2, + 'ꦪꦶ': 2, + 'ꦪꦸ': 2, + 'ꦪꦸꦁ': 3, + 'ꦪꦸꦃ': 3, + 'ꦪꦺꦴ': 3, + 'ꦫꦁ': 2, + 'ꦫꦶ': 2, + 'ꦫꦶꦁ': 3, + 'ꦫꦶꦃ': 3, + 'ꦫꦸ': 2, + 'ꦫꦸꦁ': 3, + 'ꦫꦺꦴ': 3, + 'ꦭꦁ': 2, + 'ꦭꦂ': 2, + 'ꦭꦶ': 2, + 'ꦭꦶꦁ': 3, + 'ꦭꦶꦃ': 3, + 'ꦭꦸ': 2, + 'ꦭꦸꦁ': 3, + 'ꦭꦸꦂ': 3, + 'ꦭꦺꦃ': 3, + 'ꦭꦺꦴ': 3, + 'ꦭꦺꦴꦁ': 4, + 'ꦭ꧀': 2, + 'ꦭ꧀ꦏꦁ': 4, + 'ꦭ꧀ꦚꦼꦁ': 5, + 'ꦭ꧀ꦠꦸ': 4, + 'ꦭ꧀ꦧꦸ': 4, + 'ꦭ꧀ꦩꦸ': 4, + 'ꦭ꧀ꦭ': 3, + 'ꦭ꧀ꦭꦶ': 4, + 'ꦭ꧀ꦭꦶꦁ': 5, + 'ꦭ꧀ꦭꦺ': 4, + 'ꦭ꧀ꦮ': 3, + 'ꦭ꧀ꦱ': 3, + 'ꦭ꧀ꦱꦂ': 4, + 'ꦭ꧀ꦱꦶꦁ': 5, + 'ꦭ꧀ꦱꦸ': 4, + 'ꦭ꧀ꦲ': 3, + 'ꦭ꧀ꦲꦁ': 4, + 'ꦭ꧀ꦲꦶꦁ': 5, + 'ꦭ꧀ꦲꦸ': 4, + 'ꦮꦂ': 2, + 'ꦮ꦳ꦼꦂ': 4, + 'ꦮꦶ': 2, + 'ꦮꦶꦃ': 3, + 'ꦮꦸ': 2, + 'ꦮꦸꦁ': 3, + 'ꦮꦸꦃ': 3, + 'ꦮꦺꦴ': 3, + 'ꦮꦺꦴꦁ': 4, + 'ꦮꦺꦴꦃ': 4, + 'ꦮꦼ': 2, + 'ꦮꦼꦁ': 3, + 'ꦮꦼꦂ': 3, + 'ꦮꦿꦸꦃ': 4, + 'ꦱꦂ': 2, + 'ꦱꦶ': 2, + 'ꦱꦶꦁ': 3, + 'ꦱꦶꦂ': 3, + 'ꦱꦶꦃ': 3, + 'ꦱꦸ': 2, + 'ꦱꦸꦁ': 3, + 'ꦱꦺꦴ': 3, + 'ꦱꦼ': 2, + 'ꦱꦼꦁ': 3, + 'ꦱꦿꦃ': 3, + 'ꦱ꧀': 2, + 'ꦱ꧀ꦏ': 3, + 'ꦱ꧀ꦏꦁ': 4, + 'ꦱ꧀ꦏꦿꦶ': 5, + 'ꦱ꧀ꦒ': 3, + 'ꦱ꧀ꦔꦸ': 4, + 'ꦱ꧀ꦔꦿ': 4, + 'ꦱ꧀ꦛꦶ': 4, + 'ꦱ꧀ꦠ': 3, + 'ꦱ꧀ꦠꦿ': 4, + 'ꦱ꧀ꦢꦂ': 4, + 'ꦱ꧀ꦢꦶ': 4, + 'ꦱ꧀ꦢꦸ': 4, + 'ꦱ꧀ꦥ': 3, + 'ꦱ꧀ꦧ': 3, + 'ꦱ꧀ꦧꦁ': 4, + 'ꦱ꧀ꦩ': 3, + 'ꦱ꧀ꦩꦶ': 4, + 'ꦱ꧀ꦩꦼ': 4, + 'ꦱ꧀ꦩꦿ': 4, + 'ꦱ꧀ꦭ': 3, + 'ꦱ꧀ꦮ': 3, + 'ꦱ꧀ꦱ': 3, + 'ꦱ꧀ꦱꦂ': 4, + 'ꦱ꧀ꦱꦶ': 4, + 'ꦱ꧀ꦱꦶꦁ': 5, + 'ꦱ꧀ꦱꦸ': 4, + 'ꦱ꧀ꦱꦺ': 4, + 'ꦱ꧀ꦲ': 3, + 'ꦱ꧀ꦲꦁ': 4, + 'ꦱ꧀ꦲꦶ': 4, + 'ꦱ꧀ꦲꦶꦁ': 5, + 'ꦱ꧀ꦲꦸ': 4, + 'ꦲꦁ': 2, + 'ꦲꦶ': 2, + 'ꦲꦶꦁ': 3, + 'ꦲꦶꦂ': 3, + 'ꦲꦸ': 2, + 'ꦲꦸꦂ': 3, + 'ꦲꦺꦴ': 3, + 'ꦲꦼ': 2, + 'ꪀꪰ': 2, + 'ꪀꪲ': 2, + 'ꪀꪳ': 2, + 'ꪀꪴ': 2, + 'ꪀꪷ': 2, + 'ꪀꪾ': 2, + 'ꪁꪴ': 2, + 'ꪁꪷ': 2, + 'ꪁꪸ': 2, + 'ꪁꪾ': 2, + 'ꪄꪰ': 2, + 'ꪄꪲ': 2, + 'ꪄꪳ': 2, + 'ꪄꪴ': 2, + 'ꪄꪷ': 2, + 'ꪅꪰ': 2, + 'ꪈꪲ': 2, + 'ꪈꪸ': 2, + 'ꪈꪾ': 2, + 'ꪉꪷ': 2, + 'ꪉꪸ': 2, + 'ꪊꪰ': 2, + 'ꪊꪲ': 2, + 'ꪊꪴ': 2, + 'ꪊꪸ': 2, + 'ꪋꪲ': 2, + 'ꪋꪳ': 2, + 'ꪋꪴ': 2, + 'ꪋꪷ': 2, + 'ꪋꪸ': 2, + 'ꪎꪰ': 2, + 'ꪎꪲ': 2, + 'ꪎꪳ': 2, + 'ꪎꪴ': 2, + 'ꪎꪷ': 2, + 'ꪎꪸ': 2, + 'ꪏꪴ': 2, + 'ꪏꪷ': 2, + 'ꪏꪸ': 2, + 'ꪑꪰ': 2, + 'ꪑꪲ': 2, + 'ꪑꪷ': 2, + 'ꪒꪰ': 2, + 'ꪒꪲ': 2, + 'ꪒꪴ': 2, + 'ꪒꪷ': 2, + 'ꪒꪸ': 2, + 'ꪔꪰ': 2, + 'ꪔꪲ': 2, + 'ꪔꪳ': 2, + 'ꪔꪷ': 2, + 'ꪔꪸ': 2, + 'ꪔꪾ': 2, + 'ꪕꪰ': 2, + 'ꪕꪲ': 2, + 'ꪕꪳ': 2, + 'ꪕꪴ': 2, + 'ꪕꪷ': 2, + 'ꪕꪸ': 2, + 'ꪕꪾ': 2, + 'ꪖꪸ': 2, + 'ꪘꪰ': 2, + 'ꪘꪳ': 2, + 'ꪙꪲ': 2, + 'ꪙꪴ': 2, + 'ꪙꪾ': 2, + 'ꪚꪰ': 2, + 'ꪚꪴ': 2, + 'ꪚꪷ': 2, + 'ꪜꪰ': 2, + 'ꪜꪲ': 2, + 'ꪜꪴ': 2, + 'ꪜꪸ': 2, + 'ꪝꪰ': 2, + 'ꪝꪲ': 2, + 'ꪝꪳ': 2, + 'ꪝꪷ': 2, + 'ꪝꪸ': 2, + 'ꪠꪲ': 2, + 'ꪠꪴ': 2, + 'ꪠꪸ': 2, + 'ꪢꪲ': 2, + 'ꪢꪴ': 2, + 'ꪢꪸ': 2, + 'ꪣꪰ': 2, + 'ꪣꪲ': 2, + 'ꪣꪳ': 2, + 'ꪣꪷ': 2, + 'ꪣꪸ': 2, + 'ꪤꪰ': 2, + 'ꪤꪴ': 2, + 'ꪤꪷ': 2, + 'ꪤꪸ': 2, + 'ꪤꪾ': 2, + 'ꪥꪰ': 2, + 'ꪥꪴ': 2, + 'ꪥꪸ': 2, + 'ꪨꪰ': 2, + 'ꪨꪷ': 2, + 'ꪨꪸ': 2, + 'ꪩꪰ': 2, + 'ꪩꪳ': 2, + 'ꪩꪴ': 2, + 'ꪩꪷ': 2, + 'ꪩꪾ': 2, + 'ꪪꪴ': 2, + 'ꪫꪸ': 2, + 'ꪬꪷ': 2, + 'ꪬꪸ': 2, + 'ꪭꪰ': 2, + 'ꪭꪲ': 2, + 'ꪭꪳ': 2, + 'ꪭꪴ': 2, + 'ꪭꪷ': 2, + 'ꪭꪸ': 2, + 'ꪮꪲ': 2, + 'ꪮꪳ': 2, + 'ꪮꪴ': 2, + 'ꪮꪸ': 2, + 'ꪯꪸ': 2, + '𑄃𑄧': 2, + '𑄃𑄧𑄁': 3, + '𑄃𑄨': 2, + '𑄃𑄪': 2, + '𑄃𑄬𑄭': 3, + '𑄃𑄮': 2, + '𑄃𑄰': 2, + '𑄃𑄳𑄃': 3, + '𑄃𑄳𑄦𑄧': 4, + '𑄃𑄳𑄦𑄨': 4, + '𑄅𑄪': 2, + '𑄅𑄫': 2, + '𑄇𑄧': 2, + '𑄇𑄧𑄧': 3, + '𑄇𑄧𑄧𑄱': 4, + '𑄇𑄨': 2, + '𑄇𑄨𑄁': 3, + '𑄇𑄪': 2, + '𑄇𑄭𑄭': 3, + '𑄇𑄳𑄠': 3, + '𑄇𑄳𑄠𑄬': 4, + '𑄇𑄳𑄠𑄴': 4, + '𑄇𑄳𑄢𑄨': 4, + '𑄇𑄳𑄦𑄧': 4, + '𑄇𑄳𑄦𑄴': 4, + '𑄇𑄴': 2, + '𑄈𑄧': 2, + '𑄈𑄪': 2, + '𑄈𑄳𑄠': 3, + '𑄈𑄳𑄠𑄬': 4, + '𑄉𑄧': 2, + '𑄉𑄨': 2, + '𑄉𑄩': 2, + '𑄉𑄪': 2, + '𑄉𑄬𑄭': 3, + '𑄉𑄭': 2, + '𑄉𑄮': 2, + '𑄉𑄳𑄠': 3, + '𑄉𑄳𑄢𑄧': 4, + '𑄊𑄧': 2, + '𑄊𑄨': 2, + '𑄋𑄧': 2, + '𑄋𑄨': 2, + '𑄌𑄧': 2, + '𑄌𑄨': 2, + '𑄌𑄮': 2, + '𑄌𑄯': 2, + '𑄌𑄳𑄠𑄬': 4, + '𑄌𑄳𑄠𑄴': 4, + '𑄌𑄴': 2, + '𑄍𑄨': 2, + '𑄍𑄪': 2, + '𑄎𑄧': 2, + '𑄎𑄨': 2, + '𑄎𑄨𑄁': 3, + '𑄎𑄩': 2, + '𑄎𑄩𑄁': 3, + '𑄎𑄪': 2, + '𑄎𑄪𑄁': 3, + '𑄎𑄬𑄭': 3, + '𑄎𑄮': 2, + '𑄎𑄳𑄠𑄧': 4, + '𑄎𑄳𑄠𑄬': 4, + '𑄏𑄨': 2, + '𑄏𑄮': 2, + '𑄑𑄨': 2, + '𑄑𑄳𑄢𑄬': 4, + '𑄒𑄨': 2, + '𑄓𑄧': 2, + '𑄓𑄨': 2, + '𑄓𑄮': 2, + '𑄕𑄧': 2, + '𑄖𑄧': 2, + '𑄖𑄧𑄧': 3, + '𑄖𑄧𑄧𑄱': 4, + '𑄖𑄨': 2, + '𑄖𑄨𑄨': 3, + '𑄖𑄨𑄨𑄪': 4, + '𑄖𑄩': 2, + '𑄖𑄪': 2, + '𑄖𑄳𑄠𑄧': 4, + '𑄖𑄳𑄠𑄬': 4, + '𑄖𑄳𑄠𑄴': 4, + '𑄖𑄳𑄢𑄨': 4, + '𑄖𑄳𑄢𑄬': 4, + '𑄖𑄳𑄦𑄴': 4, + '𑄖𑄴': 2, + '𑄗𑄧': 2, + '𑄗𑄪': 2, + '𑄗𑄭': 2, + '𑄘𑄁': 2, + '𑄘𑄧': 2, + '𑄘𑄨': 2, + '𑄘𑄪': 2, + '𑄘𑄮': 2, + '𑄘𑄳𑄠𑄧': 4, + '𑄘𑄳𑄠𑄬': 4, + '𑄙𑄧': 2, + '𑄙𑄨': 2, + '𑄙𑄩': 2, + '𑄙𑄪': 2, + '𑄙𑄮': 2, + '𑄙𑄳𑄠': 3, + '𑄙𑄳𑄠𑄧': 4, + '𑄚𑄧': 2, + '𑄚𑄧𑄧𑄱': 4, + '𑄚𑄨': 2, + '𑄚𑄩': 2, + '𑄚𑄪': 2, + '𑄚𑄬𑄭': 3, + '𑄚𑄮': 2, + '𑄚𑄳𑄠𑄧': 4, + '𑄚𑄳𑄠𑄬': 4, + '𑄚𑄳𑄠𑄴': 4, + '𑄚𑄳𑄢𑄧𑄧': 5, + '𑄚𑄳𑄦𑄧': 4, + '𑄚𑄳𑄦𑄨': 4, + '𑄚𑄳𑄦𑄴': 4, + '𑄚𑄴': 2, + '𑄛𑄧': 2, + '𑄛𑄨': 2, + '𑄛𑄪': 2, + '𑄛𑄭': 2, + '𑄛𑄮': 2, + '𑄛𑄱': 2, + '𑄛𑄳𑄢': 3, + '𑄛𑄳𑄢𑄧': 4, + '𑄛𑄳𑄦': 3, + '𑄛𑄴': 2, + '𑄜𑄧': 2, + '𑄜𑄨': 2, + '𑄜𑄪': 2, + '𑄜𑄯': 2, + '𑄝𑄧': 2, + '𑄝𑄧𑄧': 3, + '𑄝𑄨': 2, + '𑄝𑄪': 2, + '𑄝𑄮': 2, + '𑄝𑄰': 2, + '𑄝𑄳𑄠': 3, + '𑄝𑄳𑄢𑄨': 4, + '𑄝𑄳𑄢𑄬': 4, + '𑄞𑄧': 2, + '𑄞𑄨': 2, + '𑄞𑄩': 2, + '𑄞𑄪': 2, + '𑄞𑄬𑄭': 3, + '𑄞𑄮': 2, + '𑄞𑄯': 2, + '𑄟𑄧': 2, + '𑄟𑄨': 2, + '𑄟𑄪': 2, + '𑄟𑄫': 2, + '𑄟𑄮': 2, + '𑄟𑄱': 2, + '𑄟𑄳𑄠𑄬': 4, + '𑄟𑄴': 2, + '𑄠𑄧': 2, + '𑄠𑄨': 2, + '𑄠𑄬𑄭': 3, + '𑄠𑄮': 2, + '𑄠𑄴': 2, + '𑄡𑄧': 2, + '𑄡𑄨': 2, + '𑄡𑄪': 2, + '𑄡𑄬𑄭': 3, + '𑄡𑄳𑄠𑄬': 4, + '𑄢𑄁': 2, + '𑄢𑄧': 2, + '𑄢𑄨': 2, + '𑄢𑄩': 2, + '𑄢𑄪': 2, + '𑄢𑄬𑄭': 3, + '𑄢𑄮': 2, + '𑄢𑄳𑄦𑄧𑄱𑄱': 6, + '𑄢𑄴': 2, + '𑄣𑄧': 2, + '𑄣𑄨': 2, + '𑄣𑄩': 2, + '𑄣𑄪': 2, + '𑄣𑄬𑄭': 3, + '𑄣𑄮': 2, + '𑄣𑄰𑄱': 3, + '𑄣𑄳𑄦𑄬': 4, + '𑄣𑄳𑄦𑄴': 4, + '𑄣𑄴': 2, + '𑄥𑄁': 2, + '𑄥𑄧': 2, + '𑄥𑄧𑄁': 3, + '𑄥𑄨': 2, + '𑄥𑄩': 2, + '𑄥𑄪': 2, + '𑄥𑄳𑄠': 3, + '𑄥𑄳𑄠𑄧': 4, + '𑄥𑄳𑄢𑄧': 4, + '𑄥𑄳𑄢𑄨𑄁': 5, + '𑄦𑄧': 2, + '𑄦𑄨': 2, + '𑄦𑄮': 2, + '𑄦𑄰': 2, + '𑌕𑌾𑌂': 3, + '𑌕𑌾𑌃': 3, + '𑌕𑍀': 2, + '𑌕𑍍': 2, + '𑌗𑍀': 2, + '𑌗𑍍': 2, + '𑌚𑍍': 2, + '𑌜𑌼': 2, + '𑌜𑍀': 2, + '𑌜𑍍': 2, + '𑌞𑍍': 2, + '𑌟𑌾𑌂': 3, + '𑌟𑌾𑌃': 3, + '𑌟𑍍': 2, + '𑌠𑍈𑌃': 3, + '𑌢𑌼𑍀': 3, + '𑌣𑌾𑌂': 3, + '𑌣𑍀': 2, + '𑌣𑍍': 2, + '𑌤𑌾𑌂': 3, + '𑌤𑌾𑌃': 3, + '𑌤𑌿𑌂': 3, + '𑌤𑌿𑌃': 3, + '𑌤𑍀': 2, + '𑌤𑍁𑌂': 3, + '𑌤𑍇𑌃': 3, + '𑌤𑍍': 2, + '𑌥𑌾𑌂': 3, + '𑌦𑍀': 2, + '𑌦𑍍': 2, + '𑌧𑌾𑌃': 3, + '𑌧𑌿𑌂': 3, + '𑌧𑍀': 2, + '𑌧𑍇𑌃': 3, + '𑌧𑍈𑌃': 3, + '𑌧𑍍': 2, + '𑌨𑌾𑌂': 3, + '𑌨𑌾𑌃': 3, + '𑌨𑌿𑌃': 3, + '𑌨𑍀': 2, + '𑌨𑍈𑌃': 3, + '𑌨𑍍': 2, + '𑌪𑍀': 2, + '𑌪𑍍': 2, + '𑌬𑍍': 2, + '𑌭𑌿𑌃': 3, + '𑌭𑍍': 2, + '𑌮𑌿𑌂': 3, + '𑌮𑍀': 2, + '𑌮𑍍': 2, + '𑌯𑌾𑌂': 3, + '𑌯𑌾𑌃': 3, + '𑌯𑍀': 2, + '𑌯𑍁𑌃': 3, + '𑌯𑍈𑌃': 3, + '𑌯𑍋𑌃': 3, + '𑌯𑍍': 2, + '𑌰𑌾𑌃': 3, + '𑌰𑌿𑌂': 3, + '𑌰𑍀': 2, + '𑌰𑍍': 2, + '𑌲𑌾𑌃': 3, + '𑌲𑌿𑌂': 3, + '𑌲𑍍': 2, + '𑌵𑌾𑌂': 3, + '𑌵𑌾𑌃': 3, + '𑌵𑍀': 2, + '𑌵𑍍': 2, + '𑌶𑌾𑌃': 3, + '𑌶𑍀': 2, + '𑌶𑍍': 2, + '𑌷𑌾𑌂': 3, + '𑌷𑌾𑌃': 3, + '𑌷𑍍': 2, + '𑌸𑌾𑌂': 3, + '𑌸𑍀': 2, + '𑌸𑍍': 2, + '𑌹𑌾𑌃': 3, + '𑌹𑍀': 2, + '𞤀𞥄': 2, + '𞤢𞥄': 2, + '𞤣𞥆': 2, + '𞤂𞥆': 2, + '𞤤𞥆': 2, + '𞤥𞥆': 2, + '𞤦𞥆': 2, + '𞤨𞥆': 2, + '𞤩𞥆': 2, + '𞤫𞥅': 2, + '𞤭𞥅': 2, + '𞤌𞥅': 2, + '𞤮𞥅': 2, + '𞤍𞥆': 2, + '𞤯𞥆': 2, + '𞤰𞥆': 2, + '𞤲𞥆': 2, + '𞤳𞥆': 2, + '𞤵𞥅': 2, + '𞤔𞥆': 2, + '𞤶𞥆': 2, + '𞤷𞥆': 2, + '𞤺𞥆': 2, + '𞤼𞥆': 2, + '🍄‍🟫': 5, + '🍋‍🟩': 5, + '🏃‍♀️': 5, + '🏃‍♀️‍➡️': 8, + '🏃‍♂️': 5, + '🏃‍♂️‍➡️': 8, + '🏃‍➡️': 5, + '🏃🏻‍♀️': 7, + '🏃🏻‍♀️‍➡️': 10, + '🏃🏻‍♂️': 7, + '🏃🏻‍♂️‍➡️': 10, + '🏃🏻‍➡️': 7, + '🏃🏼‍♀️': 7, + '🏃🏼‍♀️‍➡️': 10, + '🏃🏼‍♂️': 7, + '🏃🏼‍♂️‍➡️': 10, + '🏃🏼‍➡️': 7, + '🏃🏽‍♀️': 7, + '🏃🏽‍♀️‍➡️': 10, + '🏃🏽‍♂️': 7, + '🏃🏽‍♂️‍➡️': 10, + '🏃🏽‍➡️': 7, + '🏃🏾‍♀️': 7, + '🏃🏾‍♀️‍➡️': 10, + '🏃🏾‍♂️': 7, + '🏃🏾‍♂️‍➡️': 10, + '🏃🏾‍➡️': 7, + '🏃🏿‍♀️': 7, + '🏃🏿‍♀️‍➡️': 10, + '🏃🏿‍♂️': 7, + '🏃🏿‍♂️‍➡️': 10, + '🏃🏿‍➡️': 7, + '🏄‍♀️': 5, + '🏄‍♂️': 5, + '🏄🏻‍♀️': 7, + '🏄🏻‍♂️': 7, + '🏄🏼‍♀️': 7, + '🏄🏼‍♂️': 7, + '🏄🏽‍♀️': 7, + '🏄🏽‍♂️': 7, + '🏄🏾‍♀️': 7, + '🏄🏾‍♂️': 7, + '🏄🏿‍♀️': 7, + '🏄🏿‍♂️': 7, + '🏊‍♀️': 5, + '🏊‍♂️': 5, + '🏊🏻‍♀️': 7, + '🏊🏻‍♂️': 7, + '🏊🏼‍♀️': 7, + '🏊🏼‍♂️': 7, + '🏊🏽‍♀️': 7, + '🏊🏽‍♂️': 7, + '🏊🏾‍♀️': 7, + '🏊🏾‍♂️': 7, + '🏊🏿‍♀️': 7, + '🏊🏿‍♂️': 7, + '🏋️‍♀️': 5, + '🏋️‍♂️': 5, + '🏋🏻‍♀️': 6, + '🏋🏻‍♂️': 6, + '🏋🏼‍♀️': 6, + '🏋🏼‍♂️': 6, + '🏋🏽‍♀️': 6, + '🏋🏽‍♂️': 6, + '🏋🏾‍♀️': 6, + '🏋🏾‍♂️': 6, + '🏋🏿‍♀️': 6, + '🏋🏿‍♂️': 6, + '🏌️‍♀️': 5, + '🏌️‍♂️': 5, + '🏌🏻‍♀️': 6, + '🏌🏻‍♂️': 6, + '🏌🏼‍♀️': 6, + '🏌🏼‍♂️': 6, + '🏌🏽‍♀️': 6, + '🏌🏽‍♂️': 6, + '🏌🏾‍♀️': 6, + '🏌🏾‍♂️': 6, + '🏌🏿‍♀️': 6, + '🏌🏿‍♂️': 6, + '🏳️‍⚧️': 5, + '🏳️‍🌈': 5, + '🏴‍☠️': 5, + '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 8, + '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 8, + '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 8, + '🐈‍⬛': 5, + '🐕‍🦺': 5, + '🐦‍⬛': 5, + '🐦‍🔥': 5, + '🐻‍❄️': 5, + '👁️‍🗨️': 5, + '👨‍⚕️': 5, + '👨‍⚖️': 5, + '👨‍✈️': 5, + '👨‍❤️‍👨': 8, + '👨‍❤️‍💋‍👨': 11, + '👨‍🌾': 5, + '👨‍🍳': 5, + '👨‍🍼': 5, + '👨‍🎓': 5, + '👨‍🎤': 5, + '👨‍🎨': 5, + '👨‍🏫': 5, + '👨‍🏭': 5, + '👨‍👦': 5, + '👨‍👦‍👦': 8, + '👨‍👧': 5, + '👨‍👧‍👦': 8, + '👨‍👧‍👧': 8, + '👨‍👨‍👦': 8, + '👨‍👨‍👦‍👦': 11, + '👨‍👨‍👧': 8, + '👨‍👨‍👧‍👦': 11, + '👨‍👨‍👧‍👧': 11, + '👨‍👩‍👦': 8, + '👨‍👩‍👦‍👦': 11, + '👨‍👩‍👧': 8, + '👨‍👩‍👧‍👦': 11, + '👨‍👩‍👧‍👧': 11, + '👨‍💻': 5, + '👨‍💼': 5, + '👨‍🔧': 5, + '👨‍🔬': 5, + '👨‍🚀': 5, + '👨‍🚒': 5, + '👨‍🦯': 5, + '👨‍🦯‍➡️': 8, + '👨‍🦰': 5, + '👨‍🦱': 5, + '👨‍🦲': 5, + '👨‍🦳': 5, + '👨‍🦼': 5, + '👨‍🦼‍➡️': 8, + '👨‍🦽': 5, + '👨‍🦽‍➡️': 8, + '👨🏻‍⚕️': 7, + '👨🏻‍⚖️': 7, + '👨🏻‍✈️': 7, + '👨🏻‍❤️‍👨🏻': 12, + '👨🏻‍❤️‍👨🏽': 12, + '👨🏻‍❤️‍👨🏿': 12, + '👨🏻‍❤️‍💋‍👨🏻': 15, + '👨🏻‍❤️‍💋‍👨🏽': 15, + '👨🏻‍❤️‍💋‍👨🏾': 15, + '👨🏻‍❤️‍💋‍👨🏿': 15, + '👨🏻‍🌾': 7, + '👨🏻‍🍳': 7, + '👨🏻‍🍼': 7, + '👨🏻‍🎓': 7, + '👨🏻‍🎤': 7, + '👨🏻‍🎨': 7, + '👨🏻‍🏫': 7, + '👨🏻‍🏭': 7, + '👨🏻‍🐰‍👨🏼': 12, + '👨🏻‍🐰‍👨🏽': 12, + '👨🏻‍🐰‍👨🏾': 12, + '👨🏻‍🐰‍👨🏿': 12, + '👨🏻‍💻': 7, + '👨🏻‍💼': 7, + '👨🏻‍🔧': 7, + '👨🏻‍🔬': 7, + '👨🏻‍🚀': 7, + '👨🏻‍🚒': 7, + '👨🏻‍🤝‍👨🏼': 12, + '👨🏻‍🤝‍👨🏽': 12, + '👨🏻‍🤝‍👨🏾': 12, + '👨🏻‍🤝‍👨🏿': 12, + '👨🏻‍🦯': 7, + '👨🏻‍🦯‍➡️': 10, + '👨🏻‍🦰': 7, + '👨🏻‍🦱': 7, + '👨🏻‍🦲': 7, + '👨🏻‍🦳': 7, + '👨🏻‍🦼': 7, + '👨🏻‍🦼‍➡️': 10, + '👨🏻‍🦽': 7, + '👨🏻‍🦽‍➡️': 10, + '👨🏻‍🫯‍👨🏼': 11, + '👨🏻‍🫯‍👨🏽': 11, + '👨🏻‍🫯‍👨🏾': 11, + '👨🏻‍🫯‍👨🏿': 11, + '👨🏼‍⚕️': 7, + '👨🏼‍⚖️': 7, + '👨🏼‍✈️': 7, + '👨🏼‍❤️‍👨🏼': 12, + '👨🏼‍❤️‍💋‍👨🏼': 15, + '👨🏼‍❤️‍💋‍👨🏿': 15, + '👨🏼‍🌾': 7, + '👨🏼‍🍳': 7, + '👨🏼‍🍼': 7, + '👨🏼‍🎓': 7, + '👨🏼‍🎤': 7, + '👨🏼‍🎨': 7, + '👨🏼‍🏫': 7, + '👨🏼‍🏭': 7, + '👨🏼‍🐰‍👨🏻': 12, + '👨🏼‍🐰‍👨🏽': 12, + '👨🏼‍🐰‍👨🏿': 12, + '👨🏼‍💻': 7, + '👨🏼‍💼': 7, + '👨🏼‍🔧': 7, + '👨🏼‍🔬': 7, + '👨🏼‍🚀': 7, + '👨🏼‍🚒': 7, + '👨🏼‍🤝‍👨🏻': 12, + '👨🏼‍🤝‍👨🏽': 12, + '👨🏼‍🤝‍👨🏿': 12, + '👨🏼‍🦯': 7, + '👨🏼‍🦯‍➡️': 10, + '👨🏼‍🦰': 7, + '👨🏼‍🦱': 7, + '👨🏼‍🦲': 7, + '👨🏼‍🦳': 7, + '👨🏼‍🦼': 7, + '👨🏼‍🦽': 7, + '👨🏼‍🦽‍➡️': 10, + '👨🏼‍🫯‍👨🏻': 11, + '👨🏼‍🫯‍👨🏽': 11, + '👨🏼‍🫯‍👨🏾': 11, + '👨🏼‍🫯‍👨🏿': 11, + '👨🏽‍⚕️': 7, + '👨🏽‍⚖️': 7, + '👨🏽‍✈️': 7, + '👨🏽‍❤️‍👨🏻': 12, + '👨🏽‍❤️‍👨🏽': 12, + '👨🏽‍❤️‍👨🏿': 12, + '👨🏽‍❤️‍💋‍👨🏻': 15, + '👨🏽‍❤️‍💋‍👨🏽': 15, + '👨🏽‍❤️‍💋‍👨🏿': 15, + '👨🏽‍🌾': 7, + '👨🏽‍🍳': 7, + '👨🏽‍🍼': 7, + '👨🏽‍🎓': 7, + '👨🏽‍🎤': 7, + '👨🏽‍🎨': 7, + '👨🏽‍🏫': 7, + '👨🏽‍🏭': 7, + '👨🏽‍🐰‍👨🏻': 12, + '👨🏽‍🐰‍👨🏼': 12, + '👨🏽‍🐰‍👨🏾': 12, + '👨🏽‍🐰‍👨🏿': 12, + '👨🏽‍💻': 7, + '👨🏽‍💼': 7, + '👨🏽‍🔧': 7, + '👨🏽‍🔬': 7, + '👨🏽‍🚀': 7, + '👨🏽‍🚒': 7, + '👨🏽‍🤝‍👨🏻': 12, + '👨🏽‍🤝‍👨🏼': 12, + '👨🏽‍🤝‍👨🏾': 12, + '👨🏽‍🤝‍👨🏿': 12, + '👨🏽‍🦯': 7, + '👨🏽‍🦯‍➡️': 10, + '👨🏽‍🦰': 7, + '👨🏽‍🦱': 7, + '👨🏽‍🦲': 7, + '👨🏽‍🦳': 7, + '👨🏽‍🦼': 7, + '👨🏽‍🦼‍➡️': 10, + '👨🏽‍🦽': 7, + '👨🏽‍🦽‍➡️': 10, + '👨🏽‍🫯‍👨🏻': 11, + '👨🏽‍🫯‍👨🏼': 11, + '👨🏽‍🫯‍👨🏾': 11, + '👨🏽‍🫯‍👨🏿': 11, + '👨🏾‍⚕️': 7, + '👨🏾‍⚖️': 7, + '👨🏾‍✈️': 7, + '👨🏾‍❤️‍👨🏾': 12, + '👨🏾‍❤️‍💋‍👨🏻': 15, + '👨🏾‍❤️‍💋‍👨🏾': 15, + '👨🏾‍❤️‍💋‍👨🏿': 15, + '👨🏾‍🌾': 7, + '👨🏾‍🍳': 7, + '👨🏾‍🍼': 7, + '👨🏾‍🎓': 7, + '👨🏾‍🎤': 7, + '👨🏾‍🎨': 7, + '👨🏾‍🏫': 7, + '👨🏾‍🏭': 7, + '👨🏾‍🐰‍👨🏻': 12, + '👨🏾‍🐰‍👨🏽': 12, + '👨🏾‍🐰‍👨🏿': 12, + '👨🏾‍💻': 7, + '👨🏾‍💼': 7, + '👨🏾‍🔧': 7, + '👨🏾‍🔬': 7, + '👨🏾‍🚀': 7, + '👨🏾‍🚒': 7, + '👨🏾‍🤝‍👨🏻': 12, + '👨🏾‍🤝‍👨🏽': 12, + '👨🏾‍🤝‍👨🏿': 12, + '👨🏾‍🦯': 7, + '👨🏾‍🦯‍➡️': 10, + '👨🏾‍🦰': 7, + '👨🏾‍🦱': 7, + '👨🏾‍🦲': 7, + '👨🏾‍🦳': 7, + '👨🏾‍🦼': 7, + '👨🏾‍🦽': 7, + '👨🏾‍🦽‍➡️': 10, + '👨🏾‍🫯‍👨🏻': 11, + '👨🏾‍🫯‍👨🏼': 11, + '👨🏾‍🫯‍👨🏽': 11, + '👨🏾‍🫯‍👨🏿': 11, + '👨🏿‍⚕️': 7, + '👨🏿‍⚖️': 7, + '👨🏿‍✈️': 7, + '👨🏿‍❤️‍👨🏻': 12, + '👨🏿‍❤️‍👨🏽': 12, + '👨🏿‍❤️‍👨🏿': 12, + '👨🏿‍❤️‍💋‍👨🏻': 15, + '👨🏿‍❤️‍💋‍👨🏼': 15, + '👨🏿‍❤️‍💋‍👨🏽': 15, + '👨🏿‍❤️‍💋‍👨🏾': 15, + '👨🏿‍❤️‍💋‍👨🏿': 15, + '👨🏿‍🌾': 7, + '👨🏿‍🍳': 7, + '👨🏿‍🍼': 7, + '👨🏿‍🎓': 7, + '👨🏿‍🎤': 7, + '👨🏿‍🎨': 7, + '👨🏿‍🏫': 7, + '👨🏿‍🏭': 7, + '👨🏿‍🐰‍👨🏻': 12, + '👨🏿‍🐰‍👨🏼': 12, + '👨🏿‍🐰‍👨🏽': 12, + '👨🏿‍🐰‍👨🏾': 12, + '👨🏿‍💻': 7, + '👨🏿‍💼': 7, + '👨🏿‍🔧': 7, + '👨🏿‍🔬': 7, + '👨🏿‍🚀': 7, + '👨🏿‍🚒': 7, + '👨🏿‍🤝‍👨🏻': 12, + '👨🏿‍🤝‍👨🏼': 12, + '👨🏿‍🤝‍👨🏽': 12, + '👨🏿‍🤝‍👨🏾': 12, + '👨🏿‍🦯': 7, + '👨🏿‍🦯‍➡️': 10, + '👨🏿‍🦰': 7, + '👨🏿‍🦱': 7, + '👨🏿‍🦲': 7, + '👨🏿‍🦳': 7, + '👨🏿‍🦼': 7, + '👨🏿‍🦼‍➡️': 10, + '👨🏿‍🦽': 7, + '👨🏿‍🦽‍➡️': 10, + '👨🏿‍🫯‍👨🏻': 11, + '👨🏿‍🫯‍👨🏼': 11, + '👨🏿‍🫯‍👨🏽': 11, + '👨🏿‍🫯‍👨🏾': 11, + '👩‍⚕️': 5, + '👩‍⚖️': 5, + '👩‍✈️': 5, + '👩‍❤️‍👨': 8, + '👩‍❤️‍👩': 8, + '👩‍❤️‍💋‍👨': 11, + '👩‍❤️‍💋‍👩': 11, + '👩‍🌾': 5, + '👩‍🍳': 5, + '👩‍🍼': 5, + '👩‍🎓': 5, + '👩‍🎤': 5, + '👩‍🎨': 5, + '👩‍🏫': 5, + '👩‍🏭': 5, + '👩‍👦': 5, + '👩‍👦‍👦': 8, + '👩‍👧': 5, + '👩‍👧‍👦': 8, + '👩‍👧‍👧': 8, + '👩‍👩‍👦': 8, + '👩‍👩‍👦‍👦': 11, + '👩‍👩‍👧': 8, + '👩‍👩‍👧‍👦': 11, + '👩‍👩‍👧‍👧': 11, + '👩‍💻': 5, + '👩‍💼': 5, + '👩‍🔧': 5, + '👩‍🔬': 5, + '👩‍🚀': 5, + '👩‍🚒': 5, + '👩‍🦯': 5, + '👩‍🦯‍➡️': 8, + '👩‍🦰': 5, + '👩‍🦱': 5, + '👩‍🦲': 5, + '👩‍🦳': 5, + '👩‍🦼': 5, + '👩‍🦼‍➡️': 8, + '👩‍🦽': 5, + '👩‍🦽‍➡️': 8, + '👩🏻‍⚕️': 7, + '👩🏻‍⚖️': 7, + '👩🏻‍✈️': 7, + '👩🏻‍❤️‍👨🏻': 12, + '👩🏻‍❤️‍👨🏿': 12, + '👩🏻‍❤️‍👩🏻': 12, + '👩🏻‍❤️‍💋‍👨🏻': 15, + '👩🏻‍❤️‍💋‍👨🏽': 15, + '👩🏻‍❤️‍💋‍👨🏿': 15, + '👩🏻‍❤️‍💋‍👩🏻': 15, + '👩🏻‍❤️‍💋‍👩🏽': 15, + '👩🏻‍❤️‍💋‍👩🏿': 15, + '👩🏻‍🌾': 7, + '👩🏻‍🍳': 7, + '👩🏻‍🍼': 7, + '👩🏻‍🎓': 7, + '👩🏻‍🎤': 7, + '👩🏻‍🎨': 7, + '👩🏻‍🏫': 7, + '👩🏻‍🏭': 7, + '👩🏻‍🐰‍👩🏼': 12, + '👩🏻‍🐰‍👩🏽': 12, + '👩🏻‍🐰‍👩🏾': 12, + '👩🏻‍🐰‍👩🏿': 12, + '👩🏻‍💻': 7, + '👩🏻‍💼': 7, + '👩🏻‍🔧': 7, + '👩🏻‍🔬': 7, + '👩🏻‍🚀': 7, + '👩🏻‍🚒': 7, + '👩🏻‍🤝‍👨🏽': 12, + '👩🏻‍🤝‍👨🏿': 12, + '👩🏻‍🤝‍👩🏼': 12, + '👩🏻‍🤝‍👩🏽': 12, + '👩🏻‍🤝‍👩🏾': 12, + '👩🏻‍🤝‍👩🏿': 12, + '👩🏻‍🦯': 7, + '👩🏻‍🦯‍➡️': 10, + '👩🏻‍🦰': 7, + '👩🏻‍🦱': 7, + '👩🏻‍🦲': 7, + '👩🏻‍🦳': 7, + '👩🏻‍🦼': 7, + '👩🏻‍🦼‍➡️': 10, + '👩🏻‍🦽': 7, + '👩🏻‍🦽‍➡️': 10, + '👩🏻‍🫯‍👩🏼': 11, + '👩🏻‍🫯‍👩🏽': 11, + '👩🏻‍🫯‍👩🏾': 11, + '👩🏻‍🫯‍👩🏿': 11, + '👩🏼‍⚕️': 7, + '👩🏼‍⚖️': 7, + '👩🏼‍✈️': 7, + '👩🏼‍❤️‍👨🏼': 12, + '👩🏼‍❤️‍👩🏼': 12, + '👩🏼‍❤️‍💋‍👨🏼': 15, + '👩🏼‍❤️‍💋‍👩🏼': 15, + '👩🏼‍🌾': 7, + '👩🏼‍🍳': 7, + '👩🏼‍🍼': 7, + '👩🏼‍🎓': 7, + '👩🏼‍🎤': 7, + '👩🏼‍🎨': 7, + '👩🏼‍🏫': 7, + '👩🏼‍🏭': 7, + '👩🏼‍🐰‍👩🏻': 12, + '👩🏼‍🐰‍👩🏿': 12, + '👩🏼‍💻': 7, + '👩🏼‍💼': 7, + '👩🏼‍🔧': 7, + '👩🏼‍🔬': 7, + '👩🏼‍🚀': 7, + '👩🏼‍🚒': 7, + '👩🏼‍🤝‍👩🏻': 12, + '👩🏼‍🤝‍👩🏽': 12, + '👩🏼‍🤝‍👩🏿': 12, + '👩🏼‍🦯': 7, + '👩🏼‍🦯‍➡️': 10, + '👩🏼‍🦰': 7, + '👩🏼‍🦱': 7, + '👩🏼‍🦲': 7, + '👩🏼‍🦳': 7, + '👩🏼‍🦼': 7, + '👩🏼‍🦽': 7, + '👩🏼‍🫯‍👩🏻': 11, + '👩🏼‍🫯‍👩🏽': 11, + '👩🏼‍🫯‍👩🏾': 11, + '👩🏼‍🫯‍👩🏿': 11, + '👩🏽‍⚕️': 7, + '👩🏽‍⚖️': 7, + '👩🏽‍✈️': 7, + '👩🏽‍❤️‍👨🏽': 12, + '👩🏽‍❤️‍👩🏽': 12, + '👩🏽‍❤️‍💋‍👨🏻': 15, + '👩🏽‍❤️‍💋‍👨🏽': 15, + '👩🏽‍❤️‍💋‍👨🏿': 15, + '👩🏽‍❤️‍💋‍👩🏻': 15, + '👩🏽‍❤️‍💋‍👩🏽': 15, + '👩🏽‍❤️‍💋‍👩🏿': 15, + '👩🏽‍🌾': 7, + '👩🏽‍🍳': 7, + '👩🏽‍🍼': 7, + '👩🏽‍🎓': 7, + '👩🏽‍🎤': 7, + '👩🏽‍🎨': 7, + '👩🏽‍🏫': 7, + '👩🏽‍🏭': 7, + '👩🏽‍🐰‍👩🏻': 12, + '👩🏽‍🐰‍👩🏾': 12, + '👩🏽‍🐰‍👩🏿': 12, + '👩🏽‍💻': 7, + '👩🏽‍💼': 7, + '👩🏽‍🔧': 7, + '👩🏽‍🔬': 7, + '👩🏽‍🚀': 7, + '👩🏽‍🚒': 7, + '👩🏽‍🤝‍👨🏻': 12, + '👩🏽‍🤝‍👨🏿': 12, + '👩🏽‍🤝‍👩🏻': 12, + '👩🏽‍🤝‍👩🏼': 12, + '👩🏽‍🤝‍👩🏾': 12, + '👩🏽‍🤝‍👩🏿': 12, + '👩🏽‍🦯': 7, + '👩🏽‍🦯‍➡️': 10, + '👩🏽‍🦰': 7, + '👩🏽‍🦱': 7, + '👩🏽‍🦲': 7, + '👩🏽‍🦳': 7, + '👩🏽‍🦼': 7, + '👩🏽‍🦼‍➡️': 10, + '👩🏽‍🦽': 7, + '👩🏽‍🦽‍➡️': 10, + '👩🏽‍🫯‍👩🏻': 11, + '👩🏽‍🫯‍👩🏼': 11, + '👩🏽‍🫯‍👩🏾': 11, + '👩🏽‍🫯‍👩🏿': 11, + '👩🏾‍⚕️': 7, + '👩🏾‍⚖️': 7, + '👩🏾‍✈️': 7, + '👩🏾‍❤️‍👨🏾': 12, + '👩🏾‍❤️‍👩🏾': 12, + '👩🏾‍❤️‍💋‍👨🏾': 15, + '👩🏾‍❤️‍💋‍👩🏾': 15, + '👩🏾‍🌾': 7, + '👩🏾‍🍳': 7, + '👩🏾‍🍼': 7, + '👩🏾‍🎓': 7, + '👩🏾‍🎤': 7, + '👩🏾‍🎨': 7, + '👩🏾‍🏫': 7, + '👩🏾‍🏭': 7, + '👩🏾‍🐰‍👩🏻': 12, + '👩🏾‍🐰‍👩🏽': 12, + '👩🏾‍🐰‍👩🏿': 12, + '👩🏾‍💻': 7, + '👩🏾‍💼': 7, + '👩🏾‍🔧': 7, + '👩🏾‍🔬': 7, + '👩🏾‍🚀': 7, + '👩🏾‍🚒': 7, + '👩🏾‍🤝‍👩🏻': 12, + '👩🏾‍🤝‍👩🏽': 12, + '👩🏾‍🤝‍👩🏿': 12, + '👩🏾‍🦯': 7, + '👩🏾‍🦯‍➡️': 10, + '👩🏾‍🦰': 7, + '👩🏾‍🦱': 7, + '👩🏾‍🦲': 7, + '👩🏾‍🦳': 7, + '👩🏾‍🦼': 7, + '👩🏾‍🦽': 7, + '👩🏾‍🦽‍➡️': 10, + '👩🏾‍🫯‍👩🏻': 11, + '👩🏾‍🫯‍👩🏼': 11, + '👩🏾‍🫯‍👩🏽': 11, + '👩🏾‍🫯‍👩🏿': 11, + '👩🏿‍⚕️': 7, + '👩🏿‍⚖️': 7, + '👩🏿‍✈️': 7, + '👩🏿‍❤️‍👨🏻': 12, + '👩🏿‍❤️‍👨🏿': 12, + '👩🏿‍❤️‍👩🏿': 12, + '👩🏿‍❤️‍💋‍👨🏻': 15, + '👩🏿‍❤️‍💋‍👨🏽': 15, + '👩🏿‍❤️‍💋‍👨🏿': 15, + '👩🏿‍❤️‍💋‍👩🏻': 15, + '👩🏿‍❤️‍💋‍👩🏽': 15, + '👩🏿‍❤️‍💋‍👩🏿': 15, + '👩🏿‍🌾': 7, + '👩🏿‍🍳': 7, + '👩🏿‍🍼': 7, + '👩🏿‍🎓': 7, + '👩🏿‍🎤': 7, + '👩🏿‍🎨': 7, + '👩🏿‍🏫': 7, + '👩🏿‍🏭': 7, + '👩🏿‍🐰‍👩🏻': 12, + '👩🏿‍🐰‍👩🏼': 12, + '👩🏿‍🐰‍👩🏽': 12, + '👩🏿‍🐰‍👩🏾': 12, + '👩🏿‍💻': 7, + '👩🏿‍💼': 7, + '👩🏿‍🔧': 7, + '👩🏿‍🔬': 7, + '👩🏿‍🚀': 7, + '👩🏿‍🚒': 7, + '👩🏿‍🤝‍👨🏻': 12, + '👩🏿‍🤝‍👨🏽': 12, + '👩🏿‍🤝‍👩🏻': 12, + '👩🏿‍🤝‍👩🏼': 12, + '👩🏿‍🤝‍👩🏽': 12, + '👩🏿‍🤝‍👩🏾': 12, + '👩🏿‍🦯': 7, + '👩🏿‍🦯‍➡️': 10, + '👩🏿‍🦰': 7, + '👩🏿‍🦱': 7, + '👩🏿‍🦲': 7, + '👩🏿‍🦳': 7, + '👩🏿‍🦼': 7, + '👩🏿‍🦼‍➡️': 10, + '👩🏿‍🦽': 7, + '👩🏿‍🦽‍➡️': 10, + '👩🏿‍🫯‍👩🏻': 11, + '👩🏿‍🫯‍👩🏼': 11, + '👩🏿‍🫯‍👩🏽': 11, + '👩🏿‍🫯‍👩🏾': 11, + '👮‍♀️': 5, + '👮‍♂️': 5, + '👮🏻‍♀️': 7, + '👮🏻‍♂️': 7, + '👮🏼‍♀️': 7, + '👮🏼‍♂️': 7, + '👮🏽‍♀️': 7, + '👮🏽‍♂️': 7, + '👮🏾‍♀️': 7, + '👮🏾‍♂️': 7, + '👮🏿‍♀️': 7, + '👮🏿‍♂️': 7, + '👯‍♀️': 5, + '👯‍♂️': 5, + '👯🏻‍♀️': 7, + '👯🏻‍♂️': 7, + '👯🏼‍♀️': 7, + '👯🏼‍♂️': 7, + '👯🏽‍♀️': 7, + '👯🏽‍♂️': 7, + '👯🏾‍♀️': 7, + '👯🏾‍♂️': 7, + '👯🏿‍♀️': 7, + '👯🏿‍♂️': 7, + '👰‍♀️': 5, + '👰‍♂️': 5, + '👰🏻‍♀️': 7, + '👰🏻‍♂️': 7, + '👰🏼‍♀️': 7, + '👰🏼‍♂️': 7, + '👰🏽‍♀️': 7, + '👰🏽‍♂️': 7, + '👰🏾‍♀️': 7, + '👰🏾‍♂️': 7, + '👰🏿‍♀️': 7, + '👰🏿‍♂️': 7, + '👱‍♀️': 5, + '👱‍♂️': 5, + '👱🏻‍♀️': 7, + '👱🏻‍♂️': 7, + '👱🏼‍♀️': 7, + '👱🏼‍♂️': 7, + '👱🏽‍♀️': 7, + '👱🏽‍♂️': 7, + '👱🏾‍♀️': 7, + '👱🏾‍♂️': 7, + '👱🏿‍♀️': 7, + '👱🏿‍♂️': 7, + '👳‍♀️': 5, + '👳‍♂️': 5, + '👳🏻‍♀️': 7, + '👳🏻‍♂️': 7, + '👳🏼‍♀️': 7, + '👳🏼‍♂️': 7, + '👳🏽‍♀️': 7, + '👳🏽‍♂️': 7, + '👳🏾‍♀️': 7, + '👳🏾‍♂️': 7, + '👳🏿‍♀️': 7, + '👳🏿‍♂️': 7, + '👷‍♀️': 5, + '👷‍♂️': 5, + '👷🏻‍♀️': 7, + '👷🏻‍♂️': 7, + '👷🏼‍♀️': 7, + '👷🏼‍♂️': 7, + '👷🏽‍♀️': 7, + '👷🏽‍♂️': 7, + '👷🏾‍♀️': 7, + '👷🏾‍♂️': 7, + '👷🏿‍♀️': 7, + '👷🏿‍♂️': 7, + '💁‍♀️': 5, + '💁‍♂️': 5, + '💁🏻‍♀️': 7, + '💁🏻‍♂️': 7, + '💁🏼‍♀️': 7, + '💁🏼‍♂️': 7, + '💁🏽‍♀️': 7, + '💁🏽‍♂️': 7, + '💁🏾‍♀️': 7, + '💁🏾‍♂️': 7, + '💁🏿‍♀️': 7, + '💁🏿‍♂️': 7, + '💂‍♀️': 5, + '💂‍♂️': 5, + '💂🏻‍♀️': 7, + '💂🏻‍♂️': 7, + '💂🏼‍♀️': 7, + '💂🏼‍♂️': 7, + '💂🏽‍♀️': 7, + '💂🏽‍♂️': 7, + '💂🏾‍♀️': 7, + '💂🏾‍♂️': 7, + '💂🏿‍♀️': 7, + '💂🏿‍♂️': 7, + '💆‍♀️': 5, + '💆‍♂️': 5, + '💆🏻‍♀️': 7, + '💆🏻‍♂️': 7, + '💆🏼‍♀️': 7, + '💆🏼‍♂️': 7, + '💆🏽‍♀️': 7, + '💆🏽‍♂️': 7, + '💆🏾‍♀️': 7, + '💆🏾‍♂️': 7, + '💆🏿‍♀️': 7, + '💆🏿‍♂️': 7, + '💇‍♀️': 5, + '💇‍♂️': 5, + '💇🏻‍♀️': 7, + '💇🏻‍♂️': 7, + '💇🏼‍♀️': 7, + '💇🏼‍♂️': 7, + '💇🏽‍♀️': 7, + '💇🏽‍♂️': 7, + '💇🏾‍♀️': 7, + '💇🏾‍♂️': 7, + '💇🏿‍♀️': 7, + '💇🏿‍♂️': 7, + '🕵️‍♀️': 5, + '🕵️‍♂️': 5, + '🕵🏻‍♀️': 6, + '🕵🏻‍♂️': 6, + '🕵🏼‍♀️': 6, + '🕵🏼‍♂️': 6, + '🕵🏽‍♀️': 6, + '🕵🏽‍♂️': 6, + '🕵🏾‍♀️': 6, + '🕵🏾‍♂️': 6, + '🕵🏿‍♀️': 6, + '🕵🏿‍♂️': 6, + '😮‍💨': 5, + '😵‍💫': 5, + '😶‍🌫️': 5, + '🙂‍↔️': 5, + '🙂‍↕️': 5, + '🙅‍♀️': 5, + '🙅‍♂️': 5, + '🙅🏻‍♀️': 7, + '🙅🏻‍♂️': 7, + '🙅🏼‍♀️': 7, + '🙅🏼‍♂️': 7, + '🙅🏽‍♀️': 7, + '🙅🏽‍♂️': 7, + '🙅🏾‍♀️': 7, + '🙅🏾‍♂️': 7, + '🙅🏿‍♀️': 7, + '🙅🏿‍♂️': 7, + '🙆‍♀️': 5, + '🙆‍♂️': 5, + '🙆🏻‍♀️': 7, + '🙆🏻‍♂️': 7, + '🙆🏼‍♀️': 7, + '🙆🏼‍♂️': 7, + '🙆🏽‍♀️': 7, + '🙆🏽‍♂️': 7, + '🙆🏾‍♀️': 7, + '🙆🏾‍♂️': 7, + '🙆🏿‍♀️': 7, + '🙆🏿‍♂️': 7, + '🙇‍♀️': 5, + '🙇‍♂️': 5, + '🙇🏻‍♀️': 7, + '🙇🏻‍♂️': 7, + '🙇🏼‍♀️': 7, + '🙇🏼‍♂️': 7, + '🙇🏽‍♀️': 7, + '🙇🏽‍♂️': 7, + '🙇🏾‍♀️': 7, + '🙇🏾‍♂️': 7, + '🙇🏿‍♀️': 7, + '🙇🏿‍♂️': 7, + '🙋‍♀️': 5, + '🙋‍♂️': 5, + '🙋🏻‍♀️': 7, + '🙋🏻‍♂️': 7, + '🙋🏼‍♀️': 7, + '🙋🏼‍♂️': 7, + '🙋🏽‍♀️': 7, + '🙋🏽‍♂️': 7, + '🙋🏾‍♀️': 7, + '🙋🏾‍♂️': 7, + '🙋🏿‍♀️': 7, + '🙋🏿‍♂️': 7, + '🙍‍♀️': 5, + '🙍‍♂️': 5, + '🙍🏻‍♀️': 7, + '🙍🏻‍♂️': 7, + '🙍🏼‍♀️': 7, + '🙍🏼‍♂️': 7, + '🙍🏽‍♀️': 7, + '🙍🏽‍♂️': 7, + '🙍🏾‍♀️': 7, + '🙍🏾‍♂️': 7, + '🙍🏿‍♀️': 7, + '🙍🏿‍♂️': 7, + '🙎‍♀️': 5, + '🙎‍♂️': 5, + '🙎🏻‍♀️': 7, + '🙎🏻‍♂️': 7, + '🙎🏼‍♀️': 7, + '🙎🏼‍♂️': 7, + '🙎🏽‍♀️': 7, + '🙎🏽‍♂️': 7, + '🙎🏾‍♀️': 7, + '🙎🏾‍♂️': 7, + '🙎🏿‍♀️': 7, + '🙎🏿‍♂️': 7, + '🚣‍♀️': 5, + '🚣‍♂️': 5, + '🚣🏻‍♀️': 7, + '🚣🏻‍♂️': 7, + '🚣🏼‍♀️': 7, + '🚣🏼‍♂️': 7, + '🚣🏽‍♀️': 7, + '🚣🏽‍♂️': 7, + '🚣🏾‍♀️': 7, + '🚣🏾‍♂️': 7, + '🚣🏿‍♀️': 7, + '🚣🏿‍♂️': 7, + '🚴‍♀️': 5, + '🚴‍♂️': 5, + '🚴🏻‍♀️': 7, + '🚴🏻‍♂️': 7, + '🚴🏼‍♀️': 7, + '🚴🏼‍♂️': 7, + '🚴🏽‍♀️': 7, + '🚴🏽‍♂️': 7, + '🚴🏾‍♀️': 7, + '🚴🏾‍♂️': 7, + '🚴🏿‍♀️': 7, + '🚴🏿‍♂️': 7, + '🚵‍♀️': 5, + '🚵‍♂️': 5, + '🚵🏻‍♀️': 7, + '🚵🏻‍♂️': 7, + '🚵🏼‍♀️': 7, + '🚵🏼‍♂️': 7, + '🚵🏽‍♀️': 7, + '🚵🏽‍♂️': 7, + '🚵🏾‍♀️': 7, + '🚵🏾‍♂️': 7, + '🚵🏿‍♀️': 7, + '🚵🏿‍♂️': 7, + '🚶‍♀️': 5, + '🚶‍♀️‍➡️': 8, + '🚶‍♂️': 5, + '🚶‍♂️‍➡️': 8, + '🚶‍➡️': 5, + '🚶🏻‍♀️': 7, + '🚶🏻‍♀️‍➡️': 10, + '🚶🏻‍♂️': 7, + '🚶🏻‍♂️‍➡️': 10, + '🚶🏻‍➡️': 7, + '🚶🏼‍♀️': 7, + '🚶🏼‍♀️‍➡️': 10, + '🚶🏼‍♂️': 7, + '🚶🏼‍♂️‍➡️': 10, + '🚶🏼‍➡️': 7, + '🚶🏽‍♀️': 7, + '🚶🏽‍♀️‍➡️': 10, + '🚶🏽‍♂️': 7, + '🚶🏽‍♂️‍➡️': 10, + '🚶🏽‍➡️': 7, + '🚶🏾‍♀️': 7, + '🚶🏾‍♀️‍➡️': 10, + '🚶🏾‍♂️': 7, + '🚶🏾‍♂️‍➡️': 10, + '🚶🏾‍➡️': 7, + '🚶🏿‍♀️': 7, + '🚶🏿‍♀️‍➡️': 10, + '🚶🏿‍♂️': 7, + '🚶🏿‍♂️‍➡️': 10, + '🚶🏿‍➡️': 7, + '🤦‍♀️': 5, + '🤦‍♂️': 5, + '🤦🏻‍♀️': 7, + '🤦🏻‍♂️': 7, + '🤦🏼‍♀️': 7, + '🤦🏼‍♂️': 7, + '🤦🏽‍♀️': 7, + '🤦🏽‍♂️': 7, + '🤦🏾‍♀️': 7, + '🤦🏾‍♂️': 7, + '🤦🏿‍♀️': 7, + '🤦🏿‍♂️': 7, + '🤵‍♀️': 5, + '🤵‍♂️': 5, + '🤵🏻‍♀️': 7, + '🤵🏻‍♂️': 7, + '🤵🏼‍♀️': 7, + '🤵🏼‍♂️': 7, + '🤵🏽‍♀️': 7, + '🤵🏽‍♂️': 7, + '🤵🏾‍♀️': 7, + '🤵🏾‍♂️': 7, + '🤵🏿‍♀️': 7, + '🤵🏿‍♂️': 7, + '🤷‍♀️': 5, + '🤷‍♂️': 5, + '🤷🏻‍♀️': 7, + '🤷🏻‍♂️': 7, + '🤷🏼‍♀️': 7, + '🤷🏼‍♂️': 7, + '🤷🏽‍♀️': 7, + '🤷🏽‍♂️': 7, + '🤷🏾‍♀️': 7, + '🤷🏾‍♂️': 7, + '🤷🏿‍♀️': 7, + '🤷🏿‍♂️': 7, + '🤸‍♀️': 5, + '🤸‍♂️': 5, + '🤸🏻‍♀️': 7, + '🤸🏻‍♂️': 7, + '🤸🏼‍♀️': 7, + '🤸🏼‍♂️': 7, + '🤸🏽‍♀️': 7, + '🤸🏽‍♂️': 7, + '🤸🏾‍♀️': 7, + '🤸🏾‍♂️': 7, + '🤸🏿‍♀️': 7, + '🤸🏿‍♂️': 7, + '🤹‍♀️': 5, + '🤹‍♂️': 5, + '🤹🏻‍♀️': 7, + '🤹🏻‍♂️': 7, + '🤹🏼‍♀️': 7, + '🤹🏼‍♂️': 7, + '🤹🏽‍♀️': 7, + '🤹🏽‍♂️': 7, + '🤹🏾‍♀️': 7, + '🤹🏾‍♂️': 7, + '🤹🏿‍♀️': 7, + '🤹🏿‍♂️': 7, + '🤼‍♀️': 5, + '🤼‍♂️': 5, + '🤼🏻‍♀️': 7, + '🤼🏻‍♂️': 7, + '🤼🏼‍♀️': 7, + '🤼🏼‍♂️': 7, + '🤼🏽‍♀️': 7, + '🤼🏽‍♂️': 7, + '🤼🏾‍♀️': 7, + '🤼🏾‍♂️': 7, + '🤼🏿‍♀️': 7, + '🤼🏿‍♂️': 7, + '🤽‍♀️': 5, + '🤽‍♂️': 5, + '🤽🏻‍♀️': 7, + '🤽🏻‍♂️': 7, + '🤽🏼‍♀️': 7, + '🤽🏼‍♂️': 7, + '🤽🏽‍♀️': 7, + '🤽🏽‍♂️': 7, + '🤽🏾‍♀️': 7, + '🤽🏾‍♂️': 7, + '🤽🏿‍♀️': 7, + '🤽🏿‍♂️': 7, + '🤾‍♀️': 5, + '🤾‍♂️': 5, + '🤾🏻‍♀️': 7, + '🤾🏻‍♂️': 7, + '🤾🏼‍♀️': 7, + '🤾🏼‍♂️': 7, + '🤾🏽‍♀️': 7, + '🤾🏽‍♂️': 7, + '🤾🏾‍♀️': 7, + '🤾🏾‍♂️': 7, + '🤾🏿‍♀️': 7, + '🤾🏿‍♂️': 7, + '🦸‍♀️': 5, + '🦸‍♂️': 5, + '🦸🏻‍♀️': 7, + '🦸🏻‍♂️': 7, + '🦸🏼‍♀️': 7, + '🦸🏼‍♂️': 7, + '🦸🏽‍♀️': 7, + '🦸🏽‍♂️': 7, + '🦸🏾‍♀️': 7, + '🦸🏾‍♂️': 7, + '🦸🏿‍♀️': 7, + '🦸🏿‍♂️': 7, + '🦹‍♀️': 5, + '🦹‍♂️': 5, + '🦹🏻‍♀️': 7, + '🦹🏻‍♂️': 7, + '🦹🏼‍♀️': 7, + '🦹🏼‍♂️': 7, + '🦹🏽‍♀️': 7, + '🦹🏽‍♂️': 7, + '🦹🏾‍♀️': 7, + '🦹🏾‍♂️': 7, + '🦹🏿‍♀️': 7, + '🦹🏿‍♂️': 7, + '🧍‍♀️': 5, + '🧍‍♂️': 5, + '🧍🏻‍♀️': 7, + '🧍🏻‍♂️': 7, + '🧍🏼‍♀️': 7, + '🧍🏼‍♂️': 7, + '🧍🏽‍♀️': 7, + '🧍🏽‍♂️': 7, + '🧍🏾‍♀️': 7, + '🧍🏾‍♂️': 7, + '🧍🏿‍♀️': 7, + '🧍🏿‍♂️': 7, + '🧎‍♀️': 5, + '🧎‍♀️‍➡️': 8, + '🧎‍♂️': 5, + '🧎‍♂️‍➡️': 8, + '🧎‍➡️': 5, + '🧎🏻‍♀️': 7, + '🧎🏻‍♀️‍➡️': 10, + '🧎🏻‍♂️': 7, + '🧎🏻‍♂️‍➡️': 10, + '🧎🏻‍➡️': 7, + '🧎🏼‍♀️': 7, + '🧎🏼‍♀️‍➡️': 10, + '🧎🏼‍♂️': 7, + '🧎🏼‍♂️‍➡️': 10, + '🧎🏼‍➡️': 7, + '🧎🏽‍♀️': 7, + '🧎🏽‍♀️‍➡️': 10, + '🧎🏽‍♂️': 7, + '🧎🏽‍♂️‍➡️': 10, + '🧎🏽‍➡️': 7, + '🧎🏾‍♀️': 7, + '🧎🏾‍♀️‍➡️': 10, + '🧎🏾‍♂️': 7, + '🧎🏾‍♂️‍➡️': 10, + '🧎🏾‍➡️': 7, + '🧎🏿‍♀️': 7, + '🧎🏿‍♀️‍➡️': 10, + '🧎🏿‍♂️': 7, + '🧎🏿‍♂️‍➡️': 10, + '🧎🏿‍➡️': 7, + '🧏‍♀️': 5, + '🧏‍♂️': 5, + '🧏🏻‍♀️': 7, + '🧏🏻‍♂️': 7, + '🧏🏼‍♀️': 7, + '🧏🏼‍♂️': 7, + '🧏🏽‍♀️': 7, + '🧏🏽‍♂️': 7, + '🧏🏾‍♀️': 7, + '🧏🏾‍♂️': 7, + '🧏🏿‍♀️': 7, + '🧏🏿‍♂️': 7, + '🧑‍⚕️': 5, + '🧑‍⚖️': 5, + '🧑‍✈️': 5, + '🧑‍🌾': 5, + '🧑‍🍳': 5, + '🧑‍🍼': 5, + '🧑‍🎄': 5, + '🧑‍🎓': 5, + '🧑‍🎤': 5, + '🧑‍🎨': 5, + '🧑‍🏫': 5, + '🧑‍🏭': 5, + '🧑‍💻': 5, + '🧑‍💼': 5, + '🧑‍🔧': 5, + '🧑‍🔬': 5, + '🧑‍🚀': 5, + '🧑‍🚒': 5, + '🧑‍🤝‍🧑': 8, + '🧑‍🦯': 5, + '🧑‍🦯‍➡️': 8, + '🧑‍🦰': 5, + '🧑‍🦱': 5, + '🧑‍🦲': 5, + '🧑‍🦳': 5, + '🧑‍🦼': 5, + '🧑‍🦼‍➡️': 8, + '🧑‍🦽': 5, + '🧑‍🦽‍➡️': 8, + '🧑‍🧑‍🧒': 8, + '🧑‍🧑‍🧒‍🧒': 11, + '🧑‍🧒': 5, + '🧑‍🧒‍🧒': 8, + '🧑‍🩰': 5, + '🧑🏻‍⚕️': 7, + '🧑🏻‍⚖️': 7, + '🧑🏻‍✈️': 7, + '🧑🏻‍❤️‍💋‍🧑🏿': 15, + '🧑🏻‍🌾': 7, + '🧑🏻‍🍳': 7, + '🧑🏻‍🍼': 7, + '🧑🏻‍🎄': 7, + '🧑🏻‍🎓': 7, + '🧑🏻‍🎤': 7, + '🧑🏻‍🎨': 7, + '🧑🏻‍🏫': 7, + '🧑🏻‍🏭': 7, + '🧑🏻‍🐰‍🧑🏽': 12, + '🧑🏻‍🐰‍🧑🏾': 12, + '🧑🏻‍🐰‍🧑🏿': 12, + '🧑🏻‍💻': 7, + '🧑🏻‍💼': 7, + '🧑🏻‍🔧': 7, + '🧑🏻‍🔬': 7, + '🧑🏻‍🚀': 7, + '🧑🏻‍🚒': 7, + '🧑🏻‍🤝‍🧑🏻': 12, + '🧑🏻‍🤝‍🧑🏼': 12, + '🧑🏻‍🤝‍🧑🏽': 12, + '🧑🏻‍🤝‍🧑🏾': 12, + '🧑🏻‍🤝‍🧑🏿': 12, + '🧑🏻‍🦯': 7, + '🧑🏻‍🦯‍➡️': 10, + '🧑🏻‍🦰': 7, + '🧑🏻‍🦱': 7, + '🧑🏻‍🦲': 7, + '🧑🏻‍🦳': 7, + '🧑🏻‍🦼': 7, + '🧑🏻‍🦼‍➡️': 10, + '🧑🏻‍🦽': 7, + '🧑🏻‍🦽‍➡️': 10, + '🧑🏻‍🩰': 7, + '🧑🏻‍🫯‍🧑🏼': 11, + '🧑🏻‍🫯‍🧑🏽': 11, + '🧑🏻‍🫯‍🧑🏾': 11, + '🧑🏻‍🫯‍🧑🏿': 11, + '🧑🏼‍⚕️': 7, + '🧑🏼‍⚖️': 7, + '🧑🏼‍✈️': 7, + '🧑🏼‍🌾': 7, + '🧑🏼‍🍳': 7, + '🧑🏼‍🍼': 7, + '🧑🏼‍🎄': 7, + '🧑🏼‍🎓': 7, + '🧑🏼‍🎤': 7, + '🧑🏼‍🎨': 7, + '🧑🏼‍🏫': 7, + '🧑🏼‍🏭': 7, + '🧑🏼‍🐰‍🧑🏿': 12, + '🧑🏼‍💻': 7, + '🧑🏼‍💼': 7, + '🧑🏼‍🔧': 7, + '🧑🏼‍🔬': 7, + '🧑🏼‍🚀': 7, + '🧑🏼‍🚒': 7, + '🧑🏼‍🤝‍🧑🏻': 12, + '🧑🏼‍🤝‍🧑🏼': 12, + '🧑🏼‍🤝‍🧑🏽': 12, + '🧑🏼‍🤝‍🧑🏿': 12, + '🧑🏼‍🦯': 7, + '🧑🏼‍🦯‍➡️': 10, + '🧑🏼‍🦰': 7, + '🧑🏼‍🦱': 7, + '🧑🏼‍🦲': 7, + '🧑🏼‍🦳': 7, + '🧑🏼‍🦼': 7, + '🧑🏼‍🦽': 7, + '🧑🏼‍🩰': 7, + '🧑🏼‍🫯‍🧑🏻': 11, + '🧑🏼‍🫯‍🧑🏽': 11, + '🧑🏼‍🫯‍🧑🏿': 11, + '🧑🏽‍⚕️': 7, + '🧑🏽‍⚖️': 7, + '🧑🏽‍✈️': 7, + '🧑🏽‍❤️‍💋‍🧑🏿': 15, + '🧑🏽‍🌾': 7, + '🧑🏽‍🍳': 7, + '🧑🏽‍🍼': 7, + '🧑🏽‍🎄': 7, + '🧑🏽‍🎓': 7, + '🧑🏽‍🎤': 7, + '🧑🏽‍🎨': 7, + '🧑🏽‍🏫': 7, + '🧑🏽‍🏭': 7, + '🧑🏽‍🐰‍🧑🏻': 12, + '🧑🏽‍🐰‍🧑🏿': 12, + '🧑🏽‍💻': 7, + '🧑🏽‍💼': 7, + '🧑🏽‍🔧': 7, + '🧑🏽‍🔬': 7, + '🧑🏽‍🚀': 7, + '🧑🏽‍🚒': 7, + '🧑🏽‍🤝‍🧑🏻': 12, + '🧑🏽‍🤝‍🧑🏼': 12, + '🧑🏽‍🤝‍🧑🏽': 12, + '🧑🏽‍🤝‍🧑🏾': 12, + '🧑🏽‍🤝‍🧑🏿': 12, + '🧑🏽‍🦯': 7, + '🧑🏽‍🦯‍➡️': 10, + '🧑🏽‍🦰': 7, + '🧑🏽‍🦱': 7, + '🧑🏽‍🦲': 7, + '🧑🏽‍🦳': 7, + '🧑🏽‍🦼': 7, + '🧑🏽‍🦼‍➡️': 10, + '🧑🏽‍🦽': 7, + '🧑🏽‍🦽‍➡️': 10, + '🧑🏽‍🩰': 7, + '🧑🏽‍🫯‍🧑🏻': 11, + '🧑🏽‍🫯‍🧑🏼': 11, + '🧑🏽‍🫯‍🧑🏾': 11, + '🧑🏽‍🫯‍🧑🏿': 11, + '🧑🏾‍⚕️': 7, + '🧑🏾‍⚖️': 7, + '🧑🏾‍✈️': 7, + '🧑🏾‍🌾': 7, + '🧑🏾‍🍳': 7, + '🧑🏾‍🍼': 7, + '🧑🏾‍🎄': 7, + '🧑🏾‍🎓': 7, + '🧑🏾‍🎤': 7, + '🧑🏾‍🎨': 7, + '🧑🏾‍🏫': 7, + '🧑🏾‍🏭': 7, + '🧑🏾‍🐰‍🧑🏻': 12, + '🧑🏾‍🐰‍🧑🏿': 12, + '🧑🏾‍💻': 7, + '🧑🏾‍💼': 7, + '🧑🏾‍🔧': 7, + '🧑🏾‍🔬': 7, + '🧑🏾‍🚀': 7, + '🧑🏾‍🚒': 7, + '🧑🏾‍🤝‍🧑🏻': 12, + '🧑🏾‍🤝‍🧑🏽': 12, + '🧑🏾‍🤝‍🧑🏾': 12, + '🧑🏾‍🤝‍🧑🏿': 12, + '🧑🏾‍🦯': 7, + '🧑🏾‍🦯‍➡️': 10, + '🧑🏾‍🦰': 7, + '🧑🏾‍🦱': 7, + '🧑🏾‍🦲': 7, + '🧑🏾‍🦳': 7, + '🧑🏾‍🦼': 7, + '🧑🏾‍🦽': 7, + '🧑🏾‍🩰': 7, + '🧑🏾‍🫯‍🧑🏻': 11, + '🧑🏾‍🫯‍🧑🏽': 11, + '🧑🏾‍🫯‍🧑🏿': 11, + '🧑🏿‍⚕️': 7, + '🧑🏿‍⚖️': 7, + '🧑🏿‍✈️': 7, + '🧑🏿‍❤️‍💋‍🧑🏻': 15, + '🧑🏿‍❤️‍💋‍🧑🏽': 15, + '🧑🏿‍🌾': 7, + '🧑🏿‍🍳': 7, + '🧑🏿‍🍼': 7, + '🧑🏿‍🎄': 7, + '🧑🏿‍🎓': 7, + '🧑🏿‍🎤': 7, + '🧑🏿‍🎨': 7, + '🧑🏿‍🏫': 7, + '🧑🏿‍🏭': 7, + '🧑🏿‍🐰‍🧑🏻': 12, + '🧑🏿‍🐰‍🧑🏼': 12, + '🧑🏿‍🐰‍🧑🏽': 12, + '🧑🏿‍🐰‍🧑🏾': 12, + '🧑🏿‍💻': 7, + '🧑🏿‍💼': 7, + '🧑🏿‍🔧': 7, + '🧑🏿‍🔬': 7, + '🧑🏿‍🚀': 7, + '🧑🏿‍🚒': 7, + '🧑🏿‍🤝‍🧑🏻': 12, + '🧑🏿‍🤝‍🧑🏼': 12, + '🧑🏿‍🤝‍🧑🏽': 12, + '🧑🏿‍🤝‍🧑🏾': 12, + '🧑🏿‍🤝‍🧑🏿': 12, + '🧑🏿‍🦯': 7, + '🧑🏿‍🦯‍➡️': 10, + '🧑🏿‍🦰': 7, + '🧑🏿‍🦱': 7, + '🧑🏿‍🦲': 7, + '🧑🏿‍🦳': 7, + '🧑🏿‍🦼': 7, + '🧑🏿‍🦼‍➡️': 10, + '🧑🏿‍🦽': 7, + '🧑🏿‍🦽‍➡️': 10, + '🧑🏿‍🩰': 7, + '🧑🏿‍🫯‍🧑🏻': 11, + '🧑🏿‍🫯‍🧑🏼': 11, + '🧑🏿‍🫯‍🧑🏽': 11, + '🧑🏿‍🫯‍🧑🏾': 11, + '🧔‍♀️': 5, + '🧔‍♂️': 5, + '🧔🏻‍♀️': 7, + '🧔🏻‍♂️': 7, + '🧔🏼‍♀️': 7, + '🧔🏼‍♂️': 7, + '🧔🏽‍♀️': 7, + '🧔🏽‍♂️': 7, + '🧔🏾‍♀️': 7, + '🧔🏾‍♂️': 7, + '🧔🏿‍♀️': 7, + '🧔🏿‍♂️': 7, + '🧖‍♀️': 5, + '🧖‍♂️': 5, + '🧖🏻‍♀️': 7, + '🧖🏻‍♂️': 7, + '🧖🏼‍♀️': 7, + '🧖🏼‍♂️': 7, + '🧖🏽‍♀️': 7, + '🧖🏽‍♂️': 7, + '🧖🏾‍♀️': 7, + '🧖🏾‍♂️': 7, + '🧖🏿‍♀️': 7, + '🧖🏿‍♂️': 7, + '🧗‍♀️': 5, + '🧗‍♂️': 5, + '🧗🏻‍♀️': 7, + '🧗🏻‍♂️': 7, + '🧗🏼‍♀️': 7, + '🧗🏼‍♂️': 7, + '🧗🏽‍♀️': 7, + '🧗🏽‍♂️': 7, + '🧗🏾‍♀️': 7, + '🧗🏾‍♂️': 7, + '🧗🏿‍♀️': 7, + '🧗🏿‍♂️': 7, + '🧘‍♀️': 5, + '🧘‍♂️': 5, + '🧘🏻‍♀️': 7, + '🧘🏻‍♂️': 7, + '🧘🏼‍♀️': 7, + '🧘🏼‍♂️': 7, + '🧘🏽‍♀️': 7, + '🧘🏽‍♂️': 7, + '🧘🏾‍♀️': 7, + '🧘🏾‍♂️': 7, + '🧘🏿‍♀️': 7, + '🧘🏿‍♂️': 7, + '🧙‍♀️': 5, + '🧙‍♂️': 5, + '🧙🏻‍♀️': 7, + '🧙🏻‍♂️': 7, + '🧙🏼‍♀️': 7, + '🧙🏼‍♂️': 7, + '🧙🏽‍♀️': 7, + '🧙🏽‍♂️': 7, + '🧙🏾‍♀️': 7, + '🧙🏾‍♂️': 7, + '🧙🏿‍♀️': 7, + '🧙🏿‍♂️': 7, + '🧚‍♀️': 5, + '🧚‍♂️': 5, + '🧚🏻‍♀️': 7, + '🧚🏻‍♂️': 7, + '🧚🏼‍♀️': 7, + '🧚🏼‍♂️': 7, + '🧚🏽‍♀️': 7, + '🧚🏽‍♂️': 7, + '🧚🏾‍♀️': 7, + '🧚🏾‍♂️': 7, + '🧚🏿‍♀️': 7, + '🧚🏿‍♂️': 7, + '🧛‍♀️': 5, + '🧛‍♂️': 5, + '🧛🏻‍♀️': 7, + '🧛🏻‍♂️': 7, + '🧛🏼‍♀️': 7, + '🧛🏼‍♂️': 7, + '🧛🏽‍♀️': 7, + '🧛🏽‍♂️': 7, + '🧛🏾‍♀️': 7, + '🧛🏾‍♂️': 7, + '🧛🏿‍♀️': 7, + '🧛🏿‍♂️': 7, + '🧜‍♀️': 5, + '🧜‍♂️': 5, + '🧜🏻‍♀️': 7, + '🧜🏻‍♂️': 7, + '🧜🏼‍♀️': 7, + '🧜🏼‍♂️': 7, + '🧜🏽‍♀️': 7, + '🧜🏽‍♂️': 7, + '🧜🏾‍♀️': 7, + '🧜🏾‍♂️': 7, + '🧜🏿‍♀️': 7, + '🧜🏿‍♂️': 7, + '🧝‍♀️': 5, + '🧝‍♂️': 5, + '🧝🏻‍♀️': 7, + '🧝🏻‍♂️': 7, + '🧝🏼‍♀️': 7, + '🧝🏼‍♂️': 7, + '🧝🏽‍♀️': 7, + '🧝🏽‍♂️': 7, + '🧝🏾‍♀️': 7, + '🧝🏾‍♂️': 7, + '🧝🏿‍♀️': 7, + '🧝🏿‍♂️': 7, + '🧞‍♀️': 5, + '🧞‍♂️': 5, + '🧟‍♀️': 5, + '🧟‍♂️': 5, + '🫱🏻‍🫲🏼': 9, + '🫱🏻‍🫲🏽': 9, + '🫱🏻‍🫲🏾': 9, + '🫱🏻‍🫲🏿': 9, + '🫱🏼‍🫲🏻': 9, + '🫱🏼‍🫲🏽': 9, + '🫱🏼‍🫲🏾': 9, + '🫱🏼‍🫲🏿': 9, + '🫱🏽‍🫲🏻': 9, + '🫱🏽‍🫲🏼': 9, + '🫱🏽‍🫲🏾': 9, + '🫱🏽‍🫲🏿': 9, + '🫱🏾‍🫲🏻': 9, + '🫱🏾‍🫲🏼': 9, + '🫱🏾‍🫲🏽': 9, + '🫱🏾‍🫲🏿': 9, + '🫱🏿‍🫲🏻': 9, + '🫱🏿‍🫲🏼': 9, + '🫱🏿‍🫲🏽': 9, + '🫱🏿‍🫲🏾': 9, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_known_fd9d4c44.py b/libs/wcwidth/table_grapheme_overrides/_known_fd9d4c44.py new file mode 100644 index 000000000..14567efbe --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_known_fd9d4c44.py @@ -0,0 +1,3671 @@ +""" +Grapheme overrides for _known_fd9d4c44. + +This code generated by python wcwidth using ucs-detect project data. + +Terminals: contour +""" +# pylint: skip-file +GRAPHEMES = { + '؁۱': 1, + 'का': 1, + 'कां': 1, + 'कि': 1, + 'किं': 1, + 'की': 1, + 'केा': 1, + 'को': 1, + 'कौ': 1, + 'क्क': 1, + 'क्का': 1, + 'क्कां': 1, + 'क्को': 1, + 'क्घ्घि्': 1, + 'क्छ': 1, + 'क्त': 1, + 'क्ता': 1, + 'क्ति': 1, + 'क्ती': 1, + 'क्तीं': 1, + 'क्तुं': 1, + 'क्तेः': 1, + 'क्त्योः': 1, + 'क्ना': 1, + 'क्नि': 1, + 'क्य': 1, + 'क्या': 1, + 'क्र': 1, + 'क्रा': 1, + 'क्रि': 1, + 'क्रू': 1, + 'क्ष': 1, + 'क्षं': 1, + 'क्षा': 1, + 'क्षां': 1, + 'क्षाः': 1, + 'क्षि': 1, + 'क्षे': 1, + 'क्षै': 1, + 'क्षो': 1, + 'क्ष्ण': 1, + 'क्ष्य': 1, + 'क्ष्या': 1, + 'क्सा': 1, + 'खा': 1, + 'खि': 1, + 'खी': 1, + 'खो': 1, + 'ख्त': 1, + 'ख्ने': 1, + 'ख्य': 1, + 'ख्यः': 1, + 'ख्या': 1, + 'ख्‍ने': 1, + 'ग़ा': 1, + 'गा': 1, + 'गाँ': 1, + 'गां': 1, + 'गि': 1, + 'गी': 1, + 'गो': 1, + 'गों': 1, + 'गौ': 1, + 'ग्ण': 1, + 'ग्न': 1, + 'ग्ने': 1, + 'ग्भे': 1, + 'ग्य': 1, + 'ग्या': 1, + 'ग्ये': 1, + 'ग्यैः': 1, + 'ग्र': 1, + 'ग्रे': 1, + 'ग्र्य': 1, + 'घा': 1, + 'घि': 1, + 'घी': 1, + 'घेि': 1, + 'घो': 1, + 'घ्ंत': 1, + 'घ्का': 1, + 'घ्त': 1, + 'घ्र': 1, + 'घ्र्व्य': 1, + 'घ्लि': 1, + 'घ्व': 1, + 'घ्श्य': 1, + 'घ्स्ट': 1, + 'ङ्ग': 1, + 'ङ्घ': 1, + 'चा': 1, + 'चां': 1, + 'चि': 1, + 'ची': 1, + 'चो': 1, + 'चौ': 1, + 'च्च': 1, + 'च्चा': 1, + 'च्चे': 1, + 'च्चों': 1, + 'च्छ': 1, + 'च्छं': 1, + 'च्छा': 1, + 'च्छि': 1, + 'च्छु': 1, + 'च्छे': 1, + 'च्या': 1, + 'च्यां': 1, + 'च्यो': 1, + 'छि': 1, + 'छुी': 1, + 'छो': 1, + 'जा': 1, + 'जि': 1, + 'जिं': 1, + 'जी': 1, + 'जो': 1, + 'ज्ज': 1, + 'ज्ञा': 1, + 'ज्ञे': 1, + 'ज्ने': 1, + 'ज्य': 1, + 'ज्या': 1, + 'ज्या‌': 1, + 'ज्रा': 1, + 'झा': 1, + 'ञ्च': 1, + 'ञ्चा': 1, + 'ञ्चि': 1, + 'टा': 1, + 'टां': 1, + 'टि': 1, + 'टी': 1, + 'टों': 1, + 'ट्ज़': 1, + 'ट्टि': 1, + 'ट्टी': 1, + 'ट्या': 1, + 'ट्र': 1, + 'ट्रा': 1, + 'ट्रे': 1, + 'ठा': 1, + 'ठी': 1, + 'ठैः': 1, + 'ड़ा': 1, + 'डा': 1, + 'डि': 1, + 'डी': 1, + 'डो': 1, + 'ढ़ा': 1, + 'ढ़ि': 1, + 'ढ़ी': 1, + 'ढ़ो': 1, + 'ढा': 1, + 'ढाँ': 1, + 'ढि': 1, + 'णा': 1, + 'णां': 1, + 'णि': 1, + 'णी': 1, + 'णो': 1, + 'णों': 1, + 'ण्ड': 1, + 'ण्डं': 1, + 'ण्डः': 1, + 'ण्डि': 1, + 'ण्य': 1, + 'ण्या': 1, + 'ण्यां': 1, + 'तः': 1, + 'ता': 1, + 'तां': 1, + 'ताः': 1, + 'ति': 1, + 'तिं': 1, + 'ती': 1, + 'तो': 1, + 'तों': 1, + 'तौ': 1, + 'त्का': 1, + 'त्त': 1, + 'त्ता': 1, + 'त्ति': 1, + 'त्ते': 1, + 'त्त्थि': 1, + 'त्त्व': 1, + 'त्त्वा': 1, + 'त्त्वे': 1, + 'त्न': 1, + 'त्नी': 1, + 'त्नों': 1, + 'त्प': 1, + 'त्पा': 1, + 'त्पी': 1, + 'त्पू': 1, + 'त्म': 1, + 'त्मा': 1, + 'त्य': 1, + 'त्यं': 1, + 'त्या': 1, + 'त्यां': 1, + 'त्यि': 1, + 'त्ये': 1, + 'त्यै': 1, + 'त्र': 1, + 'त्रा': 1, + 'त्राा': 1, + 'त्राि': 1, + 'त्रि': 1, + 'त्री': 1, + 'त्रे': 1, + 'त्र्त': 1, + 'त्र्य': 1, + 'त्र्या': 1, + 'त्र्ये': 1, + 'त्व': 1, + 'त्वं': 1, + 'त्वा': 1, + 'त्वां': 1, + 'त्वे': 1, + 'त्सं': 1, + 'त्सा': 1, + 'त्सो': 1, + 'त्स्वा': 1, + 'था': 1, + 'थि': 1, + 'थ्य': 1, + 'दः': 1, + 'दा': 1, + 'दां': 1, + 'दि': 1, + 'दी': 1, + 'दो': 1, + 'दौ': 1, + 'द्घ': 1, + 'द्घि': 1, + 'द्घो': 1, + 'द्द': 1, + 'द्दा': 1, + 'द्दि': 1, + 'द्दी': 1, + 'द्दे': 1, + 'द्ध': 1, + 'द्धः': 1, + 'द्धा': 1, + 'द्धां': 1, + 'द्धि': 1, + 'द्धिं': 1, + 'द्धी': 1, + 'द्धीं': 1, + 'द्धैः': 1, + 'द्भ': 1, + 'द्भा': 1, + 'द्य': 1, + 'द्या': 1, + 'द्र': 1, + 'द्रो': 1, + 'द्व': 1, + 'द्वा': 1, + 'द्वि': 1, + 'द्वे': 1, + 'द्स': 1, + 'धा': 1, + 'धां': 1, + 'धाः': 1, + 'धि': 1, + 'धी': 1, + 'धेः': 1, + 'धो': 1, + 'धों': 1, + 'ध्य': 1, + 'ध्या': 1, + 'ध्ये': 1, + 'ध्व': 1, + 'नः': 1, + 'ना': 1, + 'नां': 1, + 'नाः': 1, + 'ना‌': 1, + 'नि': 1, + 'निँ': 1, + 'निः': 1, + 'नी': 1, + 'नैः': 1, + 'नो': 1, + 'नों': 1, + 'नौ': 1, + 'न्का': 1, + 'न्चि': 1, + 'न्छे': 1, + 'न्जे': 1, + 'न्ञ्या': 1, + 'न्त': 1, + 'न्तं': 1, + 'न्तः': 1, + 'न्ता': 1, + 'न्ति': 1, + 'न्तु': 1, + 'न्तुं': 1, + 'न्ते': 1, + 'न्तो': 1, + 'न्तों': 1, + 'न्त्र': 1, + 'न्त्रा': 1, + 'न्त्राः': 1, + 'न्त्रि': 1, + 'न्त्री': 1, + 'न्त्र्य': 1, + 'न्त्र्यं': 1, + 'न्त्र्या': 1, + 'न्त्र्ये': 1, + 'न्द': 1, + 'न्दा': 1, + 'न्दी': 1, + 'न्दो': 1, + 'न्ध': 1, + 'न्धा': 1, + 'न्धि': 1, + 'न्धी': 1, + 'न्धु': 1, + 'न्धों': 1, + 'न्न': 1, + 'न्ना': 1, + 'न्नाः': 1, + 'न्नि': 1, + 'न्ने': 1, + 'न्म': 1, + 'न्मा': 1, + 'न्मु': 1, + 'न्मे': 1, + 'न्य': 1, + 'न्या': 1, + 'न्ये': 1, + 'न्यै': 1, + 'न्यो': 1, + 'न्व': 1, + 'न्वे': 1, + 'न्स': 1, + 'न्हा': 1, + 'न्हें': 1, + 'न्ह्यां': 1, + 'पा': 1, + 'पाँ': 1, + 'पां': 1, + 'पि': 1, + 'पी': 1, + 'पो': 1, + 'पों': 1, + 'प्त': 1, + 'प्तं': 1, + 'प्ति': 1, + 'प्तुं': 1, + 'प्तौ': 1, + 'प्य': 1, + 'प्यं': 1, + 'प्या': 1, + 'प्र': 1, + 'प्रा': 1, + 'फा': 1, + 'फाँ': 1, + 'फौ': 1, + 'फ्त': 1, + 'फ्ता': 1, + 'फ्ना': 1, + 'फ्नो': 1, + 'फ्रां': 1, + 'बा': 1, + 'बाँ': 1, + 'बि': 1, + 'बी': 1, + 'बो': 1, + 'ब्द': 1, + 'ब्ध': 1, + 'ब्धि': 1, + 'ब्यू': 1, + 'भा': 1, + 'भि': 1, + 'भिः': 1, + 'भी': 1, + 'भो': 1, + 'भौ': 1, + 'भ्य': 1, + 'भ्यः': 1, + 'भ्या': 1, + 'भ्यां': 1, + 'भ्यु': 1, + 'भ्यो': 1, + 'भ्र': 1, + 'मा': 1, + 'मां': 1, + 'मि': 1, + 'मी': 1, + 'मो': 1, + 'मों': 1, + 'मौ': 1, + 'म्ति': 1, + 'म्न': 1, + 'म्नि': 1, + 'म्प': 1, + 'म्पा': 1, + 'म्प्र': 1, + 'म्ब': 1, + 'म्बु': 1, + 'म्भि': 1, + 'म्म': 1, + 'म्मा': 1, + 'म्मि': 1, + 'म्मे': 1, + 'म्या': 1, + 'म्से': 1, + 'म्ह': 1, + 'म्ही': 1, + 'म्हो': 1, + 'यः': 1, + 'या': 1, + 'यां': 1, + 'याः': 1, + 'यि': 1, + 'यी': 1, + 'यैः': 1, + 'यो': 1, + 'यों': 1, + 'य्य': 1, + 'य्यो': 1, + 'रः': 1, + 'रा': 1, + 'रां': 1, + 'राः': 1, + 'रि': 1, + 'रिं': 1, + 'री': 1, + 'रो': 1, + 'रों': 1, + 'रौ': 1, + 'र्क': 1, + 'र्कि': 1, + 'र्ग': 1, + 'र्गी': 1, + 'र्गे': 1, + 'र्चि': 1, + 'र्छ': 1, + 'र्ज': 1, + 'र्जा': 1, + 'र्जि': 1, + 'र्जी': 1, + 'र्ट': 1, + 'र्ढ': 1, + 'र्ण': 1, + 'र्णं': 1, + 'र्णां': 1, + 'र्णी': 1, + 'र्त': 1, + 'र्ता': 1, + 'र्ति': 1, + 'र्तिः': 1, + 'र्ती': 1, + 'र्तुं': 1, + 'र्त्त': 1, + 'र्त्ति': 1, + 'र्थ': 1, + 'र्थं': 1, + 'र्थि': 1, + 'र्थी': 1, + 'र्द': 1, + 'र्दा': 1, + 'र्दि': 1, + 'र्दु': 1, + 'र्दे': 1, + 'र्दै': 1, + 'र्दो': 1, + 'र्द्ध': 1, + 'र्ध': 1, + 'र्धा': 1, + 'र्धि': 1, + 'र्न': 1, + 'र्नि': 1, + 'र्नु': 1, + 'र्नू': 1, + 'र्ने': 1, + 'र्फ': 1, + 'र्ब': 1, + 'र्बं': 1, + 'र्बा': 1, + 'र्भ': 1, + 'र्भा': 1, + 'र्म': 1, + 'र्मं': 1, + 'र्मा': 1, + 'र्मि': 1, + 'र्य': 1, + 'र्यं': 1, + 'र्या': 1, + 'र्यां': 1, + 'र्ये': 1, + 'र्यों': 1, + 'र्रा': 1, + 'र्व': 1, + 'र्वा': 1, + 'र्वां': 1, + 'र्वृ': 1, + 'र्वे': 1, + 'र्वो': 1, + 'र्श': 1, + 'र्ष': 1, + 'र्षे': 1, + 'र्स': 1, + 'र्सा': 1, + 'र्ह': 1, + 'र्हाः': 1, + 'र्हि': 1, + 'र्हो': 1, + 'र्‍या': 1, + 'ला': 1, + 'लां': 1, + 'लाः': 1, + 'लि': 1, + 'लिं': 1, + 'ली': 1, + 'लीं': 1, + 'लो': 1, + 'लों': 1, + 'लौ': 1, + 'ल्क': 1, + 'ल्का': 1, + 'ल्ने': 1, + 'ल्प': 1, + 'ल्पि': 1, + 'ल्म': 1, + 'ल्य': 1, + 'ल्यं': 1, + 'ल्या': 1, + 'ल्यां': 1, + 'ल्याः': 1, + 'ल्ये': 1, + 'ल्ल': 1, + 'ल्लं': 1, + 'ल्ला': 1, + 'ल्लि': 1, + 'ल्ले': 1, + 'ल्ह': 1, + 'ळा': 1, + 'ळी': 1, + 'वः': 1, + 'वा': 1, + 'वां': 1, + 'वाः': 1, + 'वि': 1, + 'वी': 1, + 'वो': 1, + 'वों': 1, + 'व्य': 1, + 'व्या': 1, + 'व्ये': 1, + 'व्यों': 1, + 'शः': 1, + 'शा': 1, + 'शां': 1, + 'शाः': 1, + 'शि': 1, + 'शी': 1, + 'शो': 1, + 'शों': 1, + 'श्च': 1, + 'श्चि': 1, + 'श्य': 1, + 'श्या': 1, + 'श्यि': 1, + 'श्यों': 1, + 'श्र': 1, + 'श्रा': 1, + 'श्रां': 1, + 'श्रु': 1, + 'श्व': 1, + 'श्वा': 1, + 'श्वि': 1, + 'षा': 1, + 'षां': 1, + 'षाः': 1, + 'षि': 1, + 'षी': 1, + 'षों': 1, + 'ष्का': 1, + 'ष्ट': 1, + 'ष्टः': 1, + 'ष्टा': 1, + 'ष्टां': 1, + 'ष्टाः': 1, + 'ष्टि': 1, + 'ष्टी': 1, + 'ष्टीं': 1, + 'ष्ट्र': 1, + 'ष्ट्रा': 1, + 'ष्ट्रां': 1, + 'ष्ट्रि': 1, + 'ष्ट्री': 1, + 'ष्ट्रे': 1, + 'ष्ट्रो': 1, + 'ष्ट्रों': 1, + 'ष्ठा': 1, + 'ष्ठे': 1, + 'ष्ठों': 1, + 'ष्णु': 1, + 'ष्प': 1, + 'ष्प्र': 1, + 'ष्य': 1, + 'ष्यः': 1, + 'ष्यों': 1, + 'सः': 1, + 'सा': 1, + 'साँ': 1, + 'सां': 1, + 'सि': 1, + 'सी': 1, + 'सो': 1, + 'सौ': 1, + 'स्क': 1, + 'स्का': 1, + 'स्काः': 1, + 'स्कु': 1, + 'स्कू': 1, + 'स्कृ': 1, + 'स्ट': 1, + 'स्ट्या': 1, + 'स्त': 1, + 'स्ता': 1, + 'स्ति': 1, + 'स्ती': 1, + 'स्तु': 1, + 'स्तुं': 1, + 'स्तै': 1, + 'स्तो': 1, + 'स्त्र': 1, + 'स्त्रा': 1, + 'स्त्रि': 1, + 'स्त्री': 1, + 'स्थ': 1, + 'स्था': 1, + 'स्थां': 1, + 'स्थि': 1, + 'स्थी': 1, + 'स्थे': 1, + 'स्थ्य': 1, + 'स्धी': 1, + 'स्प': 1, + 'स्पे': 1, + 'स्म': 1, + 'स्मि': 1, + 'स्मिं': 1, + 'स्मै': 1, + 'स्य': 1, + 'स्या': 1, + 'स्यां': 1, + 'स्याः': 1, + 'स्युः': 1, + 'स्यों': 1, + 'स्रो': 1, + 'स्ल': 1, + 'स्व': 1, + 'स्वं': 1, + 'स्वा': 1, + 'स्वी': 1, + 'स्वे': 1, + 'स्स': 1, + 'स्सा': 1, + 'हा': 1, + 'हाँ': 1, + 'हां': 1, + 'हि': 1, + 'हिं': 1, + 'ही': 1, + 'हीं': 1, + 'हैः': 1, + 'हो': 1, + 'हों': 1, + 'हौ': 1, + 'ह्या': 1, + 'ह्रा': 1, + 'क़ा': 1, + 'क़ि': 1, + 'ज़ा': 1, + 'ड़ा': 1, + 'ढ़ा': 1, + 'फ़ा': 1, + 'फ़्ता': 1, + 'অং': 1, + 'কা': 1, + 'কা‌': 1, + 'কি': 1, + 'কিং': 1, + 'কে': 1, + 'কে‌': 1, + 'কো': 1, + 'ক্ত': 1, + 'ক্তি': 1, + 'ক্য': 1, + 'ক্র': 1, + 'ক্রী': 1, + 'ক্ষ': 1, + 'ক্ষা': 1, + 'ক্ষি': 1, + 'ক্ষে': 1, + 'ক্ষ্য': 1, + 'ক্ষ্যে': 1, + 'খা': 1, + 'খি': 1, + 'খে': 1, + 'খ্যা': 1, + 'গি': 1, + 'গে': 1, + 'গো': 1, + 'গ্য': 1, + 'গ্র': 1, + 'গ্রা': 1, + 'গ্রে': 1, + 'ঘা': 1, + 'ঘে': 1, + 'ঘো': 1, + 'ঘো': 1, + 'ঙ্খা': 1, + 'ঙ্গ': 1, + 'ঙ্গী': 1, + 'ঙ্গে': 1, + 'ঙ্ঘ': 1, + 'চা': 1, + 'চি': 1, + 'চে': 1, + 'চো': 1, + 'চৌ': 1, + 'চ্চ': 1, + 'চ্ছ': 1, + 'চ্ছা': 1, + 'চ্ছা‌': 1, + 'চ্ছু': 1, + 'চ্ছে': 1, + 'ছা': 1, + 'ছি': 1, + 'ছে': 1, + 'জা': 1, + 'জি': 1, + 'জী': 1, + 'জে': 1, + 'জে‌': 1, + 'জ্ঞ': 1, + 'জ্ঞা': 1, + 'জ্য': 1, + 'ঝো': 1, + 'ঞ্চি': 1, + 'ঞ্ছি': 1, + 'টা': 1, + 'টি': 1, + 'ট্রে': 1, + 'ঠি': 1, + 'ড়া': 1, + 'ণা': 1, + 'ণি': 1, + 'ণে': 1, + 'ণ্ড': 1, + 'ণ্ডে': 1, + 'ণ্য': 1, + 'তঃ': 1, + 'তা': 1, + 'তাঁ': 1, + 'তা‌': 1, + 'তি': 1, + 'তি‌': 1, + 'তী': 1, + 'তে': 1, + 'তে‌': 1, + 'ত্ত': 1, + 'ত্তা': 1, + 'ত্তি': 1, + 'ত্ন': 1, + 'ত্ব': 1, + 'ত্বে': 1, + 'ত্ম': 1, + 'ত্য': 1, + 'ত্যা': 1, + 'ত্যি': 1, + 'ত্যে': 1, + 'ত্র': 1, + 'ত্রা': 1, + 'ত্রে': 1, + 'ত্‍প': 1, + 'ত্‍পী': 1, + 'ত্‍সা': 1, + 'থা': 1, + 'থি': 1, + 'থে': 1, + 'থ্য': 1, + 'দা': 1, + 'দি': 1, + 'দে': 1, + 'দে‌': 1, + 'দৈ': 1, + 'দো': 1, + 'দ্দি': 1, + 'দ্দে': 1, + 'দ্ধ': 1, + 'দ্ধা': 1, + 'দ্ধি': 1, + 'দ্ধে': 1, + 'দ্বা': 1, + 'দ্ভ': 1, + 'দ্ভূ': 1, + 'দ্য': 1, + 'দ্রো': 1, + 'ধা': 1, + 'ধি': 1, + 'ধী': 1, + 'ধে': 1, + 'ধ্য': 1, + 'ধ্যে': 1, + 'না': 1, + 'না‌': 1, + 'নি': 1, + 'নী': 1, + 'নে': 1, + 'নৈ': 1, + 'নো': 1, + 'ন্ড': 1, + 'ন্ত': 1, + 'ন্তা': 1, + 'ন্তি': 1, + 'ন্তে': 1, + 'ন্ত্রি': 1, + 'ন্ত্রে': 1, + 'ন্থী': 1, + 'ন্ধ': 1, + 'ন্ধা': 1, + 'ন্ধু': 1, + 'ন্ন': 1, + 'ন্ম': 1, + 'ন্মা': 1, + 'ন্মু': 1, + 'ন্য': 1, + 'ন্যা': 1, + 'ন্যে': 1, + 'পা': 1, + 'পা‌': 1, + 'পি': 1, + 'পী': 1, + 'পে': 1, + 'পো': 1, + 'প্ত': 1, + 'প্তা': 1, + 'প্র': 1, + 'প্রা': 1, + 'ফে': 1, + 'ফৌ': 1, + 'বং': 1, + 'বা': 1, + 'বা‌': 1, + 'বি': 1, + 'বী': 1, + 'বে': 1, + 'বৈ': 1, + 'বো': 1, + 'ব্ধি': 1, + 'ব্য': 1, + 'ব্যা': 1, + 'ব্র': 1, + 'ভা': 1, + 'ভি': 1, + 'ভে': 1, + 'ভো': 1, + 'ভৌ': 1, + 'ভ্য': 1, + 'ভ্রা': 1, + 'মা': 1, + 'মা‌': 1, + 'মি': 1, + 'মে': 1, + 'মৌ': 1, + 'ম্প': 1, + 'ম্পা': 1, + 'ম্ব': 1, + 'ম্ভ': 1, + 'ম্ম': 1, + 'ম্মা': 1, + 'ম্য': 1, + 'য়া': 1, + 'য়ি': 1, + 'য়ে': 1, + 'য়ো': 1, + 'যা': 1, + 'যে': 1, + 'যো': 1, + 'য্য': 1, + 'রা': 1, + 'রাং': 1, + 'রি': 1, + 'রী': 1, + 'রে': 1, + 'রো': 1, + 'র্ক': 1, + 'র্জ': 1, + 'র্জা': 1, + 'র্ণ': 1, + 'র্ত': 1, + 'র্থ': 1, + 'র্থে': 1, + 'র্দো': 1, + 'র্ধ': 1, + 'র্ধা': 1, + 'র্নী': 1, + 'র্ব': 1, + 'র্বা': 1, + 'র্বি': 1, + 'র্বো': 1, + 'র্ব্য': 1, + 'র্ভূ': 1, + 'র্ম': 1, + 'র্মে': 1, + 'র্য': 1, + 'র্যা': 1, + 'লা': 1, + 'লি': 1, + 'লী': 1, + 'লে': 1, + 'লে‌': 1, + 'ল্প': 1, + 'ল্য': 1, + 'ল্যা': 1, + 'ল্যে': 1, + 'ল্লে': 1, + 'শং': 1, + 'শা': 1, + 'শি': 1, + 'শী': 1, + 'শৃং': 1, + 'শে': 1, + 'শৈ': 1, + 'শ্চ': 1, + 'শ্চি': 1, + 'শ্বা': 1, + 'শ্বে': 1, + 'শ্য': 1, + 'শ্যে': 1, + 'শ্র': 1, + 'শ্রা': 1, + 'ষা': 1, + 'ষি': 1, + 'ষী': 1, + 'ষে': 1, + 'ষ্ট': 1, + 'ষ্টা': 1, + 'ষ্টি': 1, + 'ষ্ট্র': 1, + 'ষ্ট্রী': 1, + 'ষ্ট্রে': 1, + 'ষ্ঠা': 1, + 'ষ্ঠি': 1, + 'ষ্ঠী': 1, + 'ষ্ঠু': 1, + 'ষ্ণু': 1, + 'সং': 1, + 'সা': 1, + 'সাং': 1, + 'সি': 1, + 'সী': 1, + 'সে': 1, + 'সে‌': 1, + 'স্ক': 1, + 'স্কৃ': 1, + 'স্ত': 1, + 'স্তি': 1, + 'স্তি‌': 1, + 'স্ত্র': 1, + 'স্থ': 1, + 'স্থা': 1, + 'স্থ্য': 1, + 'স্ব': 1, + 'স্বা': 1, + 'স্বী': 1, + 'স্য': 1, + 'স্যা': 1, + 'স্যে': 1, + 'হা': 1, + 'হি': 1, + 'হে': 1, + 'হো': 1, + 'হ্য': 1, + 'য়ে': 1, + 'ਕਾ': 1, + 'ਕਾਂ': 1, + 'ਕਿ': 1, + 'ਕਿ੍': 1, + 'ਕਿੱ': 1, + 'ਕੀ': 1, + 'ਖਾ': 1, + 'ਖਾਂ': 1, + 'ਖਿ': 1, + 'ਖਿ਼': 1, + 'ਖੀ': 1, + 'ਗਾ': 1, + 'ਗਿ': 1, + 'ਗਿ੍': 1, + 'ਗੀ': 1, + 'ਚਾ': 1, + 'ਚਿ': 1, + 'ਚਿੱ': 1, + 'ਚੀ': 1, + 'ਜਾ': 1, + 'ਜਾਂ': 1, + 'ਜਿ': 1, + 'ਜਿ਼': 1, + 'ਜਿ਼ੰ': 1, + 'ਜੀ': 1, + 'ਝਿ': 1, + 'ਝੀ': 1, + 'ਟਾ': 1, + 'ਟਿ': 1, + 'ਟਿ੍': 1, + 'ਟੀ': 1, + 'ਠੀ': 1, + 'ਡਾ': 1, + 'ਣਾ': 1, + 'ਣਾਂ': 1, + 'ਣੀ': 1, + 'ਤਾ': 1, + 'ਤਾਂ': 1, + 'ਤਿ': 1, + 'ਤੀ': 1, + 'ਦਾ': 1, + 'ਦਾਂ': 1, + 'ਦਿ': 1, + 'ਦਿੱ': 1, + 'ਦੀ': 1, + 'ਧਾ': 1, + 'ਧਾਂ': 1, + 'ਧਿ': 1, + 'ਧੀ': 1, + 'ਨਾ': 1, + 'ਨਾਂ': 1, + 'ਨਿ': 1, + 'ਨੀ': 1, + 'ਨੀਂ': 1, + 'ਪਾ': 1, + 'ਪਿ': 1, + 'ਪੀ': 1, + 'ਫਿ': 1, + 'ਬਾ': 1, + 'ਬਿ': 1, + 'ਬੀ': 1, + 'ਭਾ': 1, + 'ਭਿ': 1, + 'ਮਾ': 1, + 'ਮਾਂ': 1, + 'ਮਿ': 1, + 'ਮੀ': 1, + 'ਯਾ': 1, + 'ਰਾ': 1, + 'ਰਾਂ': 1, + 'ਰਿ': 1, + 'ਰੀ': 1, + 'ਲਾ': 1, + 'ਲਾਂ': 1, + 'ਲਿ': 1, + 'ਲਿੰ': 1, + 'ਲੀ': 1, + 'ਵਾ': 1, + 'ਵਾਂ': 1, + 'ਵਿ': 1, + 'ਵਿੱ': 1, + 'ਵੀ': 1, + 'ਸ਼ਾ': 1, + 'ਸ਼ਾਂ': 1, + 'ਸ਼ਿ': 1, + 'ਸ਼ੀ': 1, + 'ਸਾ': 1, + 'ਸਾਂ': 1, + 'ਸਿ': 1, + 'ਸਿੱ': 1, + 'ਸੀ': 1, + 'ਹਾ': 1, + 'ਹਾਂ': 1, + 'ਹਿ': 1, + 'ਹਿੰ': 1, + 'ਹਿੱ': 1, + 'ਹੀ': 1, + 'ਹੀਂ': 1, + 'ਜ਼ਾ': 1, + 'ਜ਼ੀ': 1, + 'ੜਾ': 1, + 'ੜੀਂ': 1, + 'ਫ਼ਾ': 1, + 'કઃ': 1, + 'કા': 1, + 'કાં': 1, + 'કિ': 1, + 'કી': 1, + 'કો': 1, + 'ક્ક': 1, + 'ક્કા': 1, + 'ક્કો': 1, + 'ક્તિ': 1, + 'ક્ર': 1, + 'ક્ષ': 1, + 'ક્ષા': 1, + 'ખા': 1, + 'ખી': 1, + 'ખો': 1, + 'ખ્ત': 1, + 'ગા': 1, + 'ગિ': 1, + 'ગી': 1, + 'ગો': 1, + 'ગ્ન': 1, + 'ગ્ય': 1, + 'ઘા': 1, + 'ઘો': 1, + 'ચા': 1, + 'ચિ': 1, + 'ચી': 1, + 'ચ્ચ': 1, + 'ચ્ચા': 1, + 'ચ્ચે': 1, + 'ચ્છા': 1, + 'ચ્છે': 1, + 'છા': 1, + 'છો': 1, + 'જા': 1, + 'જાં': 1, + 'જિ': 1, + 'જી': 1, + 'જો': 1, + 'જ્ઞા': 1, + 'જ્ય': 1, + 'જ્યો': 1, + 'ટઃ': 1, + 'ટ્ર': 1, + 'ટ્રે': 1, + 'ડા': 1, + 'ડાં': 1, + 'ડી': 1, + 'ઢા': 1, + 'ઢિ': 1, + 'ણા': 1, + 'ણી': 1, + 'તઃ': 1, + 'તા': 1, + 'તાં': 1, + 'તિ': 1, + 'તી': 1, + 'તો': 1, + 'ત્તા': 1, + 'ત્તિ': 1, + 'ત્ત્વ': 1, + 'ત્ત્વા': 1, + 'ત્ન': 1, + 'ત્મ': 1, + 'ત્ય': 1, + 'ત્યા': 1, + 'ત્યાં': 1, + 'ત્યિ': 1, + 'ત્યે': 1, + 'ત્યો': 1, + 'ત્ર': 1, + 'ત્રી': 1, + 'ત્ર્ય': 1, + 'ત્વ': 1, + 'થા': 1, + 'થી': 1, + 'દા': 1, + 'દી': 1, + 'દો': 1, + 'દ્ધ': 1, + 'દ્ધા': 1, + 'દ્ધાં': 1, + 'દ્ધિ': 1, + 'દ્ભ': 1, + 'દ્વા': 1, + 'ધા': 1, + 'ધાં': 1, + 'ધિ': 1, + 'ધી': 1, + 'ધો': 1, + 'ધ્ય': 1, + 'ધ્યા': 1, + 'નઃ': 1, + 'ના': 1, + 'નાં': 1, + 'નિ': 1, + 'ની': 1, + 'નો': 1, + 'ન્ત': 1, + 'ન્તો': 1, + 'ન્મ': 1, + 'ન્મા': 1, + 'ન્મે': 1, + 'ન્ય': 1, + 'ન્યા': 1, + 'ન્યૂં': 1, + 'પા': 1, + 'પિ': 1, + 'પી': 1, + 'પો': 1, + 'પ્ત': 1, + 'પ્ર': 1, + 'પ્રા': 1, + 'ફા': 1, + 'ફો': 1, + 'બા': 1, + 'બાં': 1, + 'બી': 1, + 'બ્ધ': 1, + 'ભા': 1, + 'ભાં': 1, + 'ભિ': 1, + 'ભો': 1, + 'ભૌ': 1, + 'ભ્ય': 1, + 'મઃ': 1, + 'મા': 1, + 'માં': 1, + 'મિ': 1, + 'મી': 1, + 'મો': 1, + 'મ્ય': 1, + 'મ્યાં': 1, + 'યા': 1, + 'યિ': 1, + 'યી': 1, + 'યો': 1, + 'રા': 1, + 'રિ': 1, + 'રી': 1, + 'રો': 1, + 'ર્જ': 1, + 'ર્ણ': 1, + 'ર્ત': 1, + 'ર્તા': 1, + 'ર્થ': 1, + 'ર્થિ': 1, + 'ર્દો': 1, + 'ર્મ': 1, + 'ર્મિ': 1, + 'ર્મે': 1, + 'ર્ય': 1, + 'ર્યા': 1, + 'ર્યો': 1, + 'ર્વ': 1, + 'લા': 1, + 'લાં': 1, + 'લિ': 1, + 'લિં': 1, + 'લી': 1, + 'લો': 1, + 'લ્મ': 1, + 'લ્ય': 1, + 'ળા': 1, + 'ળી': 1, + 'વા': 1, + 'વાં': 1, + 'વિ': 1, + 'વી': 1, + 'વો': 1, + 'વ્ય': 1, + 'વ્યા': 1, + 'વ્યો': 1, + 'શા': 1, + 'શાં': 1, + 'શિ': 1, + 'શી': 1, + 'શો': 1, + 'શ્કે': 1, + 'શ્ચ': 1, + 'શ્ય': 1, + 'શ્ર': 1, + 'શ્વ': 1, + 'ષા': 1, + 'ષી': 1, + 'ષો': 1, + 'ષ્ટિ': 1, + 'ષ્ટ્ર': 1, + 'ષ્ટ્રી': 1, + 'ષ્ટ્રો': 1, + 'ષ્ઠા': 1, + 'ષ્ણુ': 1, + 'ષ્પ': 1, + 'સા': 1, + 'સાં': 1, + 'સિ': 1, + 'સી': 1, + 'સો': 1, + 'સ્કૃ': 1, + 'સ્ટ': 1, + 'સ્તિ': 1, + 'સ્તી': 1, + 'સ્ત્રી': 1, + 'સ્થા': 1, + 'સ્થિ': 1, + 'સ્પ': 1, + 'સ્રી': 1, + 'સ્વ': 1, + 'સ્વા': 1, + 'સ્વી': 1, + 'સ્સા': 1, + 'હા': 1, + 'હિ': 1, + 'હી': 1, + 'હો': 1, + 'கா': 1, + 'கி': 1, + 'கு': 1, + 'கூ': 1, + 'கெ': 1, + 'கே': 1, + 'கை': 1, + 'கொ': 1, + 'கோ': 1, + 'சா': 1, + 'சி': 1, + 'சு': 1, + 'சூ': 1, + 'செ': 1, + 'சே': 1, + 'சை': 1, + 'சொ': 1, + 'டா': 1, + 'டி': 1, + 'டு': 1, + 'டெ': 1, + 'டை': 1, + 'ணா': 1, + 'ணி': 1, + 'ணு': 1, + 'ணை': 1, + 'தா': 1, + 'தி': 1, + 'து': 1, + 'தூ': 1, + 'தெ': 1, + 'தே': 1, + 'தை': 1, + 'தொ': 1, + 'தோ': 1, + 'நா': 1, + 'நி': 1, + 'நு': 1, + 'நெ': 1, + 'நே': 1, + 'நோ': 1, + 'னா': 1, + 'னி': 1, + 'னு': 1, + 'னே': 1, + 'னை': 1, + 'னொ': 1, + 'பா': 1, + 'பி': 1, + 'பு': 1, + 'பெ': 1, + 'பே': 1, + 'பை': 1, + 'பொ': 1, + 'போ': 1, + 'மா': 1, + 'மி': 1, + 'மு': 1, + 'மூ': 1, + 'மெ': 1, + 'மே': 1, + 'மை': 1, + 'மொ': 1, + 'மோ': 1, + 'யா': 1, + 'யி': 1, + 'யு': 1, + 'யே': 1, + 'யை': 1, + 'யோ': 1, + 'ரா': 1, + 'ரி': 1, + 'ரு': 1, + 'ரெ': 1, + 'ரை': 1, + 'றா': 1, + 'றி': 1, + 'று': 1, + 'றே': 1, + 'றை': 1, + 'றொ': 1, + 'றோ': 1, + 'லா': 1, + 'லி': 1, + 'லு': 1, + 'லெ': 1, + 'லே': 1, + 'லை': 1, + 'லோ': 1, + 'ளா': 1, + 'ளி': 1, + 'ளு': 1, + 'ளெ': 1, + 'ளை': 1, + 'ழி': 1, + 'ழு': 1, + 'ழை': 1, + 'வா': 1, + 'வி': 1, + 'வு': 1, + 'வூ': 1, + 'வெ': 1, + 'வே': 1, + 'வை': 1, + 'வொ': 1, + 'வோ': 1, + 'అం': 1, + 'ఆం': 1, + 'ఒం': 1, + 'కం': 1, + 'కాం': 1, + 'కు': 1, + 'కుం': 1, + 'కూ': 1, + 'కృ': 1, + 'కెం': 1, + 'క్క': 1, + 'క్కు': 1, + 'క్త': 1, + 'క్తి': 1, + 'క్తీ': 1, + 'క్తు': 1, + 'క్య': 1, + 'క్ర': 1, + 'క్రాం': 1, + 'క్రి': 1, + 'క్రూ': 1, + 'క్ష': 1, + 'క్షా': 1, + 'క్షి': 1, + 'క్షిం': 1, + 'క్షే': 1, + 'క్షో': 1, + 'క్స్వా': 1, + 'ఖ్య': 1, + 'ఖ్యా': 1, + 'గిం': 1, + 'గు': 1, + 'గూ': 1, + 'గృ': 1, + 'గ్భా': 1, + 'గ్మ': 1, + 'గ్య': 1, + 'గ్ర': 1, + 'గ్రి': 1, + 'చు': 1, + 'చెం': 1, + 'చ్చు': 1, + 'చ్ఛ': 1, + 'చ్ఛా': 1, + 'చ్ఛే': 1, + 'ఛ్ఛా': 1, + 'జ్ఞ': 1, + 'జ్ఞా': 1, + 'జ్య': 1, + 'టం': 1, + 'టిం': 1, + 'టు': 1, + 'టుం': 1, + 'ట్టి': 1, + 'ట్లు': 1, + 'డు': 1, + 'డ్డ': 1, + 'డ్రు': 1, + 'ణిం': 1, + 'తం': 1, + 'తః': 1, + 'తు': 1, + 'తృ': 1, + 'త్కా': 1, + 'త్త': 1, + 'త్తి': 1, + 'త్తు': 1, + 'త్న': 1, + 'త్నిం': 1, + 'త్ప': 1, + 'త్ఫ': 1, + 'త్య': 1, + 'త్యం': 1, + 'త్యా': 1, + 'త్ర': 1, + 'త్రు': 1, + 'త్ర్య': 1, + 'త్వ': 1, + 'త్వా': 1, + 'త్స': 1, + 'దం': 1, + 'దిం': 1, + 'దు': 1, + 'దుం': 1, + 'దృ': 1, + 'ద్ఘో': 1, + 'ద్ది': 1, + 'ద్దు': 1, + 'ద్దే': 1, + 'ద్ధ': 1, + 'ద్ధి': 1, + 'ద్ధిం': 1, + 'ద్య': 1, + 'ద్యా': 1, + 'ద్యో': 1, + 'ద్ర్య': 1, + 'ద్వా': 1, + 'ధిం': 1, + 'ధు': 1, + 'ధ్య': 1, + 'నం': 1, + 'నః': 1, + 'నాం': 1, + 'నిం': 1, + 'ను': 1, + 'నుం': 1, + 'నొం': 1, + 'న్ధ': 1, + 'న్న': 1, + 'న్నా': 1, + 'న్ని': 1, + 'న్ను': 1, + 'న్మ': 1, + 'న్య': 1, + 'న్యా': 1, + 'న్వే': 1, + 'పం': 1, + 'పిం': 1, + 'పు': 1, + 'పూ': 1, + 'పెం': 1, + 'పొం': 1, + 'ప్త': 1, + 'ప్ర': 1, + 'ప్రా': 1, + 'ప్రే': 1, + 'బం': 1, + 'బ్ర': 1, + 'భం': 1, + 'భు': 1, + 'భ్య': 1, + 'భ్యు': 1, + 'భ్ర': 1, + 'భ్రా': 1, + 'మిం': 1, + 'ము': 1, + 'మూ': 1, + 'మ్య': 1, + 'యం': 1, + 'యః': 1, + 'యిం': 1, + 'యు': 1, + 'యుం': 1, + 'రం': 1, + 'రిం': 1, + 'రు': 1, + 'రూ': 1, + 'ర్గ': 1, + 'ర్చి': 1, + 'ర్చు': 1, + 'ర్ణ': 1, + 'ర్ణాం': 1, + 'ర్ణు': 1, + 'ర్త': 1, + 'ర్తి': 1, + 'ర్తిం': 1, + 'ర్తృ': 1, + 'ర్థ': 1, + 'ర్థి': 1, + 'ర్థిం': 1, + 'ర్ద': 1, + 'ర్దో': 1, + 'ర్ధ': 1, + 'ర్ప': 1, + 'ర్బం': 1, + 'ర్భ': 1, + 'ర్భా': 1, + 'ర్య': 1, + 'ర్యా': 1, + 'ర్రా': 1, + 'ర్వ': 1, + 'ర్వా': 1, + 'ర్హ': 1, + 'ర్హాం': 1, + 'ర్హు': 1, + 'ఱు': 1, + 'లం': 1, + 'లిం': 1, + 'లు': 1, + 'ల్క': 1, + 'ల్గొ': 1, + 'ల్ప': 1, + 'ల్ల': 1, + 'ల్లి': 1, + 'విం': 1, + 'వు': 1, + 'వృ': 1, + 'వ్య': 1, + 'వ్యా': 1, + 'వ్వ': 1, + 'శాం': 1, + 'శిం': 1, + 'శు': 1, + 'శ్చ': 1, + 'శ్చి': 1, + 'శ్య': 1, + 'శ్ర': 1, + 'శ్రాం': 1, + 'శ్లే': 1, + 'శ్వా': 1, + 'షిం': 1, + 'షు': 1, + 'ష్ట': 1, + 'ష్టు': 1, + 'ష్ట్ర': 1, + 'ష్ట్రీ': 1, + 'ష్ఠ': 1, + 'ష్ఠా': 1, + 'ష్ఠు': 1, + 'ష్థ': 1, + 'ష్ప': 1, + 'సం': 1, + 'సాం': 1, + 'సు': 1, + 'సూ': 1, + 'స్కృ': 1, + 'స్తా': 1, + 'స్తి': 1, + 'స్త్ర': 1, + 'స్త్రీ': 1, + 'స్థ': 1, + 'స్థా': 1, + 'స్థి': 1, + 'స్థ్య': 1, + 'స్ప': 1, + 'స్య': 1, + 'స్వ': 1, + 'స్వా': 1, + 'స్వీ': 1, + 'స్వే': 1, + 'హిం': 1, + 'ಅಂ': 1, + 'ಇಂ': 1, + 'ಉಂ': 1, + 'ಎಂ': 1, + 'ಒಂ': 1, + 'ಕಾ': 1, + 'ಕಾಂ': 1, + 'ಕಿಂ': 1, + 'ಕೀ': 1, + 'ಕು': 1, + 'ಕುಂ': 1, + 'ಕೂ': 1, + 'ಕೃ': 1, + 'ಕೆಂ': 1, + 'ಕೇ': 1, + 'ಕೈ': 1, + 'ಕೊ': 1, + 'ಕೊಂ': 1, + 'ಖಾಂ': 1, + 'ಗಾ': 1, + 'ಗಾಂ': 1, + 'ಗೀ': 1, + 'ಗು': 1, + 'ಗೂ': 1, + 'ಗೈ': 1, + 'ಗೊ': 1, + 'ಗೊಂ': 1, + 'ಗೋ': 1, + 'ಚಾ': 1, + 'ಚೀ': 1, + 'ಚು': 1, + 'ಛಾ': 1, + 'ಜಾ': 1, + 'ಜೀ': 1, + 'ಟು': 1, + 'ಟುಂ': 1, + 'ಟೇ': 1, + 'ಠಾ': 1, + 'ಡಾ': 1, + 'ಡು': 1, + 'ಡೂ': 1, + 'ಢೀ': 1, + 'ಣಂ': 1, + 'ಣಾ': 1, + 'ಣು': 1, + 'ಣೈ': 1, + 'ತಂ': 1, + 'ತಃ': 1, + 'ತಾ': 1, + 'ತೀ': 1, + 'ತು': 1, + 'ತೃ': 1, + 'ತೊ': 1, + 'ತೋ': 1, + 'ಥಾ': 1, + 'ದಂ': 1, + 'ದಾ': 1, + 'ದಿಂ': 1, + 'ದು': 1, + 'ದೂ': 1, + 'ದೃ': 1, + 'ದೇ': 1, + 'ದೊ': 1, + 'ದೋ': 1, + 'ಧಾ': 1, + 'ಧೀ': 1, + 'ಧೂ': 1, + 'ನಃ': 1, + 'ನಾ': 1, + 'ನಾಂ': 1, + 'ನಿಂ': 1, + 'ನು': 1, + 'ನೂ': 1, + 'ನೇ': 1, + 'ನೈ': 1, + 'ನೊ': 1, + 'ಪಂ': 1, + 'ಪಾ': 1, + 'ಪು': 1, + 'ಪೂ': 1, + 'ಪೇ': 1, + 'ಬಂ': 1, + 'ಬಾ': 1, + 'ಬು': 1, + 'ಬೇ': 1, + 'ಬೋ': 1, + 'ಭಾ': 1, + 'ಭೀ': 1, + 'ಭು': 1, + 'ಭೇ': 1, + 'ಭೋ': 1, + 'ಮಾ': 1, + 'ಮೀ': 1, + 'ಮು': 1, + 'ಮೂ': 1, + 'ಮೇ': 1, + 'ಮೈ': 1, + 'ಯಂ': 1, + 'ಯಾ': 1, + 'ಯಿಂ': 1, + 'ಯು': 1, + 'ಯುಂ': 1, + 'ಯೂ': 1, + 'ಯೆಂ': 1, + 'ಯೇ': 1, + 'ಯೊ': 1, + 'ಯೋ': 1, + 'ರಂ': 1, + 'ರಾ': 1, + 'ರಾಂ': 1, + 'ರಿಂ': 1, + 'ರೀ': 1, + 'ರು': 1, + 'ರೂ': 1, + 'ರೆಂ': 1, + 'ರೊ': 1, + 'ರೋ': 1, + 'ಲಂ': 1, + 'ಲಾ': 1, + 'ಲೀ': 1, + 'ಲು': 1, + 'ಲೂ': 1, + 'ಲೆಂ': 1, + 'ಲೇ': 1, + 'ಲೋ': 1, + 'ಳಾ': 1, + 'ಳಿಂ': 1, + 'ಳು': 1, + 'ಳೂ': 1, + 'ವಾ': 1, + 'ವೀ': 1, + 'ವು': 1, + 'ವುಂ': 1, + 'ವೂ': 1, + 'ವೃ': 1, + 'ವೆಂ': 1, + 'ವೇ': 1, + 'ವೈ': 1, + 'ವೋ': 1, + 'ಶಾ': 1, + 'ಶಾಂ': 1, + 'ಶೀ': 1, + 'ಶೇ': 1, + 'ಷಾ': 1, + 'ಷೇ': 1, + 'ಸಂ': 1, + 'ಸಾ': 1, + 'ಸಾಂ': 1, + 'ಸು': 1, + 'ಸೂ': 1, + 'ಸೇ': 1, + 'ಹಾ': 1, + 'ಹು': 1, + 'ಹೇ': 1, + 'ಹೊ': 1, + 'ಹೊಂ': 1, + 'ಹೋ': 1, + 'അം': 1, + 'കാ': 1, + 'കി': 1, + 'കെ': 1, + 'കേ': 1, + 'കൈ': 1, + 'കൊ': 1, + 'കോ': 1, + 'ക്ക': 1, + 'ക്കാ': 1, + 'ക്കി': 1, + 'ക്കു': 1, + 'ക്കും': 1, + 'ക്കെ': 1, + 'ക്കേ': 1, + 'ക്കൊ': 1, + 'ക്കോ': 1, + 'ക്ക്‌': 1, + 'ക്തി': 1, + 'ക്യ': 1, + 'ക്യം': 1, + 'ക്ര': 1, + 'ക്രൂ': 1, + 'ക്ഷ': 1, + 'ക്ഷാ': 1, + 'ക്ഷി': 1, + 'ക്ഷേ': 1, + 'ഖ്യാ': 1, + 'ഗം': 1, + 'ഗി': 1, + 'ഗീ': 1, + 'ഗ്ഗം': 1, + 'ഗ്ദാ': 1, + 'ഗ്യ': 1, + 'ഗ്യം': 1, + 'ഗ്ര': 1, + 'ഘി': 1, + 'ങ്കി': 1, + 'ങ്കെ': 1, + 'ങ്കേ': 1, + 'ങ്ങ': 1, + 'ങ്ങി': 1, + 'ചാ': 1, + 'ചി': 1, + 'ചെ': 1, + 'ചേ': 1, + 'ചോ': 1, + 'ച്ച': 1, + 'ച്ചി': 1, + 'ച്ചു': 1, + 'ച്ചും': 1, + 'ച്ചെ': 1, + 'ച്ചേ': 1, + 'ജാ': 1, + 'ജി': 1, + 'ജീ': 1, + 'ജ്യ': 1, + 'ജ്യാ': 1, + 'ഞ്ച': 1, + 'ഞ്ഞ': 1, + 'ഞ്ഞി': 1, + 'ഞ്ഞെ': 1, + 'ടാ': 1, + 'ടി': 1, + 'ടും': 1, + 'ടെ': 1, + 'ടേ': 1, + 'ട്ട': 1, + 'ട്ടാ': 1, + 'ട്ടി': 1, + 'ട്ടു': 1, + 'ട്ടോ': 1, + 'ഠി': 1, + 'ണം': 1, + 'ണാ': 1, + 'ണി': 1, + 'ണെ': 1, + 'ണ്ട': 1, + 'ണ്ടാ': 1, + 'ണ്ടി': 1, + 'ണ്ടു': 1, + 'ണ്ടും': 1, + 'ണ്ടെ': 1, + 'ണ്ട്‌': 1, + 'ണ്ഡ': 1, + 'ണ്ണ': 1, + 'തം': 1, + 'താ': 1, + 'തി': 1, + 'തീ': 1, + 'തും': 1, + 'തെ': 1, + 'തേ': 1, + 'തൊ': 1, + 'തോ': 1, + 'ത്ത': 1, + 'ത്താ': 1, + 'ത്തി': 1, + 'ത്തു': 1, + 'ത്തും': 1, + 'ത്തെ': 1, + 'ത്തേ': 1, + 'ത്തോ': 1, + 'ത്ത്‌': 1, + 'ത്ന': 1, + 'ത്നം': 1, + 'ത്നി': 1, + 'ത്യ': 1, + 'ത്യാ': 1, + 'ത്യേ': 1, + 'ത്ര': 1, + 'ത്വ': 1, + 'ത്വം': 1, + 'ദം': 1, + 'ദാ': 1, + 'ദി': 1, + 'ദേ': 1, + 'ദ്ദ': 1, + 'ദ്ധ': 1, + 'ദ്ധി': 1, + 'ദ്യ': 1, + 'ദ്യാ': 1, + 'ദ്ര': 1, + 'ധാ': 1, + 'ധി': 1, + 'ധ്യ': 1, + 'നം': 1, + 'നാ': 1, + 'നി': 1, + 'നീ': 1, + 'നും': 1, + 'നെ': 1, + 'നേ': 1, + 'ന്ത': 1, + 'ന്തം': 1, + 'ന്തെ': 1, + 'ന്ത്ര': 1, + 'ന്ത്ര്യ': 1, + 'ന്ത്ര്യം': 1, + 'ന്ഥ': 1, + 'ന്ദ': 1, + 'ന്ധ': 1, + 'ന്ധി': 1, + 'ന്ന': 1, + 'ന്നാ': 1, + 'ന്നി': 1, + 'ന്നീ': 1, + 'ന്നു': 1, + 'ന്നും': 1, + 'ന്നെ': 1, + 'ന്നേ': 1, + 'ന്നോ': 1, + 'ന്ന്': 1, + 'ന്മ': 1, + 'ന്മാ': 1, + 'ന്മു': 1, + 'ന്മൂ': 1, + 'ന്യ': 1, + 'ന്യം': 1, + 'ന്യാ': 1, + 'ന്യേ': 1, + 'ന്യോ': 1, + 'ന്റെ': 1, + 'ന്‍കൂ': 1, + 'ന്‍പി': 1, + 'പാ': 1, + 'പി': 1, + 'പീ': 1, + 'പെ': 1, + 'പൈ': 1, + 'പൊ': 1, + 'പോ': 1, + 'പൌ': 1, + 'പ്പ': 1, + 'പ്പി': 1, + 'പ്പു': 1, + 'പ്പെ': 1, + 'പ്പോ': 1, + 'പ്പ്‌': 1, + 'പ്ര': 1, + 'പ്രാ': 1, + 'ബം': 1, + 'ബാ': 1, + 'ബോ': 1, + 'ബ്ലി': 1, + 'ഭാ': 1, + 'ഭി': 1, + 'ഭേ': 1, + 'ഭൌ': 1, + 'ഭ്യാ': 1, + 'ഭ്രാ': 1, + 'മം': 1, + 'മാ': 1, + 'മി': 1, + 'മെ': 1, + 'മേ': 1, + 'മൊ': 1, + 'മോ': 1, + 'മൌ': 1, + 'മ്പ': 1, + 'മ്പാ': 1, + 'മ്പ്ര': 1, + 'മ്മ': 1, + 'മ്മി': 1, + 'യം': 1, + 'യാ': 1, + 'യി': 1, + 'യും': 1, + 'യെ': 1, + 'യേ': 1, + 'യോ': 1, + 'യ്ക്കു': 1, + 'യ്തി': 1, + 'യ്തു': 1, + 'യ്മ': 1, + 'യ്യാ': 1, + 'യ്യി': 1, + 'രം': 1, + 'രാ': 1, + 'രി': 1, + 'രീ': 1, + 'രും': 1, + 'രെ': 1, + 'രേ': 1, + 'രോ': 1, + 'ര്യ': 1, + 'ര്യം': 1, + 'ര്‍ക്കു': 1, + 'ര്‍ക്കും': 1, + 'ര്‍ക്ക്‌': 1, + 'ര്‍ഗ്ഗം': 1, + 'ര്‍ച്ച': 1, + 'ര്‍ണ്ണ': 1, + 'ര്‍ത്ത': 1, + 'ര്‍ത്തി': 1, + 'ര്‍ത്തു': 1, + 'ര്‍ത്തേ': 1, + 'ര്‍ദേ': 1, + 'ര്‍ദ്ദ': 1, + 'ര്‍പ്പെ': 1, + 'ര്‍ബ': 1, + 'ര്‍ഭ': 1, + 'ര്‍മ്മി': 1, + 'ര്‍രാ': 1, + 'ര്‍വ്വ': 1, + 'ര്‍ഹ': 1, + 'ര്‍ഹി': 1, + 'റം': 1, + 'റി': 1, + 'റെ': 1, + 'റ്റ': 1, + 'റ്റം': 1, + 'റ്റു': 1, + 'റ്റേ': 1, + 'ലം': 1, + 'ലാ': 1, + 'ലി': 1, + 'ലും': 1, + 'ലെ': 1, + 'ലേ': 1, + 'ലോ': 1, + 'ല്യ': 1, + 'ല്യാ': 1, + 'ല്ല': 1, + 'ല്ലാ': 1, + 'ല്ലാം': 1, + 'ല്ലെ': 1, + 'ല്‍കി': 1, + 'ല്‍കു': 1, + 'ല്‍കേ': 1, + 'ല്‍ത': 1, + 'ല്‍നി': 1, + 'ല്‍പ്പ': 1, + 'ളം': 1, + 'ളാ': 1, + 'ളി': 1, + 'ളും': 1, + 'ളെ': 1, + 'ളേ': 1, + 'ളോ': 1, + 'ള്ള': 1, + 'ള്ളാ': 1, + 'ള്ളി': 1, + 'ള്ളൂ': 1, + 'ള്‍കൊ': 1, + 'ള്‍ക്ക': 1, + 'ള്‍ക്കി': 1, + 'ള്‍ക്കു': 1, + 'ള്‍ക്കും': 1, + 'ള്‍ക്കെ': 1, + 'ള്‍ക്ക്‌': 1, + 'ഴി': 1, + 'വാ': 1, + 'വി': 1, + 'വും': 1, + 'വെ': 1, + 'വേ': 1, + 'വൈ': 1, + 'വോ': 1, + 'വ്യ': 1, + 'വ്യം': 1, + 'വ്യാ': 1, + 'വ്വ': 1, + 'ശാ': 1, + 'ശി': 1, + 'ശീ': 1, + 'ശേ': 1, + 'ശ്യ': 1, + 'ശ്ര': 1, + 'ശ്വാ': 1, + 'ഷം': 1, + 'ഷി': 1, + 'ഷ്ട': 1, + 'ഷ്ട്യാ': 1, + 'ഷ്ട്ര': 1, + 'ഷ്ട്രീ': 1, + 'ഷ്ണു': 1, + 'ഷ്യ': 1, + 'ഷ്യാ': 1, + 'സം': 1, + 'സാ': 1, + 'സാം': 1, + 'സി': 1, + 'സൌ': 1, + 'സ്കാ': 1, + 'സ്തു': 1, + 'സ്ത്രം': 1, + 'സ്ത്രീ': 1, + 'സ്ഥ': 1, + 'സ്ഥാ': 1, + 'സ്ഥി': 1, + 'സ്യ': 1, + 'സ്റ്റ്‌': 1, + 'സ്വ': 1, + 'സ്വാ': 1, + 'സ്വീ': 1, + 'സ്സ': 1, + 'സ്സാ': 1, + 'സ്സി': 1, + 'സ്സും': 1, + 'സ്സോ': 1, + 'ഹം': 1, + 'ഹാ': 1, + 'ഹി': 1, + 'ഹിം': 1, + 'කා': 1, + 'කැ': 1, + 'කෘ': 1, + 'කෙ': 1, + 'කො': 1, + 'කෝ': 1, + 'කෲ': 1, + 'ගා': 1, + 'ගැ': 1, + 'ගෙ': 1, + 'ගේ': 1, + 'චා': 1, + 'චැ': 1, + 'චෝ': 1, + 'ජා': 1, + 'ඥා': 1, + 'ටා': 1, + 'ටේ': 1, + 'ඨා': 1, + 'ඩං': 1, + 'ඩා': 1, + 'තා': 1, + 'තැ': 1, + 'තෘ': 1, + 'තෙ': 1, + 'තේ': 1, + 'තො': 1, + 'තෝ': 1, + 'ථා': 1, + 'දා': 1, + 'දැ': 1, + 'දිං': 1, + 'දෙ': 1, + 'දේ': 1, + 'දෝ': 1, + 'ධා': 1, + 'නා': 1, + 'නැ': 1, + 'නෑ': 1, + 'නෙ': 1, + 'නේ': 1, + 'නො': 1, + 'ඳා': 1, + 'පා': 1, + 'පැ': 1, + 'පෑ': 1, + 'පෙ': 1, + 'පේ': 1, + 'පො': 1, + 'පෞ': 1, + 'බා': 1, + 'බැ': 1, + 'බෙ': 1, + 'බේ': 1, + 'බෝ': 1, + 'භා': 1, + 'භෞ': 1, + 'මා': 1, + 'මැ': 1, + 'මෙ': 1, + 'මේ': 1, + 'යා': 1, + 'යෙ': 1, + 'යේ': 1, + 'යො': 1, + 'යෝ': 1, + 'රා': 1, + 'රැ': 1, + 'රෙ': 1, + 'රේ': 1, + 'රෝ': 1, + 'ලං': 1, + 'ලා': 1, + 'ලැ': 1, + 'ලෙ': 1, + 'ලේ': 1, + 'ලෝ': 1, + 'වං': 1, + 'වා': 1, + 'වැ': 1, + 'වෘ': 1, + 'වෙ': 1, + 'වේ': 1, + 'වෝ': 1, + 'ශේ': 1, + 'ෂා': 1, + 'සං': 1, + 'සා': 1, + 'සැ': 1, + 'සෑ': 1, + 'සෙ': 1, + 'සේ': 1, + 'සෞ': 1, + 'හා': 1, + 'හැ': 1, + 'හිං': 1, + 'හෘ': 1, + 'හෙ': 1, + 'හො': 1, + 'හෝ': 1, + 'ළැ': 1, + 'กำ': 1, + 'คำ': 1, + 'จำ': 1, + 'ดำ': 1, + 'ต่ำ': 1, + 'ทำ': 1, + 'ธำ': 1, + 'นำ': 1, + 'บำ': 1, + 'พำ': 1, + 'ย่ำ': 1, + 'รำ': 1, + 'ลำ': 1, + 'สำ': 1, + 'อำ': 1, + 'ກຳ': 1, + 'ຄຳ': 1, + 'ຈຳ': 1, + 'ດຳ': 1, + 'ຕຳ': 1, + 'ທຳ': 1, + 'ນຳ': 1, + 'ລຳ': 1, + 'ສຳ': 1, + 'ອຳ': 1, + 'ကေ': 1, + 'ကေု': 1, + 'က္က': 1, + 'က္ခ': 1, + 'က္ဍ': 1, + 'က္ဍို': 1, + 'က်ျ': 1, + 'ကျ': 1, + 'ကျို': 1, + 'ကျူ': 1, + 'ကျေ': 1, + 'ကျွ': 1, + 'ကြ': 1, + 'ကြို': 1, + 'ကြီ': 1, + 'ကြု': 1, + 'ကြေ': 1, + 'ကြံ': 1, + 'ကြွေ': 1, + 'ကၠေ': 1, + 'ကၠေံ': 1, + 'ခေ': 1, + 'ချ': 1, + 'ချိ': 1, + 'ချို': 1, + 'ချို့': 1, + 'ချီ': 1, + 'ချု': 1, + 'ချေ': 1, + 'ချဳ': 1, + 'ခြ': 1, + 'ခြုံ': 1, + 'ခြေ': 1, + 'ဂ္ဂ': 1, + 'ဂ္ဂို': 1, + 'ဂျို': 1, + 'ဂၠေ': 1, + 'င်္ဂ': 1, + 'ငြ': 1, + 'ငြိ': 1, + 'ငွေ': 1, + 'ငႄ': 1, + 'စေ': 1, + 'စ္စ': 1, + 'စှ်ေ': 1, + 'စၞေ': 1, + 'စၠေ': 1, + 'ဆေ': 1, + 'ဇြ': 1, + 'ဇြေ': 1, + 'ဇြဳ': 1, + 'ဇၞေ': 1, + 'ညေ': 1, + 'ဋ္ဌ': 1, + 'တေ': 1, + 'တေံ': 1, + 'တ္တ': 1, + 'တ္ၚဲ': 1, + 'တြ': 1, + 'တြုံ': 1, + 'တွေ': 1, + 'တှ်ေ': 1, + 'တၟေ': 1, + 'ထေ': 1, + 'ထ္ၜ': 1, + 'ထွေ': 1, + 'ဒေ': 1, + 'ဒေံ': 1, + 'ဒ္ဂေ': 1, + 'ဒြ': 1, + 'နေ': 1, + 'နေ့': 1, + 'န္ဒ': 1, + 'န္နိ': 1, + 'နှေ': 1, + 'ပေ': 1, + 'ပေဲ': 1, + 'ပ္ဍဲ': 1, + 'ပ္တံ': 1, + 'ပ္ပ': 1, + 'ပ္ပံ': 1, + 'ပျ': 1, + 'ပျေ': 1, + 'ပျဲ': 1, + 'ပြ': 1, + 'ပြီ': 1, + 'ပြု': 1, + 'ပြေ': 1, + 'ပြဳ': 1, + 'ပြံ': 1, + 'ဖေ': 1, + 'ဖျ': 1, + 'ဖျု': 1, + 'ဖျေံ': 1, + 'ဖြ': 1, + 'ဖြို': 1, + 'ဗ္တေ': 1, + 'ဗ္ၜေ': 1, + 'ဗျဵု': 1, + 'ဗြေ': 1, + 'ဗြဴ': 1, + 'ဗၠေ': 1, + 'ဘေ': 1, + 'မေ': 1, + 'မ္ဂ': 1, + 'မ္တီ': 1, + 'မ္ဘ': 1, + 'မ္မ': 1, + 'မ္ၚဵု': 1, + 'မျ': 1, + 'မျို': 1, + 'မျီု': 1, + 'မျှ': 1, + 'မြ': 1, + 'မြေ': 1, + 'မြှ': 1, + 'မွေ': 1, + 'မွေ့': 1, + 'မှေ': 1, + 'မၠေ': 1, + 'မႄ': 1, + 'ယေ': 1, + 'ယျ': 1, + 'ယျဵု': 1, + 'ယြဴ': 1, + 'ရေ': 1, + 'ရေံ': 1, + 'ရွေ': 1, + 'ရွှေ့': 1, + 'ရှေ': 1, + 'ရှေ့': 1, + 'ရှ်ေ': 1, + 'လေ': 1, + 'လ္ပ': 1, + 'လ္ၚ': 1, + 'လျ': 1, + 'လျို': 1, + 'လျေ': 1, + 'လျှ': 1, + 'လျှို့': 1, + 'လျှေ': 1, + 'လၟေ': 1, + 'လႄ': 1, + 'ဝေ': 1, + 'သေ': 1, + 'သ္က': 1, + 'သ္ကံ': 1, + 'သ္ဂု': 1, + 'သ္ဂေ': 1, + 'သ္ဇို': 1, + 'သ္ဒ': 1, + 'သ္ပ': 1, + 'သ္ၚိ': 1, + 'သြ': 1, + 'သြို': 1, + 'သှ်ေ': 1, + 'သၞေ': 1, + 'ဟေ': 1, + 'အေ': 1, + 'ၜေ': 1, + 'ၵေ': 1, + 'ၵႄ': 1, + 'ၶေ': 1, + 'ၼေ': 1, + 'ၼႄ': 1, + 'ၽႄ': 1, + 'ႁေ': 1, + 'ႁႄ': 1, + 'កា': 1, + 'កើ': 1, + 'កេ': 1, + 'កោ': 1, + 'ក្ខ': 1, + 'ក្ដិ': 1, + 'ក្ដី': 1, + 'ក្ដៅ': 1, + 'ក្នុ': 1, + 'ក្រ': 1, + 'ក្រុ': 1, + 'ក្រោ': 1, + 'ក្ស': 1, + 'ក្សា': 1, + 'ខា': 1, + 'ខែ': 1, + 'ខ្ជា': 1, + 'ខ្ព': 1, + 'ខ្លា': 1, + 'ខ្លួ': 1, + 'គា': 1, + 'គាំ': 1, + 'គេ': 1, + 'គោ': 1, + 'គ្គ': 1, + 'គ្នា': 1, + 'គ្មា': 1, + 'គ្រ': 1, + 'គ្រា': 1, + 'គ្រឹះ': 1, + 'គ្រួ': 1, + 'ឃា': 1, + 'ឃើ': 1, + 'ឃោ': 1, + 'ឃៅ': 1, + 'ឃ្ន': 1, + 'ងា': 1, + 'ង្ក': 1, + 'ង្កើ': 1, + 'ង្ខិ': 1, + 'ង្ខំ': 1, + 'ង្គ': 1, + 'ង្រី': 1, + 'ង្រឹ': 1, + 'ង្រៀ': 1, + 'ង្ហា': 1, + 'ង្អ': 1, + 'ចា': 1, + 'ចាំ': 1, + 'ចេ': 1, + 'ចែ': 1, + 'ចោ': 1, + 'ច្ច': 1, + 'ច្ចេ': 1, + 'ច្បា': 1, + 'ឆាំ': 1, + 'ឆ្នាំ': 1, + 'ឆ្នោ': 1, + 'ឆ្ល': 1, + 'ឆ្លើ': 1, + 'ជា': 1, + 'ជៀ': 1, + 'ជ្ជ': 1, + 'ជ្ជា': 1, + 'ជ្ញា': 1, + 'ជ្យ': 1, + 'ជ្រ': 1, + 'ជ្រើ': 1, + 'ជ្រៀ': 1, + 'ជ្រែ': 1, + 'ឈា': 1, + 'ឈ្មោះ': 1, + 'ញុះ': 1, + 'ញ្ចូ': 1, + 'ញ្ជា': 1, + 'ញ្ញ': 1, + 'ញ្ញា': 1, + 'ញ្ញៈ': 1, + 'ដា': 1, + 'ដើ': 1, + 'ដែ': 1, + 'ដៃ': 1, + 'ដោ': 1, + 'ដៅ': 1, + 'ដ្ឋ': 1, + 'ដ្ឋា': 1, + 'ឋា': 1, + 'ឌ្ឍ': 1, + 'ណា': 1, + 'ណើ': 1, + 'ណែ': 1, + 'ណោ': 1, + 'ណោះ': 1, + 'ណៈ': 1, + 'ណ្ដា': 1, + 'ណ្ដុះ': 1, + 'ណ្ឌ': 1, + 'ណ្ណោះ': 1, + 'តា': 1, + 'តេ': 1, + 'តែ': 1, + 'ត្ដ': 1, + 'ត្ដិ': 1, + 'ត្ថ': 1, + 'ត្ថិ': 1, + 'ត្យ': 1, + 'ត្រ': 1, + 'ត្រា': 1, + 'ត្រឹ': 1, + 'ត្រូ': 1, + 'ត្ស': 1, + 'ថា': 1, + 'ថែ': 1, + 'ថ្ងៃ': 1, + 'ថ្នា': 1, + 'ថ្នូ': 1, + 'ថ្មី': 1, + 'ថ្លៃ': 1, + 'ទា': 1, + 'ទាំ': 1, + 'ទៀ': 1, + 'ទេ': 1, + 'ទៃ': 1, + 'ទោ': 1, + 'ទោះ': 1, + 'ទៅ': 1, + 'ទ្ធ': 1, + 'ទ្ធិ': 1, + 'ទ្យា': 1, + 'ទ្រ': 1, + 'ធា': 1, + 'ធ្ង': 1, + 'ធ្នា': 1, + 'ធ្នូ': 1, + 'ធ្យា': 1, + 'ធ្យោ': 1, + 'ធ្វើ': 1, + 'នា': 1, + 'នាំ': 1, + 'នឿ': 1, + 'នេះ': 1, + 'នៃ': 1, + 'នោះ': 1, + 'នៅ': 1, + 'នៈ': 1, + 'ន្ដ': 1, + 'ន្ដិ': 1, + 'ន្ថែ': 1, + 'ន្ថោ': 1, + 'ន្ទា': 1, + 'ន្ទៈ': 1, + 'ន្ធ': 1, + 'ន្និ': 1, + 'ន្ម': 1, + 'ន្យា': 1, + 'ន្លែ': 1, + 'បា': 1, + 'បើ': 1, + 'បៀ': 1, + 'បែ': 1, + 'បោ': 1, + 'បោះ': 1, + 'បះ': 1, + 'ប៉ះ': 1, + 'ប្ដី': 1, + 'ប្ដូ': 1, + 'ប្ដេ': 1, + 'ប្ប': 1, + 'ប្បី': 1, + 'ប្រ': 1, + 'ប្រា': 1, + 'ប្រឹ': 1, + 'ប្រុ': 1, + 'ប្រើ': 1, + 'ប្រែ': 1, + 'ប្អូ': 1, + 'ផ្ដ': 1, + 'ផ្ដា': 1, + 'ផ្ទា': 1, + 'ផ្ទុ': 1, + 'ផ្ទេ': 1, + 'ផ្នែ': 1, + 'ផ្លា': 1, + 'ផ្លិ': 1, + 'ផ្ស': 1, + 'ផ្សា': 1, + 'ផ្សេ': 1, + 'ផ្សៃ': 1, + 'ពា': 1, + 'ពើ': 1, + 'ពេ': 1, + 'ពោះ': 1, + 'ព្យ': 1, + 'ព្យា': 1, + 'ព្រ': 1, + 'ព្រឹ': 1, + 'ព្រៀ': 1, + 'ព្រៃ': 1, + 'ព្រំ': 1, + 'ព្វ': 1, + 'ភា': 1, + 'ភេ': 1, + 'មា': 1, + 'មើ': 1, + 'មេ': 1, + 'មែ': 1, + 'ម៉ា': 1, + 'ម៉ោ': 1, + 'ម្ងា': 1, + 'ម្ចា': 1, + 'ម្ដា': 1, + 'ម្ដែ': 1, + 'ម្នា': 1, + 'ម្នេ': 1, + 'ម្ប': 1, + 'ម្បី': 1, + 'ម្បុ': 1, + 'ម្ភា': 1, + 'ម្ម': 1, + 'ម្រ': 1, + 'ម្រា': 1, + 'ម្រិ': 1, + 'ម្រូ': 1, + 'ម្រើ': 1, + 'ម្រេ': 1, + 'ម្លៀ': 1, + 'ម្លៃ': 1, + 'យា': 1, + 'យោ': 1, + 'យៈ': 1, + 'យ៉ា': 1, + 'យ្យ': 1, + 'រា': 1, + 'រាំ': 1, + 'រើ': 1, + 'រឿ': 1, + 'រៀ': 1, + 'រះ': 1, + 'រៈ': 1, + 'រ្ដិ៍': 1, + 'លា': 1, + 'លុះ': 1, + 'លើ': 1, + 'លេ': 1, + 'លោ': 1, + 'លៈ': 1, + 'ល្បៈ': 1, + 'ល្មើ': 1, + 'វា': 1, + 'វេ': 1, + 'វៈ': 1, + 'សា': 1, + 'សើ': 1, + 'សេ': 1, + 'សៈ': 1, + 'ស្គា': 1, + 'ស្ដី': 1, + 'ស្ដ្រី': 1, + 'ស្ត្រ': 1, + 'ស្ថា': 1, + 'ស្ថិ': 1, + 'ស្ទួ': 1, + 'ស្នា': 1, + 'ស្មា': 1, + 'ស្មើ': 1, + 'ស្រ': 1, + 'ស្រា': 1, + 'ស្រី': 1, + 'ស្រ័': 1, + 'ស្លា': 1, + 'ស្វែ': 1, + 'ស្វ័': 1, + 'ស្ស': 1, + 'ហា': 1, + 'ហើ': 1, + 'ហេ': 1, + 'ហែ': 1, + 'ហោ': 1, + 'ហ្ម': 1, + 'ឡា': 1, + 'ឡើ': 1, + 'អា': 1, + 'អើ': 1, + 'អៀ': 1, + 'អ្ន': 1, + 'អ្វី': 1, + 'ឱ្យ': 1, + 'ᨠ᩠ᨠ': 1, + 'ᨠ᩠ᨦᩢᩢ': 1, + 'ᨠ᩠ᨦᩨ᩵': 1, + 'ᨠ᩠ᨭᩫ': 1, + 'ᨠ᩠ᨯᩢ': 1, + 'ᨠ᩠ᨶᩢ': 1, + 'ᨠ᩠ᨶᩥ': 1, + 'ᨠ᩠ᩅ': 1, + 'ᨠ᩠ᩅᩢ': 1, + 'ᨠ᩠ᩅᩫ': 1, + 'ᨠ᩠ᩋ᩵': 1, + 'ᨠᩢ᩠ᨷ': 1, + 'ᨠᩮ᩠': 1, + 'ᨠᩮᩢ᩵': 1, + 'ᨠᩮ᩵': 1, + 'ᨠᩯ᩠': 1, + 'ᨠᩯ᩵': 1, + 'ᨡ᩠ᨦᩢ': 1, + 'ᨡ᩠ᨯᩢ': 1, + 'ᨡ᩠ᨶ': 1, + 'ᨡ᩠ᨶᩢ': 1, + 'ᨡ᩠ᨶᩨ᩶': 1, + 'ᨡ᩠ᩅ': 1, + 'ᨡ᩠ᩋ': 1, + 'ᨡᩮ᩠': 1, + 'ᨡᩮᩢ': 1, + 'ᨡᩮᩢᩢ': 1, + 'ᨡᩯ᩠': 1, + 'ᨡᩱᩢ': 1, + 'ᨣᩕ': 1, + 'ᨣᩕ᩠': 1, + 'ᨣᩕᩮ᩠': 1, + 'ᨣ᩠ᨣ': 1, + 'ᨣ᩠ᨯᩧ': 1, + 'ᨣ᩠ᨶᩢ': 1, + 'ᨣ᩠ᨾᩫ': 1, + 'ᨣ᩠ᩅ': 1, + 'ᨣ᩠ᩋᩢ': 1, + 'ᨣᩢ᩠ᨷ': 1, + 'ᨣᩭ': 1, + 'ᨣᩮᩢᩢ': 1, + 'ᨣᩯ᩠': 1, + 'ᨤ᩠ᨯᩥ': 1, + 'ᨤ᩠ᨶᩨ': 1, + 'ᨤ᩠ᨶᩫ': 1, + 'ᨤ᩠ᩅᩫ': 1, + 'ᨤ᩠ᩋ': 1, + 'ᨤᩢ᩠ᨬ': 1, + 'ᨤᩮᩢ': 1, + 'ᨤᩱ᩵': 1, + 'ᨧ᩠ᨦᩥ᩵': 1, + 'ᨧ᩠ᨯᩢ': 1, + 'ᨧᩮ': 1, + 'ᨧᩮ᩠': 1, + 'ᨧᩮᩢᩢ': 1, + 'ᨧᩱ': 1, + 'ᨧᩱ᩠': 1, + 'ᨨ᩠ᨶᩢ': 1, + 'ᨩ᩠ᨶᩢᩢ': 1, + 'ᨩ᩠ᨶ᩼': 1, + 'ᨩᩭ᩵': 1, + 'ᨩᩮ᩠': 1, + 'ᨩᩱᩢ': 1, + 'ᨩ᩵ᩭ': 1, + 'ᨪ᩠ᨦᩨ᩵': 1, + 'ᨪᩮᩢ': 1, + 'ᨬ᩠ᨬ': 1, + 'ᨯ᩠ᨦᩢ᩵': 1, + 'ᨯ᩠ᨰ': 1, + 'ᨯ᩠ᨶᩥ': 1, + 'ᨯ᩠ᩅ᩠ᨿᩢ': 1, + 'ᨯᩢ᩠ᩅ᩠ᨿ': 1, + 'ᨯᩪᩯ': 1, + 'ᨯᩯ᩠': 1, + 'ᨯᩰ᩠': 1, + 'ᨯᩱ': 1, + 'ᨯᩱᩢ': 1, + 'ᨯᩴ᩠ᨿ': 1, + 'ᨱ᩠ᨱ': 1, + 'ᨲᩕ': 1, + 'ᨲᩕᩦ': 1, + 'ᨲᩕ᩼': 1, + 'ᨲ᩠ᨠᩫ': 1, + 'ᨲ᩠ᨦᩢᩢ': 1, + 'ᨲ᩠ᨯᩢ': 1, + 'ᨲ᩠ᨲᩥ': 1, + 'ᨲ᩠ᨳᩩ': 1, + 'ᨲ᩠ᨶᩫ': 1, + 'ᨲ᩠ᨶᩫᩢ': 1, + 'ᨲ᩠ᩅᩫ': 1, + 'ᨲ᩠ᩋᩢ': 1, + 'ᨲᩮ᩠': 1, + 'ᨲᩯ᩠': 1, + 'ᨲᩯ᩵': 1, + 'ᨲᩱ': 1, + 'ᨲᩱ᩠': 1, + 'ᨲᩱᩢ': 1, + 'ᨳ᩠ᨦᩨ': 1, + 'ᨳ᩠ᩅᩢ': 1, + 'ᨳᩫ᩠ᨷ': 1, + 'ᨳᩮ᩠': 1, + 'ᨳᩮᩢᩢ': 1, + 'ᨳᩯ᩠': 1, + 'ᨴ᩠ᨦᩢ': 1, + 'ᨴ᩠ᨦᩨ': 1, + 'ᨴ᩠ᨵᩥ': 1, + 'ᨴ᩠ᨿ': 1, + 'ᨴ᩠ᨿ᩵': 1, + 'ᨴ᩠ᩅᩢ': 1, + 'ᨴ᩠ᩅᩫ᩵': 1, + 'ᨴᩮ᩠': 1, + 'ᨴᩮᩢᩢ': 1, + 'ᨴᩮᩢ᩵': 1, + 'ᨴᩯ᩠': 1, + 'ᨴᩯᩢ': 1, + 'ᨴᩰ᩠': 1, + 'ᨶ᩠ᨲᩥ': 1, + 'ᨶ᩠ᨶᩢᩢ': 1, + 'ᨶ᩠ᨶᩥ': 1, + 'ᨶ᩠ᩋ': 1, + 'ᨶ᩠ᩋᩢ': 1, + 'ᨶᩢ᩠ᨷ': 1, + 'ᨶᩮ': 1, + 'ᨶᩯ᩠': 1, + 'ᨶᩱ': 1, + 'ᨷᩕ': 1, + 'ᨷ᩠ᨦᩢ': 1, + 'ᨷ᩠ᨯᩢ': 1, + 'ᨷ᩠ᨲᩢ': 1, + 'ᨷ᩠ᨲᩥ': 1, + 'ᨷ᩠ᨴᩫ': 1, + 'ᨷ᩠ᨿ': 1, + 'ᨷ᩠ᩅᩫ': 1, + 'ᨷ᩠ᩋ': 1, + 'ᨷᩮ᩠': 1, + 'ᨸ᩠ᨠᩫ': 1, + 'ᨸ᩠ᨯᩨ': 1, + 'ᨸ᩠ᨶᩢ': 1, + 'ᨸ᩠ᩋ': 1, + 'ᨸᩮ᩠': 1, + 'ᨸᩮᩢ᩵': 1, + 'ᨸ᩠᩵ᨿ': 1, + 'ᨹ᩠ᨯᩥ': 1, + 'ᨹ᩠ᨿ᩵': 1, + 'ᨹ᩠ᩅᩥ': 1, + 'ᨹᩪᩢᩱ': 1, + 'ᨹᩫ᩠ᩃ': 1, + 'ᨹᩮᩨ᩠': 1, + 'ᨻᩕ᩠': 1, + 'ᨻ᩠ᨦ': 1, + 'ᨻ᩠ᨶᩨ᩶': 1, + 'ᨻ᩠ᨶᩫ᩶': 1, + 'ᨻ᩠ᨿ': 1, + 'ᨻ᩠ᩋ': 1, + 'ᨻᩮ᩠': 1, + 'ᨻᩱ': 1, + 'ᨽᩱ᩠': 1, + 'ᨾ᩠ᨲᩥ': 1, + 'ᨾ᩠ᨾ': 1, + 'ᨾ᩠ᨾ᩼': 1, + 'ᨾ᩠ᩅ': 1, + 'ᨾ᩠ᩅ᩵': 1, + 'ᨾᩮ᩠': 1, + 'ᨾᩯ᩵': 1, + 'ᨾᩱ': 1, + 'ᨿ᩠ᨦᩢ': 1, + 'ᨿ᩠ᨦᩢᩢ': 1, + 'ᨿ᩠ᨦᩥ': 1, + 'ᨿ᩠ᨦᩥ᩵': 1, + 'ᨿ᩠ᩋ': 1, + 'ᨿᩢ᩠ᨷ': 1, + 'ᨿᩫ᩠ᩈ': 1, + 'ᨿᩮ᩠': 1, + 'ᨿᩰ': 1, + 'ᩁ᩠ᨠᩢ': 1, + 'ᩁ᩠ᨮᩢ': 1, + 'ᩁ᩠ᨾᩫ': 1, + 'ᩁ᩠ᨿ': 1, + 'ᩁ᩠ᩅ': 1, + 'ᩁ᩠ᩅ᩵': 1, + 'ᩁ᩠ᩋ': 1, + 'ᩁᩢ᩠ᨷ': 1, + 'ᩁᩢᩭ': 1, + 'ᩁᩫ᩠ᨷ': 1, + 'ᩁᩮ᩠': 1, + 'ᩁᩮᩨ᩠': 1, + 'ᩁᩯ᩠': 1, + 'ᩃ᩠ᨦᩫ': 1, + 'ᩃ᩠ᩅ᩵': 1, + 'ᩃᩢ᩠ᨿ': 1, + 'ᩃᩨ᩠ᨷ': 1, + 'ᩃᩮ᩠': 1, + 'ᩃᩯ': 1, + 'ᩃᩰ᩠': 1, + 'ᩅ᩠ᨲᩥ': 1, + 'ᩅ᩠ᨶᩢ': 1, + 'ᩅ᩠ᨿ': 1, + 'ᩅ᩠ᩅ᩵': 1, + 'ᩅᩮ': 1, + 'ᩅᩮ᩠': 1, + 'ᩅᩱᩢ': 1, + 'ᩈ᩠ᨠᩢ': 1, + 'ᩈ᩠ᨠᩧ': 1, + 'ᩈ᩠ᨦᩢ': 1, + 'ᩈ᩠ᨦᩥ᩵': 1, + 'ᩈ᩠ᨦᩫ': 1, + 'ᩈ᩠ᨶᩫ': 1, + 'ᩈ᩠ᨾᩫ': 1, + 'ᩈ᩠ᨿ': 1, + 'ᩈ᩠ᩅ᩵': 1, + 'ᩈ᩠ᩋ': 1, + 'ᩈᩢ᩠ᨿ': 1, + 'ᩈᩫ᩠ᨾ': 1, + 'ᩈᩮ': 1, + 'ᩈᩮ᩠': 1, + 'ᩈᩯ᩠': 1, + 'ᩈᩱ': 1, + 'ᩈᩱ᩠': 1, + 'ᩉᩕᩨ': 1, + 'ᩉ᩠ᨶᩢ': 1, + 'ᩉ᩠ᨶᩦ': 1, + 'ᩉ᩠ᨶᩧ': 1, + 'ᩉ᩠ᨶᩧ᩵': 1, + 'ᩉ᩠ᨶᩫ': 1, + 'ᩉ᩠ᨾ': 1, + 'ᩉ᩠ᨾᩢ': 1, + 'ᩉ᩠ᨾᩢᩢ': 1, + 'ᩉ᩠ᨾᩪ᩵': 1, + 'ᩉ᩠ᨾᩳ': 1, + 'ᩉ᩠ᨿᩢ᩠ᩋ': 1, + 'ᩉ᩠ᨿᩩ': 1, + 'ᩉ᩠ᩅᩫ': 1, + 'ᩉ᩠ᩅ᩵': 1, + 'ᩉᩮᩖᩨ': 1, + 'ᩉᩮ᩠': 1, + 'ᩉᩯ᩠': 1, + 'ᩉᩯ᩵': 1, + 'ᩉᩱ᩠': 1, + 'ᩋ᩠ᨶᩢ': 1, + 'ᩋ᩠ᨶᩨ᩵': 1, + 'ᩋ᩠ᩋ': 1, + 'ᩓ᩠ᩅᩢ': 1, + '⛓️‍💥': 1, + '⛹️‍♀️': 1, + '⛹️‍♂️': 1, + '⛹🏻‍♀️': 1, + '⛹🏻‍♂️': 1, + '⛹🏼‍♀️': 1, + '⛹🏼‍♂️': 1, + '⛹🏽‍♀️': 1, + '⛹🏽‍♂️': 1, + '⛹🏾‍♀️': 1, + '⛹🏾‍♂️': 1, + '⛹🏿‍♀️': 1, + '⛹🏿‍♂️': 1, + '❤️‍🔥': 1, + '❤️‍🩹': 1, + 'ꦏꦃ': 1, + 'ꦏꦺ': 1, + 'ꦏꦺꦴ': 1, + 'ꦏꦿ': 1, + 'ꦏꦿꦁ': 1, + 'ꦏꦿꦂ': 1, + 'ꦏ꧀ꦏ': 1, + 'ꦏ꧀ꦏꦁ': 1, + 'ꦏ꧀ꦏꦺ': 1, + 'ꦏ꧀ꦏꦼ': 1, + 'ꦏ꧀ꦔ': 1, + 'ꦏ꧀ꦔꦸ': 1, + 'ꦏ꧀ꦔꦿ': 1, + 'ꦏ꧀ꦕ': 1, + 'ꦏ꧀ꦠ': 1, + 'ꦏ꧀ꦠꦶ': 1, + 'ꦏ꧀ꦠꦸ': 1, + 'ꦏ꧀ꦠꦺ': 1, + 'ꦏ꧀ꦢ': 1, + 'ꦏ꧀ꦢꦂ': 1, + 'ꦏ꧀ꦢꦶ': 1, + 'ꦏ꧀ꦤ': 1, + 'ꦏ꧀ꦤꦶꦁ': 1, + 'ꦏ꧀ꦥ': 1, + 'ꦏ꧀ꦥꦶ': 1, + 'ꦏ꧀ꦥꦿꦶ': 1, + 'ꦏ꧀ꦧ': 1, + 'ꦏ꧀ꦧꦶ': 1, + 'ꦏ꧀ꦧꦺ': 1, + 'ꦏ꧀ꦩ': 1, + 'ꦏ꧀ꦩꦸ': 1, + 'ꦏ꧀ꦪ': 1, + 'ꦏ꧀ꦭ': 1, + 'ꦏ꧀ꦮ': 1, + 'ꦏ꧀ꦱ': 1, + 'ꦏ꧀ꦱꦂ': 1, + 'ꦏ꧀ꦱꦶ': 1, + 'ꦏ꧀ꦱꦸ': 1, + 'ꦏ꧀ꦱꦺ': 1, + 'ꦏ꧀ꦲ': 1, + 'ꦏ꧀ꦲꦁ': 1, + 'ꦏ꧀ꦲꦶꦁ': 1, + 'ꦏ꧀ꦲꦸ': 1, + 'ꦏ꧀ꦲꦺ': 1, + 'ꦏ꧀ꦲꦺꦴ': 1, + 'ꦒꦸꦃ': 1, + 'ꦒꦺꦴ': 1, + 'ꦒꦿ': 1, + 'ꦒꦿꦺ': 1, + 'ꦒ꧀ꦒ': 1, + 'ꦔꦃ': 1, + 'ꦔꦺ': 1, + 'ꦔꦺꦴ': 1, + 'ꦔꦿ': 1, + 'ꦔ꧀ꦒꦼ': 1, + 'ꦔ꧀ꦒꦿꦼꦁ': 1, + 'ꦔ꧀ꦭ': 1, + 'ꦔ꧀ꦭꦁ': 1, + 'ꦔ꧀ꦭꦸ': 1, + 'ꦔ꧀ꦭꦼ': 1, + 'ꦕꦃ': 1, + 'ꦕꦺꦴ': 1, + 'ꦗꦃ': 1, + 'ꦗꦺ': 1, + 'ꦗꦿꦺꦴ': 1, + 'ꦚ꧀ꦕ': 1, + 'ꦚ꧀ꦕꦶ': 1, + 'ꦚ꧀ꦗ': 1, + 'ꦚ꧀ꦗꦶ': 1, + 'ꦚ꧀ꦗꦸꦁ': 1, + 'ꦚ꧀ꦗꦸꦂ': 1, + 'ꦝꦃ': 1, + 'ꦝꦺ': 1, + 'ꦝꦺꦴ': 1, + 'ꦝꦺꦴꦂ': 1, + 'ꦠꦸꦃ': 1, + 'ꦠꦺ': 1, + 'ꦠꦺꦃ': 1, + 'ꦠꦺꦴ': 1, + 'ꦠꦾ': 1, + 'ꦠꦿ': 1, + 'ꦠꦿꦶ': 1, + 'ꦠ꧀ꦏꦁ': 1, + 'ꦠ꧀ꦏꦸ': 1, + 'ꦠ꧀ꦒ': 1, + 'ꦠ꧀ꦔꦸ': 1, + 'ꦠ꧀ꦚ': 1, + 'ꦠ꧀ꦠ': 1, + 'ꦠ꧀ꦠꦶ': 1, + 'ꦠ꧀ꦠꦶꦁ': 1, + 'ꦠ꧀ꦠꦸ': 1, + 'ꦠ꧀ꦠꦺ': 1, + 'ꦠ꧀ꦤ': 1, + 'ꦠ꧀ꦤꦶ': 1, + 'ꦠ꧀ꦩ': 1, + 'ꦠ꧀ꦩꦼ': 1, + 'ꦠ꧀ꦭ': 1, + 'ꦠ꧀ꦭꦺ': 1, + 'ꦠ꧀ꦱ': 1, + 'ꦠ꧀ꦱꦶꦁ': 1, + 'ꦠ꧀ꦱꦶꦃ': 1, + 'ꦠ꧀ꦱꦾ': 1, + 'ꦠ꧀ꦱ꧀ꦮ': 1, + 'ꦠ꧀ꦲ': 1, + 'ꦠ꧀ꦲꦶꦁ': 1, + 'ꦠ꧀ꦲꦺꦴ': 1, + 'ꦢꦃ': 1, + 'ꦢꦺ': 1, + 'ꦢꦺꦴ': 1, + 'ꦢꦾ': 1, + 'ꦢꦿ': 1, + 'ꦢ꧀ꦏ': 1, + 'ꦢ꧀ꦠ': 1, + 'ꦢ꧀ꦠꦶꦁ': 1, + 'ꦢ꧀ꦢ': 1, + 'ꦢ꧀ꦥ': 1, + 'ꦢ꧀ꦩꦺꦴ': 1, + 'ꦢ꧀ꦲ': 1, + 'ꦤꦺ': 1, + 'ꦤꦺꦃ': 1, + 'ꦤꦺꦴ': 1, + 'ꦤ꧀ꦏ': 1, + 'ꦤ꧀ꦏꦁ': 1, + 'ꦤ꧀ꦏꦸ': 1, + 'ꦤ꧀ꦏꦼ': 1, + 'ꦤ꧀ꦒ': 1, + 'ꦤ꧀ꦔ': 1, + 'ꦤ꧀ꦔꦁ': 1, + 'ꦤ꧀ꦔꦶ': 1, + 'ꦤ꧀ꦔꦸ': 1, + 'ꦤ꧀ꦔꦸꦂ': 1, + 'ꦤ꧀ꦔꦺ': 1, + 'ꦤ꧀ꦔꦼ': 1, + 'ꦤ꧀ꦔꦿꦸ': 1, + 'ꦤ꧀ꦗ': 1, + 'ꦤ꧀ꦗꦸ': 1, + 'ꦤ꧀ꦗꦼ': 1, + 'ꦤ꧀ꦚꦶ': 1, + 'ꦤ꧀ꦛꦃ': 1, + 'ꦤ꧀ꦛꦶ': 1, + 'ꦤ꧀ꦝ': 1, + 'ꦤ꧀ꦝꦁ': 1, + 'ꦤ꧀ꦝꦶ': 1, + 'ꦤ꧀ꦝꦸ': 1, + 'ꦤ꧀ꦝꦸꦁ': 1, + 'ꦤ꧀ꦝꦺꦁ': 1, + 'ꦤ꧀ꦝꦺꦴꦁ': 1, + 'ꦤ꧀ꦝꦼ': 1, + 'ꦤ꧀ꦠ': 1, + 'ꦤ꧀ꦠꦂ': 1, + 'ꦤ꧀ꦠꦃ': 1, + 'ꦤ꧀ꦠꦶ': 1, + 'ꦤ꧀ꦠꦸ': 1, + 'ꦤ꧀ꦠꦸꦁ': 1, + 'ꦤ꧀ꦠꦺ': 1, + 'ꦤ꧀ꦠꦼ': 1, + 'ꦤ꧀ꦠꦼꦂ': 1, + 'ꦤ꧀ꦠꦽ': 1, + 'ꦤ꧀ꦢ': 1, + 'ꦤ꧀ꦢꦁ': 1, + 'ꦤ꧀ꦢꦂ': 1, + 'ꦤ꧀ꦢꦶ': 1, + 'ꦤ꧀ꦢꦸ': 1, + 'ꦤ꧀ꦢꦺ': 1, + 'ꦤ꧀ꦢꦼ': 1, + 'ꦤ꧀ꦤ': 1, + 'ꦤ꧀ꦤꦶ': 1, + 'ꦤ꧀ꦤꦶꦁ': 1, + 'ꦤ꧀ꦤꦺ': 1, + 'ꦤ꧀ꦤꦼ': 1, + 'ꦤ꧀ꦥ': 1, + 'ꦤ꧀ꦥꦁ': 1, + 'ꦤ꧀ꦥ꦳ꦺꦴꦂ': 1, + 'ꦤ꧀ꦥꦶ': 1, + 'ꦤ꧀ꦥꦸ': 1, + 'ꦤ꧀ꦥꦼ': 1, + 'ꦤ꧀ꦥꦿ': 1, + 'ꦤ꧀ꦥꦿꦶ': 1, + 'ꦤ꧀ꦧ': 1, + 'ꦤ꧀ꦧꦁ': 1, + 'ꦤ꧀ꦧꦶ': 1, + 'ꦤ꧀ꦧꦸ': 1, + 'ꦤ꧀ꦧꦺ': 1, + 'ꦤ꧀ꦧꦺꦴ': 1, + 'ꦤ꧀ꦧꦼ': 1, + 'ꦤ꧀ꦩ': 1, + 'ꦤ꧀ꦩꦂ': 1, + 'ꦤ꧀ꦩꦶ': 1, + 'ꦤ꧀ꦩꦸꦂ': 1, + 'ꦤ꧀ꦩꦼ': 1, + 'ꦤ꧀ꦭ': 1, + 'ꦤ꧀ꦭꦶ': 1, + 'ꦤ꧀ꦭꦸ': 1, + 'ꦤ꧀ꦮ': 1, + 'ꦤ꧀ꦮꦂ': 1, + 'ꦤ꧀ꦮꦶ': 1, + 'ꦤ꧀ꦮꦸ': 1, + 'ꦤ꧀ꦮꦼ': 1, + 'ꦤ꧀ꦱ': 1, + 'ꦤ꧀ꦱꦂ': 1, + 'ꦤ꧀ꦱꦃ': 1, + 'ꦤ꧀ꦱꦶ': 1, + 'ꦤ꧀ꦱꦶꦁ': 1, + 'ꦤ꧀ꦱꦸ': 1, + 'ꦤ꧀ꦱꦺꦴ': 1, + 'ꦤ꧀ꦱꦼ': 1, + 'ꦤ꧀ꦲ': 1, + 'ꦤ꧀ꦲꦁ': 1, + 'ꦤ꧀ꦲꦶ': 1, + 'ꦤ꧀ꦲꦶꦁ': 1, + 'ꦤ꧀ꦲꦸ': 1, + 'ꦤ꧀ꦲꦺꦴ': 1, + 'ꦥꦃ': 1, + 'ꦥꦺ': 1, + 'ꦥꦺꦴ': 1, + 'ꦥꦿ': 1, + 'ꦥꦿꦃ': 1, + 'ꦥꦿꦶ': 1, + 'ꦥꦿꦺꦴ': 1, + 'ꦥ꧀ꦏ': 1, + 'ꦥ꧀ꦏꦁ': 1, + 'ꦥ꧀ꦠ': 1, + 'ꦥ꧀ꦠꦶ': 1, + 'ꦥ꧀ꦢꦸ': 1, + 'ꦥ꧀ꦤ': 1, + 'ꦥ꧀ꦤꦶ': 1, + 'ꦥ꧀ꦥ': 1, + 'ꦥ꧀ꦥꦶ': 1, + 'ꦥ꧀ꦥꦺ': 1, + 'ꦥ꧀ꦧꦺ': 1, + 'ꦥ꧀ꦩ': 1, + 'ꦥ꧀ꦩꦂ': 1, + 'ꦥ꧀ꦫꦺ': 1, + 'ꦥ꧀ꦭ': 1, + 'ꦥ꧀ꦭꦸ': 1, + 'ꦥ꧀ꦱ': 1, + 'ꦥ꧀ꦱꦶꦂ': 1, + 'ꦥ꧀ꦱꦼ': 1, + 'ꦥ꧀ꦲ': 1, + 'ꦧꦺ': 1, + 'ꦧꦺꦃ': 1, + 'ꦧꦺꦴ': 1, + 'ꦧꦿ': 1, + 'ꦧ꧀ꦏ': 1, + 'ꦧ꧀ꦠꦸ': 1, + 'ꦧ꧀ꦥ': 1, + 'ꦧ꧀ꦧ': 1, + 'ꦧ꧀ꦧꦺ': 1, + 'ꦧ꧀ꦲ': 1, + 'ꦧ꧀ꦲꦶ': 1, + 'ꦩꦃ': 1, + 'ꦩꦺ': 1, + 'ꦩꦺꦃ': 1, + 'ꦩꦺꦴ': 1, + 'ꦩꦿ': 1, + 'ꦩꦿꦃ': 1, + 'ꦩꦿꦶꦃ': 1, + 'ꦩ꧀ꦏꦁ': 1, + 'ꦩ꧀ꦔꦼ': 1, + 'ꦩ꧀ꦠꦸ': 1, + 'ꦩ꧀ꦠꦺꦴ': 1, + 'ꦩ꧀ꦤꦶꦁ': 1, + 'ꦩ꧀ꦥ': 1, + 'ꦩ꧀ꦥꦶ': 1, + 'ꦩ꧀ꦥꦸ': 1, + 'ꦩ꧀ꦥꦼ': 1, + 'ꦩ꧀ꦧ': 1, + 'ꦩ꧀ꦧꦁ': 1, + 'ꦩ꧀ꦧꦂ': 1, + 'ꦩ꧀ꦧꦃ': 1, + 'ꦩ꧀ꦧꦶ': 1, + 'ꦩ꧀ꦧꦸ': 1, + 'ꦩ꧀ꦧꦸꦁ': 1, + 'ꦩ꧀ꦧꦺ': 1, + 'ꦩ꧀ꦧꦺꦂ': 1, + 'ꦩ꧀ꦧꦼꦂ': 1, + 'ꦩ꧀ꦧꦾꦸꦂ': 1, + 'ꦩ꧀ꦩ': 1, + 'ꦩ꧀ꦩꦶ': 1, + 'ꦩ꧀ꦫ': 1, + 'ꦩ꧀ꦫꦃ': 1, + 'ꦩ꧀ꦭ': 1, + 'ꦩ꧀ꦲꦶ': 1, + 'ꦪꦃ': 1, + 'ꦪꦸꦃ': 1, + 'ꦪꦺꦴ': 1, + 'ꦫꦶꦃ': 1, + 'ꦫꦺ': 1, + 'ꦫꦺꦴ': 1, + 'ꦫꦾ': 1, + 'ꦭꦃ': 1, + 'ꦭꦶꦃ': 1, + 'ꦭꦺ': 1, + 'ꦭꦺꦃ': 1, + 'ꦭꦺꦴ': 1, + 'ꦭꦺꦴꦁ': 1, + 'ꦭ꧀ꦏꦁ': 1, + 'ꦭ꧀ꦚꦼꦁ': 1, + 'ꦭ꧀ꦠꦸ': 1, + 'ꦭ꧀ꦧꦸ': 1, + 'ꦭ꧀ꦩꦸ': 1, + 'ꦭ꧀ꦭ': 1, + 'ꦭ꧀ꦭꦶ': 1, + 'ꦭ꧀ꦭꦶꦁ': 1, + 'ꦭ꧀ꦭꦺ': 1, + 'ꦭ꧀ꦮ': 1, + 'ꦭ꧀ꦱ': 1, + 'ꦭ꧀ꦱꦂ': 1, + 'ꦭ꧀ꦱꦶꦁ': 1, + 'ꦭ꧀ꦱꦸ': 1, + 'ꦭ꧀ꦲ': 1, + 'ꦭ꧀ꦲꦁ': 1, + 'ꦭ꧀ꦲꦶꦁ': 1, + 'ꦭ꧀ꦲꦸ': 1, + 'ꦮꦃ': 1, + 'ꦮꦶꦃ': 1, + 'ꦮꦸꦃ': 1, + 'ꦮꦺ': 1, + 'ꦮꦺꦴ': 1, + 'ꦮꦺꦴꦁ': 1, + 'ꦮꦺꦴꦃ': 1, + 'ꦮꦿꦸꦃ': 1, + 'ꦱꦶꦃ': 1, + 'ꦱꦺ': 1, + 'ꦱꦺꦴ': 1, + 'ꦱꦾ': 1, + 'ꦱꦿ': 1, + 'ꦱꦿꦃ': 1, + 'ꦱ꧀ꦏ': 1, + 'ꦱ꧀ꦏꦁ': 1, + 'ꦱ꧀ꦏꦿꦶ': 1, + 'ꦱ꧀ꦒ': 1, + 'ꦱ꧀ꦔꦸ': 1, + 'ꦱ꧀ꦔꦿ': 1, + 'ꦱ꧀ꦛꦶ': 1, + 'ꦱ꧀ꦠ': 1, + 'ꦱ꧀ꦠꦿ': 1, + 'ꦱ꧀ꦢꦂ': 1, + 'ꦱ꧀ꦢꦶ': 1, + 'ꦱ꧀ꦢꦸ': 1, + 'ꦱ꧀ꦥ': 1, + 'ꦱ꧀ꦧ': 1, + 'ꦱ꧀ꦧꦁ': 1, + 'ꦱ꧀ꦩ': 1, + 'ꦱ꧀ꦩꦶ': 1, + 'ꦱ꧀ꦩꦼ': 1, + 'ꦱ꧀ꦩꦿ': 1, + 'ꦱ꧀ꦭ': 1, + 'ꦱ꧀ꦮ': 1, + 'ꦱ꧀ꦱ': 1, + 'ꦱ꧀ꦱꦂ': 1, + 'ꦱ꧀ꦱꦶ': 1, + 'ꦱ꧀ꦱꦶꦁ': 1, + 'ꦱ꧀ꦱꦸ': 1, + 'ꦱ꧀ꦱꦺ': 1, + 'ꦱ꧀ꦲ': 1, + 'ꦱ꧀ꦲꦁ': 1, + 'ꦱ꧀ꦲꦶ': 1, + 'ꦱ꧀ꦲꦶꦁ': 1, + 'ꦱ꧀ꦲꦸ': 1, + 'ꦲꦺ': 1, + 'ꦲꦺꦴ': 1, + '𑄃𑄬': 1, + '𑄃𑄬𑄭': 1, + '𑄃𑄳𑄃': 1, + '𑄃𑄳𑄦𑄧': 1, + '𑄃𑄳𑄦𑄨': 1, + '𑄇𑄬': 1, + '𑄇𑄳𑄠': 1, + '𑄇𑄳𑄠𑄬': 1, + '𑄇𑄳𑄠𑄴': 1, + '𑄇𑄳𑄢𑄨': 1, + '𑄇𑄳𑄦𑄧': 1, + '𑄇𑄳𑄦𑄴': 1, + '𑄈𑄬': 1, + '𑄈𑄳𑄠': 1, + '𑄈𑄳𑄠𑄬': 1, + '𑄉𑄬': 1, + '𑄉𑄬𑄭': 1, + '𑄉𑄳𑄠': 1, + '𑄉𑄳𑄢𑄧': 1, + '𑄊𑄬': 1, + '𑄌𑄬': 1, + '𑄌𑄳𑄠𑄬': 1, + '𑄌𑄳𑄠𑄴': 1, + '𑄎𑄬': 1, + '𑄎𑄬𑄭': 1, + '𑄎𑄳𑄠𑄧': 1, + '𑄎𑄳𑄠𑄬': 1, + '𑄏𑄬': 1, + '𑄑𑄬': 1, + '𑄑𑄳𑄢𑄬': 1, + '𑄖𑄬': 1, + '𑄖𑄳𑄠𑄧': 1, + '𑄖𑄳𑄠𑄬': 1, + '𑄖𑄳𑄠𑄴': 1, + '𑄖𑄳𑄢𑄨': 1, + '𑄖𑄳𑄢𑄬': 1, + '𑄖𑄳𑄦𑄴': 1, + '𑄗𑄬': 1, + '𑄘𑄬': 1, + '𑄘𑄳𑄠𑄧': 1, + '𑄘𑄳𑄠𑄬': 1, + '𑄙𑄬': 1, + '𑄙𑄳𑄠': 1, + '𑄙𑄳𑄠𑄧': 1, + '𑄚𑄬': 1, + '𑄚𑄬𑄭': 1, + '𑄚𑄳𑄠𑄧': 1, + '𑄚𑄳𑄠𑄬': 1, + '𑄚𑄳𑄠𑄴': 1, + '𑄚𑄳𑄢𑄧𑄧': 1, + '𑄚𑄳𑄦𑄧': 1, + '𑄚𑄳𑄦𑄨': 1, + '𑄚𑄳𑄦𑄴': 1, + '𑄛𑄬': 1, + '𑄛𑄳𑄢': 1, + '𑄛𑄳𑄢𑄧': 1, + '𑄛𑄳𑄦': 1, + '𑄜𑄬': 1, + '𑄝𑄬': 1, + '𑄝𑄳𑄠': 1, + '𑄝𑄳𑄢𑄨': 1, + '𑄝𑄳𑄢𑄬': 1, + '𑄞𑄬': 1, + '𑄞𑄬𑄭': 1, + '𑄟𑄬': 1, + '𑄟𑄳𑄠𑄬': 1, + '𑄠𑄬': 1, + '𑄠𑄬𑄭': 1, + '𑄡𑄬': 1, + '𑄡𑄬𑄭': 1, + '𑄡𑄳𑄠𑄬': 1, + '𑄢𑄬': 1, + '𑄢𑄬𑄭': 1, + '𑄢𑄳𑄦𑄧𑄱𑄱': 1, + '𑄣𑄬': 1, + '𑄣𑄬𑄭': 1, + '𑄣𑄳𑄦𑄬': 1, + '𑄣𑄳𑄦𑄴': 1, + '𑄥𑄬': 1, + '𑄥𑄳𑄠': 1, + '𑄥𑄳𑄠𑄧': 1, + '𑄥𑄳𑄢𑄧': 1, + '𑄥𑄳𑄢𑄨𑄁': 1, + '𑄦𑄬': 1, + '𑌅𑌂': 1, + '𑌕𑌂': 1, + '𑌕𑌾': 1, + '𑌕𑌾𑌂': 1, + '𑌕𑌾𑌃': 1, + '𑌕𑌿': 1, + '𑌕𑍂': 1, + '𑌕𑍃': 1, + '𑌕𑍇': 1, + '𑌕𑍋': 1, + '𑌗𑌾': 1, + '𑌗𑌿': 1, + '𑌗𑍁': 1, + '𑌗𑍃': 1, + '𑌗𑍇': 1, + '𑌗𑍌': 1, + '𑌘𑌾': 1, + '𑌘𑍇': 1, + '𑌘𑍋': 1, + '𑌚𑌾': 1, + '𑌚𑌿': 1, + '𑌚𑍇': 1, + '𑌚𑍈': 1, + '𑌛𑌾': 1, + '𑌛𑍁': 1, + '𑌛𑍇': 1, + '𑌜𑌾': 1, + '𑌜𑌿': 1, + '𑌜𑍇': 1, + '𑌞𑌾': 1, + '𑌟𑌃': 1, + '𑌟𑌾': 1, + '𑌟𑌾𑌂': 1, + '𑌟𑌾𑌃': 1, + '𑌠𑌾': 1, + '𑌠𑍇': 1, + '𑌠𑍈𑌃': 1, + '𑌡𑌂': 1, + '𑌡𑌃': 1, + '𑌡𑌿': 1, + '𑌣𑌂': 1, + '𑌣𑌾': 1, + '𑌣𑌾𑌂': 1, + '𑌣𑌿': 1, + '𑌣𑍁': 1, + '𑌣𑍇': 1, + '𑌤𑌂': 1, + '𑌤𑌃': 1, + '𑌤𑌾': 1, + '𑌤𑌾𑌂': 1, + '𑌤𑌾𑌃': 1, + '𑌤𑌿': 1, + '𑌤𑌿𑌂': 1, + '𑌤𑌿𑌃': 1, + '𑌤𑍁': 1, + '𑌤𑍁𑌂': 1, + '𑌤𑍃': 1, + '𑌤𑍇': 1, + '𑌤𑍇𑌃': 1, + '𑌤𑍈': 1, + '𑌤𑍋': 1, + '𑌤𑍌': 1, + '𑌥𑌂': 1, + '𑌥𑌾': 1, + '𑌥𑌾𑌂': 1, + '𑌥𑌿': 1, + '𑌦𑌂': 1, + '𑌦𑌃': 1, + '𑌦𑌾': 1, + '𑌦𑌿': 1, + '𑌦𑍁': 1, + '𑌦𑍃': 1, + '𑌦𑍇': 1, + '𑌦𑍋': 1, + '𑌧𑌃': 1, + '𑌧𑌾': 1, + '𑌧𑌾𑌃': 1, + '𑌧𑌿': 1, + '𑌧𑌿𑌂': 1, + '𑌧𑍁': 1, + '𑌧𑍃': 1, + '𑌧𑍇': 1, + '𑌧𑍇𑌃': 1, + '𑌧𑍈𑌃': 1, + '𑌨𑌂': 1, + '𑌨𑌃': 1, + '𑌨𑌾': 1, + '𑌨𑌾𑌂': 1, + '𑌨𑌾𑌃': 1, + '𑌨𑌿': 1, + '𑌨𑌿𑌃': 1, + '𑌨𑍁': 1, + '𑌨𑍂': 1, + '𑌨𑍇': 1, + '𑌨𑍈': 1, + '𑌨𑍈𑌃': 1, + '𑌨𑍋': 1, + '𑌪𑌂': 1, + '𑌪𑌾': 1, + '𑌪𑌿': 1, + '𑌪𑍁': 1, + '𑌪𑍂': 1, + '𑌪𑍇': 1, + '𑌪𑍋': 1, + '𑌬𑌾': 1, + '𑌬𑍃': 1, + '𑌭𑌾': 1, + '𑌭𑌿': 1, + '𑌭𑌿𑌃': 1, + '𑌭𑍁': 1, + '𑌭𑍂': 1, + '𑌭𑍇': 1, + '𑌮𑌂': 1, + '𑌮𑌾': 1, + '𑌮𑌿': 1, + '𑌮𑌿𑌂': 1, + '𑌮𑍁': 1, + '𑌮𑍂': 1, + '𑌮𑍇': 1, + '𑌮𑍈': 1, + '𑌮𑍋': 1, + '𑌮𑍌': 1, + '𑌯𑌂': 1, + '𑌯𑌃': 1, + '𑌯𑌾': 1, + '𑌯𑌾𑌂': 1, + '𑌯𑌾𑌃': 1, + '𑌯𑌿': 1, + '𑌯𑍁': 1, + '𑌯𑍁𑌃': 1, + '𑌯𑍇': 1, + '𑌯𑍈': 1, + '𑌯𑍈𑌃': 1, + '𑌯𑍋': 1, + '𑌯𑍋𑌃': 1, + '𑌰𑌂': 1, + '𑌰𑌃': 1, + '𑌰𑌾': 1, + '𑌰𑌾𑌃': 1, + '𑌰𑌿': 1, + '𑌰𑌿𑌂': 1, + '𑌰𑍁': 1, + '𑌰𑍂': 1, + '𑌰𑍇': 1, + '𑌰𑍋': 1, + '𑌰𑍌': 1, + '𑌲𑌂': 1, + '𑌲𑌾': 1, + '𑌲𑌾𑌃': 1, + '𑌲𑌿': 1, + '𑌲𑌿𑌂': 1, + '𑌲𑍇': 1, + '𑌲𑍈': 1, + '𑌲𑍋': 1, + '𑌵𑌂': 1, + '𑌵𑌃': 1, + '𑌵𑌾': 1, + '𑌵𑌾𑌂': 1, + '𑌵𑌾𑌃': 1, + '𑌵𑌿': 1, + '𑌵𑍃': 1, + '𑌵𑍇': 1, + '𑌵𑍈': 1, + '𑌵𑍋': 1, + '𑌶𑌂': 1, + '𑌶𑌃': 1, + '𑌶𑌾': 1, + '𑌶𑌾𑌃': 1, + '𑌶𑌿': 1, + '𑌶𑍁': 1, + '𑌶𑍂': 1, + '𑌶𑍇': 1, + '𑌶𑍋': 1, + '𑌷𑌂': 1, + '𑌷𑌾': 1, + '𑌷𑌾𑌂': 1, + '𑌷𑌾𑌃': 1, + '𑌷𑌿': 1, + '𑌷𑍁': 1, + '𑌷𑍇': 1, + '𑌷𑍈': 1, + '𑌷𑍋': 1, + '𑌸𑌂': 1, + '𑌸𑌃': 1, + '𑌸𑌾': 1, + '𑌸𑌾𑌂': 1, + '𑌸𑌿': 1, + '𑌸𑍁': 1, + '𑌸𑍂': 1, + '𑌸𑍇': 1, + '𑌸𑍋': 1, + '𑌸𑍌': 1, + '𑌹𑌾': 1, + '𑌹𑌾𑌃': 1, + '𑌹𑌿': 1, + '𑌹𑍇': 1, + '𑌹𑍋': 1, + '🏋️‍♀️': 1, + '🏋️‍♂️': 1, + '🏋🏻‍♀️': 1, + '🏋🏻‍♂️': 1, + '🏋🏼‍♀️': 1, + '🏋🏼‍♂️': 1, + '🏋🏽‍♀️': 1, + '🏋🏽‍♂️': 1, + '🏋🏾‍♀️': 1, + '🏋🏾‍♂️': 1, + '🏋🏿‍♀️': 1, + '🏋🏿‍♂️': 1, + '🏌️‍♀️': 1, + '🏌️‍♂️': 1, + '🏌🏻‍♀️': 1, + '🏌🏻‍♂️': 1, + '🏌🏼‍♀️': 1, + '🏌🏼‍♂️': 1, + '🏌🏽‍♀️': 1, + '🏌🏽‍♂️': 1, + '🏌🏾‍♀️': 1, + '🏌🏾‍♂️': 1, + '🏌🏿‍♀️': 1, + '🏌🏿‍♂️': 1, + '🏳️‍⚧️': 1, + '🏳️‍🌈': 1, + '👁️‍🗨️': 1, + '🕵️‍♀️': 1, + '🕵️‍♂️': 1, + '🕵🏻‍♀️': 1, + '🕵🏻‍♂️': 1, + '🕵🏼‍♀️': 1, + '🕵🏼‍♂️': 1, + '🕵🏽‍♀️': 1, + '🕵🏽‍♂️': 1, + '🕵🏾‍♀️': 1, + '🕵🏾‍♂️': 1, + '🕵🏿‍♀️': 1, + '🕵🏿‍♂️': 1, +} diff --git a/libs/wcwidth/table_grapheme_overrides/_registry.py b/libs/wcwidth/table_grapheme_overrides/_registry.py new file mode 100644 index 000000000..d676165e0 --- /dev/null +++ b/libs/wcwidth/table_grapheme_overrides/_registry.py @@ -0,0 +1,32 @@ +""" +Terminal to grapheme mapping of overrides, using a hash registry. + +This code generated by python wcwidth using ucs-detect project data. +""" +# pylint: skip-file +_REGISTRY = { + 'alacritty': '45d92e98', + 'apple_terminal': 'e08bd75e', + 'bobcat': '970dbe10', + 'contour': 'fd9d4c44', + 'extraterm': 'fcc05a0f', + 'foot': '529fbb4a', + 'ghostty': '4cdf59ce', + 'iterm2': '8589765c', + 'kitty': 'c0a2cdbf', + 'konsole': 'c2157f7e', + 'mintty': '3d4826b8', + 'mlterm': 'c3db41c0', + 'pterm': '5bfac390', + 'rio': '8f94b404', + 'st': '813fee16', + 'terminology': 'e22030f3', + 'urxvt': '813fee16', + 'vte': '5bfac390', + 'warp': '27e0693f', + 'wezterm': 'da9ceb0a', + 'xterm': '813fee16', + 'xterm.js': '50bf0759', +} + +__all__ = ('_REGISTRY',) diff --git a/libs/wcwidth/table_mc.py b/libs/wcwidth/table_mc.py index 7c2e69159..663e93b76 100644 --- a/libs/wcwidth/table_mc.py +++ b/libs/wcwidth/table_mc.py @@ -1,7 +1,7 @@ """ Exports CATEGORY_MC table keyed by supporting unicode version level. -This code generated by wcwidth/bin/update-tables.py on 2026-01-29 00:47:54 UTC. +This code generated by python wcwidth project. """ # pylint: disable=duplicate-code CATEGORY_MC = { diff --git a/libs/wcwidth/table_overrides.py b/libs/wcwidth/table_overrides.py new file mode 100644 index 000000000..95e107635 --- /dev/null +++ b/libs/wcwidth/table_overrides.py @@ -0,0 +1,853 @@ +""" +Terminal override tables keyed by canonical terminal software name. + +This code generated by python wcwidth using ucs-detect project data. +""" +# pylint: skip-file +from __future__ import annotations + +# For terminals: rio +_SET_WIDE_OVERRIDES_243C8ED7: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1f6d8, 0x1f6d8,), # Landslide + (0x1fa8a, 0x1fa8a,), # Trombone + (0x1fa8e, 0x1fa8e,), # Treasure Chest + (0x1fac8, 0x1fac8,), # Hairy Creature + (0x1facd, 0x1facd,), # Orca + (0x1faea, 0x1faea,), # Distorted Face + (0x1faef, 0x1faef,), # Fight Cloud + ), +} + +# For terminals: konsole +_SET_WIDE_OVERRIDES_25894E07: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x02ffc, 0x02fff,), # Ideographic Description ..Ideographic Description + (0x031e4, 0x031e5,), # Cjk Stroke Hxg ..Cjk Stroke Szp + (0x031ef, 0x031ef,), # Ideographic Description Character Subtraction + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1f6d8, 0x1f6d8,), # Landslide + (0x1fa89, 0x1fa8a,), # Harp ..Trombone + (0x1fa8e, 0x1fa8f,), # Treasure Chest ..Shovel + (0x1fabe, 0x1fabe,), # Leafless Tree + (0x1fac6, 0x1fac6,), # Fingerprint + (0x1fac8, 0x1fac8,), # Hairy Creature + (0x1facd, 0x1facd,), # Orca + (0x1fadc, 0x1fadc,), # Root Vegetable + (0x1fadf, 0x1fadf,), # Splatter + (0x1fae9, 0x1faea,), # Face With Bags Under Eye..Distorted Face + (0x1faef, 0x1faef,), # Fight Cloud + ), +} + +# For terminals: extraterm +_SET_WIDE_OVERRIDES_6C1578E8: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x02ffc, 0x02fff,), # Ideographic Description ..Ideographic Description + (0x031e4, 0x031e5,), # Cjk Stroke Hxg ..Cjk Stroke Szp + (0x031ef, 0x031ef,), # Ideographic Description Character Subtraction + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1f6d8, 0x1f6d8,), # Landslide + (0x1f6dc, 0x1f6dc,), # Wireless + (0x1fa75, 0x1fa77,), # Light Blue Heart ..Pink Heart + (0x1fa87, 0x1fa8a,), # Maracas ..Trombone + (0x1fa8e, 0x1fa8f,), # Treasure Chest ..Shovel + (0x1faad, 0x1faaf,), # Folding Hand Fan ..Khanda + (0x1fabb, 0x1fabf,), # Hyacinth ..Goose + (0x1fac6, 0x1fac6,), # Fingerprint + (0x1fac8, 0x1fac8,), # Hairy Creature + (0x1facd, 0x1facf,), # Orca ..Donkey + (0x1fada, 0x1fadc,), # Ginger Root ..Root Vegetable + (0x1fadf, 0x1fadf,), # Splatter + (0x1fae8, 0x1faea,), # Shaking Face ..Distorted Face + (0x1faef, 0x1faef,), # Fight Cloud + (0x1faf7, 0x1faf8,), # Leftwards Pushing Hand ..Rightwards Pushing Hand + ), +} + +# For terminals: alacritty, apple_terminal, pterm, st, urxvt, xterm +_SET_WIDE_OVERRIDES_6E5EF6CC: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + ), +} + +# For terminals: kitty +_SET_WIDE_OVERRIDES_73E5B60D: dict[str, tuple[tuple[int, int], ...]] = { + 'zeroer': ( + (0x1f3fb, 0x1f3ff,), # Emoji Modifier Fitzpatri..Emoji Modifier Fitzpatri + ), +} + +# For terminals: mintty +_SET_WIDE_OVERRIDES_79ED351B: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x02ffc, 0x02fff,), # Ideographic Description ..Ideographic Description + (0x031e4, 0x031e5,), # Cjk Stroke Hxg ..Cjk Stroke Szp + (0x031ef, 0x031ef,), # Ideographic Description Character Subtraction + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1fa89, 0x1fa89,), # Harp + (0x1fa8f, 0x1fa8f,), # Shovel + (0x1fabe, 0x1fabe,), # Leafless Tree + (0x1fac6, 0x1fac6,), # Fingerprint + (0x1fadc, 0x1fadc,), # Root Vegetable + (0x1fadf, 0x1fadf,), # Splatter + (0x1fae9, 0x1fae9,), # Face With Bags Under Eyes + ), +} + +# For terminals: bobcat +_SET_WIDE_OVERRIDES_8A2DF0C6: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x03030, 0x03030,), # Wavy Dash + (0x0303d, 0x0303d,), # Part Alternation Mark + (0x03297, 0x03297,), # Circled Ideograph Congratulation + (0x03299, 0x03299,), # Circled Ideograph Secret + (0x1f202, 0x1f202,), # Squared Katakana Sa + (0x1f237, 0x1f237,), # Squared Cjk Unified Ideograph-6708 + ), + 'zeroer': ( + (0x1f3fb, 0x1f3ff,), # Emoji Modifier Fitzpatri..Emoji Modifier Fitzpatri + ), +} + +# For terminals: foot +_SET_WIDE_OVERRIDES_98684D9B: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1f3fb, 0x1f3ff,), # Emoji Modifier Fitzpatri..Emoji Modifier Fitzpatri + ), +} + +# For terminals: xterm.js +_SET_WIDE_OVERRIDES_9D8AD458: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x02ffc, 0x02fff,), # Ideographic Description ..Ideographic Description + (0x031bb, 0x031bf,), # Bopomofo Final Letter G ..Bopomofo Letter Ah + (0x031e4, 0x031e5,), # Cjk Stroke Hxg ..Cjk Stroke Szp + (0x031ef, 0x031ef,), # Ideographic Description Character Subtraction + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x1aff0, 0x1aff3,), # Katakana Letter Minnan T..Katakana Letter Minnan T + (0x1aff5, 0x1affb,), # Katakana Letter Minnan T..Katakana Letter Minnan N + (0x1affd, 0x1affe,), # Katakana Letter Minnan N..Katakana Letter Minnan N + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1f6d6, 0x1f6d8,), # Hut ..Landslide + (0x1f6dc, 0x1f6df,), # Wireless ..Ring Buoy + (0x1f6fb, 0x1f6fc,), # Pickup Truck ..Roller Skate + (0x1f7f0, 0x1f7f0,), # Heavy Equals Sign + (0x1f90c, 0x1f90c,), # Pinched Fingers + (0x1f972, 0x1f972,), # Smiling Face With Tear + (0x1f977, 0x1f979,), # Ninja ..Face Holding Back Tears + (0x1f9a3, 0x1f9a4,), # Mammoth ..Dodo + (0x1f9ab, 0x1f9ad,), # Beaver ..Seal + (0x1f9cb, 0x1f9cc,), # Bubble Tea ..Troll + (0x1fa74, 0x1fa77,), # Thong Sandal ..Pink Heart + (0x1fa7b, 0x1fa7c,), # X-ray ..Crutch + (0x1fa83, 0x1fa8a,), # Boomerang ..Trombone + (0x1fa8e, 0x1fa8f,), # Treasure Chest ..Shovel + (0x1fa96, 0x1fac6,), # Military Helmet ..Fingerprint + (0x1fac8, 0x1fac8,), # Hairy Creature + (0x1facd, 0x1fadc,), # Orca ..Root Vegetable + (0x1fadf, 0x1faea,), # Splatter ..Distorted Face + (0x1faef, 0x1faf8,), # Fight Cloud ..Rightwards Pushing Hand + ), +} + +# For terminals: vte, wezterm +_SET_WIDE_OVERRIDES_A5DF05D4: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x02ffc, 0x02fff,), # Ideographic Description ..Ideographic Description + (0x031e4, 0x031e5,), # Cjk Stroke Hxg ..Cjk Stroke Szp + (0x031ef, 0x031ef,), # Ideographic Description Character Subtraction + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1fa89, 0x1fa89,), # Harp + (0x1fa8f, 0x1fa8f,), # Shovel + (0x1fabe, 0x1fabe,), # Leafless Tree + (0x1fac6, 0x1fac6,), # Fingerprint + (0x1fadc, 0x1fadc,), # Root Vegetable + (0x1fadf, 0x1fadf,), # Splatter + (0x1fae9, 0x1fae9,), # Face With Bags Under Eyes + ), +} + +# For terminals: terminology +_SET_WIDE_OVERRIDES_C908F105: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x0fada, 0x0faff,), # (nil) + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1f6d8, 0x1f6d8,), # Landslide + (0x1f6dc, 0x1f6df,), # Wireless ..Ring Buoy + (0x1f7f0, 0x1f7f0,), # Heavy Equals Sign + (0x1fad7, 0x1fadc,), # Pouring Liquid ..Root Vegetable + (0x1fadf, 0x1faea,), # Splatter ..Distorted Face + (0x1faef, 0x1faf8,), # Fight Cloud ..Rightwards Pushing Hand + ), +} + +# For terminals: ghostty, iterm2 +_SET_WIDE_OVERRIDES_CF30CE25: dict[str, tuple[tuple[int, int], ...]] = { + 'zeroer': ( + (0x0115f, 0x0115f,), # Hangul Choseong Filler + ), +} + +# For terminals: mlterm, warp +_SET_WIDE_OVERRIDES_D289A768: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x02630, 0x02637,), # Trigram For Heaven ..Trigram For Earth + (0x0268a, 0x0268f,), # Monogram For Yang ..Digram For Greater Yin + (0x031e4, 0x031e5,), # Cjk Stroke Hxg ..Cjk Stroke Szp + (0x04dc0, 0x04dff,), # Hexagram For The Creativ..Hexagram For Before Comp + (0x1d300, 0x1d356,), # Monogram For Earth ..Tetragram For Fostering + (0x1d360, 0x1d376,), # Counting Rod Unit Digit ..Ideographic Tally Mark F + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + (0x1f6d8, 0x1f6d8,), # Landslide + (0x1fa89, 0x1fa8a,), # Harp ..Trombone + (0x1fa8e, 0x1fa8f,), # Treasure Chest ..Shovel + (0x1fabe, 0x1fabe,), # Leafless Tree + (0x1fac6, 0x1fac6,), # Fingerprint + (0x1fac8, 0x1fac8,), # Hairy Creature + (0x1facd, 0x1facd,), # Orca + (0x1fadc, 0x1fadc,), # Root Vegetable + (0x1fadf, 0x1fadf,), # Splatter + (0x1fae9, 0x1faea,), # Face With Bags Under Eye..Distorted Face + (0x1faef, 0x1faef,), # Fight Cloud + ), +} + +WIDE_OVERRIDES: dict[str, dict[str, tuple[tuple[int, int], ...]]] = { + 'alacritty': _SET_WIDE_OVERRIDES_6E5EF6CC, + 'apple_terminal': _SET_WIDE_OVERRIDES_6E5EF6CC, + 'bobcat': _SET_WIDE_OVERRIDES_8A2DF0C6, + 'extraterm': _SET_WIDE_OVERRIDES_6C1578E8, + 'foot': _SET_WIDE_OVERRIDES_98684D9B, + 'ghostty': _SET_WIDE_OVERRIDES_CF30CE25, + 'iterm2': _SET_WIDE_OVERRIDES_CF30CE25, + 'kitty': _SET_WIDE_OVERRIDES_73E5B60D, + 'konsole': _SET_WIDE_OVERRIDES_25894E07, + 'mintty': _SET_WIDE_OVERRIDES_79ED351B, + 'mlterm': _SET_WIDE_OVERRIDES_D289A768, + 'pterm': _SET_WIDE_OVERRIDES_6E5EF6CC, + 'rio': _SET_WIDE_OVERRIDES_243C8ED7, + 'st': _SET_WIDE_OVERRIDES_6E5EF6CC, + 'terminology': _SET_WIDE_OVERRIDES_C908F105, + 'urxvt': _SET_WIDE_OVERRIDES_6E5EF6CC, + 'vte': _SET_WIDE_OVERRIDES_A5DF05D4, + 'warp': _SET_WIDE_OVERRIDES_D289A768, + 'wezterm': _SET_WIDE_OVERRIDES_A5DF05D4, + 'xterm': _SET_WIDE_OVERRIDES_6E5EF6CC, + 'xterm.js': _SET_WIDE_OVERRIDES_9D8AD458, +} + +# For terminals: alacritty, apple_terminal, extraterm, foot, mlterm, pterm, rio, st, terminology, urxvt, vte, warp, wezterm, xterm, xterm.js +_SET_SRI_OVERRIDES_6E5EF6CC: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x1f1e6, 0x1f1ff,), # Regional Indicator Symbo..Regional Indicator Symbo + ), +} + +SRI_OVERRIDES: dict[str, dict[str, tuple[tuple[int, int], ...]]] = { + 'alacritty': _SET_SRI_OVERRIDES_6E5EF6CC, + 'apple_terminal': _SET_SRI_OVERRIDES_6E5EF6CC, + 'extraterm': _SET_SRI_OVERRIDES_6E5EF6CC, + 'foot': _SET_SRI_OVERRIDES_6E5EF6CC, + 'mlterm': _SET_SRI_OVERRIDES_6E5EF6CC, + 'pterm': _SET_SRI_OVERRIDES_6E5EF6CC, + 'rio': _SET_SRI_OVERRIDES_6E5EF6CC, + 'st': _SET_SRI_OVERRIDES_6E5EF6CC, + 'terminology': _SET_SRI_OVERRIDES_6E5EF6CC, + 'urxvt': _SET_SRI_OVERRIDES_6E5EF6CC, + 'vte': _SET_SRI_OVERRIDES_6E5EF6CC, + 'warp': _SET_SRI_OVERRIDES_6E5EF6CC, + 'wezterm': _SET_SRI_OVERRIDES_6E5EF6CC, + 'xterm': _SET_SRI_OVERRIDES_6E5EF6CC, + 'xterm.js': _SET_SRI_OVERRIDES_6E5EF6CC, +} + +# For terminals: bobcat, kitty +_SET_SFZ_OVERRIDES_73E5B60D: dict[str, tuple[tuple[int, int], ...]] = { + 'zeroer': ( + (0x1f3fb, 0x1f3ff,), # Emoji Modifier Fitzpatri..Emoji Modifier Fitzpatri + ), +} + +# For terminals: foot +_SET_SFZ_OVERRIDES_8DDFF0C4: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x1f3fb, 0x1f3ff,), # Emoji Modifier Fitzpatri..Emoji Modifier Fitzpatri + ), +} + +SFZ_OVERRIDES: dict[str, dict[str, tuple[tuple[int, int], ...]]] = { + 'bobcat': _SET_SFZ_OVERRIDES_73E5B60D, + 'foot': _SET_SFZ_OVERRIDES_8DDFF0C4, + 'kitty': _SET_SFZ_OVERRIDES_73E5B60D, +} + +# For terminals: iterm2 +_SET_VS16_OVERRIDES_82FD018F: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x00023, 0x00023,), # Number Sign + (0x0002a, 0x0002a,), # Asterisk + (0x00030, 0x00039,), # Digit Zero ..Digit Nine + ), +} + +# For terminals: alacritty, apple_terminal, contour, mlterm, pterm, st, terminology, urxvt, vte, wezterm, xterm, xterm.js +_SET_VS16_OVERRIDES_F48531B2: dict[str, tuple[tuple[int, int], ...]] = { + 'narrower': ( + (0x00023, 0x00023,), # Number Sign + (0x0002a, 0x0002a,), # Asterisk + (0x00030, 0x00039,), # Digit Zero ..Digit Nine + (0x000a9, 0x000a9,), # Copyright Sign + (0x000ae, 0x000ae,), # Registered Sign + (0x0203c, 0x0203c,), # Double Exclamation Mark + (0x02049, 0x02049,), # Exclamation Question Mark + (0x02122, 0x02122,), # Trade Mark Sign + (0x02139, 0x02139,), # Information Source + (0x02194, 0x02199,), # Left Right Arrow ..South West Arrow + (0x021a9, 0x021aa,), # Leftwards Arrow With Hoo..Rightwards Arrow With Ho + (0x02328, 0x02328,), # Keyboard + (0x023cf, 0x023cf,), # Eject Symbol + (0x023ed, 0x023ef,), # Black Right-pointing Dou..Black Right-pointing Tri + (0x023f1, 0x023f2,), # Stopwatch ..Timer Clock + (0x023f8, 0x023fa,), # Double Vertical Bar ..Black Circle For Record + (0x024c2, 0x024c2,), # Circled Latin Capital Letter M + (0x025aa, 0x025ab,), # Black Small Square ..White Small Square + (0x025b6, 0x025b6,), # Black Right-pointing Triangle + (0x025c0, 0x025c0,), # Black Left-pointing Triangle + (0x025fb, 0x025fc,), # White Medium Square ..Black Medium Square + (0x02600, 0x02604,), # Black Sun With Rays ..Comet + (0x0260e, 0x0260e,), # Black Telephone + (0x02611, 0x02611,), # Ballot Box With Check + (0x02618, 0x02618,), # Shamrock + (0x0261d, 0x0261d,), # White Up Pointing Index + (0x02620, 0x02620,), # Skull And Crossbones + (0x02622, 0x02623,), # Radioactive Sign ..Biohazard Sign + (0x02626, 0x02626,), # Orthodox Cross + (0x0262a, 0x0262a,), # Star And Crescent + (0x0262e, 0x0262f,), # Peace Symbol ..Yin Yang + (0x02638, 0x0263a,), # Wheel Of Dharma ..White Smiling Face + (0x02640, 0x02640,), # Female Sign + (0x02642, 0x02642,), # Male Sign + (0x0265f, 0x02660,), # Black Chess Pawn ..Black Spade Suit + (0x02663, 0x02663,), # Black Club Suit + (0x02665, 0x02666,), # Black Heart Suit ..Black Diamond Suit + (0x02668, 0x02668,), # Hot Springs + (0x0267b, 0x0267b,), # Black Universal Recycling Symbol + (0x0267e, 0x0267e,), # Permanent Paper Sign + (0x02692, 0x02692,), # Hammer And Pick + (0x02694, 0x02697,), # Crossed Swords ..Alembic + (0x02699, 0x02699,), # Gear + (0x0269b, 0x0269c,), # Atom Symbol ..Fleur-de-lis + (0x026a0, 0x026a0,), # Warning Sign + (0x026a7, 0x026a7,), # Male With Stroke And Male And Female Sign + (0x026b0, 0x026b1,), # Coffin ..Funeral Urn + (0x026c8, 0x026c8,), # Thunder Cloud And Rain + (0x026cf, 0x026cf,), # Pick + (0x026d1, 0x026d1,), # Helmet With White Cross + (0x026d3, 0x026d3,), # Chains + (0x026e9, 0x026e9,), # Shinto Shrine + (0x026f0, 0x026f1,), # Mountain ..Umbrella On Ground + (0x026f4, 0x026f4,), # Ferry + (0x026f7, 0x026f9,), # Skier ..Person With Ball + (0x02702, 0x02702,), # Black Scissors + (0x02708, 0x02709,), # Airplane ..Envelope + (0x0270c, 0x0270d,), # Victory Hand ..Writing Hand + (0x0270f, 0x0270f,), # Pencil + (0x02712, 0x02712,), # Black Nib + (0x02714, 0x02714,), # Heavy Check Mark + (0x02716, 0x02716,), # Heavy Multiplication X + (0x0271d, 0x0271d,), # Latin Cross + (0x02721, 0x02721,), # Star Of David + (0x02733, 0x02734,), # Eight Spoked Asterisk ..Eight Pointed Black Star + (0x02744, 0x02744,), # Snowflake + (0x02747, 0x02747,), # Sparkle + (0x02763, 0x02764,), # Heavy Heart Exclamation ..Heavy Black Heart + (0x027a1, 0x027a1,), # Black Rightwards Arrow + (0x02934, 0x02935,), # Arrow Pointing Rightward..Arrow Pointing Rightward + (0x02b05, 0x02b07,), # Leftwards Black Arrow ..Downwards Black Arrow + (0x1f170, 0x1f171,), # Negative Squared Latin C..Negative Squared Latin C + (0x1f17e, 0x1f17f,), # Negative Squared Latin C..Negative Squared Latin C + (0x1f321, 0x1f321,), # Thermometer + (0x1f324, 0x1f32c,), # White Sun With Small Clo..Wind Blowing Face + (0x1f336, 0x1f336,), # Hot Pepper + (0x1f37d, 0x1f37d,), # Fork And Knife With Plate + (0x1f396, 0x1f397,), # Military Medal ..Reminder Ribbon + (0x1f399, 0x1f39b,), # Studio Microphone ..Control Knobs + (0x1f39e, 0x1f39f,), # Film Frames ..Admission Tickets + (0x1f3cb, 0x1f3ce,), # Weight Lifter ..Racing Car + (0x1f3d4, 0x1f3df,), # Snow Capped Mountain ..Stadium + (0x1f3f3, 0x1f3f3,), # Waving White Flag + (0x1f3f5, 0x1f3f5,), # Rosette + (0x1f3f7, 0x1f3f7,), # Label + (0x1f43f, 0x1f43f,), # Chipmunk + (0x1f441, 0x1f441,), # Eye + (0x1f4fd, 0x1f4fd,), # Film Projector + (0x1f549, 0x1f54a,), # Om Symbol ..Dove Of Peace + (0x1f56f, 0x1f570,), # Candle ..Mantelpiece Clock + (0x1f573, 0x1f579,), # Hole ..Joystick + (0x1f587, 0x1f587,), # Linked Paperclips + (0x1f58a, 0x1f58d,), # Lower Left Ballpoint Pen..Lower Left Crayon + (0x1f590, 0x1f590,), # Raised Hand With Fingers Splayed + (0x1f5a5, 0x1f5a5,), # Desktop Computer + (0x1f5a8, 0x1f5a8,), # Printer + (0x1f5b1, 0x1f5b2,), # Three Button Mouse ..Trackball + (0x1f5bc, 0x1f5bc,), # Frame With Picture + (0x1f5c2, 0x1f5c4,), # Card Index Dividers ..File Cabinet + (0x1f5d1, 0x1f5d3,), # Wastebasket ..Spiral Calendar Pad + (0x1f5dc, 0x1f5de,), # Compression ..Rolled-up Newspaper + (0x1f5e1, 0x1f5e1,), # Dagger Knife + (0x1f5e3, 0x1f5e3,), # Speaking Head In Silhouette + (0x1f5e8, 0x1f5e8,), # Left Speech Bubble + (0x1f5ef, 0x1f5ef,), # Right Anger Bubble + (0x1f5f3, 0x1f5f3,), # Ballot Box With Ballot + (0x1f5fa, 0x1f5fa,), # World Map + (0x1f6cb, 0x1f6cb,), # Couch And Lamp + (0x1f6cd, 0x1f6cf,), # Shopping Bags ..Bed + (0x1f6e0, 0x1f6e5,), # Hammer And Wrench ..Motor Boat + (0x1f6e9, 0x1f6e9,), # Small Airplane + (0x1f6f0, 0x1f6f0,), # Satellite + (0x1f6f3, 0x1f6f3,), # Passenger Ship + ), +} + +VS16_OVERRIDES: dict[str, dict[str, tuple[tuple[int, int], ...]]] = { + 'alacritty': _SET_VS16_OVERRIDES_F48531B2, + 'apple_terminal': _SET_VS16_OVERRIDES_F48531B2, + 'contour': _SET_VS16_OVERRIDES_F48531B2, + 'iterm2': _SET_VS16_OVERRIDES_82FD018F, + 'mlterm': _SET_VS16_OVERRIDES_F48531B2, + 'pterm': _SET_VS16_OVERRIDES_F48531B2, + 'st': _SET_VS16_OVERRIDES_F48531B2, + 'terminology': _SET_VS16_OVERRIDES_F48531B2, + 'urxvt': _SET_VS16_OVERRIDES_F48531B2, + 'vte': _SET_VS16_OVERRIDES_F48531B2, + 'wezterm': _SET_VS16_OVERRIDES_F48531B2, + 'xterm': _SET_VS16_OVERRIDES_F48531B2, + 'xterm.js': _SET_VS16_OVERRIDES_F48531B2, +} + +# For terminals: rio +_SET_VS15_OVERRIDES_0F19A03D: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x023eb, 0x023ec,), # Black Up-pointing Double..Black Down-pointing Doub + (0x023f0, 0x023f0,), # Alarm Clock + (0x026ce, 0x026ce,), # Ophiuchus + (0x02705, 0x02705,), # White Heavy Check Mark + (0x0270a, 0x0270b,), # Raised Fist ..Raised Hand + (0x02728, 0x02728,), # Sparkles + (0x0274c, 0x0274c,), # Cross Mark + (0x0274e, 0x0274e,), # Negative Squared Cross Mark + (0x02754, 0x02755,), # White Question Mark Orna..White Exclamation Mark O + (0x02795, 0x02797,), # Heavy Plus Sign ..Heavy Division Sign + (0x027b0, 0x027b0,), # Curly Loop + (0x027bf, 0x027bf,), # Double Curly Loop + ), +} + +# For terminals: bobcat +_SET_VS15_OVERRIDES_72B9888B: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x03030, 0x03030,), # Wavy Dash + (0x0303d, 0x0303d,), # Part Alternation Mark + (0x03297, 0x03297,), # Circled Ideograph Congratulation + (0x03299, 0x03299,), # Circled Ideograph Secret + (0x1f202, 0x1f202,), # Squared Katakana Sa + (0x1f237, 0x1f237,), # Squared Cjk Unified Ideograph-6708 + ), +} + +# For terminals: warp +_SET_VS15_OVERRIDES_D32653C3: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x0231a, 0x0231b,), # Watch ..Hourglass + (0x023e9, 0x023ec,), # Black Right-pointing Dou..Black Down-pointing Doub + (0x023f0, 0x023f0,), # Alarm Clock + (0x023f3, 0x023f3,), # Hourglass With Flowing Sand + (0x025fd, 0x025fe,), # White Medium Small Squar..Black Medium Small Squar + (0x02614, 0x02615,), # Umbrella With Rain Drops..Hot Beverage + (0x02648, 0x02653,), # Aries ..Pisces + (0x0267f, 0x0267f,), # Wheelchair Symbol + (0x02693, 0x02693,), # Anchor + (0x026a1, 0x026a1,), # High Voltage Sign + (0x026aa, 0x026ab,), # Medium White Circle ..Medium Black Circle + (0x026bd, 0x026be,), # Soccer Ball ..Baseball + (0x026c4, 0x026c5,), # Snowman Without Snow ..Sun Behind Cloud + (0x026ce, 0x026ce,), # Ophiuchus + (0x026d4, 0x026d4,), # No Entry + (0x026ea, 0x026ea,), # Church + (0x026f2, 0x026f3,), # Fountain ..Flag In Hole + (0x026f5, 0x026f5,), # Sailboat + (0x026fa, 0x026fa,), # Tent + (0x026fd, 0x026fd,), # Fuel Pump + (0x02705, 0x02705,), # White Heavy Check Mark + (0x0270a, 0x0270b,), # Raised Fist ..Raised Hand + (0x02728, 0x02728,), # Sparkles + (0x0274c, 0x0274c,), # Cross Mark + (0x0274e, 0x0274e,), # Negative Squared Cross Mark + (0x02753, 0x02755,), # Black Question Mark Orna..White Exclamation Mark O + (0x02757, 0x02757,), # Heavy Exclamation Mark Symbol + (0x02795, 0x02797,), # Heavy Plus Sign ..Heavy Division Sign + (0x027b0, 0x027b0,), # Curly Loop + (0x027bf, 0x027bf,), # Double Curly Loop + (0x02b1b, 0x02b1c,), # Black Large Square ..White Large Square + (0x02b50, 0x02b50,), # White Medium Star + (0x02b55, 0x02b55,), # Heavy Large Circle + (0x1f004, 0x1f004,), # Mahjong Tile Red Dragon + (0x1f30d, 0x1f30f,), # Earth Globe Europe-afric..Earth Globe Asia-austral + (0x1f315, 0x1f315,), # Full Moon Symbol + (0x1f31c, 0x1f31c,), # Last Quarter Moon With Face + (0x1f378, 0x1f378,), # Cocktail Glass + (0x1f393, 0x1f393,), # Graduation Cap + (0x1f3a7, 0x1f3a7,), # Headphone + (0x1f3ac, 0x1f3ae,), # Clapper Board ..Video Game + (0x1f3c2, 0x1f3c2,), # Snowboarder + (0x1f3c4, 0x1f3c4,), # Surfer + (0x1f3c6, 0x1f3c6,), # Trophy + (0x1f3ca, 0x1f3ca,), # Swimmer + (0x1f3e0, 0x1f3e0,), # House Building + (0x1f3ed, 0x1f3ed,), # Factory + (0x1f408, 0x1f408,), # Cat + (0x1f415, 0x1f415,), # Dog + (0x1f41f, 0x1f41f,), # Fish + (0x1f426, 0x1f426,), # Bird + (0x1f442, 0x1f442,), # Ear + (0x1f446, 0x1f449,), # White Up Pointing Backha..White Right Pointing Bac + (0x1f44d, 0x1f44e,), # Thumbs Up Sign ..Thumbs Down Sign + (0x1f453, 0x1f453,), # Eyeglasses + (0x1f46a, 0x1f46a,), # Family + (0x1f47d, 0x1f47d,), # Extraterrestrial Alien + (0x1f4a3, 0x1f4a3,), # Bomb + (0x1f4b0, 0x1f4b0,), # Money Bag + (0x1f4b3, 0x1f4b3,), # Credit Card + (0x1f4bb, 0x1f4bb,), # Personal Computer + (0x1f4bf, 0x1f4bf,), # Optical Disc + (0x1f4cb, 0x1f4cb,), # Clipboard + (0x1f4da, 0x1f4da,), # Books + (0x1f4df, 0x1f4df,), # Pager + (0x1f4e4, 0x1f4e6,), # Outbox Tray ..Package + (0x1f4ea, 0x1f4ed,), # Closed Mailbox With Lowe..Open Mailbox With Lowere + (0x1f4f7, 0x1f4f7,), # Camera + (0x1f4f9, 0x1f4fb,), # Video Camera ..Radio + (0x1f508, 0x1f508,), # Speaker + (0x1f50d, 0x1f50d,), # Left-pointing Magnifying Glass + (0x1f512, 0x1f513,), # Lock ..Open Lock + (0x1f550, 0x1f567,), # Clock Face One Oclock ..Clock Face Twelve-thirty + (0x1f610, 0x1f610,), # Neutral Face + (0x1f687, 0x1f687,), # Metro + (0x1f68d, 0x1f68d,), # Oncoming Bus + (0x1f691, 0x1f691,), # Ambulance + (0x1f694, 0x1f694,), # Oncoming Police Car + (0x1f698, 0x1f698,), # Oncoming Automobile + (0x1f6ad, 0x1f6ad,), # No Smoking Symbol + (0x1f6b2, 0x1f6b2,), # Bicycle + (0x1f6b9, 0x1f6ba,), # Mens Symbol ..Womens Symbol + (0x1f6bc, 0x1f6bc,), # Baby Symbol + ), +} + +# For terminals: alacritty, apple_terminal, contour, extraterm, iterm2, konsole, mlterm, pterm, st, terminology, urxvt, vte, wezterm, xterm, xterm.js +_SET_VS15_OVERRIDES_EC2D40AC: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x0231a, 0x0231b,), # Watch ..Hourglass + (0x023e9, 0x023ec,), # Black Right-pointing Dou..Black Down-pointing Doub + (0x023f0, 0x023f0,), # Alarm Clock + (0x023f3, 0x023f3,), # Hourglass With Flowing Sand + (0x025fd, 0x025fe,), # White Medium Small Squar..Black Medium Small Squar + (0x02614, 0x02615,), # Umbrella With Rain Drops..Hot Beverage + (0x02648, 0x02653,), # Aries ..Pisces + (0x0267f, 0x0267f,), # Wheelchair Symbol + (0x02693, 0x02693,), # Anchor + (0x026a1, 0x026a1,), # High Voltage Sign + (0x026aa, 0x026ab,), # Medium White Circle ..Medium Black Circle + (0x026bd, 0x026be,), # Soccer Ball ..Baseball + (0x026c4, 0x026c5,), # Snowman Without Snow ..Sun Behind Cloud + (0x026ce, 0x026ce,), # Ophiuchus + (0x026d4, 0x026d4,), # No Entry + (0x026ea, 0x026ea,), # Church + (0x026f2, 0x026f3,), # Fountain ..Flag In Hole + (0x026f5, 0x026f5,), # Sailboat + (0x026fa, 0x026fa,), # Tent + (0x026fd, 0x026fd,), # Fuel Pump + (0x02705, 0x02705,), # White Heavy Check Mark + (0x0270a, 0x0270b,), # Raised Fist ..Raised Hand + (0x02728, 0x02728,), # Sparkles + (0x0274c, 0x0274c,), # Cross Mark + (0x0274e, 0x0274e,), # Negative Squared Cross Mark + (0x02753, 0x02755,), # Black Question Mark Orna..White Exclamation Mark O + (0x02757, 0x02757,), # Heavy Exclamation Mark Symbol + (0x02795, 0x02797,), # Heavy Plus Sign ..Heavy Division Sign + (0x027b0, 0x027b0,), # Curly Loop + (0x027bf, 0x027bf,), # Double Curly Loop + (0x02b1b, 0x02b1c,), # Black Large Square ..White Large Square + (0x02b50, 0x02b50,), # White Medium Star + (0x02b55, 0x02b55,), # Heavy Large Circle + (0x03030, 0x03030,), # Wavy Dash + (0x0303d, 0x0303d,), # Part Alternation Mark + (0x03297, 0x03297,), # Circled Ideograph Congratulation + (0x03299, 0x03299,), # Circled Ideograph Secret + (0x1f004, 0x1f004,), # Mahjong Tile Red Dragon + (0x1f202, 0x1f202,), # Squared Katakana Sa + (0x1f21a, 0x1f21a,), # Squared Cjk Unified Ideograph-7121 + (0x1f22f, 0x1f22f,), # Squared Cjk Unified Ideograph-6307 + (0x1f237, 0x1f237,), # Squared Cjk Unified Ideograph-6708 + (0x1f30d, 0x1f30f,), # Earth Globe Europe-afric..Earth Globe Asia-austral + (0x1f315, 0x1f315,), # Full Moon Symbol + (0x1f31c, 0x1f31c,), # Last Quarter Moon With Face + (0x1f378, 0x1f378,), # Cocktail Glass + (0x1f393, 0x1f393,), # Graduation Cap + (0x1f3a7, 0x1f3a7,), # Headphone + (0x1f3ac, 0x1f3ae,), # Clapper Board ..Video Game + (0x1f3c2, 0x1f3c2,), # Snowboarder + (0x1f3c4, 0x1f3c4,), # Surfer + (0x1f3c6, 0x1f3c6,), # Trophy + (0x1f3ca, 0x1f3ca,), # Swimmer + (0x1f3e0, 0x1f3e0,), # House Building + (0x1f3ed, 0x1f3ed,), # Factory + (0x1f408, 0x1f408,), # Cat + (0x1f415, 0x1f415,), # Dog + (0x1f41f, 0x1f41f,), # Fish + (0x1f426, 0x1f426,), # Bird + (0x1f442, 0x1f442,), # Ear + (0x1f446, 0x1f449,), # White Up Pointing Backha..White Right Pointing Bac + (0x1f44d, 0x1f44e,), # Thumbs Up Sign ..Thumbs Down Sign + (0x1f453, 0x1f453,), # Eyeglasses + (0x1f46a, 0x1f46a,), # Family + (0x1f47d, 0x1f47d,), # Extraterrestrial Alien + (0x1f4a3, 0x1f4a3,), # Bomb + (0x1f4b0, 0x1f4b0,), # Money Bag + (0x1f4b3, 0x1f4b3,), # Credit Card + (0x1f4bb, 0x1f4bb,), # Personal Computer + (0x1f4bf, 0x1f4bf,), # Optical Disc + (0x1f4cb, 0x1f4cb,), # Clipboard + (0x1f4da, 0x1f4da,), # Books + (0x1f4df, 0x1f4df,), # Pager + (0x1f4e4, 0x1f4e6,), # Outbox Tray ..Package + (0x1f4ea, 0x1f4ed,), # Closed Mailbox With Lowe..Open Mailbox With Lowere + (0x1f4f7, 0x1f4f7,), # Camera + (0x1f4f9, 0x1f4fb,), # Video Camera ..Radio + (0x1f508, 0x1f508,), # Speaker + (0x1f50d, 0x1f50d,), # Left-pointing Magnifying Glass + (0x1f512, 0x1f513,), # Lock ..Open Lock + (0x1f550, 0x1f567,), # Clock Face One Oclock ..Clock Face Twelve-thirty + (0x1f610, 0x1f610,), # Neutral Face + (0x1f687, 0x1f687,), # Metro + (0x1f68d, 0x1f68d,), # Oncoming Bus + (0x1f691, 0x1f691,), # Ambulance + (0x1f694, 0x1f694,), # Oncoming Police Car + (0x1f698, 0x1f698,), # Oncoming Automobile + (0x1f6ad, 0x1f6ad,), # No Smoking Symbol + (0x1f6b2, 0x1f6b2,), # Bicycle + (0x1f6b9, 0x1f6ba,), # Mens Symbol ..Womens Symbol + (0x1f6bc, 0x1f6bc,), # Baby Symbol + ), +} + +VS15_OVERRIDES: dict[str, dict[str, tuple[tuple[int, int], ...]]] = { + 'alacritty': _SET_VS15_OVERRIDES_EC2D40AC, + 'apple_terminal': _SET_VS15_OVERRIDES_EC2D40AC, + 'bobcat': _SET_VS15_OVERRIDES_72B9888B, + 'contour': _SET_VS15_OVERRIDES_EC2D40AC, + 'extraterm': _SET_VS15_OVERRIDES_EC2D40AC, + 'iterm2': _SET_VS15_OVERRIDES_EC2D40AC, + 'konsole': _SET_VS15_OVERRIDES_EC2D40AC, + 'mlterm': _SET_VS15_OVERRIDES_EC2D40AC, + 'pterm': _SET_VS15_OVERRIDES_EC2D40AC, + 'rio': _SET_VS15_OVERRIDES_0F19A03D, + 'st': _SET_VS15_OVERRIDES_EC2D40AC, + 'terminology': _SET_VS15_OVERRIDES_EC2D40AC, + 'urxvt': _SET_VS15_OVERRIDES_EC2D40AC, + 'vte': _SET_VS15_OVERRIDES_EC2D40AC, + 'warp': _SET_VS15_OVERRIDES_D32653C3, + 'wezterm': _SET_VS15_OVERRIDES_EC2D40AC, + 'xterm': _SET_VS15_OVERRIDES_EC2D40AC, + 'xterm.js': _SET_VS15_OVERRIDES_EC2D40AC, +} + +# For terminals: alacritty +_SET_NARROW_OVERRIDES_19FF3EBB: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x017a4, 0x017a4,), # Khmer Independent Vowel Qaa + (0x017d8, 0x017d8,), # Khmer Sign Beyyal + ), + 'narrow_zeroer': ( + (0x000ad, 0x000ad,), # Soft Hyphen + (0x00605, 0x00605,), # Arabic Number Mark Above + (0x0070f, 0x0070f,), # Syriac Abbreviation Mark + (0x00890, 0x00891,), # Arabic Pound Mark Above ..Arabic Piastre Mark Abov + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x00d4e, 0x00d4e,), # Malayalam Letter Dot Reph + (0x0a8fa, 0x0a8fa,), # Devanagari Caret + (0x0ff9e, 0x0ff9f,), # Halfwidth Katakana Voice..Halfwidth Katakana Semi- + (0x111c2, 0x111c3,), # Sharada Sign Jihvamuliya..Sharada Sign Upadhmaniya + (0x1193f, 0x1193f,), # Dives Akuru Prefixed Nasal Sign + (0x11941, 0x11941,), # Dives Akuru Initial Ra + (0x11a84, 0x11a89,), # Soyombo Sign Jihvamuliya..Soyombo Cluster-initial + (0x11d46, 0x11d46,), # Masaram Gondi Repha + (0x11f02, 0x11f02,), # Kawi Sign Repha + ), +} + +# For terminals: konsole, wezterm +_SET_NARROW_OVERRIDES_3E5607A0: dict[str, tuple[tuple[int, int], ...]] = { + 'narrow_zeroer': ( + (0x000ad, 0x000ad,), # Soft Hyphen + (0x00600, 0x00605,), # Arabic Number Sign ..Arabic Number Mark Above + (0x006dd, 0x006dd,), # Arabic End Of Ayah + (0x0070f, 0x0070f,), # Syriac Abbreviation Mark + (0x00890, 0x00891,), # Arabic Pound Mark Above ..Arabic Piastre Mark Abov + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x110bd, 0x110bd,), # Kaithi Number Sign + (0x110cd, 0x110cd,), # Kaithi Number Sign Above + ), +} + +# For terminals: kitty +_SET_NARROW_OVERRIDES_4F7FBF42: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x0261d, 0x0261d,), # White Up Pointing Index + (0x026f9, 0x026f9,), # Person With Ball + (0x0270c, 0x0270d,), # Victory Hand ..Writing Hand + (0x1f3cb, 0x1f3cc,), # Weight Lifter ..Golfer + (0x1f574, 0x1f575,), # Man In Business Suit Lev..Sleuth Or Spy + (0x1f590, 0x1f590,), # Raised Hand With Fingers Splayed + ), + 'narrow_zeroer': ( + (0x000ad, 0x000ad,), # Soft Hyphen + (0x00600, 0x00605,), # Arabic Number Sign ..Arabic Number Mark Above + (0x006dd, 0x006dd,), # Arabic End Of Ayah + (0x0070f, 0x0070f,), # Syriac Abbreviation Mark + (0x00890, 0x00891,), # Arabic Pound Mark Above ..Arabic Piastre Mark Abov + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x0ff9e, 0x0ff9f,), # Halfwidth Katakana Voice..Halfwidth Katakana Semi- + (0x110bd, 0x110bd,), # Kaithi Number Sign + (0x110cd, 0x110cd,), # Kaithi Number Sign Above + ), +} + +# For terminals: xterm.js +_SET_NARROW_OVERRIDES_763C1244: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x1f93b, 0x1f93b,), # Modern Pentathlon + (0x1f946, 0x1f946,), # Rifle + ), + 'narrow_zeroer': ( + (0x00600, 0x00605,), # Arabic Number Sign ..Arabic Number Mark Above + (0x006dd, 0x006dd,), # Arabic End Of Ayah + (0x0070f, 0x0070f,), # Syriac Abbreviation Mark + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x110bd, 0x110bd,), # Kaithi Number Sign + (0x110cd, 0x110cd,), # Kaithi Number Sign Above + ), +} + +# For terminals: foot, pterm, rio, vte +_SET_NARROW_OVERRIDES_8B501630: dict[str, tuple[tuple[int, int], ...]] = { + 'narrow_zeroer': ( + (0x00600, 0x00605,), # Arabic Number Sign ..Arabic Number Mark Above + (0x006dd, 0x006dd,), # Arabic End Of Ayah + (0x0070f, 0x0070f,), # Syriac Abbreviation Mark + (0x00890, 0x00891,), # Arabic Pound Mark Above ..Arabic Piastre Mark Abov + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x110bd, 0x110bd,), # Kaithi Number Sign + (0x110cd, 0x110cd,), # Kaithi Number Sign Above + ), +} + +# For terminals: st, urxvt, xterm +_SET_NARROW_OVERRIDES_9BC01E8D: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x03248, 0x0324f,), # Circled Number Ten On Bl..Circled Number Eighty On + ), +} + +# For terminals: bobcat +_SET_NARROW_OVERRIDES_B19CC1BB: dict[str, tuple[tuple[int, int], ...]] = { + 'narrow_zeroer': ( + (0x00890, 0x00891,), # Arabic Pound Mark Above ..Arabic Piastre Mark Abov + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x110bd, 0x110bd,), # Kaithi Number Sign + (0x110cd, 0x110cd,), # Kaithi Number Sign Above + ), +} + +# For terminals: iterm2 +_SET_NARROW_OVERRIDES_C5500A15: dict[str, tuple[tuple[int, int], ...]] = { + 'narrow_zeroer': ( + (0x000ad, 0x000ad,), # Soft Hyphen + ), +} + +# For terminals: warp +_SET_NARROW_OVERRIDES_FDEEB1C7: dict[str, tuple[tuple[int, int], ...]] = { + 'wider': ( + (0x017a4, 0x017a4,), # Khmer Independent Vowel Qaa + (0x017d8, 0x017d8,), # Khmer Sign Beyyal + ), + 'narrow_zeroer': ( + (0x000ad, 0x000ad,), # Soft Hyphen + (0x00605, 0x00605,), # Arabic Number Mark Above + (0x0070f, 0x0070f,), # Syriac Abbreviation Mark + (0x00890, 0x00891,), # Arabic Pound Mark Above ..Arabic Piastre Mark Abov + (0x008e2, 0x008e2,), # Arabic Disputed End Of Ayah + (0x00d4e, 0x00d4e,), # Malayalam Letter Dot Reph + (0x0a8fa, 0x0a8fa,), # Devanagari Caret + (0x0ff9e, 0x0ff9f,), # Halfwidth Katakana Voice..Halfwidth Katakana Semi- + (0x111c2, 0x111c3,), # Sharada Sign Jihvamuliya..Sharada Sign Upadhmaniya + (0x1193f, 0x1193f,), # Dives Akuru Prefixed Nasal Sign + (0x11941, 0x11941,), # Dives Akuru Initial Ra + (0x11a3a, 0x11a3a,), # Zanabazar Square Cluster-initial Letter Ra + (0x11a84, 0x11a89,), # Soyombo Sign Jihvamuliya..Soyombo Cluster-initial + (0x11d46, 0x11d46,), # Masaram Gondi Repha + (0x11f02, 0x11f02,), # Kawi Sign Repha + ), +} + +NARROW_OVERRIDES: dict[str, dict[str, tuple[tuple[int, int], ...]]] = { + 'alacritty': _SET_NARROW_OVERRIDES_19FF3EBB, + 'bobcat': _SET_NARROW_OVERRIDES_B19CC1BB, + 'foot': _SET_NARROW_OVERRIDES_8B501630, + 'iterm2': _SET_NARROW_OVERRIDES_C5500A15, + 'kitty': _SET_NARROW_OVERRIDES_4F7FBF42, + 'konsole': _SET_NARROW_OVERRIDES_3E5607A0, + 'pterm': _SET_NARROW_OVERRIDES_8B501630, + 'rio': _SET_NARROW_OVERRIDES_8B501630, + 'st': _SET_NARROW_OVERRIDES_9BC01E8D, + 'urxvt': _SET_NARROW_OVERRIDES_9BC01E8D, + 'vte': _SET_NARROW_OVERRIDES_8B501630, + 'warp': _SET_NARROW_OVERRIDES_FDEEB1C7, + 'wezterm': _SET_NARROW_OVERRIDES_3E5607A0, + 'xterm': _SET_NARROW_OVERRIDES_9BC01E8D, + 'xterm.js': _SET_NARROW_OVERRIDES_763C1244, +} + +__all__ = ['WIDE_OVERRIDES', 'SRI_OVERRIDES', 'SFZ_OVERRIDES', 'VS16_OVERRIDES', 'VS15_OVERRIDES', 'NARROW_OVERRIDES'] diff --git a/libs/wcwidth/table_term_programs.py b/libs/wcwidth/table_term_programs.py new file mode 100644 index 000000000..08d06dc1a --- /dev/null +++ b/libs/wcwidth/table_term_programs.py @@ -0,0 +1,49 @@ +""" +Canonical terminal names and TERM/TERM_PROGRAM aliases. + +This code generated by python wcwidth using ucs-detect project data. + +Terminal multiplexers (tmux, zellij, libvterm, screen) are excluded because their displayed +presentation depends on the host terminal; cursor-position reports from ucs-detect testing are not +reliable indicators of actual width. +""" +# pylint: skip-file + +KNOWN_TERMINALS = frozenset({ + 'alacritty', + 'apple_terminal', + 'bobcat', + 'contour', + 'extraterm', + 'foot', + 'ghostty', + 'iterm2', + 'kitty', + 'konsole', + 'mintty', + 'mlterm', + 'pterm', + 'rio', + 'st', + 'terminology', + 'urxvt', + 'vte', + 'warp', + 'wezterm', + 'xterm', + 'xterm.js', +}) + +ALIASES = { + 'hyper': 'xterm.js', + 'iterm.app': 'iterm2', + 'putty': 'pterm', + 'rxvt': 'urxvt', + 'rxvt-unicode-256color': 'urxvt', + 'st-256color': 'st', + 'tabby': 'xterm.js', + 'vscode': 'xterm.js', + 'warpterminal': 'warp', + 'xterm-ghostty': 'ghostty', + 'xterm-kitty': 'kitty', +} diff --git a/libs/wcwidth/table_vs15.py b/libs/wcwidth/table_vs15.py new file mode 100644 index 000000000..84fddad82 --- /dev/null +++ b/libs/wcwidth/table_vs15.py @@ -0,0 +1,104 @@ +""" +Exports VS15_WIDE_TO_NARROW table keyed by supporting unicode version level. + +This code generated by python wcwidth project. +""" +# pylint: disable=duplicate-code +VS15_WIDE_TO_NARROW = { + '9.0.0': ( + # Source: 9.0.0 + # Date: 2025-01-30, 21:48:29 GMT + # + (0x0231a, 0x0231b,), # Watch ..Hourglass + (0x023e9, 0x023ec,), # Black Right-pointing Dou..Black Down-pointing Doub + (0x023f0, 0x023f0,), # Alarm Clock + (0x023f3, 0x023f3,), # Hourglass With Flowing Sand + (0x025fd, 0x025fe,), # White Medium Small Squar..Black Medium Small Squar + (0x02614, 0x02615,), # Umbrella With Rain Drops..Hot Beverage + (0x02648, 0x02653,), # Aries ..Pisces + (0x0267f, 0x0267f,), # Wheelchair Symbol + (0x02693, 0x02693,), # Anchor + (0x026a1, 0x026a1,), # High Voltage Sign + (0x026aa, 0x026ab,), # Medium White Circle ..Medium Black Circle + (0x026bd, 0x026be,), # Soccer Ball ..Baseball + (0x026c4, 0x026c5,), # Snowman Without Snow ..Sun Behind Cloud + (0x026ce, 0x026ce,), # Ophiuchus + (0x026d4, 0x026d4,), # No Entry + (0x026ea, 0x026ea,), # Church + (0x026f2, 0x026f3,), # Fountain ..Flag In Hole + (0x026f5, 0x026f5,), # Sailboat + (0x026fa, 0x026fa,), # Tent + (0x026fd, 0x026fd,), # Fuel Pump + (0x02705, 0x02705,), # White Heavy Check Mark + (0x0270a, 0x0270b,), # Raised Fist ..Raised Hand + (0x02728, 0x02728,), # Sparkles + (0x0274c, 0x0274c,), # Cross Mark + (0x0274e, 0x0274e,), # Negative Squared Cross Mark + (0x02753, 0x02755,), # Black Question Mark Orna..White Exclamation Mark O + (0x02757, 0x02757,), # Heavy Exclamation Mark Symbol + (0x02795, 0x02797,), # Heavy Plus Sign ..Heavy Division Sign + (0x027b0, 0x027b0,), # Curly Loop + (0x027bf, 0x027bf,), # Double Curly Loop + (0x02b1b, 0x02b1c,), # Black Large Square ..White Large Square + (0x02b50, 0x02b50,), # White Medium Star + (0x02b55, 0x02b55,), # Heavy Large Circle + (0x03030, 0x03030,), # Wavy Dash + (0x0303d, 0x0303d,), # Part Alternation Mark + (0x03297, 0x03297,), # Circled Ideograph Congratulation + (0x03299, 0x03299,), # Circled Ideograph Secret + (0x1f004, 0x1f004,), # Mahjong Tile Red Dragon + (0x1f202, 0x1f202,), # Squared Katakana Sa + (0x1f21a, 0x1f21a,), # Squared Cjk Unified Ideograph-7121 + (0x1f22f, 0x1f22f,), # Squared Cjk Unified Ideograph-6307 + (0x1f237, 0x1f237,), # Squared Cjk Unified Ideograph-6708 + (0x1f30d, 0x1f30f,), # Earth Globe Europe-afric..Earth Globe Asia-austral + (0x1f315, 0x1f315,), # Full Moon Symbol + (0x1f31c, 0x1f31c,), # Last Quarter Moon With Face + (0x1f378, 0x1f378,), # Cocktail Glass + (0x1f393, 0x1f393,), # Graduation Cap + (0x1f3a7, 0x1f3a7,), # Headphone + (0x1f3ac, 0x1f3ae,), # Clapper Board ..Video Game + (0x1f3c2, 0x1f3c2,), # Snowboarder + (0x1f3c4, 0x1f3c4,), # Surfer + (0x1f3c6, 0x1f3c6,), # Trophy + (0x1f3ca, 0x1f3ca,), # Swimmer + (0x1f3e0, 0x1f3e0,), # House Building + (0x1f3ed, 0x1f3ed,), # Factory + (0x1f408, 0x1f408,), # Cat + (0x1f415, 0x1f415,), # Dog + (0x1f41f, 0x1f41f,), # Fish + (0x1f426, 0x1f426,), # Bird + (0x1f442, 0x1f442,), # Ear + (0x1f446, 0x1f449,), # White Up Pointing Backha..White Right Pointing Bac + (0x1f44d, 0x1f44e,), # Thumbs Up Sign ..Thumbs Down Sign + (0x1f453, 0x1f453,), # Eyeglasses + (0x1f46a, 0x1f46a,), # Family + (0x1f47d, 0x1f47d,), # Extraterrestrial Alien + (0x1f4a3, 0x1f4a3,), # Bomb + (0x1f4b0, 0x1f4b0,), # Money Bag + (0x1f4b3, 0x1f4b3,), # Credit Card + (0x1f4bb, 0x1f4bb,), # Personal Computer + (0x1f4bf, 0x1f4bf,), # Optical Disc + (0x1f4cb, 0x1f4cb,), # Clipboard + (0x1f4da, 0x1f4da,), # Books + (0x1f4df, 0x1f4df,), # Pager + (0x1f4e4, 0x1f4e6,), # Outbox Tray ..Package + (0x1f4ea, 0x1f4ed,), # Closed Mailbox With Lowe..Open Mailbox With Lowere + (0x1f4f7, 0x1f4f7,), # Camera + (0x1f4f9, 0x1f4fb,), # Video Camera ..Radio + (0x1f508, 0x1f508,), # Speaker + (0x1f50d, 0x1f50d,), # Left-pointing Magnifying Glass + (0x1f512, 0x1f513,), # Lock ..Open Lock + (0x1f550, 0x1f567,), # Clock Face One Oclock ..Clock Face Twelve-thirty + (0x1f610, 0x1f610,), # Neutral Face + (0x1f687, 0x1f687,), # Metro + (0x1f68d, 0x1f68d,), # Oncoming Bus + (0x1f691, 0x1f691,), # Ambulance + (0x1f694, 0x1f694,), # Oncoming Police Car + (0x1f698, 0x1f698,), # Oncoming Automobile + (0x1f6ad, 0x1f6ad,), # No Smoking Symbol + (0x1f6b2, 0x1f6b2,), # Bicycle + (0x1f6b9, 0x1f6ba,), # Mens Symbol ..Womens Symbol + (0x1f6bc, 0x1f6bc,), # Baby Symbol + ), +} diff --git a/libs/wcwidth/table_vs16.py b/libs/wcwidth/table_vs16.py index 70e4a7373..9420156e4 100644 --- a/libs/wcwidth/table_vs16.py +++ b/libs/wcwidth/table_vs16.py @@ -1,7 +1,7 @@ """ Exports VS16_NARROW_TO_WIDE table keyed by supporting unicode version level. -This code generated by wcwidth/bin/update-tables.py on 2025-09-15 16:57:50 UTC. +This code generated by python wcwidth project. """ # pylint: disable=duplicate-code VS16_NARROW_TO_WIDE = { diff --git a/libs/wcwidth/table_wide.py b/libs/wcwidth/table_wide.py index ed6f48a73..4ad7bc1e0 100644 --- a/libs/wcwidth/table_wide.py +++ b/libs/wcwidth/table_wide.py @@ -1,7 +1,7 @@ """ Exports WIDE_EASTASIAN table keyed by supporting unicode version level. -This code generated by wcwidth/bin/update-tables.py on 2026-01-30 00:58:17 UTC. +This code generated by python wcwidth project. """ # pylint: disable=duplicate-code WIDE_EASTASIAN = { @@ -72,8 +72,8 @@ WIDE_EASTASIAN = { (0x0ffe0, 0x0ffe6,), # Fullwidth Cent Sign ..Fullwidth Won Sign (0x16fe0, 0x16fe3,), # Tangut Iteration Mark ..Old Chinese Iteration Ma (0x16ff2, 0x16ff6,), # Chinese Small Simplified..Yangqin Sign Slow Two Be - (0x17000, 0x18cd5,), # (nil) ..Khitan Small Script Char - (0x18cff, 0x18d1e,), # Khitan Small Script Char..(nil) + (0x17000, 0x18cd5,), # Tangut Ideograph-17000 ..Khitan Small Script Char + (0x18cff, 0x18d1e,), # Khitan Small Script Char..Tangut Ideograph-18d1e (0x18d80, 0x18df2,), # Tangut Component-769 ..Tangut Component-883 (0x1aff0, 0x1aff3,), # Katakana Letter Minnan T..Katakana Letter Minnan T (0x1aff5, 0x1affb,), # Katakana Letter Minnan T..Katakana Letter Minnan N diff --git a/libs/wcwidth/table_zero.py b/libs/wcwidth/table_zero.py index c440bfcf1..bee2431ab 100644 --- a/libs/wcwidth/table_zero.py +++ b/libs/wcwidth/table_zero.py @@ -1,7 +1,7 @@ """ Exports ZERO_WIDTH table keyed by supporting unicode version level. -This code generated by wcwidth/bin/update-tables.py on 2026-01-30 00:48:24 UTC. +This code generated by python wcwidth project. """ # pylint: disable=duplicate-code ZERO_WIDTH = { diff --git a/libs/wcwidth/text_sizing.py b/libs/wcwidth/text_sizing.py new file mode 100644 index 000000000..2f0069f09 --- /dev/null +++ b/libs/wcwidth/text_sizing.py @@ -0,0 +1,202 @@ +r""" +`kitty text sizing protocol`_ (OSC 66) parsing and measurement. + +The kitty text sizing protocol allows terminal apps to explicitly tell +terminals how many cells text occupies, using the escape sequence:: + + ESC ] 66 ; metadata ; text BEL/ST + +Metadata is colon-separated ``key=value`` pairs: + +- ``s``: scale +- ``w``: width in cells +- ``n``: fractional numerator +- ``d``: fractional denominator +- ``v``: vertical alignment +- ``h``: horizontal alignment + +Parsing is pretty straight-forward: + +- When ``w > 0``, return ``s * w``. +- Otherwise ``w == 0``, ``s * wcswidth(inner_text_width)`` cells. + +Numerator, denominator, and alignment codes and values are parsed but otherwise ignored +and have no effect on measurements made in this library. + +.. _`kitty text sizing protocol`: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ + +.. versionadded:: 0.7.0 +""" + +from __future__ import annotations + +# std imports +import re + +import typing + +# local +from ._wcswidth import wcswidth + + +class _FieldMeta(typing.NamedTuple): + name: str + low: int + high: int + default: int + + +TEXT_FIELD_MAPPING: dict[str, _FieldMeta] = { + 's': _FieldMeta(name='scale', low=1, high=7, default=1), + 'w': _FieldMeta(name='width', low=0, high=7, default=0), + 'n': _FieldMeta(name='numerator', low=0, high=15, default=0), + 'd': _FieldMeta(name='denominator', low=0, high=15, default=0), + 'v': _FieldMeta(name='vertical_align', low=0, high=2, default=0), + 'h': _FieldMeta(name='horizontal_align', low=0, high=2, default=0)} + + +class TextSizingParams(typing.NamedTuple): + """ + Parsed parameters from a text sizing escape sequence (OSC 66). + + :param scale: Scale factor (1-7). Text occupies ``scale`` rows tall and ``scale * width`` + columns wide. + :param width: Width in cells (0-7). When 0, width is auto-calculated from the inner text. + :param numerator: Fractional scaling numerator (0-15). + :param denominator: Fractional scaling denominator (0-15). + :param vertical_align: Vertical alignment (0=top, 1=bottom, 2=center). + :param horizontal_align: Horizontal alignment (0=left, 1=right, 2=center). + """ + + scale: int = 1 + width: int = 0 + numerator: int = 0 + denominator: int = 0 + vertical_align: int = 0 + horizontal_align: int = 0 + + def __repr__(self) -> str: + """ + Return a compact representation including only non-default fields. + + This avoids verbose output when most fields are defaults. + """ + # modified to show values only when non-default + repr_fmt = ', '.join(f'{field.name}={getattr(self, field.name)}' + for field in TEXT_FIELD_MAPPING.values() + if getattr(self, field.name) != field.default) + return f'{self.__class__.__name__}({repr_fmt})' + + def make_sequence(self) -> str: + """Build and return sub-part of an OSC 66 sequence.""" + parts = [] + # build string for all known parameters of non-default values + for field_key, field in TEXT_FIELD_MAPPING.items(): + if (val := getattr(self, field.name)) != field.default: + parts.append(f'{field_key}={val}') + return ':'.join(parts) + + @classmethod + def from_params(cls, raw: str, control_codes: str = 'parse') -> TextSizingParams: + """ + Parse colon-separated ``key=value`` metadata string. + + :param raw: Metadata string, e.g. ``'s=2:w=3'``. + :param control_codes: 'parse' or 'strict'. + :raises ValueError: If ``control_codes='strict'`` unrecognized text sizing parameters raise + ValueError. + :returns: Parsed parameters with values clamped to valid ranges. + Unknown keys are ignored. Non-integer values use defaults. + + Example:: + + >>> TextSizingParams.from_params('s=2:w=3') + TextSizingParams(scale=2, width=3, numerator=0, denominator=0, \ + vertical_align=0, horizontal_align=0) + """ + kwargs: typing.Dict[str, int] = {} + if not raw: + return cls() + for part in raw.split(':'): + if '=' not in part: + if control_codes == 'strict': + raise ValueError(f"Expected '=' in text sizing parameter (key=val), " + f"got {part!r} in OSC 66 sequence, {raw!r}") + continue + key, _eq, val = part.partition('=') + field = TEXT_FIELD_MAPPING.get(key) + if field is None: + if control_codes == 'strict': + raise ValueError(f"Unknown text sizing field '{key}' " + f"in OSC 66 sequence, {raw!r}") + # ignore unknown fields unless 'strict' + continue + try: + value = int(val) + except ValueError as exc: + if control_codes == 'strict': + raise ValueError(f"Illegal text sizing value '{val}' " + f"in OSC 66 sequence, {raw!r}: {exc}") from exc + # ignore value, uses default value without warning unless 'strict' + continue + if control_codes == 'strict' and (value > field.high or value < field.low): + raise ValueError(f"Out of bounds text sizing value '{val}' " + f"in OSC 66 sequence, {raw!r}: " + f"allowed range for '{key}' ({field.name}) " + f"is {field.low} to {field.high}") + kwargs[field.name] = max(field.low, min(field.high, value)) + return cls(**kwargs) + + +class TextSizing(typing.NamedTuple): + """Basic horizontal width measurement for kitty text sizing protocol.""" + + params: TextSizingParams + text: str + terminator: str + + @classmethod + def from_match(cls, match: re.Match[str], control_codes: str = 'parse') -> TextSizing: + r""" + Parse using matching OSC 66 Sequence. + + :param match: match object from :attr:`wcwidth.escape_sequences.TEXT_SIZING_PATTERN`. + :param control_codes: 'parse' or 'strict', same meaning as delegated by + :func:`wcwidth.width`. + :raises ValueError: When ``control_codes='strict'`` for unrecognized, invalid, or out of + bounds text sizing parameters. + :returns: TextSizing object from parsed sequence + + Example:: + + from wcwidth.escape_sequences import TEXT_SIZING_PATTERN + >>> TextSizing.from_match(TEXT_SIZING_PATTERN.match('\x1b]66;w=2;XY\x07')) + TextSizing(params=TextSizingParams(scale=1, width=2, numerator=0, denominator=0, \ + vertical_align=0, horizontal_align=0), text='XY', terminator='\x07') + """ + return cls(params=TextSizingParams.from_params(match.group(1), control_codes=control_codes), + text=match.group(2), + terminator=match.group(3)) + + def display_width(self, ambiguous_width: int = 1) -> int: + """ + Calculate the display width of a text sizing sequence. + + :param ambiguous_width: Width for East Asian Ambiguous characters. + :returns: Display width in terminal cells. When ``width > 0``, returns + ``params.scale * params.width``. When ``width == 0``, returns + ``params.scale * measured_inner_width``. + + .. note: Fractional scaling (numerator/denominator) does not affect the + cell count, it adjusts only the font size within the cells allocated by 'w'. + """ + if self.params.width > 0: + return self.params.scale * self.params.width + w = wcswidth(self.text, ambiguous_width=ambiguous_width) + if w < 0: + w = 0 + return self.params.scale * w + + def make_sequence(self) -> str: + """Build and return complete OSC 66 Terminal Sequence.""" + return f'\x1b]66;{self.params.make_sequence()};{self.text}{self.terminator}' diff --git a/libs/wcwidth/textwrap.py b/libs/wcwidth/textwrap.py index 4582cd5e0..0cf2ced4f 100644 --- a/libs/wcwidth/textwrap.py +++ b/libs/wcwidth/textwrap.py @@ -4,55 +4,26 @@ Sequence-aware text wrapping functions. This module provides functions for wrapping text that may contain terminal escape sequences, with proper handling of Unicode grapheme clusters and character display widths. """ + from __future__ import annotations # std imports -import re import secrets import textwrap -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Optional # local -from .wcwidth import width as _width -from .wcwidth import iter_sequences +from ._width import width as wcwidth_width from .grapheme import iter_graphemes +from .hyperlink import HyperlinkParams from .sgr_state import propagate_sgr as _propagate_sgr -from .escape_sequences import ZERO_WIDTH_PATTERN +from .escape_sequences import ZERO_WIDTH_PATTERN, iter_sequences if TYPE_CHECKING: # pragma: no cover from typing import Any, Literal -class _HyperlinkState(NamedTuple): - """State for tracking an open OSC 8 hyperlink across line breaks.""" - - url: str # hyperlink target URL - params: str # id=xxx and other key=value pairs separated by : - terminator: str # BEL (\x07) or ST (\x1b\\) - - -# Hyperlink parsing: captures (params, url, terminator) -_HYPERLINK_OPEN_RE = re.compile(r'\x1b]8;([^;]*);([^\x07\x1b]*)(\x07|\x1b\\)') - - -def _parse_hyperlink_open(seq: str) -> _HyperlinkState | None: - """Parse OSC 8 open sequence, return state or None.""" - if (m := _HYPERLINK_OPEN_RE.match(seq)): - return _HyperlinkState(url=m.group(2), params=m.group(1), terminator=m.group(3)) - return None - - -def _make_hyperlink_open(url: str, params: str, terminator: str) -> str: - """Generate OSC 8 open sequence.""" - return f'\x1b]8;{params};{url}{terminator}' - - -def _make_hyperlink_close(terminator: str) -> str: - """Generate OSC 8 close sequence.""" - return f'\x1b]8;;{terminator}' - - class SequenceTextWrapper(textwrap.TextWrapper): """ Sequence-aware text wrapper extending :class:`textwrap.TextWrapper`. @@ -77,6 +48,7 @@ class SequenceTextWrapper(textwrap.TextWrapper): control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', tabsize: int = 8, ambiguous_width: int = 1, + term_program: bool | str = False, **kwargs: Any) -> None: """ Initialize the wrapper. @@ -85,12 +57,20 @@ class SequenceTextWrapper(textwrap.TextWrapper): :param control_codes: How to handle control sequences (see :func:`~.width`). :param tabsize: Tab stop width for tab expansion. :param ambiguous_width: Width to use for East Asian Ambiguous (A) characters. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 :param kwargs: Additional arguments passed to :class:`textwrap.TextWrapper`. """ super().__init__(width=width, **kwargs) self.control_codes = control_codes self.tabsize = tabsize self.ambiguous_width = ambiguous_width + self.term_program = term_program @staticmethod def _next_hyperlink_id() -> str: @@ -99,8 +79,9 @@ class SequenceTextWrapper(textwrap.TextWrapper): def _width(self, text: str) -> int: """Measure text width accounting for sequences.""" - return _width(text, control_codes=self.control_codes, tabsize=self.tabsize, - ambiguous_width=self.ambiguous_width) + return wcwidth_width(text, control_codes=self.control_codes, tabsize=self.tabsize, + ambiguous_width=self.ambiguous_width, + term_program=self.term_program) def _strip_sequences(self, text: str) -> str: """Strip all terminal sequences from text.""" @@ -241,9 +222,9 @@ class SequenceTextWrapper(textwrap.TextWrapper): lines: list[str] = [] is_first_line = True - hyperlink_state: _HyperlinkState | None = None + hyperlink_state: Optional[HyperlinkParams] = None # Track the id we're using for the current hyperlink continuation - current_hyperlink_id: str | None = None + current_hyperlink_id: Optional[str] = None # Arrange in reverse order so items can be efficiently popped chunks = list(reversed(chunks)) @@ -258,8 +239,11 @@ class SequenceTextWrapper(textwrap.TextWrapper): # If continuing a hyperlink from previous line, prepend open sequence if hyperlink_state is not None: - open_seq = _make_hyperlink_open( - hyperlink_state.url, hyperlink_state.params, hyperlink_state.terminator) + open_seq = HyperlinkParams( + url=hyperlink_state.url, + params=hyperlink_state.params, + terminator=hyperlink_state.terminator, + ).make_open() chunks[-1] = open_seq + chunks[-1] # Drop leading whitespace (except at very start) @@ -332,26 +316,33 @@ class SequenceTextWrapper(textwrap.TextWrapper): if 'id=' in new_state.params: current_hyperlink_id = new_state.params elif new_state.params: - # Prepend id to existing params (per OSC 8 spec, params can have - # multiple key=value pairs separated by :) + # Prepend id to existing params. Per OSC 8 spec, params can have + # multiple key=value pairs separated by ':'. current_hyperlink_id = ( f'id={self._next_hyperlink_id()}:{new_state.params}') else: current_hyperlink_id = f'id={self._next_hyperlink_id()}' - line_content += _make_hyperlink_close(new_state.terminator) + line_content += HyperlinkParams( + terminator=new_state.terminator, url='').make_close() # Also need to inject the id into the opening # sequence if it didn't have one if 'id=' not in new_state.params: # Find and replace the original open sequence with one that has id - old_open = _make_hyperlink_open( - new_state.url, new_state.params, new_state.terminator) - new_open = _make_hyperlink_open( - new_state.url, current_hyperlink_id, new_state.terminator) + old_open = HyperlinkParams( + url=new_state.url, + params=new_state.params, + terminator=new_state.terminator, + ).make_open() + new_open = HyperlinkParams( + url=new_state.url, + params=current_hyperlink_id, + terminator=new_state.terminator, + ).make_open() line_content = line_content.replace(old_open, new_open, 1) # Update state for next line, using computed id - hyperlink_state = _HyperlinkState( + hyperlink_state = HyperlinkParams( new_state.url, current_hyperlink_id, new_state.terminator) else: hyperlink_state = None @@ -364,7 +355,7 @@ class SequenceTextWrapper(textwrap.TextWrapper): lines.append(indent + line_content) is_first_line = False else: - # max_lines reached with remaining content — + # max_lines reached with remaining content. # pop chunks until placeholder fits, then break. placeholder_w = self._width(self.placeholder) while current_line: @@ -375,8 +366,8 @@ class SequenceTextWrapper(textwrap.TextWrapper): new_state = self._track_hyperlink_state( line_content, hyperlink_state) if new_state is not None: - line_content += _make_hyperlink_close( - new_state.terminator) + line_content += HyperlinkParams( + terminator=new_state.terminator, url='').make_close() lines.append(indent + line_content + self.placeholder) break current_width -= self._width(current_line[-1]) @@ -395,7 +386,7 @@ class SequenceTextWrapper(textwrap.TextWrapper): def _track_hyperlink_state( self, text: str, - state: _HyperlinkState | None) -> _HyperlinkState | None: + state: Optional[HyperlinkParams]) -> Optional[HyperlinkParams]: """ Track hyperlink state through text. @@ -405,7 +396,7 @@ class SequenceTextWrapper(textwrap.TextWrapper): """ for segment, is_seq in iter_sequences(text): if is_seq: - parsed_link = _parse_hyperlink_open(segment) + parsed_link = HyperlinkParams.parse(segment) if parsed_link is not None and parsed_link.url: # has URL = open state = parsed_link elif segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')): # close @@ -539,13 +530,14 @@ def wrap(text: str, width: int = 70, *, expand_tabs: bool = True, replace_whitespace: bool = True, ambiguous_width: int = 1, + term_program: bool | str = False, initial_indent: str = '', subsequent_indent: str = '', fix_sentence_endings: bool = False, break_long_words: bool = True, break_on_hyphens: bool = True, drop_whitespace: bool = True, - max_lines: int | None = None, + max_lines: Optional[int] = None, placeholder: str = ' [...]', propagate_sgr: bool = True) -> list[str]: r""" @@ -568,6 +560,13 @@ def wrap(text: str, width: int = 70, *, may differ from stdlib for non-space whitespace characters. :param ambiguous_width: Width to use for East Asian Ambiguous (A) characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. + :param term_program: Terminal software identifier for table correction. + ``False`` (default) disables override lookup. ``True`` reads the + ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. + Accepts a canonical terminal name matching :func:`list_term_programs`, + such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. + + .. versionadded:: 0.8.0 :param initial_indent: String prepended to first line. :param subsequent_indent: String prepended to subsequent lines. :param fix_sentence_endings: If True, ensure sentences are always @@ -639,6 +638,7 @@ def wrap(text: str, width: int = 70, *, expand_tabs=expand_tabs, replace_whitespace=replace_whitespace, ambiguous_width=ambiguous_width, + term_program=term_program, initial_indent=initial_indent, subsequent_indent=subsequent_indent, fix_sentence_endings=fix_sentence_endings, diff --git a/libs/wcwidth/unicode_versions.py b/libs/wcwidth/unicode_versions.py index e8484116a..fdeb4d600 100644 --- a/libs/wcwidth/unicode_versions.py +++ b/libs/wcwidth/unicode_versions.py @@ -1,7 +1,7 @@ """ Exports function list_versions() for unicode version level support. -This code generated by wcwidth/bin/update-tables.py on 2026-01-27 00:41:01 UTC. +This code generated by python wcwidth project. """ from __future__ import annotations diff --git a/libs/wcwidth/wcwidth.py b/libs/wcwidth/wcwidth.py index 98e7a635f..523c34f9d 100644 --- a/libs/wcwidth/wcwidth.py +++ b/libs/wcwidth/wcwidth.py @@ -1,163 +1,72 @@ """ -This is a python implementation of wcwidth() and wcswidth(). +Legacy compatibility module for wcwidth.wcwidth. -https://github.com/jquast/wcwidth +This file contains no new definitions and is provided only for backwards compatibility. This module +exists solely to support legacy "absolute" import paths, when this module published everything under +this single file, 'wcwidth':: -from Markus Kuhn's C code, retrieved from: - - http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c - -This is an implementation of wcwidth() and wcswidth() (defined in -IEEE Std 1002.1-2001) for Unicode. - -http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html -http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html - -In fixed-width output devices, Latin characters all occupy a single -"cell" position of equal width, whereas ideographic CJK characters -occupy two such cells. Interoperability between terminal-line -applications and (teletype-style) character terminals using the -UTF-8 encoding requires agreement on which character should advance -the cursor by how many cell positions. No established formal -standards exist at present on which Unicode character shall occupy -how many cell positions on character terminals. These routines are -a first attempt of defining such behavior based on simple rules -applied to data provided by the Unicode Consortium. - -For some graphical characters, the Unicode standard explicitly -defines a character-cell width via the definition of the East Asian -FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes. -In all these cases, there is no ambiguity about which width a -terminal shall use. For characters in the East Asian Ambiguous (A) -class, the width choice depends purely on a preference of backward -compatibility with either historic CJK or Western practice. -Choosing single-width for these characters is easy to justify as -the appropriate long-term solution, as the CJK practice of -displaying these characters as double-width comes from historic -implementation simplicity (8-bit encoded characters were displayed -single-width and 16-bit ones double-width, even for Greek, -Cyrillic, etc.) and not any typographic considerations. - -Much less clear is the choice of width for the Not East Asian -(Neutral) class. Existing practice does not dictate a width for any -of these characters. It would nevertheless make sense -typographically to allocate two character cells to characters such -as for instance EM SPACE or VOLUME INTEGRAL, which cannot be -represented adequately with a single-width glyph. The following -routines at present merely assign a single-cell width to all -neutral characters, in the interest of simplicity. This is not -entirely satisfactory and should be reconsidered before -establishing a formal standard in this area. At the moment, the -decision which Not East Asian (Neutral) characters should be -represented by double-width glyphs cannot yet be answered by -applying a simple rule from the Unicode database content. Setting -up a proper standard for the behavior of UTF-8 character terminals -will require a careful analysis not only of each Unicode character, -but also of each presentation form, something the author of these -routines has avoided to do so far. - -http://www.unicode.org/unicode/reports/tr11/ - -Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c + from wcwidth.wcwidth import wcswidth + import wcwidth.wcwidth as wc_module """ +# pylint: disable=unused-import -from __future__ import annotations - -# std imports -from functools import lru_cache - -from typing import TYPE_CHECKING +__lazy_modules__ = [ + "wcwidth._clip", + "wcwidth._constants", + "wcwidth._wcswidth", + "wcwidth._wcwidth", + "wcwidth._width", + "wcwidth.align", + "wcwidth.bisearch", + "wcwidth.control_codes", + "wcwidth.escape_sequences", + "wcwidth.grapheme", + "wcwidth.sgr_state", + "wcwidth.table_ambiguous", + "wcwidth.table_grapheme", + "wcwidth.table_mc", + "wcwidth.table_vs16", + "wcwidth.table_wide", + "wcwidth.table_zero", + "wcwidth.unicode_versions", +] # local +from ._clip import clip +from .align import ljust, rjust, center +from ._width import _CONTROL_CHAR_TABLE, _WIDTH_FAST_PATH_MIN_LEN, width, _width_ignored_codes +from ._wcwidth import wcwidth, _wcmatch_version, _wcversion_value from .bisearch import bisearch as _bisearch from .grapheme import iter_graphemes from .table_mc import CATEGORY_MC +from ._wcswidth import wcswidth, wcstwidth from .sgr_state import (_SGR_PATTERN, _SGR_STATE_DEFAULT, _sgr_state_update, _sgr_state_is_active, _sgr_state_to_sequence) +from ._constants import (_EMOJI_ZWJ_SET, + _ISC_VIRAMA_SET, + _LATEST_VERSION, + _AMBIGUOUS_TABLE, + _ZERO_WIDTH_TABLE, + _CATEGORY_MC_TABLE, + _FITZPATRICK_RANGE, + _WIDE_EASTASIAN_TABLE, + _REGIONAL_INDICATOR_SET) from .table_vs16 import VS16_NARROW_TO_WIDE from .table_wide import WIDE_EASTASIAN from .table_zero import ZERO_WIDTH from .control_codes import ILLEGAL_CTRL, VERTICAL_CTRL, HORIZONTAL_CTRL, ZERO_WIDTH_CTRL -from .table_grapheme import ISC_CONSONANT, EXTENDED_PICTOGRAPHIC, GRAPHEME_REGIONAL_INDICATOR from .table_ambiguous import AMBIGUOUS_EASTASIAN from .escape_sequences import (ZERO_WIDTH_PATTERN, CURSOR_LEFT_SEQUENCE, CURSOR_RIGHT_SEQUENCE, - INDETERMINATE_EFFECT_SEQUENCE) + INDETERMINATE_EFFECT_SEQUENCE, + iter_sequences, + strip_sequences) from .unicode_versions import list_versions -if TYPE_CHECKING: # pragma: no cover - # std imports - from collections.abc import Iterator - - from typing import Literal - -# Pre-compute table references for the latest (and only) Unicode version. -_LATEST_VERSION = list_versions()[-1] -_ZERO_WIDTH_TABLE = ZERO_WIDTH[_LATEST_VERSION] -_WIDE_EASTASIAN_TABLE = WIDE_EASTASIAN[_LATEST_VERSION] -_AMBIGUOUS_TABLE = AMBIGUOUS_EASTASIAN[next(iter(AMBIGUOUS_EASTASIAN))] -_CATEGORY_MC_TABLE = CATEGORY_MC[_LATEST_VERSION] -_REGIONAL_INDICATOR_SET = frozenset( - range(GRAPHEME_REGIONAL_INDICATOR[0][0], GRAPHEME_REGIONAL_INDICATOR[0][1] + 1) -) -_EMOJI_ZWJ_SET = frozenset( - cp for lo, hi in EXTENDED_PICTOGRAPHIC for cp in range(lo, hi + 1) -) | _REGIONAL_INDICATOR_SET -_FITZPATRICK_RANGE = (0x1F3FB, 0x1F3FF) -# Indic_Syllabic_Category=Virama codepoints, from IndicSyllabicCategory.txt. -# These are structurally tied to their scripts and not expected to change. -# https://www.unicode.org/Public/UCD/latest/ucd/IndicSyllabicCategory.txt -_ISC_VIRAMA_SET = frozenset(( - 0x094D, # DEVANAGARI SIGN VIRAMA - 0x09CD, # BENGALI SIGN VIRAMA - 0x0A4D, # GURMUKHI SIGN VIRAMA - 0x0ACD, # GUJARATI SIGN VIRAMA - 0x0B4D, # ORIYA SIGN VIRAMA - 0x0BCD, # TAMIL SIGN VIRAMA - 0x0C4D, # TELUGU SIGN VIRAMA - 0x0CCD, # KANNADA SIGN VIRAMA - 0x0D4D, # MALAYALAM SIGN VIRAMA - 0x0DCA, # SINHALA SIGN AL-LAKUNA - 0x1B44, # BALINESE ADEG ADEG - 0xA806, # SYLOTI NAGRI SIGN HASANTA - 0xA8C4, # SAURASHTRA SIGN VIRAMA - 0xA9C0, # JAVANESE PANGKON - 0x11046, # BRAHMI VIRAMA - 0x110B9, # KAITHI SIGN VIRAMA - 0x111C0, # SHARADA SIGN VIRAMA - 0x11235, # KHOJKI SIGN VIRAMA - 0x1134D, # GRANTHA SIGN VIRAMA - 0x11442, # NEWA SIGN VIRAMA - 0x114C2, # TIRHUTA SIGN VIRAMA - 0x115BF, # SIDDHAM SIGN VIRAMA - 0x1163F, # MODI SIGN VIRAMA - 0x116B6, # TAKRI SIGN VIRAMA - 0x11839, # DOGRA SIGN VIRAMA - 0x119E0, # NANDINAGARI SIGN VIRAMA - 0x11C3F, # BHAIKSUKI SIGN VIRAMA -)) -_ISC_CONSONANT_TABLE = ISC_CONSONANT - -# In 'parse' mode, strings longer than this are checked for cursor-movement -# controls (BS, TAB, CR, cursor sequences); when absent, mode downgrades to -# 'ignore' to skip character-by-character parsing. The detection scan cost is -# negligible for long strings but wasted on short ones like labels or headings. -_WIDTH_FAST_PATH_MIN_LEN = 20 - -# Translation table to strip C0/C1 control characters for fast 'ignore' mode. -_CONTROL_CHAR_TABLE = str.maketrans('', '', ( - ''.join(chr(c) for c in range(0x00, 0x20)) + # C0: NUL through US (including tab) - '\x7f' + # DEL - ''.join(chr(c) for c in range(0x80, 0xa0)) # C1: U+0080-U+009F -)) - -# Unlike wcwidth.__all__, wcwidth.wcwidth.__all__ is NOT for the purpose of defining a public API, -# or what we prefer to be imported with statement, "from wcwidth.wcwidth import *". Explicitly -# re-export imports here for no other reason than to satisfy the type checkers (mypy). Yak shavings. __all__ = ( 'ZERO_WIDTH', 'WIDE_EASTASIAN', @@ -166,6 +75,7 @@ __all__ = ( 'list_versions', 'wcwidth', 'wcswidth', + 'wcstwidth', 'width', 'iter_sequences', 'ljust', @@ -176,855 +86,3 @@ __all__ = ( '_wcmatch_version', '_wcversion_value', ) - - -# maxsize=1024: western scripts need ~64 unique codepoints per session, but -# CJK sessions may use ~2000 of ~3500 common hanzi/kanji. 1024 accommodates -# heavy CJK use. Performance floor at 32; bisearch is ~100ns per miss. - -@lru_cache(maxsize=1024) -def wcwidth(wc: str, unicode_version: str = 'auto', ambiguous_width: int = 1) -> int: # pylint: disable=unused-argument - r""" - Given one Unicode codepoint, return its printable length on a terminal. - - :param wc: A single Unicode character. - :param unicode_version: Ignored. Retained for backwards compatibility. - - .. deprecated:: 0.3.0 - Only the latest Unicode version is now shipped. - - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts - where ambiguous characters display as double-width. See - :ref:`ambiguous_width` for details. - :returns: The width, in cells, necessary to display the character of - Unicode string character, ``wc``. Returns 0 if the ``wc`` argument has - no printable effect on a terminal (such as NUL '\0'), -1 if ``wc`` is - not printable, or has an indeterminate effect on the terminal, such as - a control character. Otherwise, the number of column positions the - character occupies on a graphic terminal (1 or 2) is returned. - - See :ref:`Specification` for details of cell measurement. - """ - ucs = ord(wc) if wc else 0 - - # small optimization: early return of 1 for printable ASCII, this provides - # approximately 40% performance improvement for mostly-ascii documents, with - # less than 1% impact to others. - if 32 <= ucs < 0x7f: - return 1 - - # C0/C1 control characters are -1 for compatibility with POSIX-like calls - if ucs and ucs < 32 or 0x07F <= ucs < 0x0A0: - return -1 - - # Zero width - if _bisearch(ucs, _ZERO_WIDTH_TABLE): - return 0 - - # Wide (F/W categories) - if _bisearch(ucs, _WIDE_EASTASIAN_TABLE): - return 2 - - # Ambiguous width (A category) - only when ambiguous_width=2 - if ambiguous_width == 2 and _bisearch(ucs, _AMBIGUOUS_TABLE): - return 2 - - return 1 - - -def wcswidth( - pwcs: str, - n: int | None = None, - unicode_version: str = 'auto', - ambiguous_width: int = 1, -) -> int: - """ - Given a unicode string, return its printable length on a terminal. - - :param pwcs: Measure width of given unicode string. - :param n: When ``n`` is None (default), return the length of the entire - string, otherwise only the first ``n`` characters are measured. - - Better to use string slicing capability, ``wcswidth(pwcs[:n])``, instead, - for performance. This argument is a holdover from the POSIX function for - matching signatures. Be careful that ``n`` is at grapheme boundaries. - - :param unicode_version: Ignored. Retained for backwards compatibility. - - .. deprecated:: 0.3.0 - Only the latest Unicode version is now shipped. - - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. - :returns: The width, in cells, needed to display the first ``n`` characters - of the unicode string ``pwcs``. Returns ``-1`` for C0 and C1 control - characters! - - See :ref:`Specification` for details of cell measurement. - """ - # pylint: disable=unused-argument,too-many-locals,too-many-statements - # pylint: disable=too-complex,too-many-branches - # This function intentionally kept long without delegating functions to reduce function calls in - # "hot path", the overhead per-character adds up. - - # Fast path: pure ASCII printable strings are always width == length - if n is None and pwcs.isascii() and pwcs.isprintable(): - return len(pwcs) - - # Select wcwidth call pattern for best lru_cache performance: - # - ambiguous_width=1 (default): single-arg calls share cache with direct wcwidth() calls - # - ambiguous_width=2: full positional args needed (results differ, separate cache is correct) - _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) - - end = len(pwcs) if n is None else n - total_width = 0 - idx = 0 - last_measured_idx = -2 # Track index of last measured char for VS16 - last_measured_ucs = -1 # Codepoint of last measured char (for deferred emoji check) - last_was_virama = False # Virama conjunct formation state - conjunct_pending = False # Deferred +1 for bare conjuncts (no trailing Mc) - while idx < end: - char = pwcs[idx] - ucs = ord(char) - if ucs == 0x200D: - if last_was_virama: - # ZWJ after virama requests explicit half-form rendering but - # does not change cell count — consume ZWJ only, let the next - # consonant be handled by the virama conjunct rule. - idx += 1 - elif idx + 1 < end: - # Emoji ZWJ: skip next character unconditionally. - idx += 2 - last_was_virama = False - else: - idx += 1 - last_was_virama = False - continue - if ucs == 0xFE0F and last_measured_idx >= 0: - # VS16 following a measured character: add 1 if that character is - # known to be converted from narrow to wide by VS16. - total_width += _bisearch(ord(pwcs[last_measured_idx]), - VS16_NARROW_TO_WIDE["9.0.0"]) - last_measured_idx = -2 # Prevent double application - # VS16 preserves emoji context: last_measured_ucs stays as the base - idx += 1 - continue - # Regional Indicator & Fitzpatrick: both above BMP (U+1F1E6+) - if ucs > 0xFFFF: - if ucs in _REGIONAL_INDICATOR_SET: - # Lazy RI pairing: count preceding consecutive RIs only when the last one is - # received, because RI's are received so rarely its better than per-loop tracking of - # 'last char was an RI'. - ri_before = 0 - j = idx - 1 - while j >= 0 and ord(pwcs[j]) in _REGIONAL_INDICATOR_SET: - ri_before += 1 - j -= 1 - if ri_before % 2 == 1: - # Second RI in pair: contributes 0 (pair = one 2-cell flag) using an even-or-odd - # check to determine, 'CAUS' would be two flags, but 'CAU' would be 1 flag - # and wide 'U'. - idx += 1 - last_measured_ucs = ucs - continue - # First or unpaired RI: measured normally (width 2 from table) - # Fitzpatrick modifier: zero-width when following emoji base - elif (_FITZPATRICK_RANGE[0] <= ucs <= _FITZPATRICK_RANGE[1] - and last_measured_ucs in _EMOJI_ZWJ_SET): - idx += 1 - continue - # Virama conjunct formation: consonant following virama contributes 0 width. - # See https://www.unicode.org/reports/tr44/#Indic_Syllabic_Category - if last_was_virama and _bisearch(ucs, _ISC_CONSONANT_TABLE): - last_measured_idx = idx - last_measured_ucs = ucs - last_was_virama = False - conjunct_pending = True - idx += 1 - continue - wcw = _wcwidth(char) - if wcw < 0: - # early return -1 on C0 and C1 control characters - return wcw - if wcw > 0: - if conjunct_pending: - total_width += 1 - conjunct_pending = False - last_measured_idx = idx - last_measured_ucs = ucs - last_was_virama = False - elif last_measured_idx >= 0 and _bisearch(ucs, _CATEGORY_MC_TABLE): - # Spacing Combining Mark (Mc) following a base character adds 1 - wcw = 1 - last_measured_idx = -2 - last_was_virama = False - conjunct_pending = False - else: - last_was_virama = ucs in _ISC_VIRAMA_SET - total_width += wcw - idx += 1 - if conjunct_pending: - total_width += 1 - return total_width - - -# NOTE: _wcversion_value and _wcmatch_version are no longer used internally -# by wcwidth since version 0.5.0 (only the latest Unicode version is shipped). -# -# They are retained for API compatibility with external tools like ucs-detect -# that may use these private functions. - - -@lru_cache(maxsize=128) -def _wcversion_value(ver_string: str) -> tuple[int, ...]: # pragma: no cover - """ - Integer-mapped value of given dotted version string. - - .. deprecated:: 0.3.0 - - This function is no longer used internally by wcwidth but is retained - for API compatibility with external tools. - - :param ver_string: Unicode version string, of form ``n.n.n``. - :returns: tuple of digit tuples, ``tuple(int, [...])``. - """ - retval = tuple(map(int, (ver_string.split('.')))) - return retval - - -@lru_cache(maxsize=8) -def _wcmatch_version(given_version: str) -> str: # pylint: disable=unused-argument - """ - Return the supported Unicode version level. - - .. deprecated:: 0.3.0 - This function now always returns the latest version. - - This function is no longer used internally by wcwidth but is retained - for API compatibility with external tools. - - :param given_version: Ignored. Any value is accepted for compatibility. - :returns: The latest unicode version string. - """ - return _LATEST_VERSION - - -def iter_sequences(text: str) -> Iterator[tuple[str, bool]]: - r""" - Iterate through text, yielding segments with sequence identification. - - This generator yields tuples of ``(segment, is_sequence)`` for each part - of the input text, where ``is_sequence`` is ``True`` if the segment is - a recognized terminal escape sequence. - - :param text: String to iterate through. - :returns: Iterator of (segment, is_sequence) tuples. - - .. versionadded:: 0.3.0 - - Example:: - - >>> list(iter_sequences('hello')) - [('hello', False)] - >>> list(iter_sequences('\x1b[31mred')) - [('\x1b[31m', True), ('red', False)] - >>> list(iter_sequences('\x1b[1m\x1b[31m')) - [('\x1b[1m', True), ('\x1b[31m', True)] - """ - idx = 0 - text_len = len(text) - segment_start = 0 - - while idx < text_len: - char = text[idx] - - if char == '\x1b': - # Yield any accumulated non-sequence text - if idx > segment_start: - yield (text[segment_start:idx], False) - - # Try to match an escape sequence - match = ZERO_WIDTH_PATTERN.match(text, idx) - if match: - yield (match.group(), True) - idx = match.end() - else: - # Lone ESC or unrecognized - yield as sequence anyway - yield (char, True) - idx += 1 - segment_start = idx - else: - idx += 1 - - # Yield any remaining text - if segment_start < text_len: - yield (text[segment_start:], False) - - -def _width_ignored_codes(text: str, ambiguous_width: int = 1) -> int: - """ - Fast path for width() with control_codes='ignore'. - - Strips escape sequences and control characters, then measures remaining text. - """ - return wcswidth( - strip_sequences(text).translate(_CONTROL_CHAR_TABLE), - ambiguous_width=ambiguous_width - ) - - -def width( - text: str, - *, - control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', - tabsize: int = 8, - ambiguous_width: int = 1, -) -> int: - r""" - Return printable width of text containing many kinds of control codes and sequences. - - Unlike :func:`wcswidth`, this function handles most control characters and many popular terminal - output sequences. Never returns -1. - - :param text: String to measure. - :param control_codes: How to handle control characters and sequences: - - - ``'parse'`` (default): Track horizontal cursor movement from BS ``\b``, CR ``\r``, TAB - ``\t``, and cursor left and right movement sequences. Vertical movement (LF, VT, FF) and - indeterminate sequences are zero-width. Never raises. - - ``'strict'``: Like parse, but raises :exc:`ValueError` on control characters with - indeterminate results of the screen or cursor, like clear or vertical movement. Generally, - these should be handled with a virtual terminal emulator (like 'pyte'). - - ``'ignore'``: All C0 and C1 control characters and escape sequences are measured as - width 0. This is the fastest measurement for text already filtered or known not to contain - any kinds of control codes or sequences. TAB ``\t`` is zero-width; for tab expansion, - pre-process: ``text.replace('\t', ' ' * 8)``. - - :param tabsize: Tab stop width for ``'parse'`` and ``'strict'`` modes. Default is 8. - Must be positive. Has no effect when ``control_codes='ignore'``. - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. - :returns: Maximum cursor position reached, "extent", accounting for cursor movement sequences - present in ``text`` according to given parameters. This represents the rightmost column the - cursor reaches. Always a non-negative integer. - - :raises ValueError: If ``control_codes='strict'`` and control characters with indeterminate - effects, such as vertical movement or clear sequences are encountered, or on unexpected - C0 or C1 control code. Also raised when ``control_codes`` is not one of the valid values. - - .. versionadded:: 0.3.0 - - Examples:: - - >>> width('hello') - 5 - >>> width('コンニチハ') - 10 - >>> width('\x1b[31mred\x1b[0m') - 3 - >>> width('\x1b[31mred\x1b[0m', control_codes='ignore') # same result (ignored) - 3 - >>> width('123\b4') # backspace overwrites previous cell (outputs '124') - 3 - >>> width('abc\t') # tab caused cursor to move to column 8 - 8 - >>> width('1\x1b[10C') # '1' + cursor right 10, cursor ends on column 11 - 11 - >>> width('1\x1b[10C', control_codes='ignore') # faster but wrong in this case - 1 - """ - # pylint: disable=too-complex,too-many-branches,too-many-statements,too-many-locals - # This could be broken into sub-functions (#1, #3, and 6 especially), but for reduced overhead - # considering this function is a likely "hot path", they are inlined, breaking many of our - # complexity rules. - - # Fast path for ASCII printable (no tabs, escapes, or control chars) - if text.isascii() and text.isprintable(): - return len(text) - - # Fast parse: if no horizontal cursor movements are possible, switch to 'ignore' mode. - # Only check for longer strings - the detection overhead hurts short string performance. - if control_codes == 'parse' and len(text) > _WIDTH_FAST_PATH_MIN_LEN: - # Check for cursor-affecting control characters - if '\b' not in text and '\t' not in text and '\r' not in text: - # Check for escape sequences - if none, or only non-cursor-movement sequences - if '\x1b' not in text or ( - not CURSOR_RIGHT_SEQUENCE.search(text) and - not CURSOR_LEFT_SEQUENCE.search(text) - ): - control_codes = 'ignore' - - # Fast path for ignore mode -- this is useful if you know the text is already "clean" - if control_codes == 'ignore': - return _width_ignored_codes(text, ambiguous_width) - - strict = control_codes == 'strict' - # Track absolute positions: tab stops need modulo on absolute column, CR resets to 0. - # Initialize max_extent to 0 so backward movement (CR, BS) won't yield negative width. - current_col = 0 - max_extent = 0 - idx = 0 - last_measured_idx = -2 # Track index of last measured char for VS16; -2 can never match idx-1 - last_measured_ucs = -1 # Codepoint of last measured char (for deferred emoji check) - last_was_virama = False # Virama conjunct formation state - conjunct_pending = False # Deferred +1 for bare conjuncts (no trailing Mc) - text_len = len(text) - - # Select wcwidth call pattern for best lru_cache performance: - # - ambiguous_width=1 (default): single-arg calls share cache with direct wcwidth() calls - # - ambiguous_width=2: full positional args needed (results differ, separate cache is correct) - _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) - - while idx < text_len: - char = text[idx] - - # 1. Handle ESC sequences - if char == '\x1b': - match = ZERO_WIDTH_PATTERN.match(text, idx) - if match: - seq = match.group() - if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq): - raise ValueError(f"Indeterminate cursor sequence at position {idx}") - # Apply cursor movement - right = CURSOR_RIGHT_SEQUENCE.match(seq) - if right: - current_col += int(right.group(1) or 1) - else: - left = CURSOR_LEFT_SEQUENCE.match(seq) - if left: - current_col = max(0, current_col - int(left.group(1) or 1)) - idx = match.end() - else: - idx += 1 - max_extent = max(max_extent, current_col) - continue - - # 2. Handle illegal and vertical control characters (zero width, error in strict) - if char in ILLEGAL_CTRL: - if strict: - raise ValueError(f"Illegal control character {ord(char):#x} at position {idx}") - idx += 1 - continue - - if char in VERTICAL_CTRL: - if strict: - raise ValueError(f"Vertical movement character {ord(char):#x} at position {idx}") - idx += 1 - continue - - # 3. Handle horizontal movement characters - if char in HORIZONTAL_CTRL: - if char == '\x09' and tabsize > 0: # Tab - current_col += tabsize - (current_col % tabsize) - elif char == '\x08': # Backspace - if current_col > 0: - current_col -= 1 - elif char == '\x0d': # Carriage return - current_col = 0 - max_extent = max(max_extent, current_col) - idx += 1 - continue - - # 4. Handle ZWJ - if char == '\u200D': - if last_was_virama: - # ZWJ after virama requests explicit half-form rendering but - # does not change cell count — consume ZWJ only, let the next - # consonant be handled by the virama conjunct rule. - idx += 1 - elif idx + 1 < text_len: - # Emoji ZWJ: skip next character unconditionally. - idx += 2 - last_was_virama = False - else: - idx += 1 - last_was_virama = False - continue - - # 5. Handle other zero-width characters (control chars) - if char in ZERO_WIDTH_CTRL: - idx += 1 - continue - - ucs = ord(char) - - # 6. Handle VS16: converts preceding narrow character to wide - if ucs == 0xFE0F: - if last_measured_idx == idx - 1: - if _bisearch(ord(text[last_measured_idx]), VS16_NARROW_TO_WIDE["9.0.0"]): - current_col += 1 - max_extent = max(max_extent, current_col) - # VS16 preserves emoji context: last_measured_ucs stays as the base - idx += 1 - continue - - # 6b. Regional Indicator & Fitzpatrick: both above BMP (U+1F1E6+) - if ucs > 0xFFFF: - if ucs in _REGIONAL_INDICATOR_SET: - # Lazy RI pairing: count preceding consecutive RIs - ri_before = 0 - j = idx - 1 - while j >= 0 and ord(text[j]) in _REGIONAL_INDICATOR_SET: - ri_before += 1 - j -= 1 - if ri_before % 2 == 1: - last_measured_ucs = ucs - idx += 1 - continue - # 6c. Fitzpatrick modifier: zero-width when following emoji base - elif (_FITZPATRICK_RANGE[0] <= ucs <= _FITZPATRICK_RANGE[1] - and last_measured_ucs in _EMOJI_ZWJ_SET): - idx += 1 - continue - - # 7. Virama conjunct formation: consonant following virama contributes 0 width. - # See https://www.unicode.org/reports/tr44/#Indic_Syllabic_Category - if last_was_virama and _bisearch(ucs, _ISC_CONSONANT_TABLE): - last_measured_idx = idx - last_measured_ucs = ucs - last_was_virama = False - conjunct_pending = True - idx += 1 - continue - - # 8. Normal characters: measure with wcwidth - w = _wcwidth(char) - if w > 0: - if conjunct_pending: - current_col += 1 - conjunct_pending = False - current_col += w - max_extent = max(max_extent, current_col) - last_measured_idx = idx - last_measured_ucs = ucs - last_was_virama = False - elif last_measured_idx >= 0 and _bisearch(ucs, _CATEGORY_MC_TABLE): - # Spacing Combining Mark (Mc) following a base character adds 1 - current_col += 1 - max_extent = max(max_extent, current_col) - last_measured_idx = -2 - last_was_virama = False - conjunct_pending = False - else: - last_was_virama = ucs in _ISC_VIRAMA_SET - idx += 1 - - if conjunct_pending: - current_col += 1 - max_extent = max(max_extent, current_col) - return max_extent - - -def ljust( - text: str, - dest_width: int, - fillchar: str = ' ', - *, - control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', - ambiguous_width: int = 1, -) -> str: - r""" - Return text left-justified in a string of given display width. - - :param text: String to justify, may contain terminal sequences. - :param dest_width: Total display width of result in terminal cells. - :param fillchar: Single character for padding (default space). Must have - display width of 1 (not wide, not zero-width, not combining). Unicode - characters like ``'·'`` are acceptable. The width is not validated. - :param control_codes: How to handle control sequences when measuring. - Passed to :func:`width` for measurement. - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. - :returns: Text padded on the right to reach ``dest_width``. - - .. versionadded:: 0.3.0 - - Example:: - - >>> wcwidth.ljust('hi', 5) - 'hi ' - >>> wcwidth.ljust('\x1b[31mhi\x1b[0m', 5) - '\x1b[31mhi\x1b[0m ' - >>> wcwidth.ljust('\U0001F468\u200D\U0001F469\u200D\U0001F467', 6) - '👨‍👩‍👧 ' - """ - if text.isascii() and text.isprintable(): - text_width = len(text) - else: - text_width = width(text, control_codes=control_codes, ambiguous_width=ambiguous_width) - padding_cells = max(0, dest_width - text_width) - return text + fillchar * padding_cells - - -def rjust( - text: str, - dest_width: int, - fillchar: str = ' ', - *, - control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', - ambiguous_width: int = 1, -) -> str: - r""" - Return text right-justified in a string of given display width. - - :param text: String to justify, may contain terminal sequences. - :param dest_width: Total display width of result in terminal cells. - :param fillchar: Single character for padding (default space). Must have - display width of 1 (not wide, not zero-width, not combining). Unicode - characters like ``'·'`` are acceptable. The width is not validated. - :param control_codes: How to handle control sequences when measuring. - Passed to :func:`width` for measurement. - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. - :returns: Text padded on the left to reach ``dest_width``. - - .. versionadded:: 0.3.0 - - Example:: - - >>> wcwidth.rjust('hi', 5) - ' hi' - >>> wcwidth.rjust('\x1b[31mhi\x1b[0m', 5) - ' \x1b[31mhi\x1b[0m' - >>> wcwidth.rjust('\U0001F468\u200D\U0001F469\u200D\U0001F467', 6) - ' 👨‍👩‍👧' - """ - if text.isascii() and text.isprintable(): - text_width = len(text) - else: - text_width = width(text, control_codes=control_codes, ambiguous_width=ambiguous_width) - padding_cells = max(0, dest_width - text_width) - return fillchar * padding_cells + text - - -def center( - text: str, - dest_width: int, - fillchar: str = ' ', - *, - control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', - ambiguous_width: int = 1, -) -> str: - r""" - Return text centered in a string of given display width. - - :param text: String to center, may contain terminal sequences. - :param dest_width: Total display width of result in terminal cells. - :param fillchar: Single character for padding (default space). Must have - display width of 1 (not wide, not zero-width, not combining). Unicode - characters like ``'·'`` are acceptable. The width is not validated. - :param control_codes: How to handle control sequences when measuring. - Passed to :func:`width` for measurement. - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. - :returns: Text padded on both sides to reach ``dest_width``. - - For odd-width padding, the extra cell goes on the right (matching - Python's :meth:`str.center` behavior). - - .. versionadded:: 0.3.0 - - Example:: - - >>> wcwidth.center('hi', 6) - ' hi ' - >>> wcwidth.center('\x1b[31mhi\x1b[0m', 6) - ' \x1b[31mhi\x1b[0m ' - >>> wcwidth.center('\U0001F468\u200D\U0001F469\u200D\U0001F467', 6) - ' 👨‍👩‍👧 ' - """ - if text.isascii() and text.isprintable(): - text_width = len(text) - else: - text_width = width(text, control_codes=control_codes, ambiguous_width=ambiguous_width) - total_padding = max(0, dest_width - text_width) - # matching https://jazcap53.github.io/pythons-eccentric-strcenter.html - left_pad = total_padding // 2 + (total_padding & dest_width & 1) - right_pad = total_padding - left_pad - return fillchar * left_pad + text + fillchar * right_pad - - -def strip_sequences(text: str) -> str: - r""" - Return text with all terminal escape sequences removed. - - Unknown or incomplete ESC sequences are preserved. - - :param text: String that may contain terminal escape sequences. - :returns: The input text with all escape sequences stripped. - - .. versionadded:: 0.3.0 - - Example:: - - >>> strip_sequences('\x1b[31mred\x1b[0m') - 'red' - >>> strip_sequences('hello') - 'hello' - >>> strip_sequences('\x1b[1m\x1b[31mbold red\x1b[0m text') - 'bold red text' - """ - return ZERO_WIDTH_PATTERN.sub('', text) - - -def clip( - text: str, - start: int, - end: int, - *, - fillchar: str = ' ', - tabsize: int = 8, - ambiguous_width: int = 1, - propagate_sgr: bool = True, -) -> str: - r""" - Clip text to display columns ``(start, end)`` while preserving all terminal sequences. - - This function extracts a substring based on visible column positions rather than - character indices. Terminal escape sequences are preserved in the output since - they have zero display width. If a wide character (width 2) would be split at - either boundary, it is replaced with ``fillchar``. - - TAB characters (``\t``) are expanded to spaces up to the next tab stop, - controlled by the ``tabsize`` parameter. - - Other cursor movement characters (backspace, carriage return) and cursor - movement sequences are passed through unchanged as zero-width. - - :param text: String to clip, may contain terminal escape sequences. - :param start: Absolute starting column (inclusive, 0-indexed). - :param end: Absolute ending column (exclusive). - :param fillchar: Character to use when a wide character must be split at - a boundary (default space). Must have display width of 1. - :param tabsize: Tab stop width (default 8). Set to 0 to pass tabs through - as zero-width (preserved in output but don't advance column position). - :param ambiguous_width: Width to use for East Asian Ambiguous (A) - characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. - :param propagate_sgr: If True (default), SGR (terminal styling) sequences - are propagated. The result begins with any active style at the start - position and ends with a reset sequence if styles are active. - :returns: Substring of ``text`` spanning display columns ``(start, end)``, - with all terminal sequences preserved and wide characters at boundaries - replaced with ``fillchar``. - - SGR (terminal styling) sequences are propagated by default. The result - begins with any active style and ends with a reset:: - - >>> clip('\x1b[1;34mHello world\x1b[0m', 6, 11) - '\x1b[1;34mworld\x1b[0m' - - Set ``propagate_sgr=False`` to disable this behavior. - - .. versionadded:: 0.3.0 - - .. versionchanged:: 0.5.0 - Added ``propagate_sgr`` parameter (default True). - - Example:: - - >>> clip('hello world', 0, 5) - 'hello' - >>> clip('中文字', 0, 3) # Wide char split at column 3 - '中 ' - >>> clip('a\tb', 0, 10) # Tab expanded to spaces - 'a b' - """ - # pylint: disable=too-complex,too-many-locals,too-many-branches,too-many-statements,too-many-nested-blocks - # Again, for 'hot path', we avoid additional delegate functions and accept the cost - # of complexity for improved python performance. - start = max(start, 0) - if end <= start: - return '' - - # Fast path: printable ASCII only (no tabs, escape sequences, or wide or zero-width chars) - if text.isascii() and text.isprintable(): - return text[start:end] - - # Fast path: no escape sequences means no SGR tracking needed - if propagate_sgr and '\x1b' not in text: - propagate_sgr = False - - # SGR tracking state (only when propagate_sgr=True) - sgr_at_clip_start = None # state when first visible char emitted (None = not yet) - if propagate_sgr: - sgr = _SGR_STATE_DEFAULT # current SGR state, updated by all sequences - - output: list[str] = [] - col = 0 - idx = 0 - - while idx < len(text): - char = text[idx] - - # Early exit: past visible region, SGR captured, no escape ahead - if col >= end and sgr_at_clip_start is not None and char != '\x1b': - break - - # Handle escape sequences - if char == '\x1b' and (match := ZERO_WIDTH_PATTERN.match(text, idx)): - seq = match.group() - if propagate_sgr and _SGR_PATTERN.match(seq): - # Update SGR state; will be applied as prefix when visible content starts - sgr = _sgr_state_update(sgr, seq) - else: - # Non-SGR sequences always preserved - output.append(seq) - idx = match.end() - continue - - # Handle bare ESC (not a valid sequence) - if char == '\x1b': - output.append(char) - idx += 1 - continue - - # TAB expansion - if char == '\t': - if tabsize > 0: - next_tab = col + (tabsize - (col % tabsize)) - while col < next_tab: - if start <= col < end: - output.append(' ') - if propagate_sgr and sgr_at_clip_start is None: - sgr_at_clip_start = sgr - col += 1 - else: - output.append(char) - idx += 1 - continue - - # Grapheme clustering for everything else - grapheme = next(iter_graphemes(text, start=idx)) - w = width(grapheme, ambiguous_width=ambiguous_width) - - if w == 0: - if start <= col < end: - output.append(grapheme) - elif col >= start and col + w <= end: - # Fully visible - output.append(grapheme) - if propagate_sgr and sgr_at_clip_start is None: - sgr_at_clip_start = sgr - col += w - elif col < end and col + w > start: - # Partially visible (wide char at boundary) - output.append(fillchar * (min(end, col + w) - max(start, col))) - if propagate_sgr and sgr_at_clip_start is None: - sgr_at_clip_start = sgr - col += w - else: - col += w - - idx += len(grapheme) - - result = ''.join(output) - - # Apply SGR prefix/suffix - if sgr_at_clip_start is not None: - if prefix := _sgr_state_to_sequence(sgr_at_clip_start): - result = prefix + result - if _sgr_state_is_active(sgr_at_clip_start): - result += '\x1b[0m' - - return result diff --git a/libs/werkzeug-3.1.6.dist-info/INSTALLER b/libs/werkzeug-3.1.8.dist-info/INSTALLER similarity index 100% rename from libs/werkzeug-3.1.6.dist-info/INSTALLER rename to libs/werkzeug-3.1.8.dist-info/INSTALLER diff --git a/libs/werkzeug-3.1.6.dist-info/METADATA b/libs/werkzeug-3.1.8.dist-info/METADATA similarity index 99% rename from libs/werkzeug-3.1.6.dist-info/METADATA rename to libs/werkzeug-3.1.8.dist-info/METADATA index 1cc4bf8fc..4974de1f2 100644 --- a/libs/werkzeug-3.1.6.dist-info/METADATA +++ b/libs/werkzeug-3.1.8.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: Werkzeug -Version: 3.1.6 +Version: 3.1.8 Summary: The comprehensive WSGI web application library. Maintainer-email: Pallets Requires-Python: >=3.9 diff --git a/libs/werkzeug-3.1.6.dist-info/RECORD b/libs/werkzeug-3.1.8.dist-info/RECORD similarity index 76% rename from libs/werkzeug-3.1.6.dist-info/RECORD rename to libs/werkzeug-3.1.8.dist-info/RECORD index afaf603c3..68a3c91ba 100644 --- a/libs/werkzeug-3.1.6.dist-info/RECORD +++ b/libs/werkzeug-3.1.8.dist-info/RECORD @@ -1,15 +1,15 @@ -werkzeug-3.1.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -werkzeug-3.1.6.dist-info/METADATA,sha256=3gkebnRugCLVmObgiGxJei3-DbVNlPDxrrIwukQWOE0,4029 -werkzeug-3.1.6.dist-info/RECORD,, -werkzeug-3.1.6.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -werkzeug-3.1.6.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 -werkzeug-3.1.6.dist-info/licenses/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +werkzeug-3.1.8.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +werkzeug-3.1.8.dist-info/METADATA,sha256=9X0phk-wTGy_K06ETBCNYjMfRiH9Fjy87SiPkN_W05I,4029 +werkzeug-3.1.8.dist-info/RECORD,, +werkzeug-3.1.8.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +werkzeug-3.1.8.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +werkzeug-3.1.8.dist-info/licenses/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 werkzeug/__init__.py,sha256=CejNWfCZKDaJ_FmshZFw8dFgjNAlu6q15ec7DwWJlM8,165 werkzeug/_internal.py,sha256=QRynfUEOngEAmm4qzhlufbhk1K-n901Q1KQWKznsaNU,5547 werkzeug/_reloader.py,sha256=IppO3kX_GxQwaV6h8SPWe0lSTSw6kd_HJXqLiUhR7AU,15100 werkzeug/datastructures/__init__.py,sha256=0Vt8Lt8KUYSoAe8ADt1jIE-w3Ib9tkrk0HEhBOktqUw,2615 werkzeug/datastructures/accept.py,sha256=xoDD4qwZeTjew_1Ze0gyyKWkbhtowwR5tHTWnobPeGI,12177 -werkzeug/datastructures/auth.py,sha256=Zcqdr8-uUpq6_czg1672S2XthG0UakhUHOuummzqz64,10055 +werkzeug/datastructures/auth.py,sha256=iAhDYwIBjE6LDJw_iceE-GlJlLUvJRCMKtokzVKP1J4,10125 werkzeug/datastructures/cache_control.py,sha256=omtSmmAh3KgQRJYzaW05LtEt5HoJlLkJTxjil-Q3cuM,9711 werkzeug/datastructures/csp.py,sha256=jWl1dxhkbc6_eE9qGnTGBJSimWNQljKKYwmlys4dFOA,3816 werkzeug/datastructures/etag.py,sha256=N2dFqhFsLR0k5hgjol3QTGpcSltJhpgLWeyz6Va80G8,3278 @@ -30,7 +30,7 @@ werkzeug/debug/shared/style.css,sha256=-xSxzUEZGw_IqlDR5iZxitNl8LQUjBM-_Y4UAvXVH werkzeug/debug/tbtools.py,sha256=L_CD1ha5WmyWrKVZ3zxovJ4gI1CATsc4Yc1Zms41EvE,13645 werkzeug/exceptions.py,sha256=7XNgddMV3UQR6UPr7lWlzSFxO8Ur8bSJgLdvJuauEX0,26888 werkzeug/formparser.py,sha256=v0-F9OcmNHuPLqXePW_b0asWzpH-Z_ZlGj8ao0CMmuo,15847 -werkzeug/http.py,sha256=gOc_P6u9Am2Z8fQolD_JnBi25FAUUpJXtp0js22Fwtg,44908 +werkzeug/http.py,sha256=4HIKX--fVLTUePdvH4jsDmuiUZc38XziDPBghUJFb0o,45732 werkzeug/local.py,sha256=o4Zz1ZAuNRBn8r-laGIxCdKXjxx1i0uJq3eNyKPy4cg,22184 werkzeug/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 werkzeug/middleware/dispatcher.py,sha256=zWN5_lqJr_sc9UDv-PPoSlDHN_zR33z6B74F_4Cxpo8,2602 @@ -44,22 +44,22 @@ werkzeug/routing/__init__.py,sha256=d8TRxsk24IWu2BdoOYUfL--deolHwiGVCBJqLoEe3YM, werkzeug/routing/converters.py,sha256=iqpee_mAjr1oGbq0etujYF9PiDv5U7DgNkARHXnMId0,7297 werkzeug/routing/exceptions.py,sha256=wNBiUmUk4OtFOpbdDSr7KKKUjH7yn84JqwBicUup8p8,4846 werkzeug/routing/map.py,sha256=a7PSRAELyNyoPFP_uhyNDAsmPjEbrcyewPnEZIl-0ZI,35863 -werkzeug/routing/matcher.py,sha256=nfBbl37eGAkZ1dQlumshFcPuyfggmFjPuSSQOE6GuYs,7849 -werkzeug/routing/rules.py,sha256=XADfK242jF_r4-q7nBo2Zz66RTzcCPOqG5WNfRFNpoo,32538 +werkzeug/routing/matcher.py,sha256=Q_EazWPuLuQjJfd1vYO9AkCtLzx04bGj2YUJqULAfs0,7848 +werkzeug/routing/rules.py,sha256=AkozsbaVGcSEh1O_9FI_ZLWb8BNOjdzrvEFu-qa8VKQ,32536 werkzeug/sansio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 werkzeug/sansio/http.py,sha256=l-N1ktc7WVgfDA9t1MhyeVFNJGvnRTC8mJViZDQYVGA,5351 -werkzeug/sansio/multipart.py,sha256=uLdLH7ffelFXRaXu2_WXK__X11uA7bHCsqfyLerqGSo,11841 -werkzeug/sansio/request.py,sha256=ijwestWQS-dpb61uKl0NRntzNtH6C72OXCNomx-XKxc,19832 +werkzeug/sansio/multipart.py,sha256=gqzwp6YgyE_DepTlnoiuFN743Q3WPa6p7ki1xn5ixlo,11963 +werkzeug/sansio/request.py,sha256=-t8CJrvjWPMu2DCQ4W0ltCHQ4nOWg_ztXJLkyN6-Q_U,19891 werkzeug/sansio/response.py,sha256=159j3TC2AbDvqppcegVmpLftot70UWtm5qZO0kqQpFk,27930 -werkzeug/sansio/utils.py,sha256=3-Gr0Qsa_Rx48mUm7zdFJZNNKMzHfPAknywujHnkhPs,5256 +werkzeug/sansio/utils.py,sha256=ZUkFKNtMDhTRQf9bkyMzoGOQAntyaTTvHB_6en2grgg,7477 werkzeug/security.py,sha256=WHb2oiwyOie5XI8WNnKYd4YVjSwBhGf3nPyKBSGGlI0,6654 -werkzeug/serving.py,sha256=jUSzy9o1iZ_LwdReXZusdoOJPjMK03xGCzhsZ_SUtpk,39826 +werkzeug/serving.py,sha256=mcc9Vfn1dMXzvm87PE5hUtyY6dQB0e98lcxqDkpWiz8,39859 werkzeug/test.py,sha256=Xf8bCYkjKJLtTXWTvw4E5AlYrK7CYjnmIolxSHkkKsc,52779 werkzeug/testapp.py,sha256=5_IS5Dh_WfWfNcTLmbydj01lomgcKA_4l9PPCNZnmdI,6332 werkzeug/urls.py,sha256=XyNKwHvK5IC37-wuIDMYWkiCJ3yLTLGv7wn2GF3ndqI,6430 werkzeug/user_agent.py,sha256=lSlLYKCcbzCUSkbdAoO8zPk2UR-8Mdn6iu_iA2kYPBA,1416 -werkzeug/utils.py,sha256=QRcyUe4JURGaOAqZGnECcsbwKsEkL7gUDZiTNlLV0W4,24623 +werkzeug/utils.py,sha256=1BirLZmTvJP9v6dxjMXpSrKCsfhqzaJPzUHC7dl5MYs,24615 werkzeug/wrappers/__init__.py,sha256=b78jCM8x96kJUGLZ5FYFR3zlK-3pnFAmP9RJIGU0ses,138 werkzeug/wrappers/request.py,sha256=f9Jpe9zqs_v0n-DTZj7FfMyU_y3LhBBBX4YCYsrqGUs,24730 -werkzeug/wrappers/response.py,sha256=VRcXJVRjaYYE0Q93PVu0Y5nhDf9TJkjhY8j_RtMalX0,32443 -werkzeug/wsgi.py,sha256=SAj53op0Hqa6Bd10FHglfR8wk7jLGw5_vn9naPSDlpM,20920 +werkzeug/wrappers/response.py,sha256=y0OOpH58zGngrrzAdG50JPyiRW4JxZeEXLoYuBu1Qo0,32707 +werkzeug/wsgi.py,sha256=DvYEIcatqemE4qtcv33D9T-yvjAiTgri65Ol-ejmrbI,21676 diff --git a/libs/zipp-3.23.0.dist-info/REQUESTED b/libs/werkzeug-3.1.8.dist-info/REQUESTED similarity index 100% rename from libs/zipp-3.23.0.dist-info/REQUESTED rename to libs/werkzeug-3.1.8.dist-info/REQUESTED diff --git a/libs/werkzeug-3.1.6.dist-info/WHEEL b/libs/werkzeug-3.1.8.dist-info/WHEEL similarity index 100% rename from libs/werkzeug-3.1.6.dist-info/WHEEL rename to libs/werkzeug-3.1.8.dist-info/WHEEL diff --git a/libs/werkzeug-3.1.6.dist-info/licenses/LICENSE.txt b/libs/werkzeug-3.1.8.dist-info/licenses/LICENSE.txt similarity index 100% rename from libs/werkzeug-3.1.6.dist-info/licenses/LICENSE.txt rename to libs/werkzeug-3.1.8.dist-info/licenses/LICENSE.txt diff --git a/libs/werkzeug/datastructures/auth.py b/libs/werkzeug/datastructures/auth.py index 42f7aa468..0a3141e5a 100644 --- a/libs/werkzeug/datastructures/auth.py +++ b/libs/werkzeug/datastructures/auth.py @@ -295,6 +295,9 @@ class WWWAuthenticate: if self.token is not None: return f"{self.type.title()} {self.token}" + if not self.parameters: + return self.type.title() + if self.type == "digest": items = [] diff --git a/libs/werkzeug/http.py b/libs/werkzeug/http.py index aee4f59a8..fc22faa60 100644 --- a/libs/werkzeug/http.py +++ b/libs/werkzeug/http.py @@ -15,7 +15,6 @@ from time import mktime from time import struct_time from urllib.parse import quote from urllib.parse import unquote -from urllib.request import parse_http_list as _parse_list_header from ._internal import _dt_as_utc from ._internal import _plain_int @@ -172,20 +171,24 @@ def quote_header_value(value: t.Any, allow_token: bool = True) -> str: return f'"{value_str}"' +_unslash_re = re.compile(r"\\(.)", re.A) + + def unquote_header_value(value: str) -> str: - """Remove double quotes and decode slash-escaped ``"`` and ``\\`` characters in a - header value. + """Remove double quotes and backslash escapes from a header value. This is the reverse of :func:`quote_header_value`. :param value: The header value to unquote. + .. versionchanged:: 3.2 + Removes escape preceding any character. + .. versionchanged:: 3.0 The ``is_filename`` parameter is removed. """ if len(value) >= 2 and value[0] == value[-1] == '"': - value = value[1:-1] - return value.replace("\\\\", "\\").replace('\\"', '"') + return _unslash_re.sub(r"\g<1>", value[1:-1]) return value @@ -301,8 +304,8 @@ def parse_list_header(value: str) -> list[str]: """Parse a header value that consists of a list of comma separated items according to `RFC 9110 `__. - This extends :func:`urllib.request.parse_http_list` to remove surrounding quotes - from values. + Surrounding quotes are removed from items, but internal quotes are left for + future parsing. Empty values are discarded. .. code-block:: python @@ -312,16 +315,50 @@ def parse_list_header(value: str) -> list[str]: This is the reverse of :func:`dump_header`. :param value: The header value to parse. + + .. versionchanged:: 3.2 + Quotes and escapes are kept if only part of an item is quoted. Empty + values are omitted. An empty list is returned if the value contains an + unclosed quoted string. """ - result = [] + items = [] + item = "" + escape = False + quote = False - for item in _parse_list_header(value): - if len(item) >= 2 and item[0] == item[-1] == '"': - item = item[1:-1] + for char in value: + if escape: + escape = False + item += char + continue - result.append(item) + if quote: + if char == "\\": + escape = True + elif char == '"': + quote = False - return result + item += char + continue + + if char == ",": + items.append(item) + item = "" + continue + + if char == '"': + quote = True + + item += char + + if quote: + # invalid, unclosed quoted string + return [] + + items.append(item) + return [ + unquote_header_value(item) for item in (item.strip() for item in items) if item + ] def parse_dict_header(value: str) -> dict[str, str | None]: @@ -343,6 +380,10 @@ def parse_dict_header(value: str) -> dict[str, str | None]: :param value: The header value to parse. + .. versionchanged:: 3.2 + An empty dict is returned if the value contains an unclosed quoted + string. + .. versionchanged:: 3.0 Passing bytes is not supported. @@ -390,10 +431,7 @@ def parse_dict_header(value: str) -> dict[str, str | None]: # invalid bytes are replaced during unquoting value = unquote(value, encoding=encoding) - if len(value) >= 2 and value[0] == value[-1] == '"': - value = value[1:-1] - - result[key] = value + result[key] = unquote_header_value(value) return result diff --git a/libs/werkzeug/routing/matcher.py b/libs/werkzeug/routing/matcher.py index 1fd00efca..1f03c4608 100644 --- a/libs/werkzeug/routing/matcher.py +++ b/libs/werkzeug/routing/matcher.py @@ -172,7 +172,7 @@ class StateMachineMatcher: if self.merge_slashes and rv is None: # Try to match again, but with slashes merged - path = re.sub("/{2,}?", "/", path) + path = re.sub("/{2,}", "/", path) try: rv = _match(self._root, [domain, *path.split("/")], []) except SlashRequired: diff --git a/libs/werkzeug/routing/rules.py b/libs/werkzeug/routing/rules.py index 9e483a54b..79716603d 100644 --- a/libs/werkzeug/routing/rules.py +++ b/libs/werkzeug/routing/rules.py @@ -81,7 +81,6 @@ _converter_args_re = re.compile( re.VERBOSE, ) - _PYTHON_CONSTANTS = {"None": None, "True": True, "False": False} @@ -722,7 +721,7 @@ class Rule(RuleFactory): self._trace.append((False, "|")) rule = self.rule if self.merge_slashes: - rule = re.sub("/{2,}?", "/", self.rule) + rule = re.sub("/{2,}", "/", self.rule) self._parts.extend(self._parse_rule(rule)) self._build: t.Callable[..., tuple[str, str]] diff --git a/libs/werkzeug/sansio/multipart.py b/libs/werkzeug/sansio/multipart.py index 5a3844ebb..9582aba3e 100644 --- a/libs/werkzeug/sansio/multipart.py +++ b/libs/werkzeug/sansio/multipart.py @@ -139,7 +139,6 @@ class MultipartDecoder: def next_event(self) -> Event: event: Event = NEED_DATA - if self.state == State.PREAMBLE: match = self.preamble_re.search(self.buffer, self._search_position) if match is not None: @@ -249,7 +248,9 @@ class MultipartDecoder: if self.buffer.find(b"--" + self.boundary) == -1: # No complete boundary in the buffer, but there may be # a partial boundary at the end. - data_end = del_index = self._last_partial_boundary_index(data) + data_end = del_index = ( + self._last_partial_boundary_index(data[data_start:]) + data_start + ) more_data = True else: match = self.boundary_re.search(data) @@ -261,15 +262,16 @@ class MultipartDecoder: data_end = match.start() del_index = match.end() else: - data_end = del_index = self._last_partial_boundary_index(data) + data_end = del_index = ( + self._last_partial_boundary_index(data[data_start:]) + data_start + ) more_data = match is None - return bytes(data[data_start:data_end]), del_index, more_data def _last_partial_boundary_index(self, data: bytes | bytearray) -> int: # Find the last index following which a partial boundary # could be present in the data. This will be the earliest - # position of a LR or a CR, unless that position is more + # position of a LF or a CR, unless that position is more # than a complete boundary from the end in which case there # is no partial boundary. complete_boundary_index = len(data) - len(b"\r\n--" + self.boundary) diff --git a/libs/werkzeug/sansio/request.py b/libs/werkzeug/sansio/request.py index 8d5fbd8f8..0d72f3283 100644 --- a/libs/werkzeug/sansio/request.py +++ b/libs/werkzeug/sansio/request.py @@ -230,6 +230,8 @@ class Request: def host(self) -> str: """The host name the request was made to, including the port if it's non-standard. Validated with :attr:`trusted_hosts`. + + See :func:`.get_host` for a detailed explanation. """ return get_host( self.scheme, self.headers.get("host"), self.server, self.trusted_hosts diff --git a/libs/werkzeug/sansio/utils.py b/libs/werkzeug/sansio/utils.py index ff7ceda34..586d1695c 100644 --- a/libs/werkzeug/sansio/utils.py +++ b/libs/werkzeug/sansio/utils.py @@ -1,30 +1,60 @@ from __future__ import annotations +import re import typing as t from urllib.parse import quote from .._internal import _plain_int from ..exceptions import SecurityError +from ..http import parse_set_header from ..urls import uri_to_iri +_host_re = re.compile( + r""" + ( + [a-z0-9.-]+ # domain or ipv4 + | + \[[a-f0-9]*:[a-f0-9.:]+] # ipv6 + ) + (?::[0-9]+)? # optional port + """, + flags=re.ASCII | re.IGNORECASE | re.VERBOSE, +) -def host_is_trusted(hostname: str | None, trusted_list: t.Iterable[str]) -> bool: - """Check if a host matches a list of trusted names. - :param hostname: The name to check. - :param trusted_list: A list of valid names to match. If a name - starts with a dot it will match all subdomains. +def host_is_trusted( + hostname: str | None, trusted_list: t.Collection[str] | None = None +) -> bool: + """Perform some checks on a ``Host`` header ``host:port``. The host must be + made up of valid characters, but this does not check validity beyond that. + If a list of trusted domains is given, the domain must match one. + + :param hostname: The ``Host`` header ``host:port`` to check. + :param trusted_list: A list of trusted domains to match. These should + already be IDNA encoded, but will be encoded if needed. The port is + ignored for this check. If a name starts with a dot it will match as a + suffix, accepting all subdomains. If empty or ``None``, all domains are + allowed. + + .. versionchanged:: 3.2 + The value's characters are validated. + + .. versionchanged:: 3.2 + ``trusted_list`` defaults to ``None``. .. versionadded:: 0.9 """ if not hostname: return False - try: - hostname = hostname.partition(":")[0].encode("idna").decode("ascii") - except UnicodeEncodeError: + if _host_re.fullmatch(hostname) is None: return False + hostname = hostname.partition(":")[0] + + if not trusted_list: + return True + if isinstance(trusted_list, str): trusted_list = [trusted_list] @@ -50,36 +80,55 @@ def get_host( scheme: str, host_header: str | None, server: tuple[str, int | None] | None = None, - trusted_hosts: t.Iterable[str] | None = None, + trusted_hosts: t.Collection[str] | None = None, ) -> str: - """Return the host for the given parameters. + """Get and validate a request's ``host:port`` based on the given values. - This first checks the ``host_header``. If it's not present, then - ``server`` is used. The host will only contain the port if it is - different than the standard port for the protocol. + The ``Host`` header sent by the client is preferred. Otherwise, the server's + configured address is used. The port is omitted if it matches the standard + HTTP or HTTPS ports. - Optionally, verify that the host is trusted using - :func:`host_is_trusted` and raise a - :exc:`~werkzeug.exceptions.SecurityError` if it is not. + The value is passed through :func:`host_is_trusted`. The host must be made + up of valid characters, but this does not check validity beyond that. If a + list of trusted domains is given, the domain must match one. - :param scheme: The protocol the request used, like ``"https"``. + If the host header is not available, such as for HTTP/0.9 and 1.0, or it has + invalid characters, the empty string is returned. Subdomain and host + routing, and external URL building, will not work in these cases. + + :param scheme: The protocol of the request. Used to omit the standard ports + 80 and 443. :param host_header: The ``Host`` header value. - :param server: Address of the server. ``(host, port)``, or - ``(path, None)`` for unix sockets. - :param trusted_hosts: A list of trusted host names. + :param server: The server's configured address ``(host, port)``. The server + may be using a Unix socket and give ``(path, None)``; this is ignored as + it would not produce a useful host value. + :param trusted_hosts: A list of trusted domains to match. These should + already be IDNA encoded, but will be encoded if needed. The port is + ignored for this check. If a name starts with a dot it will match as a + suffix, accepting all subdomains. If empty or ``None``, all domains are + allowed. :return: Host, with port if necessary. - :raise ~werkzeug.exceptions.SecurityError: If the host is not - trusted. + :raise .SecurityError: If the host is not trusted. + + .. versionchanged:: 3.1.8 + The empty string is again returned if no host header value is available, + or if the characters are invalid. + + .. versionchanged:: 3.1.7 + The characters of the host value are validated. The empty string is no + longer allowed if no header value is available. + + .. versionchanged:: 3.2 + When using the server address, Unix sockets are ignored. .. versionchanged:: 3.1.3 If ``SERVER_NAME`` is IPv6, it is wrapped in ``[]``. """ - host = "" - if host_header is not None: host = host_header - elif server is not None: + # The port server[1] will be None for a Unix socket. Ignore in that case. + elif server is not None and server[1] is not None: host = server[0] # If SERVER_NAME is IPv6, wrap it in [] to match Host header. @@ -87,18 +136,23 @@ def get_host( if ":" in host and host[0] != "[": host = f"[{host}]" - if server[1] is not None: - host = f"{host}:{server[1]}" + host = f"{host}:{server[1]}" + else: + # Pass through empty host from HTTP/0.9 and 1.0. + return "" - if scheme in {"http", "ws"} and host.endswith(":80"): - host = host[:-3] - elif scheme in {"https", "wss"} and host.endswith(":443"): - host = host[:-4] + if scheme in {"http", "ws"}: + host = host.removesuffix(":80") + elif scheme in {"https", "wss"}: + host = host.removesuffix(":443") - if trusted_hosts is not None: - if not host_is_trusted(host, trusted_hosts): + if not host_is_trusted(host, trusted_hosts): + if trusted_hosts: raise SecurityError(f"Host {host!r} is not trusted.") + # Invalid characters, treat as empty. + return "" + return host @@ -158,7 +212,10 @@ def get_content_length( .. versionadded:: 2.2 """ - if http_transfer_encoding == "chunked" or http_content_length is None: + if ( + http_transfer_encoding is not None + and "chunked" in parse_set_header(http_transfer_encoding) + ) or http_content_length is None: return None try: diff --git a/libs/werkzeug/serving.py b/libs/werkzeug/serving.py index 8605cfd6d..37f3a2d8c 100644 --- a/libs/werkzeug/serving.py +++ b/libs/werkzeug/serving.py @@ -33,6 +33,7 @@ from urllib.parse import urlsplit from ._internal import _log from ._internal import _wsgi_encoding_dance from .exceptions import InternalServerError +from .http import parse_set_header from .urls import uri_to_iri try: @@ -220,7 +221,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): value = f"{environ[key]},{value}" environ[key] = value - if environ.get("HTTP_TRANSFER_ENCODING", "").strip().lower() == "chunked": + if "chunked" in parse_set_header(environ.get("HTTP_TRANSFER_ENCODING")): environ["wsgi.input_terminated"] = True environ["wsgi.input"] = DechunkedInput(environ["wsgi.input"]) diff --git a/libs/werkzeug/utils.py b/libs/werkzeug/utils.py index 0d4a700f0..d023f9877 100644 --- a/libs/werkzeug/utils.py +++ b/libs/werkzeug/utils.py @@ -409,7 +409,7 @@ def send_file( if isinstance(path_or_file, (os.PathLike, str)) or hasattr( path_or_file, "__fspath__" ): - path_or_file = t.cast("t.Union[os.PathLike[str], str]", path_or_file) + path_or_file = t.cast("os.PathLike[str] | str", path_or_file) # Flask will pass app.root_path, allowing its send_file wrapper # to not have to deal with paths. diff --git a/libs/werkzeug/wrappers/response.py b/libs/werkzeug/wrappers/response.py index e05fa8a12..470729231 100644 --- a/libs/werkzeug/wrappers/response.py +++ b/libs/werkzeug/wrappers/response.py @@ -667,22 +667,26 @@ class Response(_SansIOResponse): :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable` if `Range` header could not be parsed or satisfied. + .. versionchanged:: 3.2 + Adds the ``Accept-Ranges`` header if ``accept_ranges`` is passed, + even if this is not a satisfiable range request. + .. versionchanged:: 2.0 Returns ``False`` if the length is 0. """ from ..exceptions import RequestedRangeNotSatisfiable - if ( - not accept_ranges - or complete_length is None - or complete_length == 0 - or not self._is_range_request_processable(environ) - ): + if not accept_ranges: return False if accept_ranges is True: accept_ranges = "bytes" + self.accept_ranges = accept_ranges + + if not (complete_length and self._is_range_request_processable(environ)): + return False + parsed_range = parse_range_header(environ.get("HTTP_RANGE")) if parsed_range is None: @@ -695,8 +699,7 @@ class Response(_SansIOResponse): raise RequestedRangeNotSatisfiable(complete_length) content_length = range_tuple[1] - range_tuple[0] - self.headers["Content-Length"] = str(content_length) - self.headers["Accept-Ranges"] = accept_ranges + self.content_length = content_length self.content_range = content_range_header self.status_code = 206 self._wrap_range_response(range_tuple[0], content_length) @@ -743,6 +746,10 @@ class Response(_SansIOResponse): :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable` if `Range` header could not be parsed or satisfied. + .. versionchangedd: 3.2 + Adds the ``Accept-Ranges`` header if ``accept_ranges`` is passed, + even if this is not a satisfiable range request. + .. versionchanged:: 2.0 Range processing is skipped if length is 0 instead of raising a 416 Range Not Satisfiable error. diff --git a/libs/werkzeug/wsgi.py b/libs/werkzeug/wsgi.py index 2d071c789..8aa1b445b 100644 --- a/libs/werkzeug/wsgi.py +++ b/libs/werkzeug/wsgi.py @@ -33,7 +33,7 @@ def get_current_url( root_only: bool = False, strip_querystring: bool = False, host_only: bool = False, - trusted_hosts: t.Iterable[str] | None = None, + trusted_hosts: t.Collection[str] | None = None, ) -> str: """Recreate the URL for a request from the parts in a WSGI environment. @@ -84,24 +84,38 @@ def _get_server( def get_host( - environ: WSGIEnvironment, trusted_hosts: t.Iterable[str] | None = None + environ: WSGIEnvironment, trusted_hosts: t.Collection[str] | None = None ) -> str: - """Return the host for the given WSGI environment. + """Get and validate a request's ``host:port`` based on the values in the + given WSGI environ. - The ``Host`` header is preferred, then ``SERVER_NAME`` if it's not - set. The returned host will only contain the port if it is different - than the standard port for the protocol. + The ``Host`` header sent by the client is preferred. Otherwise, the server's + configured address is used. If the server address is a Unix socket, it is + ignored. The port is omitted if it matches the standard HTTP or HTTPS ports. - Optionally, verify that the host is trusted using - :func:`host_is_trusted` and raise a - :exc:`~werkzeug.exceptions.SecurityError` if it is not. + The value is passed through :func:`host_is_trusted`. The host must be made + up of valid characters, but this does not check validity beyond that. If a + list of trusted domains is given, the domain must match one. - :param environ: A WSGI environment dict. - :param trusted_hosts: A list of trusted host names. + :param environ: The WSGI environ. + :param trusted_hosts: A list of trusted domains to match. These should + already be IDNA encoded, but will be encoded if needed. The port is + ignored for this check. If a name starts with a dot it will match as a + suffix, accepting all subdomains. If empty or ``None``, all domains are + allowed. :return: Host, with port if necessary. - :raise ~werkzeug.exceptions.SecurityError: If the host is not - trusted. + :raise .SecurityError: If the host is not trusted. + + .. versionchanged:: 3.2 + The characters of the host value are validated. The empty string is no + longer allowed if no header value is available. + + .. versionchanged:: 3.2 + When using the server address, Unix sockets are ignored. + + .. versionchanged:: 3.1.3 + If ``SERVER_NAME`` is IPv6, it is wrapped in ``[]``. """ return _sansio_utils.get_host( environ["wsgi.url_scheme"], diff --git a/libs/zipp-3.23.0.dist-info/RECORD b/libs/zipp-3.23.0.dist-info/RECORD deleted file mode 100644 index af8acc40f..000000000 --- a/libs/zipp-3.23.0.dist-info/RECORD +++ /dev/null @@ -1,14 +0,0 @@ -zipp-3.23.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -zipp-3.23.0.dist-info/METADATA,sha256=vdZ9TRbPC_O4k-fRjNPS13StuC837Zhbx3cMYHIms1s,3563 -zipp-3.23.0.dist-info/RECORD,, -zipp-3.23.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -zipp-3.23.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 -zipp-3.23.0.dist-info/licenses/LICENSE,sha256=WlfLTbheKi3YjCkGKJCK3VfjRRRJ4KmnH9-zh3b9dZ0,1076 -zipp-3.23.0.dist-info/top_level.txt,sha256=iAbdoSHfaGqBfVb2XuR9JqSQHCoOsOtG6y9C_LSpqFw,5 -zipp/__init__.py,sha256=ieXh9GIMdABjKRX_JUJtP9k5wdBLK4Mt5X4nszSkmYE,11976 -zipp/_functools.py,sha256=f6Kt9LxZ4TE-cY1lJVdXSId3memSXmH9IdgMbU-_x2k,575 -zipp/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -zipp/compat/overlay.py,sha256=oEIGAnbr8yGjuKTrVSO2ByewPui71uppbX18BLnYTKE,783 -zipp/compat/py310.py,sha256=S7i6N9mToEn3asNb2ILyjnzvITOXrATD_J4emjyBbDU,256 -zipp/compat/py313.py,sha256=RndvDNtuY7H2D9ecnnzcPBMZ8mZc42gmXD_IwQAXXAE,654 -zipp/glob.py,sha256=DLV9LBsDxA6YVW82e3-tkoNrus1h4R-j3BR6VqS0AzE,3382 diff --git a/libs/zipp-3.23.0.dist-info/WHEEL b/libs/zipp-3.23.0.dist-info/WHEEL deleted file mode 100644 index 0885d0555..000000000 --- a/libs/zipp-3.23.0.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (80.10.2) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/libs/zipp-3.23.0.dist-info/INSTALLER b/libs/zipp-4.1.0.dist-info/INSTALLER similarity index 100% rename from libs/zipp-3.23.0.dist-info/INSTALLER rename to libs/zipp-4.1.0.dist-info/INSTALLER diff --git a/libs/zipp-3.23.0.dist-info/METADATA b/libs/zipp-4.1.0.dist-info/METADATA similarity index 90% rename from libs/zipp-3.23.0.dist-info/METADATA rename to libs/zipp-4.1.0.dist-info/METADATA index 642011798..5e908098f 100644 --- a/libs/zipp-3.23.0.dist-info/METADATA +++ b/libs/zipp-4.1.0.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: zipp -Version: 3.23.0 +Version: 4.1.0 Summary: Backport of pathlib-compatible object wrapper for zip files Author-email: "Jason R. Coombs" License-Expression: MIT @@ -9,7 +9,7 @@ Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.9 +Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Provides-Extra: test @@ -28,14 +28,14 @@ Requires-Dist: furo; extra == "doc" Requires-Dist: sphinx-lint; extra == "doc" Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" Provides-Extra: check -Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-checkdocs>=2.14; extra == "check" Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" Provides-Extra: cover Requires-Dist: pytest-cov; extra == "cover" Provides-Extra: enabler -Requires-Dist: pytest-enabler>=2.2; extra == "enabler" +Requires-Dist: pytest-enabler>=3.4; extra == "enabler" Provides-Extra: type -Requires-Dist: pytest-mypy; extra == "type" +Requires-Dist: pytest-mypy>=1.0.1; platform_python_implementation != "PyPy" and extra == "type" Dynamic: license-file .. image:: https://img.shields.io/pypi/v/zipp.svg @@ -54,7 +54,7 @@ Dynamic: license-file .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest .. :target: https://zipp.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2025-informational +.. image:: https://img.shields.io/badge/skeleton-2026-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/zipp @@ -77,6 +77,8 @@ were contributed to different versions in the standard library: * - zipp - stdlib + * - 3.21 + - 3.15 * - 3.18 - 3.13 * - 3.16 diff --git a/libs/zipp-4.1.0.dist-info/RECORD b/libs/zipp-4.1.0.dist-info/RECORD new file mode 100644 index 000000000..bcf720a7e --- /dev/null +++ b/libs/zipp-4.1.0.dist-info/RECORD @@ -0,0 +1,14 @@ +zipp-4.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +zipp-4.1.0.dist-info/METADATA,sha256=oIaY0N1QkyWFP-oMX3u-_1BG8WzxZm_PDgPQpglMidc,3640 +zipp-4.1.0.dist-info/RECORD,, +zipp-4.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +zipp-4.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92 +zipp-4.1.0.dist-info/licenses/LICENSE,sha256=l1WhhRlmbl8PTK49qtPXASvK5IpgCzEjfXXp_hNOZoM,1076 +zipp-4.1.0.dist-info/top_level.txt,sha256=iAbdoSHfaGqBfVb2XuR9JqSQHCoOsOtG6y9C_LSpqFw,5 +zipp/__init__.py,sha256=bnsTWc5V4UfSkXK9C0ibwWYOlr3gCoToMbh7hwSU1kc,11748 +zipp/_functools.py,sha256=LZrqt6bu0I4bxxAbDsNs07fb5ad5_INdG1gzSdhTLv8,789 +zipp/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +zipp/compat/overlay.py,sha256=oEIGAnbr8yGjuKTrVSO2ByewPui71uppbX18BLnYTKE,783 +zipp/compat/py310.py,sha256=S7i6N9mToEn3asNb2ILyjnzvITOXrATD_J4emjyBbDU,256 +zipp/compat/py313.py,sha256=RndvDNtuY7H2D9ecnnzcPBMZ8mZc42gmXD_IwQAXXAE,654 +zipp/glob.py,sha256=K0GDSIoBYXeO1KBAeiCqfB2uZAP34FhLRY3X2R7wiDA,3395 diff --git a/libs/zipp-4.1.0.dist-info/REQUESTED b/libs/zipp-4.1.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/libs/tqdm-4.67.3.dist-info/WHEEL b/libs/zipp-4.1.0.dist-info/WHEEL similarity index 100% rename from libs/tqdm-4.67.3.dist-info/WHEEL rename to libs/zipp-4.1.0.dist-info/WHEEL diff --git a/libs/zipp-3.23.0.dist-info/licenses/LICENSE b/libs/zipp-4.1.0.dist-info/licenses/LICENSE similarity index 96% rename from libs/zipp-3.23.0.dist-info/licenses/LICENSE rename to libs/zipp-4.1.0.dist-info/licenses/LICENSE index f60bd5720..c891f411d 100644 --- a/libs/zipp-3.23.0.dist-info/licenses/LICENSE +++ b/libs/zipp-4.1.0.dist-info/licenses/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 +Copyright (c) 2026 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including diff --git a/libs/zipp-3.23.0.dist-info/top_level.txt b/libs/zipp-4.1.0.dist-info/top_level.txt similarity index 100% rename from libs/zipp-3.23.0.dist-info/top_level.txt rename to libs/zipp-4.1.0.dist-info/top_level.txt diff --git a/libs/zipp/__init__.py b/libs/zipp/__init__.py index ed5b21463..99ae3ea1a 100644 --- a/libs/zipp/__init__.py +++ b/libs/zipp/__init__.py @@ -14,10 +14,9 @@ import pathlib import posixpath import re import stat -import sys import zipfile -from ._functools import save_method_args +from ._functools import none_as, save_method_args from .compat.py310 import text_encoding from .glob import Translator @@ -196,12 +195,7 @@ class FastLookup(CompleteDirs): def _extract_text_encoding(encoding=None, *args, **kwargs): - # compute stack level so that the caller of the caller sees any warning. - is_pypy = sys.implementation.name == 'pypy' - # PyPy no longer special cased after 7.3.19 (or maybe 7.3.18) - # See jaraco/zipp#143 - is_old_pypi = is_pypy and sys.pypy_version_info < (7, 3, 19) - stack_level = 3 + is_old_pypi + stack_level = 3 return text_encoding(encoding, stack_level), args, kwargs @@ -277,7 +271,7 @@ class Path: resolve to the zipfile. >>> str(path) - 'mem/abcde.zip/' + 'mem/abcde.zip' >>> path.name 'abcde.zip' >>> path.filename == pathlib.Path('mem/abcde.zip') @@ -404,7 +398,7 @@ class Path: def iterdir(self): if not self.is_dir(): - raise ValueError("Can't listdir a file") + raise NotADirectoryError("Can't listdir a file") subs = map(self._next, self.root.namelist()) return filter(self._is_child, subs) @@ -435,7 +429,8 @@ class Path: return posixpath.relpath(str(self), str(other.joinpath(*extra))) def __str__(self): - return posixpath.join(self.root.filename, self.at) + root = none_as(self.root.filename, ':zipfile:') + return posixpath.join(root, self.at) if self.at else root def __repr__(self): return self.__repr.format(self=self) diff --git a/libs/zipp/_functools.py b/libs/zipp/_functools.py index 7390be218..7d82636ac 100644 --- a/libs/zipp/_functools.py +++ b/libs/zipp/_functools.py @@ -18,3 +18,14 @@ def save_method_args(method): return method(self, *args, **kwargs) return wrapper + + +# from jaraco.functools 4.3 +def none_as(value, replacement=None): + """ + >>> none_as(None, 'foo') + 'foo' + >>> none_as('bar', 'foo') + 'bar' + """ + return replacement if value is None else value diff --git a/libs/zipp/glob.py b/libs/zipp/glob.py index 1b4ffb331..bf606d775 100644 --- a/libs/zipp/glob.py +++ b/libs/zipp/glob.py @@ -71,7 +71,8 @@ class Translator: Perform the replacements for a match from :func:`separate`. """ return match.group('set') or ( - re.escape(match.group(0)) + re + .escape(match.group(0)) .replace('\\*\\*', r'.*') .replace('\\*', rf'[^{re.escape(self.seps)}]*') .replace('\\?', r'[^/]')