├── src ├── Etymology.Data.Console │ ├── Program.cs │ └── Etymology.Data.Console.csproj ├── Etymology.Web │ ├── wwwroot │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── cctv.jpg │ │ │ ├── alipay.jpg │ │ │ ├── error.png │ │ │ ├── carousel1.jpg │ │ │ ├── carousel2.jpg │ │ │ ├── carousel3.jpg │ │ │ ├── carousel4.jpg │ │ │ ├── dixin-yan.jpg │ │ │ ├── favicons │ │ │ │ ├── logo.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── mstile-70x70.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── mstile-144x144.png │ │ │ │ ├── mstile-150x150.png │ │ │ │ ├── mstile-310x150.png │ │ │ │ ├── mstile-310x310.png │ │ │ │ ├── apple-icon-57x57.png │ │ │ │ ├── apple-icon-60x60.png │ │ │ │ ├── apple-icon-72x72.png │ │ │ │ ├── apple-icon-76x76.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── android-chrome-36x36.png │ │ │ │ ├── android-chrome-48x48.png │ │ │ │ ├── android-chrome-72x72.png │ │ │ │ ├── android-chrome-96x96.png │ │ │ │ ├── android-icon-192x192.png │ │ │ │ ├── apple-icon-114x114.png │ │ │ │ ├── apple-icon-120x120.png │ │ │ │ ├── apple-icon-144x144.png │ │ │ │ ├── apple-icon-152x152.png │ │ │ │ ├── apple-icon-180x180.png │ │ │ │ ├── android-chrome-144x144.png │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-256x256.png │ │ │ │ ├── apple-touch-icon-57x57.png │ │ │ │ ├── apple-touch-icon-60x60.png │ │ │ │ ├── apple-touch-icon-72x72.png │ │ │ │ ├── apple-touch-icon-76x76.png │ │ │ │ ├── apple-touch-icon-114x114.png │ │ │ │ ├── apple-touch-icon-120x120.png │ │ │ │ ├── apple-touch-icon-144x144.png │ │ │ │ ├── apple-touch-icon-152x152.png │ │ │ │ ├── apple-touch-icon-180x180.png │ │ │ │ ├── apple-touch-icon-precomposed.png │ │ │ │ ├── apple-touch-icon-57x57-precomposed.png │ │ │ │ ├── apple-touch-icon-60x60-precomposed.png │ │ │ │ ├── apple-touch-icon-72x72-precomposed.png │ │ │ │ ├── apple-touch-icon-76x76-precomposed.png │ │ │ │ ├── apple-touch-icon-114x114-precomposed.png │ │ │ │ ├── apple-touch-icon-120x120-precomposed.png │ │ │ │ ├── apple-touch-icon-144x144-precomposed.png │ │ │ │ ├── apple-touch-icon-152x152-precomposed.png │ │ │ │ ├── apple-touch-icon-180x180-precomposed.png │ │ │ │ ├── browserconfig.xml │ │ │ │ ├── site.webmanifest │ │ │ │ └── safari-pinned-tab.svg │ │ │ ├── wechat-public.jpg │ │ │ └── wechat-wallet.jpg │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── error │ │ │ └── index.htm │ ├── Server │ │ ├── settings.Production.json │ │ ├── settings.Staging.json │ │ ├── Program.cs │ │ ├── Models │ │ │ └── EtymologyAnalyzeModel.cs │ │ ├── Cache.cs │ │ ├── ServiceCollectionExtensions.cs │ │ ├── Settings.cs │ │ ├── settings.json │ │ ├── Controllers │ │ │ └── EtymologyController.cs │ │ ├── Startup.cs │ │ └── RequestValidation.cs │ ├── Connected Services │ │ └── Application Insights │ │ │ └── ConnectedService.json │ ├── Client │ │ ├── js │ │ │ ├── baidu.js │ │ │ ├── google.js │ │ │ ├── index.js │ │ │ ├── ui.js │ │ │ ├── data.js │ │ │ └── hash.js │ │ └── css │ │ │ ├── ie10-viewport-bug-workaround.css │ │ │ └── index.css │ ├── Properties │ │ ├── serviceDependencies.json │ │ ├── serviceDependencies.etymology - Web Deploy.json │ │ ├── serviceDependencies.etymology-staging - Web Deploy.json │ │ ├── launchSettings.json │ │ ├── PublishProfiles │ │ │ ├── etymology - Web Deploy.pubxml │ │ │ └── etymology-staging - Web Deploy.pubxml │ │ └── ServiceDependencies │ │ │ ├── etymology - Web Deploy │ │ │ ├── appInsights1.arm.json │ │ │ ├── mssql1.arm.json │ │ │ └── profile.arm.json │ │ │ └── etymology-staging - Web Deploy │ │ │ ├── appInsights1.arm.json │ │ │ ├── mssql1.arm.json │ │ │ └── profile.arm.json │ ├── webpack.config.Debug.js │ ├── webpack.config.js │ ├── package.json │ ├── webpack.config.Release.js │ └── Etymology.Web.csproj ├── Etymology.Data │ ├── Cache │ │ ├── ServiceCollectionExtensions.cs │ │ └── CharacterCache.cs │ ├── Models │ │ ├── AnalyzeResult.cs │ │ ├── Bronze.cs │ │ ├── Oracle.cs │ │ ├── ServiceCollectionExtensions.cs │ │ ├── Liushutong.cs │ │ ├── ICharacter.cs │ │ ├── Seal.cs │ │ ├── VEtymology.cs │ │ ├── Etymology.cs │ │ ├── Etymology.Display.cs │ │ └── EtymologyContext.cs │ └── Etymology.Data.csproj ├── Etymology.Common │ ├── ApplicationBuilderExtensions.cs │ ├── ExceptionExtensions.cs │ ├── Svg.cs │ ├── Etymology.Common.csproj │ ├── Chinese.Conversion.cs │ └── Chinese.Validation.cs ├── Etymology.Tool │ ├── Program.cs │ ├── Unicode.cs │ ├── UnicodeOptions.cs │ ├── SvgOptions.cs │ ├── Etymology.Tool.csproj │ └── Svg.cs └── Etymology.Tests │ ├── Etymology.Tests.csproj │ ├── Common │ └── ChineseTests.cs │ ├── Web │ └── Server │ │ └── EtymologyControllerTests.cs │ └── Data │ └── Models │ └── EtymologyContextTests.cs ├── docs ├── index.md ├── _config.yml └── _posts │ ├── 2018-04-23-decomposition-of-chinese-character.md │ ├── 2017-09-19-what-is-a-traditional-chinese-character.md │ ├── 2018-04-23-simplification-of-chinese-character.md │ └── 2017-09-18-spoken-chinese-and-other-languages.md ├── Usings.props ├── GlobalUsings.cs ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── azure-staging.yml ├── stylecop.json ├── Analyzers.ruleset ├── appveyor.yml ├── README.md ├── Analyzers.props ├── Etymology.sln └── .gitignore /src/Etymology.Data.Console/Program.cs: -------------------------------------------------------------------------------- 1 | Console.WriteLine(); -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Chinese Etymology 字源 2 | 3 | [hanziyuan.net](http://hanziyuan.net) 4 | -------------------------------------------------------------------------------- /Usings.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/cctv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/cctv.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/alipay.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/error.png -------------------------------------------------------------------------------- /src/Etymology.Web/Server/settings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Connections": { 3 | "Etymology": "" 4 | }, 5 | "IsHttpsOnly": true 6 | } 7 | -------------------------------------------------------------------------------- /src/Etymology.Web/Server/settings.Staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Connections": { 3 | "Etymology": "" 4 | }, 5 | "IsHttpsOnly": true 6 | } 7 | -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/carousel1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/carousel1.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/carousel2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/carousel2.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/carousel3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/carousel3.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/carousel4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/carousel4.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/dixin-yan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/dixin-yan.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/logo.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/wechat-public.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/wechat-public.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/wechat-wallet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/wechat-wallet.jpg -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/favicon.ico -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-57x57.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-60x60.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-72x72.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-76x76.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-36x36.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-48x48.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-72x72.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-96x96.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-icon-192x192.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-114x114.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-144x144.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-152x152.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/android-chrome-256x256.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Etymology.Web.Server; 2 | using Microsoft.AspNetCore; 3 | 4 | using IWebHost host = WebHost.CreateDefaultBuilder(args).Build(); 5 | await host.RunAsync(); 6 | -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-57x57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-57x57-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-60x60-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-60x60-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-72x72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-72x72-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-76x76-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-76x76-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-114x114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-114x114-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-120x120-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-120x120-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-152x152-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-152x152-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-180x180-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dixin/Etymology/HEAD/src/Etymology.Web/wwwroot/images/favicons/apple-touch-icon-180x180-precomposed.png -------------------------------------------------------------------------------- /src/Etymology.Web/Server/Models/EtymologyAnalyzeModel.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server.Models; 2 | 3 | using Etymology.Data.Models; 4 | 5 | public record EtymologyAnalyzeModel(string Chinese, TimeSpan Elapsed, AnalyzeResult[] Results); -------------------------------------------------------------------------------- /src/Etymology.Web/Connected Services/Application Insights/ConnectedService.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", 3 | "Version": "8.8.712.1", 4 | "GettingStartedDocument": { 5 | "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" 6 | } 7 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Client/js/baidu.js: -------------------------------------------------------------------------------- 1 | var _hmt = _hmt || []; 2 | (function() { 3 | var hm = document.createElement("script"); 4 | hm.src = "https://hm.baidu.com/hm.js?44bf35035989ecd905587eb98a45525e"; 5 | var s = document.getElementsByTagName("script")[0]; 6 | s.parentNode.insertBefore(hm, s); 7 | })(); 8 | -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights", 5 | "connectionId": "APPINSIGHTS_CONNECTIONSTRING" 6 | }, 7 | "mssql1": { 8 | "type": "mssql", 9 | "connectionId": "Etymology" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Etymology.Data/Cache/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | namespace Etymology.Data.Cache; 3 | 4 | public static class ServiceCollectionExtensions 5 | { 6 | public static IServiceCollection AddCharacterCache(this IServiceCollection services) => 7 | services.AddScoped(); 8 | } -------------------------------------------------------------------------------- /GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Diagnostics; 2 | global using System.Diagnostics.CodeAnalysis; 3 | global using System.Globalization; 4 | global using System.Net; 5 | global using System.Text; 6 | global using System.Text.RegularExpressions; 7 | global using System.Xml.Linq; 8 | 9 | global using Microsoft.Extensions.DependencyInjection; 10 | global using Microsoft.Extensions.Logging; -------------------------------------------------------------------------------- /src/Etymology.Data/Models/AnalyzeResult.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | namespace Etymology.Data.Models; 3 | 4 | public record AnalyzeResult(string Chinese, Etymology Etymologies, IList Oracles, IList Bronzes, IList Seals, IList Liushutongs) 5 | { 6 | public int CharacterCount => this.Oracles.Count + this.Bronzes.Count + this.Seals.Count + this.Liushutongs.Count; 7 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Server/Cache.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server; 2 | 3 | using Microsoft.Extensions.Caching.Memory; 4 | 5 | internal static class Cache 6 | { 7 | internal static int ClientCacheMaxAge { get; } = (int)TimeSpan.FromDays(180).TotalSeconds; 8 | 9 | internal static MemoryCacheEntryOptions ServerCacheOptions { get; } = new() { SlidingExpiration = TimeSpan.FromHours(1) }; 10 | } -------------------------------------------------------------------------------- /src/Etymology.Common/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Common; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | public static class ApplicationBuilderExtensions 6 | { 7 | public static IApplicationBuilder UseEncodings(this IApplicationBuilder application) 8 | { 9 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 10 | return application; 11 | } 12 | } -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Chinese Etymology 字源 hanziyuan.net 2 | author: Dixin 3 | description: > # this means to ignore newlines until "baseurl:" 4 | Chinese Etymology 字源 hanziyuan.net 5 | show_excerpts: true # set to false to remove excerpts on the homepage 6 | theme: minima 7 | permalink: /:title 8 | 9 | # social links 10 | github_username: Dixin # DO NOT include the @ character, or else the build will fail! 11 | -------------------------------------------------------------------------------- /src/Etymology.Tool/Program.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using Etymology.Tool; 3 | 4 | Parser 5 | .Default 6 | .ParseArguments(args) 7 | .MapResult( 8 | Svg.Save, 9 | Unicode.Convert, 10 | errors => 11 | { 12 | errors.ForEach(error => Console.WriteLine(error.ToString())); 13 | return 1; 14 | }); -------------------------------------------------------------------------------- /src/Etymology.Web/Client/js/google.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject'] = r;i[r]=i[r]||function(){ 2 | (i[r].q = i[r].q || []).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 5 | 6 | ga('create', 'UA-494599-13', 'auto'); 7 | ga('send', 'pageview'); 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Website issue/etymology issue/new feature request 3 | --- 4 | 5 | If you have an issue with the website, please describe what is the issue, and how to reproduce the issue. 6 | 7 | If you found an issue with etymology, please describe what is the Chinese character, and what is the issue or what is missing with the character or its etymology. 8 | 9 | If you want a new feature, please describe how it works. 10 | -------------------------------------------------------------------------------- /src/Etymology.Web/Server/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server; 2 | 3 | public static class ServiceCollectionExtensions 4 | { 5 | public static IServiceCollection AddSettings(this IServiceCollection services, IConfiguration configuration, out TSettings settings) 6 | where TSettings : class 7 | { 8 | settings = configuration.Get(); 9 | return services.AddSingleton(settings); 10 | } 11 | } -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/Configuration.md 3 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 4 | "settings": { 5 | "documentationRules": { 6 | "documentInterfaces": false, 7 | "documentExposedElements": false, 8 | "documentInternalElements": false 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Etymology.Web/Client/css/ie10-viewport-bug-workaround.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /* 8 | * See the Getting Started docs for more information: 9 | * http://getbootstrap.com/getting-started/#support-ie10-width 10 | */ 11 | @-ms-viewport { width: device-width; } 12 | @-o-viewport { width: device-width; } 13 | @viewport { width: device-width; } 14 | -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #da532c 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Etymology.Tool/Unicode.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tool; 2 | 3 | using Etymology.Common; 4 | 5 | internal static class Unicode 6 | { 7 | internal static int Convert(UnicodeOptions unicodeOptions) 8 | { 9 | if (!string.IsNullOrEmpty(unicodeOptions.CodePoint)) 10 | { 11 | Console.WriteLine(unicodeOptions.CodePoint.CodePointToText()); 12 | } 13 | 14 | if (!string.IsNullOrEmpty(unicodeOptions.Text)) 15 | { 16 | Console.WriteLine(unicodeOptions.Text.TextToCodePoint()); 17 | } 18 | 19 | return 1; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Server/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server; 2 | 3 | public record Settings 4 | { 5 | public List AllowedHosts { get; } = new(); 6 | 7 | public Dictionary Connections { get; } = new(); 8 | 9 | public string ErrorPagePath { get; init; } = string.Empty; 10 | 11 | public List IndexPagePaths { get; } = new(); 12 | 13 | public bool IsHttpsOnly { get; init; } 14 | 15 | public List PublicPaths { get; } = new(); 16 | 17 | public Dictionary Routes { get; } = new(); 18 | 19 | public SameSiteMode SameSiteMode { get; init; } 20 | } -------------------------------------------------------------------------------- /src/Etymology.Data/Models/Bronze.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Etymology.Data.Models 7 | { 8 | public partial class Bronze 9 | { 10 | [Required] 11 | [StringLength(20)] 12 | public string Traditional { get; set; } 13 | public int? Rank { get; set; } 14 | public string ImageBase64 { get; set; } 15 | public string ImageVector { get; set; } 16 | [Key] 17 | public int BronzeId { get; set; } 18 | public string ImageVectorBase64 { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Etymology.Tool/UnicodeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tool; 2 | 3 | using CommandLine; 4 | 5 | [Verb("unicode", HelpText = "Convert from or to Unicode code point.")] 6 | #pragma warning disable CA1812 // Avoid uninstantiated internal classes 7 | internal class UnicodeOptions 8 | #pragma warning restore CA1812 // Avoid uninstantiated internal classes 9 | { 10 | [Option('c', "codepoint", Required = false, HelpText = "Convert from Unicode code point.")] 11 | public string CodePoint { get; set; } = string.Empty; 12 | 13 | [Option('t', "text", Required = false, HelpText = "Convert from Unicode code point.")] 14 | public string Text { get; set; } = string.Empty; 15 | } -------------------------------------------------------------------------------- /src/Etymology.Tool/SvgOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tool; 2 | 3 | using CommandLine; 4 | 5 | [Verb("svgtodb", HelpText = "Add SVG files to database.")] 6 | #pragma warning disable CA1812 // Avoid uninstantiated internal classes 7 | internal class SvgOptions 8 | #pragma warning restore CA1812 // Avoid uninstantiated internal classes 9 | { 10 | [Option('d', "directory", Required = false, HelpText = "Save all SVG files in the directory to database.")] 11 | public string Directory { get; set; } = string.Empty; 12 | 13 | [Option('c', "connection", Required = false, HelpText = "Connection string to database.")] 14 | public string Connection { get; set; } = string.Empty; 15 | } -------------------------------------------------------------------------------- /src/Etymology.Common/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Common; 2 | 3 | public static class ExceptionExtensions 4 | { 5 | public static bool IsNotCritical(this Exception exception) => 6 | exception is not (AccessViolationException 7 | or AppDomainUnloadedException 8 | or BadImageFormatException 9 | or CannotUnloadAppDomainException 10 | or InvalidProgramException 11 | or OutOfMemoryException 12 | or ThreadAbortException); 13 | 14 | public static bool LogErrorWith(this Exception exception, ILogger logger, string message, params object[] args) 15 | { 16 | logger.LogError(exception, message, args); 17 | return false; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Etymology.Data/Models/Oracle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Etymology.Data.Models 7 | { 8 | public partial class Oracle 9 | { 10 | [Key] 11 | public int OracleId { get; set; } 12 | [Required] 13 | [StringLength(20)] 14 | public string Traditional { get; set; } 15 | public int Rank { get; set; } 16 | [StringLength(255)] 17 | public string Combo { get; set; } 18 | public string ImageBase64 { get; set; } 19 | public string ImageVector { get; set; } 20 | public string ImageVectorBase64 { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Etymology.Common/Svg.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Common; 2 | 3 | public static class Svg 4 | { 5 | public static string ConvertToBase64(this string svgFile) 6 | { 7 | XDocument svgDocument = XDocument.Parse(svgFile); 8 | return Convert.ToBase64String(Encoding.UTF8.GetBytes(svgDocument.Root?.ToString(SaveOptions.DisableFormatting) ?? string.Empty)); 9 | } 10 | 11 | public static bool TryConvertToBase64(this string svgFile, out string base64) 12 | { 13 | try 14 | { 15 | base64 = ConvertToBase64(svgFile); 16 | return true; 17 | } 18 | catch (Exception exception) when (exception.IsNotCritical()) 19 | { 20 | base64 = string.Empty; 21 | return false; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Server/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": [ 3 | "hanziyuan.net", 4 | "etymology.azurewebsites.net", 5 | "etymology-staging.azurewebsites.net", 6 | "localhost", 7 | "127.0.0.1" 8 | ], 9 | "Connections": { 10 | "Etymology": "" 11 | }, 12 | "ErrorPagePath": "/error", 13 | "IndexPagePaths": [ "/", "/index.htm" ], 14 | "IsHttpsOnly": false, 15 | "Logging": { 16 | "IncludeScopes": false, 17 | "LogLevel": { 18 | "Default": "Warning" 19 | } 20 | }, 21 | "PublicPaths": [ 22 | "/css", 23 | "/error", 24 | "/fonts", 25 | "/images", 26 | "/js", 27 | "/favicon.ico", 28 | "/robots.txt" 29 | ], 30 | "Routes": { 31 | "default": "{controller}/{action}" 32 | }, 33 | "SameSiteMode": "Strict" 34 | } 35 | -------------------------------------------------------------------------------- /src/Etymology.Data/Models/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | namespace Etymology.Data.Models; 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | public static class ServiceCollectionExtensions 7 | { 8 | public static IServiceCollection AddDataAccess(this IServiceCollection services, string connection) 9 | { 10 | if (string.IsNullOrWhiteSpace(connection)) 11 | { 12 | throw new InvalidOperationException("Failed to get connection string."); 13 | } 14 | 15 | return services.AddDbContextPool(dbOptions => dbOptions.UseSqlServer( 16 | connection, 17 | sqlOptions => sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null))); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/serviceDependencies.etymology - Web Deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/etymology", 5 | "type": "appInsights.azure", 6 | "connectionId": "APPINSIGHTS_CONNECTIONSTRING", 7 | "secretStore": "AzureAppSettings" 8 | }, 9 | "mssql1": { 10 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Sql/servers/etymology-asia/databases/etymology-production", 11 | "type": "mssql.azure", 12 | "connectionId": "Etymology", 13 | "secretStore": "AzureAppSettings" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/serviceDependencies.etymology-staging - Web Deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/etymology-staging", 5 | "type": "appInsights.azure", 6 | "connectionId": "APPINSIGHTS_CONNECTIONSTRING", 7 | "secretStore": "AzureAppSettings" 8 | }, 9 | "mssql1": { 10 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Sql/servers/etymology/databases/etymology-staging", 11 | "type": "mssql.azure", 12 | "connectionId": "Etymology", 13 | "secretStore": "AzureAppSettings" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Etymology.Data/Models/Liushutong.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Etymology.Data.Models 7 | { 8 | public partial class Liushutong 9 | { 10 | [Required] 11 | [StringLength(20)] 12 | public string Traditional { get; set; } 13 | [Key] 14 | public int LiushutongId { get; set; } 15 | public int? SealId { get; set; } 16 | public string ImageBase64 { get; set; } 17 | public string ImageVector { get; set; } 18 | public string ImageVectorBase64 { get; set; } 19 | 20 | [ForeignKey(nameof(SealId))] 21 | [InverseProperty("Liushutong")] 22 | public virtual Seal Seal { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Analyzers.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Etymology.Common/Etymology.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | preview 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Etymology.Web/webpack.config.Debug.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin"); 4 | 5 | const { merge } = require('webpack-merge'); 6 | const common = require("./webpack.config.js"); 7 | 8 | const Paths = common.Paths; 9 | delete common.Paths; 10 | 11 | module.exports = merge(common, { 12 | mode: "development", 13 | devtool: "eval-source-map", 14 | plugins: [ 15 | new HtmlWebpackPlugin({ 16 | template: Paths.indexTemplate, 17 | filename: Paths.indexHTML, 18 | inject: "body", 19 | showErrors: true 20 | }), 21 | new webpack.ProvidePlugin({ 22 | jQuery: "jquery" 23 | }) 24 | ], 25 | optimization: { 26 | splitChunks: { 27 | cacheGroups: { 28 | commons: { 29 | test: /[\\/]node_modules[\\/]/, 30 | name: 'vendors', 31 | chunks: 'all', 32 | }, 33 | }, 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/Etymology.Data/Models/ICharacter.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Data.Models; 2 | 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | public interface IFormattable 6 | { 7 | [NotMapped] 8 | string Prefix { get; } 9 | 10 | [NotMapped] 11 | int Id { get; } 12 | } 13 | 14 | public interface ICharacter : IFormattable 15 | { 16 | string Traditional { get; set; } 17 | 18 | string ImageBase64 { get; set; } 19 | 20 | string ImageVector { get; set; } 21 | } 22 | 23 | public partial class Bronze : ICharacter 24 | { 25 | [NotMapped] 26 | public string Prefix => "B"; 27 | 28 | [NotMapped] 29 | public int Id => this.BronzeId; 30 | } 31 | 32 | public partial class Liushutong : ICharacter 33 | { 34 | [NotMapped] 35 | public string Prefix => "L"; 36 | 37 | [NotMapped] 38 | public int Id => this.LiushutongId; 39 | } 40 | 41 | public partial class Oracle : ICharacter 42 | { 43 | [NotMapped] 44 | public string Prefix => "J"; 45 | 46 | [NotMapped] 47 | public int Id => this.OracleId; 48 | } 49 | 50 | public partial class Seal : ICharacter 51 | { 52 | [NotMapped] 53 | public string Prefix => "S"; 54 | 55 | [NotMapped] 56 | public int Id => this.SealId; 57 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 2.0.{build} 2 | image: Visual Studio 2019 3 | configuration: 4 | - Debug 5 | - Release 6 | init: 7 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 8 | dotnet_csproj: 9 | patch: true 10 | file: '**\*.csproj' 11 | version: '{version}' 12 | version_prefix: '{version}' 13 | package_version: '{version}' 14 | assembly_version: '{version}' 15 | file_version: '{version}' 16 | informational_version: '{version}' 17 | environment: 18 | Etymology: 19 | secure: EXqa8qHo22jd/nq17/myGbk077tkDDe22+jT/Jxl1SBNTkWct5r/PY1QgSporP57OD1X5saev6hfDQHy4ZjU9F4C/m8/Sf2IFzSNaeHE/fsAxpe8UmuvXFZ4Suzh8/54GlWd/8wV9PPTaIVyf0bLdLjVexbqw2+Nttv6ycu/CBzsUz/SLSR4BdmLzZtmk/IwvnUgkyLWaS91S41DnwRunZ1a2us5WFWBOB164C3QbWbDQNopjcGjQnFe6zc9eVwwx62FzGjqAegtudV7ly3vY6l4ol0N+KQVedFles3w7w3lg6+62AXAAbno7zif0sqTdPY7I9exVEoNd1uxndUUKA== 20 | APPVEYOR_RDP_PASSWORD: 21 | secure: aii6TuA5V1pzlqRiUOXyTQ== 22 | ASPNETCORE_ENVIRONMENT: Staging 23 | install: 24 | - ps: Install-Product node Current 25 | before_build: 26 | - cmd: dotnet restore 27 | build: 28 | parallel: true 29 | verbosity: normal 30 | artifacts: 31 | - path: ./Publish/Release 32 | name: PublishRelease -------------------------------------------------------------------------------- /src/Etymology.Data.Console/Etymology.Data.Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | preview 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Etymology.Data/Etymology.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | preview 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Etymology.Web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const Paths = { 4 | source: path.join(__dirname, "Client"), 5 | build: path.join(__dirname, "wwwroot"), 6 | indexSourceJavaScript: "js/index.js", 7 | indexBuildJavaScript: "js/[name].[chunkhash].js", 8 | indexTemplate: "index.ejs", 9 | indexHTML: "index.htm" 10 | }; 11 | 12 | module.exports = { 13 | context: Paths.source, 14 | entry: path.join(Paths.source, Paths.indexSourceJavaScript), 15 | output: { 16 | path: Paths.build, 17 | filename: Paths.indexBuildJavaScript 18 | }, 19 | stats: { 20 | colors: true, 21 | modules: true, 22 | reasons: true, 23 | errorDetails: true 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.m?js$/, 29 | exclude: /node_modules/, 30 | use: [ 31 | { 32 | loader: "babel-loader", 33 | options: { 34 | "presets": ["@babel/preset-env"] 35 | } 36 | } 37 | ] 38 | } 39 | ] 40 | }, 41 | target: ["web", "es5"], 42 | Paths: Paths 43 | }; 44 | -------------------------------------------------------------------------------- /src/Etymology.Web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "etymology", 4 | "author": "Dixin Yan", 5 | "private": true, 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Dixin/Etymology.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/Dixin/Etymology/issues" 12 | }, 13 | "dependencies": { 14 | "bootstrap": "<4.0.0-alpha.2", 15 | "ie10-viewport-bug-workaround.js": "^1.0.0", 16 | "jquery": "^3.5.1", 17 | "js-cookie": "^2.2.1", 18 | "ladda": "^2.0.1", 19 | "string.prototype.codepointat": "^0.2.1" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.12.10", 23 | "@babel/preset-env": "^7.12.10", 24 | "babel-loader": "^8.2.2", 25 | "css-loader": "^0.28.11", 26 | "file-loader": "^6.2.0", 27 | "html-loader": "^1.3.2", 28 | "html-webpack-inline-source-plugin": "0.0.10", 29 | "html-webpack-plugin": "^4.5.0", 30 | "terser-webpack-plugin": "^5.0.3", 31 | "to-string-loader": "^1.1.6", 32 | "webpack": "^5.10.1", 33 | "webpack-cli": "^4.2.0", 34 | "webpack-merge": "^5.7.0" 35 | }, 36 | "scripts": { 37 | "buildDebug": "webpack --config webpack.config.Debug.js", 38 | "buildRelease": "webpack --config webpack.config.Release.js" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/azure-staging.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: Setup build test and deploy to Azure staging 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | setup-build-test-deploy: 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v2.1.2 20 | with: 21 | node-version: '15' 22 | check-latest: true 23 | 24 | - name: Setup .NET Core 25 | uses: actions/setup-dotnet@v1 26 | with: 27 | dotnet-version: '5.0.100' 28 | 29 | - name: Build 30 | run: dotnet build --configuration Release 31 | 32 | - name: Test 33 | run: dotnet test --no-restore --verbosity normal 34 | env: 35 | Etymology: ${{ secrets.Etymology }} 36 | 37 | - name: Publish 38 | run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp 39 | 40 | - name: Deploy 41 | uses: azure/webapps-deploy@v1 42 | with: 43 | app-name: 'etymology-staging' 44 | slot-name: 'production' 45 | publish-profile: ${{ secrets.AzureAppService_PublishProfile_65a331250c6747ebb97f9bf4e3050b77 }} 46 | package: ${{env.DOTNET_ROOT}}/myapp 47 | -------------------------------------------------------------------------------- /src/Etymology.Tool/Etymology.Tool.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | preview 8 | enable 9 | 10 | 11 | 12 | latest 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:50518/", 7 | "sslPort": 44306 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development", 16 | "Etymology": "" 17 | } 18 | }, 19 | "Etymology.Web-Development": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development", 24 | "Etymology": "" 25 | }, 26 | "applicationUrl": "http://localhost:50518/" 27 | }, 28 | "Etymology.Web-Staging": { 29 | "commandName": "Project", 30 | "launchBrowser": true, 31 | "environmentVariables": { 32 | "ASPNETCORE_ENVIRONMENT": "Staging", 33 | "Etymology": "" 34 | }, 35 | "applicationUrl": "http://localhost:50518/" 36 | }, 37 | "Etymology.Web-Production": { 38 | "commandName": "Project", 39 | "launchBrowser": true, 40 | "environmentVariables": { 41 | "ASPNETCORE_ENVIRONMENT": "Production", 42 | "Etymology": "" 43 | }, 44 | "applicationUrl": "http://localhost:50518/" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/images/favicons/android-chrome-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/images/favicons/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/images/favicons/android-chrome-72x72.png", 17 | "sizes": "72x72", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/images/favicons/android-chrome-96x96.png", 22 | "sizes": "96x96", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "/images/favicons/android-chrome-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "/images/favicons/android-chrome-192x192.png", 32 | "sizes": "192x192", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "/images/favicons/android-chrome-256x256.png", 37 | "sizes": "256x256", 38 | "type": "image/png" 39 | } 40 | ], 41 | "theme_color": "#ffffff", 42 | "background_color": "#ffffff", 43 | "start_url": "/", 44 | "display": "standalone" 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Etymology 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/osj54c5cd3kafd99?svg=true)](https://ci.appveyor.com/project/Dixin/Etymology) 4 | ![Setup build test and deploy to Azure staging](https://github.com/Dixin/Etymology/workflows/Setup%20build%20test%20and%20deploy%20to%20Azure%20staging/badge.svg) 5 | [![Gitter chat](https://badges.gitter.im/ChineseEtymology/gitter.png)](https://gitter.im/ChineseEtymology) 6 | 7 | Etymology is a ASP.NET Core architecture demonstration of SPA (single page application), with the latest and popular technologies. It is a fully functional website with well-designed architecture. The server side is based on .NET 5 and C# 9.0, and the client side is based on Bootstrap and ES 2015+, integrated with npm, webpack. It demnstrates the aspects of ASP.NET Core architecture of SPA, including: 8 | - Server-side middleware implementation 9 | - Server-side rendering with Razor engine 10 | - Tracing and logging 11 | - Error handling 12 | - Configuration management 13 | - Data access with Entity Framework Core 14 | - security, especially anti forery request 15 | - Client bundling with Webpack 16 | - ES2015+ transpile with Babel 17 | - Responsible UX design based on Boostrap 18 | - Content protection from hot link and crawler 19 | - Server-side cache 20 | - Client cache 21 | 22 | To run the code, open Etymology.sln with visual Studio 2019 or open the root folder with Visual Studio Code, build and run Etymology.Web.csproj. 23 | -------------------------------------------------------------------------------- /Analyzers.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory)Analyzers.ruleset 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Etymology.Web/webpack.config.Release.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | 5 | const { merge } = require('webpack-merge'); 6 | const common = require("./webpack.config.js"); 7 | 8 | const Paths = common.Paths; 9 | delete common.Paths; 10 | 11 | module.exports = merge(common, { 12 | mode: "production", 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: Paths.indexTemplate, 16 | filename: Paths.indexHTML, 17 | inject: "body", 18 | showErrors: true, 19 | minify: { 20 | removeComments: true, 21 | collapseWhitespace: true 22 | } 23 | }), 24 | new webpack.ProvidePlugin({ 25 | jQuery: "jquery" 26 | }) 27 | ], 28 | optimization: { 29 | splitChunks: { 30 | cacheGroups: { 31 | commons: { 32 | test: /[\\/]node_modules[\\/]/, 33 | name: 'vendors', 34 | chunks: 'all', 35 | }, 36 | }, 37 | }, 38 | minimize: true, 39 | minimizer: [ 40 | new TerserPlugin({ 41 | terserOptions: { 42 | format: { 43 | comments: false, 44 | }, 45 | }, 46 | extractComments: false, 47 | }) 48 | ] 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/PublishProfiles/etymology - Web Deploy.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | MSDeploy 9 | /subscriptions/e09d69aa-afa1-4db3-aea3-ca58cc2d82ee/resourceGroups/etymology/providers/Microsoft.Web/sites/etymology 10 | etymology 11 | AzureWebSite 12 | Release 13 | Any CPU 14 | http://hanziyuan.net 15 | True 16 | False 17 | 067fef43-b8ee-495c-aed5-c9faea45456f 18 | etymology.scm.azurewebsites.net:443 19 | etymology 20 | 21 | False 22 | WMSVC 23 | True 24 | $etymology 25 | <_SavePWD>True 26 | <_DestinationType>AzureWebSite 27 | net5.0 28 | win-x64 29 | false 30 | 31 | -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/PublishProfiles/etymology-staging - Web Deploy.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | MSDeploy 9 | /subscriptions/e09d69aa-afa1-4db3-aea3-ca58cc2d82ee/resourceGroups/etymology/providers/Microsoft.Web/sites/etymology-staging 10 | etymology 11 | AzureWebSite 12 | Release 13 | Any CPU 14 | http://etymology-staging.azurewebsites.net 15 | True 16 | False 17 | 067fef43-b8ee-495c-aed5-c9faea45456f 18 | etymology-staging.scm.azurewebsites.net:443 19 | etymology-staging 20 | 21 | False 22 | WMSVC 23 | True 24 | $etymology-staging 25 | <_SavePWD>True 26 | <_DestinationType>AzureWebSite 27 | net5.0 28 | win-x86 29 | false 30 | 31 | -------------------------------------------------------------------------------- /src/Etymology.Data/Models/Seal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Etymology.Data.Models 7 | { 8 | public partial class Seal 9 | { 10 | public Seal() 11 | { 12 | Liushutong = new HashSet(); 13 | } 14 | 15 | [Key] 16 | public int SealId { get; set; } 17 | [Required] 18 | [StringLength(20)] 19 | public string Traditional { get; set; } 20 | [StringLength(255)] 21 | public string Common { get; set; } 22 | [StringLength(255)] 23 | public string Rank { get; set; } 24 | [StringLength(255)] 25 | public string KaiDecomposition { get; set; } 26 | [StringLength(255)] 27 | public string SealDecomposition { get; set; } 28 | [StringLength(255)] 29 | public string TransitionMechanism { get; set; } 30 | [StringLength(255)] 31 | public string Mandarin { get; set; } 32 | [StringLength(255)] 33 | public string ShuowenTraditional { get; set; } 34 | [Column("BH")] 35 | [StringLength(255)] 36 | public string Bh { get; set; } 37 | [StringLength(255)] 38 | public string ZhW { get; set; } 39 | [Column("HXKZ")] 40 | [StringLength(255)] 41 | public string Hxkz { get; set; } 42 | [StringLength(255)] 43 | public string ShuowenSimplified { get; set; } 44 | [Column("DX")] 45 | [StringLength(255)] 46 | public string Dx { get; set; } 47 | [Column("DXFQ")] 48 | [StringLength(255)] 49 | public string Dxfq { get; set; } 50 | public string ImageBase64 { get; set; } 51 | public string ImageVector { get; set; } 52 | public string ImageVectorBase64 { get; set; } 53 | 54 | [InverseProperty("Seal")] 55 | public virtual ICollection Liushutong { get; set; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Etymology.Web/Etymology.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | preview 6 | enable 7 | false 8 | true 9 | 1eeb3477-bd6c-4e1c-b71d-c463ce05fa6f 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | all 45 | runtime; build; native; contentfiles; analyzers; buildtransitive 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/Etymology.Web/Client/js/index.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import "bootstrap"; 3 | import Data from "./data"; 4 | import UI from "./ui"; 5 | import Hash from "./hash"; 6 | 7 | const global = window; 8 | 9 | const trySearchFromHash = () => { 10 | const chinese = Hash.get(); 11 | if (Data.isSingleChinese(chinese) && $(`#${chinese}`).length === 0) { 12 | UI.$chinese.val(chinese); 13 | UI.startLoading(); 14 | 15 | Data.search(chinese).done(data => { 16 | UI.showResult(data); 17 | 18 | Hash.setPositions(UI.$positions()); 19 | Hash.scrollTo(chinese); 20 | }).fail((jqXHR, textStatus, error) => { 21 | UI.showError(jqXHR.status, jqXHR.responseText, textStatus, error); 22 | 23 | Hash.setPositions(UI.$positions()); 24 | Hash.scrollTo(UI.$error.prop("id")); 25 | }).always(() => UI.stopLoading()); 26 | } 27 | }; 28 | 29 | $(() => { // document.ready. 30 | UI.init(); 31 | 32 | Hash.init(UI.getNavOffset(), UI.$positions()); 33 | 34 | UI.$links.on("click", event => { 35 | event.preventDefault(); 36 | Hash.scrollTo(event.currentTarget.hash); 37 | UI.collapseMenu(); 38 | }); 39 | 40 | UI.$search.on("submit", event => { 41 | event.preventDefault(); 42 | const chinese = UI.getChinese(event.currentTarget); 43 | if (Data.isSingleChinese(chinese)) { 44 | UI.collapseMenu(); 45 | if (!Hash.scrollTo(chinese)) { 46 | Hash.set(chinese); 47 | } 48 | } else { 49 | UI.showInvalidInput(); 50 | } 51 | }); 52 | 53 | UI.$random.on("click", () => Hash.set(Data.randomChinese())); 54 | 55 | UI.$main.on("click", () => UI.collapseMenu()); 56 | 57 | trySearchFromHash(); 58 | }); 59 | 60 | $(global) 61 | .on("hashchange", trySearchFromHash) 62 | .on("scroll", () => Hash.setAfterScrollWithTimeout(UI.collapseMenu)) 63 | .on("load", UI.loadVideoThumbnails) 64 | .on("resize", () => Hash.setPositionsWithTimeout(UI.$positions())); 65 | 66 | import "ie10-viewport-bug-workaround.js"; 67 | 68 | import "./baidu"; 69 | import "./google"; 70 | -------------------------------------------------------------------------------- /src/Etymology.Data/Models/VEtymology.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Etymology.Data.Models 7 | { 8 | public partial class VEtymology 9 | { 10 | [Required] 11 | [StringLength(5)] 12 | public string Simplified { get; set; } 13 | [Required] 14 | [StringLength(5)] 15 | public string Traditional { get; set; } 16 | [StringLength(10)] 17 | public string OldTraditional { get; set; } 18 | [StringLength(15)] 19 | public string Pinyin { get; set; } 20 | [StringLength(10)] 21 | public string Index8105 { get; set; } 22 | [StringLength(30)] 23 | public string SimplificationRule { get; set; } 24 | [StringLength(200)] 25 | public string SimplificationClarified { get; set; } 26 | [StringLength(40)] 27 | public string VariantRule { get; set; } 28 | [StringLength(255)] 29 | public string VariantClarified { get; set; } 30 | [StringLength(30)] 31 | public string AppliedRule { get; set; } 32 | [StringLength(10)] 33 | public string FontRule { get; set; } 34 | [StringLength(255)] 35 | public string Decomposition { get; set; } 36 | [StringLength(255)] 37 | public string DecompositionClarified { get; set; } 38 | [StringLength(255)] 39 | public string OriginalMeaning { get; set; } 40 | [StringLength(50)] 41 | public string Videos { get; set; } 42 | [StringLength(255)] 43 | public string WordExample { get; set; } 44 | [StringLength(255)] 45 | public string EnglishSenses { get; set; } 46 | [StringLength(255)] 47 | public string PinyinOther { get; set; } 48 | [StringLength(255)] 49 | public string Pictures { get; set; } 50 | [StringLength(5)] 51 | public string LearnOrder { get; set; } 52 | [StringLength(10)] 53 | public string FrequencyOrder { get; set; } 54 | [StringLength(25)] 55 | public string IdealForms { get; set; } 56 | [StringLength(25)] 57 | public string Classification { get; set; } 58 | public int EtymologyId { get; set; } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Etymology.Data/Models/Etymology.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Etymology.Data.Models 7 | { 8 | public partial class Etymology 9 | { 10 | [Required] 11 | [StringLength(5)] 12 | public string Simplified { get; set; } 13 | [Required] 14 | [StringLength(5)] 15 | public string Traditional { get; set; } 16 | [StringLength(10)] 17 | public string OldTraditional { get; set; } 18 | [StringLength(15)] 19 | public string Pinyin { get; set; } 20 | [StringLength(10)] 21 | public string Index8105 { get; set; } 22 | [StringLength(30)] 23 | public string SimplificationRule { get; set; } 24 | [StringLength(200)] 25 | public string SimplificationClarified { get; set; } 26 | [StringLength(40)] 27 | public string VariantRule { get; set; } 28 | [StringLength(255)] 29 | public string VariantClarified { get; set; } 30 | [StringLength(30)] 31 | public string AppliedRule { get; set; } 32 | [StringLength(10)] 33 | public string FontRule { get; set; } 34 | [StringLength(255)] 35 | public string Decomposition { get; set; } 36 | [StringLength(255)] 37 | public string DecompositionClarified { get; set; } 38 | [StringLength(255)] 39 | public string OriginalMeaning { get; set; } 40 | [StringLength(50)] 41 | public string Videos { get; set; } 42 | [StringLength(255)] 43 | public string WordExample { get; set; } 44 | [StringLength(255)] 45 | public string EnglishSenses { get; set; } 46 | [StringLength(255)] 47 | public string PinyinOther { get; set; } 48 | [StringLength(255)] 49 | public string Pictures { get; set; } 50 | [StringLength(5)] 51 | public string LearnOrder { get; set; } 52 | [StringLength(10)] 53 | public string FrequencyOrder { get; set; } 54 | [StringLength(25)] 55 | public string BookIndex { get; set; } 56 | [StringLength(25)] 57 | public string Classification { get; set; } 58 | [Key] 59 | public int EtymologyId { get; set; } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Etymology.Data/Models/Etymology.Display.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Data.Models; 2 | 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using global::Etymology.Common; 5 | 6 | [DebuggerDisplay($"{{{nameof(Traditional)}}}, {{{nameof(Simplified)}}}, {{{nameof(EtymologyId)}}}, {{{nameof(OldTraditional)}}}")] 7 | public partial class Etymology : IFormattable 8 | { 9 | [NotMapped] 10 | public string SimplifiedInitial => this.Simplified.Characters().First(); 11 | 12 | [NotMapped] 13 | public int SimplifiedUnicode => char.ConvertToUtf32(this.Simplified, 0); 14 | 15 | [NotMapped] 16 | public string TraditionalInitial => this.Traditional.Characters().First(); 17 | 18 | [NotMapped] 19 | public int TraditionalUnicode => char.ConvertToUtf32(this.Traditional, 0); 20 | 21 | [NotMapped] 22 | public string Prefix => "E"; 23 | 24 | [NotMapped] 25 | public int Id => this.EtymologyId; 26 | } 27 | 28 | public static class DisplayExtensions 29 | { 30 | public static string ToHex(this int value) => 31 | value.ToString("X4"); 32 | 33 | public static bool HasSimplified(this Etymology etymology) => 34 | !string.IsNullOrEmpty(etymology.Simplified) && etymology.Simplified.Characters().First().IsSingleChineseCharacter(); 35 | 36 | public static string Title(this Etymology etymology) => 37 | etymology.HasSimplified() 38 | ? $"{etymology.FormattedId()} {etymology.TraditionalInitial}{etymology.TraditionalUnicode.ToHex()} → {etymology.SimplifiedInitial}{etymology.SimplifiedUnicode.ToHex()}" 39 | : $"{etymology.FormattedId()} {etymology.TraditionalInitial}{etymology.TraditionalUnicode.ToHex()}"; 40 | 41 | public static bool IsNullOrWhiteSpace(this string value) => 42 | string.IsNullOrWhiteSpace(value); 43 | 44 | public static string WithNotApplicable(this string value) => 45 | string.IsNullOrWhiteSpace(value) || string.Equals(value, "z", StringComparison.OrdinalIgnoreCase) 46 | ? "Not applicable." 47 | : value; 48 | 49 | public static string WithNotExist(this string value) => 50 | string.IsNullOrWhiteSpace(value) || string.Equals(value, "z", StringComparison.OrdinalIgnoreCase) 51 | ? "Not exists." 52 | : value; 53 | 54 | public static string FormattedId(this IFormattable formattable) => $"{formattable.Prefix.First()}{formattable.Id:00000}"; 55 | } -------------------------------------------------------------------------------- /src/Etymology.Tests/Etymology.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | preview 7 | enable 8 | false 9 | 10 | 11 | 12 | latest 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | all 52 | runtime; build; native; contentfiles; analyzers; buildtransitive 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Etymology.Tests/Common/ChineseTests.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tests.Common; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using static Etymology.Common.Chinese; 6 | 7 | [TestClass] 8 | public class ChineseTests 9 | { 10 | internal static readonly List<(string HexCodePoint, string Text)> ChineseCharacters = new() 11 | { 12 | ("4E00", "一"), 13 | ("3400", "㐀"), 14 | ("3426", "㐦"), 15 | ("20000", "𠀀"), 16 | ("2A700", "𪜀"), 17 | ("2B740", "𫝀"), 18 | ("2F800", "丽"), 19 | ("2E80", "⺀"), 20 | ("2F00", "⼀"), 21 | ("2FF0", "⿰"), 22 | ("3025", "〥"), 23 | ("3044", "い"), 24 | ("30A2", "ア"), 25 | ("3106", "ㄆ"), 26 | ("31A1", "ㆡ"), 27 | ("31CF", "㇏"), 28 | ("31F0", "ㇰ"), 29 | ("F900", "豈"), 30 | ("FE3D", "︽"), 31 | ("FF6C", "ャ"), 32 | ("2B8B8", "𫢸"), 33 | }; 34 | 35 | internal static readonly string?[] NonChineseCharacters = 36 | { 37 | null, string.Empty, " ", " ", ".", "@", "a", "A", "1", "𠀀".Substring(0, 1), 38 | }; 39 | 40 | static ChineseTests() 41 | { 42 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 43 | } 44 | 45 | [TestMethod] 46 | public void ValidateSingleChineseCharacterTest() 47 | { 48 | ChineseCharacters.ForEach(item => Assert.IsNull(ValidateSingleChineseCharacter(item.Text).Exception)); 49 | 50 | NonChineseCharacters.ForEach(item => Assert.IsNotNull(ValidateSingleChineseCharacter(item).Exception)); 51 | } 52 | 53 | [TestMethod] 54 | public void TextToCodePointTest() 55 | { 56 | ChineseCharacters.ForEach(item => Assert.AreEqual(item.HexCodePoint, item.Text.TextToCodePoint())); 57 | } 58 | 59 | [TestMethod] 60 | public void CodePointToTextTest() 61 | { 62 | ChineseCharacters.ForEach(item => Assert.AreEqual(item.Text, item.HexCodePoint.CodePointToText())); 63 | } 64 | 65 | [TestMethod] 66 | public void CodePointToBytesTest() 67 | { 68 | ChineseCharacters.ForEach(item => Assert.IsTrue(Encoding.Unicode.GetBytes(item.Text).SequenceEqual(item.HexCodePoint.CodePointToBytes()))); 69 | } 70 | 71 | [TestMethod] 72 | public void BytesToCodePointTest() 73 | { 74 | ChineseCharacters.ForEach(item => Assert.AreEqual(item.HexCodePoint, Encoding.Unicode.GetBytes(item.Text).BytesToCodePoint())); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/ServiceDependencies/etymology - Web Deploy/appInsights1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "etymology", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "centralus", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('etymology', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "name": "etymology", 53 | "type": "microsoft.insights/components", 54 | "location": "[parameters('resourceLocation')]", 55 | "kind": "web", 56 | "properties": {}, 57 | "apiVersion": "2015-05-01" 58 | } 59 | ] 60 | } 61 | } 62 | } 63 | ], 64 | "metadata": { 65 | "_dependencyType": "appInsights.azure" 66 | } 67 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Client/js/ui.js: -------------------------------------------------------------------------------- 1 | import "spin.js"; 2 | import * as Ladda from 'ladda'; 3 | import * as $ from 'jquery'; 4 | 5 | const global = window; 6 | 7 | const ui = { 8 | init: () => { 9 | ui.$main = $("#etymologyMain"); 10 | ui.$nav = $("#etymologyNav"); 11 | ui.$navMenu = $("#etymologyNavMenu"); 12 | ui.$positions = () => $("div.etymology-nav"); 13 | ui.$links = $("a.etymology-link"); 14 | ui.buttons = $("#etymologySearchButton, #etymologyNavSearchButton, #etymologyRandomButton").map((index, element) => Ladda.create(element)); 15 | ui.$error = $("#etymologyError"); 16 | ui.$errorMessage = ui.$error.find("#etymologyErrorMessage"); 17 | ui.$result = $("#etymologyResult"); 18 | ui.$search = $("#etymologySearch, #etymologyNavSearch"); 19 | ui.$random = $("#etymologyRandomButton"); 20 | ui.$videos = $("#videos .media-left a"); 21 | ui.$chinese = $("#etymologySearchChar, #etymologyNavSearchChar"); 22 | ui.$modal = $("#etymologyModal"); 23 | ui.$modalTitle = ui.$modal.find(".modal-title"); 24 | ui.$modalContent = ui.$modal.find(".modal-content"); 25 | 26 | ui.$modal.on("show.bs.modal", event => { 27 | const id = $.trim($(event.relatedTarget).text()); 28 | ui.$modalTitle.text(id); 29 | ui.$modalContent.prop("id", id); 30 | }); 31 | }, 32 | 33 | startLoading: () => $.each(ui.buttons, (index, button) => button.start()), 34 | 35 | stopLoading: () => $.each(ui.buttons, (index, button) => button.stop()), 36 | 37 | showResult: data => { 38 | ui.$error.hide(); 39 | ui.$result.html(data).show(); 40 | }, 41 | 42 | showError: (status, responseText, textStatus, error) => { 43 | ui.$result.hide(); 44 | const message = !status ? "Network connection failed." : `[${status} ${textStatus}] ${error}. ${responseText}`; 45 | ui.$errorMessage.text(message); 46 | ui.$error.show(); 47 | }, 48 | 49 | loadVideoThumbnails: () => { 50 | ui.$videos.each((index, element) => { 51 | const $link = $(element); 52 | $link.css({ "background-image": `url(${$link.data("background")})` }); 53 | }); 54 | }, 55 | 56 | getChinese: search => search["chinese"].value, 57 | 58 | getNavOffset: () => ui.$nav.height() - 1, 59 | 60 | collapseMenu: () => { 61 | if (ui.$navMenu.hasClass("in")) { 62 | ui.$navMenu.collapse('hide'); 63 | } 64 | }, 65 | 66 | showInvalidInput: () => global.alert("Please input single Chinese character 请输入单个汉字.") 67 | }; 68 | 69 | export default ui; 70 | -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/ServiceDependencies/etymology-staging - Web Deploy/appInsights1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "etymology", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "centralus", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('etymology-staging', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "name": "etymology-staging", 53 | "type": "microsoft.insights/components", 54 | "location": "[parameters('resourceLocation')]", 55 | "kind": "web", 56 | "properties": {}, 57 | "apiVersion": "2015-05-01" 58 | } 59 | ] 60 | } 61 | } 62 | } 63 | ], 64 | "metadata": { 65 | "_dependencyType": "appInsights.azure" 66 | } 67 | } -------------------------------------------------------------------------------- /src/Etymology.Data/Cache/CharacterCache.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | namespace Etymology.Data.Cache; 3 | 4 | using Etymology.Common; 5 | using Etymology.Data.Models; 6 | 7 | public interface ICharacterCache 8 | { 9 | IEnumerable<(string Traditional, int CodePoint)> AllTraditional(int codePoint); 10 | } 11 | 12 | public class CharacterCache : ICharacterCache 13 | { 14 | private static Dictionary traditional = new(); 15 | 16 | private static ILookup simplifiedToTraditional = Array.Empty().ToLookup(value => value, value => value); 17 | 18 | private static ILookup oldTraditionalToTraditional = Array.Empty().ToLookup(value => value, value => value); 19 | 20 | public CharacterCache(EtymologyContext etymologyContext) 21 | { 22 | if (traditional.Any()) 23 | { 24 | return; 25 | } 26 | 27 | (int Traditional, int Simplified, int[] OldTraditional)[] etymologies = etymologyContext 28 | .Etymology 29 | .Select(etymology => new { etymology.Traditional, etymology.Simplified, etymology.OldTraditional }) 30 | .AsEnumerable() 31 | .Where(etymology => !string.IsNullOrWhiteSpace(etymology.Simplified)) 32 | .Select(etymology => 33 | ( 34 | Traditional: char.ConvertToUtf32(etymology.Traditional, 0), 35 | Simplified: char.ConvertToUtf32(etymology.Simplified.Characters().First(), 0), 36 | OldTraditional: string.IsNullOrWhiteSpace(etymology.OldTraditional) 37 | ? Array.Empty() 38 | : etymology.OldTraditional.Characters().Select(old => char.ConvertToUtf32(old, 0)).ToArray() 39 | )) 40 | .ToArray(); 41 | traditional = etymologies.ToDictionary(etymology => etymology.Traditional, _ => (object?)null); 42 | simplifiedToTraditional = etymologies.ToLookup(etymology => etymology.Simplified, etymology => etymology.Traditional); 43 | oldTraditionalToTraditional = etymologies 44 | .Where(etymology => etymology.OldTraditional is not null) 45 | .SelectMany(etymology => etymology.OldTraditional, (etymology, old) => (Old: old, Traditional: etymology.Traditional)) 46 | .ToLookup(etymology => etymology.Old, etymology => etymology.Traditional); 47 | } 48 | 49 | public IEnumerable<(string Traditional, int CodePoint)> AllTraditional(int codePoint) 50 | { 51 | List allTraditional = simplifiedToTraditional[codePoint].ToList(); 52 | allTraditional.AddRange(oldTraditionalToTraditional[codePoint]); 53 | if (traditional.ContainsKey(codePoint)) 54 | { 55 | allTraditional.Add(codePoint); 56 | } 57 | 58 | return allTraditional 59 | .Distinct() 60 | .Select(traditional => (Traditional: char.ConvertFromUtf32(traditional), CodePoint: traditional)); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Etymology.Tool/Svg.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tool; 2 | 3 | using Etymology.Data.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | internal static class Svg 7 | { 8 | internal static int Save(SvgOptions svgOptions) 9 | { 10 | string[] oracleCharacters = Directory.GetFiles(svgOptions.Directory, "j*.svg", SearchOption.AllDirectories); 11 | Array.Sort(oracleCharacters); 12 | string[] bronzeCharacters = Directory.GetFiles(svgOptions.Directory, "b*.svg", SearchOption.AllDirectories); 13 | Array.Sort(bronzeCharacters); 14 | string[] sealCharacters = Directory.GetFiles(svgOptions.Directory, "s*.svg", SearchOption.AllDirectories); 15 | Array.Sort(sealCharacters); 16 | string[] liushutongCharacters = Directory.GetFiles(svgOptions.Directory, "l*.svg", SearchOption.AllDirectories); 17 | Array.Sort(liushutongCharacters); 18 | 19 | using EtymologyContext database = Database(svgOptions.Connection); 20 | if (!(Update(database, oracleCharacters) 21 | && Update(database, bronzeCharacters) 22 | && Update(database, sealCharacters) 23 | && Update(database, liushutongCharacters))) 24 | { 25 | return 1; 26 | } 27 | 28 | int count = database.SaveChanges(); 29 | Console.WriteLine($"{count} files saved to database."); 30 | return 0; 31 | } 32 | 33 | private static EtymologyContext Database(string connection) 34 | { 35 | return new(new DbContextOptionsBuilder().UseSqlServer( 36 | connection, 37 | options => options 38 | .EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null)).Options); 39 | } 40 | 41 | private static bool Update(EtymologyContext database, string[] images) 42 | where T : class, ICharacter 43 | { 44 | if (images.Any()) 45 | { 46 | T[] entities = database.Set().ToArray(); 47 | Dictionary dictionary = entities.ToDictionary(entity => entity.Id); 48 | foreach (string image in images) 49 | { 50 | if (int.TryParse(Path.GetFileNameWithoutExtension(image).Substring(1), out int id)) 51 | { 52 | if (!dictionary.ContainsKey(id)) 53 | { 54 | Console.WriteLine($"Character not in database: {image}."); 55 | return false; 56 | } 57 | 58 | dictionary[id].ImageVector = File.ReadAllText(image); 59 | } 60 | else 61 | { 62 | Console.WriteLine($"Incorrect file is ignored: {image}."); 63 | 64 | // return false; 65 | } 66 | } 67 | } 68 | 69 | return true; 70 | } 71 | } -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/images/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 19 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Etymology.Web/Client/js/data.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | import $ from "jquery"; 3 | import "String.prototype.codePointAt"; 4 | 5 | const global = window; 6 | const Math = global.Math; 7 | 8 | const commonChars = "安八白百敗般邦保寶北貝匕比妣畢賓兵丙秉并伯帛亳卜不步采倉曹曾長厂鬯車徹臣辰晨成承乘遲齒赤沖舂丑出初楚畜傳春祠此次大丹單旦稻得登帝典奠丁鼎冬東動斗豆斷隊對多而兒耳二伐反方妃分封豐酆逢缶夫弗服福甫斧簠黼父复婦復干剛高羔告戈格鬲庚工弓公攻古谷鼓官觀盥光龜歸鬼癸國果亥行蒿好禾合何河侯后厚乎呼壺虎化畫淮黃惠會或獲鑊基箕及吉即亟疾耤己季既祭家甲斝監見姜彊降角教解巾斤今進盡京兢井競九酒舊沮句絕爵君畯考可克客口寇來牢老醴立利麗良林陵令柳六龍聾盧魯陸鹿麓洛旅率馬買麥卯枚眉每媚門蒙盟皿敏名明鳴命莫母牡木目牧乃男南內逆年廿鳥寧牛農奴諾女旁轡彭辟品僕圃七戚祈齊杞啟气千前遣羌且妾侵秦卿丘區曲取去犬人壬日戎如入若卅三散嗇山商上少紹射涉申沈生省尸師十石食時史矢豕使始士氏事室視首受獸叔黍蜀戍束水司絲死巳四祀宋綏歲孫它唐天田聽同土退屯豚鼉外亡王韋唯未位畏衛文聞問我吳五午武舞勿戊夕兮昔析奚襲徙喜系下先咸獻相祥饗向象小辛新星興兄休羞宿戌須徐宣旋學旬亞言炎衍甗央羊昜陽揚夭爻頁一伊衣依匜夷宜疑彝乙亦邑易異肄義因禋寅尹郢永用攸幽猶游友有酉又幼于余盂雩魚虞漁羽雨圉玉聿育昱御禦元爰員曰月樂龠允宰載再在葬昃增乍宅旃召折貞朕征正鄭之執止旨黹至豸彘中終仲舟州周帚朱逐祝貯追茲子自宗足卒族俎祖尊作", 9 | 10 | commonCharsLength = commonChars.length, 11 | 12 | isSurrogatePair = text => text.match(/^[\uD800-\uDBFF][\uDC00-\uDFFF]$/), 13 | 14 | basicRanges = [ 15 | 0x4E00, 0x9FFF, 16 | 0x3400, 0x4DBF, 17 | 0x2E80, 0x2EFF, 18 | 0x2F00, 0x2FDF, 19 | 0x2FF0, 0x2FFF, 20 | 0x3000, 0x303F, 21 | 0x3040, 0x309F, 22 | 0x30A0, 0x30FF, 23 | 0x3100, 0x312F, 24 | 0x31A0, 0x31BF, 25 | 0x31C0, 0x31EF, 26 | 0x31F0, 0x31FF, 27 | 0xF900, 0xFAFF, 28 | 0xFE30, 0xFE4F, 29 | 0xFF00, 0xFFEF 30 | ], 31 | 32 | surrogateRanges = [ 33 | 0x20000, 0x2A6DF, 34 | 0x2A700, 0x2B73F, 35 | 0x2B740, 0x2B81F, 36 | 0x2B820, 0x2CEAF, 37 | 0x2CEB0, 0x2EBEF, 38 | 0x2F800, 0x2FA1F 39 | ]; 40 | 41 | export default { 42 | search: chinese => { 43 | const token = Cookies.get("Bronze"); 44 | return $.ajax({ 45 | type: "POST", 46 | data: { chinese, Bronze: token }, 47 | dataType: "html", 48 | url: "etymology", 49 | headers: { Chinese: chinese.codePointAt(0), Seal: token } 50 | }); 51 | }, 52 | 53 | randomChinese: () => { 54 | const index = Math.floor(Math.random() * commonCharsLength + 1); 55 | return commonChars[index]; 56 | }, 57 | 58 | isSingleChinese: text => { 59 | if (!text) { 60 | return false; 61 | } 62 | const length = text.length; 63 | if (length < 1 || length > 2) { 64 | return false; 65 | } 66 | const isSurrogate = isSurrogatePair(text); 67 | if (isSurrogate) { 68 | if (length === 1) { 69 | return false; 70 | } 71 | } else { 72 | if (length === 2) { 73 | return false; 74 | } 75 | } 76 | 77 | const codePoint = text.codePointAt(0); 78 | const range = isSurrogate ? surrogateRanges : basicRanges; 79 | for (let index = 0; index < range.length; index += 2) { 80 | if (range[index] <= codePoint && range[index + 1] >= codePoint) { 81 | return true; 82 | } 83 | } 84 | return false; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/ServiceDependencies/etymology-staging - Web Deploy/mssql1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "etymology", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "centralus", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('etymology-staging', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "kind": "v12.0", 53 | "location": "[parameters('resourceLocation')]", 54 | "name": "etymology", 55 | "type": "Microsoft.Sql/servers", 56 | "apiVersion": "2017-10-01-preview" 57 | }, 58 | { 59 | "sku": { 60 | "name": "Basic", 61 | "tier": "Basic", 62 | "capacity": 5 63 | }, 64 | "kind": "v12.0,user", 65 | "location": "[parameters('resourceLocation')]", 66 | "name": "etymology/etymology-staging", 67 | "type": "Microsoft.Sql/servers/databases", 68 | "apiVersion": "2017-10-01-preview", 69 | "dependsOn": [ 70 | "etymology" 71 | ] 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | ], 78 | "metadata": { 79 | "_dependencyType": "mssql.azure" 80 | } 81 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/ServiceDependencies/etymology - Web Deploy/mssql1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "etymology", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "centralus", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('etymology-production', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "kind": "v12.0", 53 | "location": "[parameters('resourceLocation')]", 54 | "name": "etymology-asia", 55 | "type": "Microsoft.Sql/servers", 56 | "apiVersion": "2017-10-01-preview" 57 | }, 58 | { 59 | "sku": { 60 | "name": "Basic", 61 | "tier": "Basic", 62 | "capacity": 5 63 | }, 64 | "kind": "v12.0,user", 65 | "location": "[parameters('resourceLocation')]", 66 | "name": "etymology-asia/etymology-production", 67 | "type": "Microsoft.Sql/servers/databases", 68 | "apiVersion": "2017-10-01-preview", 69 | "dependsOn": [ 70 | "etymology-asia" 71 | ] 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | ], 78 | "metadata": { 79 | "_dependencyType": "mssql.azure" 80 | } 81 | } -------------------------------------------------------------------------------- /src/Etymology.Data/Models/EtymologyContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | 5 | namespace Etymology.Data.Models 6 | { 7 | public partial class EtymologyContext : DbContext 8 | { 9 | public EtymologyContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public virtual DbSet Bronze { get; set; } 15 | public virtual DbSet Etymology { get; set; } 16 | public virtual DbSet Liushutong { get; set; } 17 | public virtual DbSet Oracle { get; set; } 18 | public virtual DbSet Seal { get; set; } 19 | public virtual DbSet VEtymology { get; set; } 20 | 21 | protected override void OnModelCreating(ModelBuilder modelBuilder) 22 | { 23 | modelBuilder.Entity(entity => 24 | { 25 | entity.HasKey(e => e.BronzeId) 26 | .IsClustered(false); 27 | 28 | entity.HasIndex(e => e.Traditional) 29 | .IsClustered(); 30 | }); 31 | 32 | modelBuilder.Entity(entity => 33 | { 34 | entity.HasKey(e => e.EtymologyId) 35 | .IsClustered(false); 36 | 37 | entity.HasIndex(e => e.OldTraditional); 38 | 39 | entity.HasIndex(e => e.Simplified); 40 | 41 | entity.HasIndex(e => e.Traditional) 42 | .IsUnique() 43 | .IsClustered(); 44 | }); 45 | 46 | modelBuilder.Entity(entity => 47 | { 48 | entity.HasKey(e => e.LiushutongId) 49 | .IsClustered(false); 50 | 51 | entity.HasIndex(e => e.Traditional) 52 | .IsClustered(); 53 | 54 | entity.HasOne(d => d.Seal) 55 | .WithMany(p => p.Liushutong) 56 | .HasForeignKey(d => d.SealId) 57 | .HasConstraintName("FK_Liushutong_Seal"); 58 | }); 59 | 60 | modelBuilder.Entity(entity => 61 | { 62 | entity.HasKey(e => e.OracleId) 63 | .IsClustered(false); 64 | 65 | entity.HasIndex(e => e.Traditional) 66 | .IsClustered(); 67 | }); 68 | 69 | modelBuilder.Entity(entity => 70 | { 71 | entity.HasKey(e => e.SealId) 72 | .IsClustered(false); 73 | 74 | entity.HasIndex(e => e.Traditional) 75 | .IsClustered(); 76 | }); 77 | 78 | modelBuilder.Entity(entity => 79 | { 80 | entity.HasNoKey(); 81 | 82 | entity.ToView("vEtymology"); 83 | 84 | entity.Property(e => e.EtymologyId).ValueGeneratedOnAdd(); 85 | }); 86 | 87 | OnModelCreatingPartial(modelBuilder); 88 | } 89 | 90 | partial void OnModelCreatingPartial(ModelBuilder modelBuilder); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Etymology.Web/Client/js/hash.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | const global = window; 4 | const location = global.location; 5 | const $document = $(global.document); 6 | const timeout = 500; 7 | 8 | let navOffset = 0; 9 | let positions = null; 10 | let oldTop = 0; 11 | let scrollTimeoutId = null; 12 | let setPositionsTimeoutId = null; 13 | 14 | const setPositions = $elements => positions = $.map($elements, element => { 15 | const $element = $(element); 16 | const position = $element.position(); 17 | return { 18 | top: position.top - navOffset, 19 | bottom: position.top - navOffset + $element.height(), 20 | id: element.id 21 | }; 22 | }); 23 | 24 | const setPositionsWithTimeout = $elements => { 25 | if (setPositionsTimeoutId) { 26 | global.clearTimeout(setPositionsTimeoutId); 27 | } 28 | setPositionsTimeoutId = global.setTimeout(() => setPositions($elements), timeout); 29 | } 30 | 31 | const set = hash => { 32 | if (hash.indexOf("#") === 0) { // IE does not support starsWith. 33 | hash = hash.substring(1); 34 | } 35 | const $hash = $(`#${hash}`); 36 | if ($hash.length > 0) { 37 | $hash.removeAttr("id"); // Prevent jumping to the target element. 38 | location.hash = hash; 39 | $hash.prop("id", hash); 40 | } else { 41 | location.hash = hash; 42 | } 43 | }; 44 | 45 | const get = () => global.decodeURIComponent(location.hash.substring(1)); 46 | 47 | export default { 48 | init: (value, $elements) => { 49 | navOffset = value; 50 | setPositions($elements); 51 | oldTop = $document.scrollTop(); 52 | }, 53 | 54 | get: get, 55 | 56 | set: set, 57 | 58 | scrollTo: hash => { 59 | if (hash.indexOf("#") === 0) { // IE does not support starsWith. 60 | hash = hash.substring(1); 61 | } 62 | const $hash = $(`#${hash}`); 63 | if ($hash.length > 0) { 64 | $("html, body").animate({ 65 | scrollTop: $hash.offset().top - navOffset + 2 66 | }, 800); 67 | return true; 68 | } 69 | return false; 70 | }, 71 | 72 | setPositions: setPositions, 73 | 74 | setPositionsWithTimeout: setPositionsWithTimeout, 75 | 76 | setAfterScrollWithTimeout: callback => { 77 | if (scrollTimeoutId) { 78 | global.clearTimeout(scrollTimeoutId); 79 | } 80 | scrollTimeoutId = global.setTimeout(() => { 81 | const newTop = $document.scrollTop(); 82 | if (newTop !== oldTop) { 83 | oldTop = newTop; 84 | $.each(positions, (index, position) => { 85 | if (position.top >= newTop || position.bottom <= newTop) { 86 | return true; // Not current. 87 | } 88 | if (get() === position.id) { 89 | return false; 90 | } 91 | set(position.id); 92 | return false; 93 | }); 94 | } 95 | if (callback) { 96 | callback(); 97 | } 98 | }, 200); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /src/Etymology.Web/Server/Controllers/EtymologyController.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server.Controllers; 2 | 3 | using Etymology.Common; 4 | using Etymology.Data.Cache; 5 | using Etymology.Data.Models; 6 | using Etymology.Web.Server.Models; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using Microsoft.Extensions.Primitives; 10 | 11 | public class EtymologyController : Controller 12 | { 13 | private readonly ILogger logger; 14 | 15 | private readonly IMemoryCache etymologyCache; 16 | 17 | private readonly ICharacterCache characterCache; 18 | 19 | private readonly EtymologyContext context; 20 | 21 | public EtymologyController(EtymologyContext context, ILogger logger, IMemoryCache etymologyCache, ICharacterCache characterCache) 22 | { 23 | this.context = context ?? throw new ArgumentNullException(nameof(context)); 24 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 25 | this.etymologyCache = etymologyCache; 26 | this.characterCache = characterCache; 27 | } 28 | 29 | [HttpPost] 30 | [Route(nameof(Etymology))] 31 | [ResponseCache(NoStore = true)] 32 | public async Task AnalyzeAsync(string chinese) 33 | { 34 | (Exception? inputException, _) = Chinese.ValidateSingleChineseCharacter(chinese, nameof(chinese)); 35 | if (inputException != null) 36 | { 37 | this.logger.LogWarning("Received {chinese} is invalid. {message}", chinese, inputException.Message); 38 | #if DEBUG 39 | return this.BadRequest(inputException); 40 | #else 41 | return this.BadRequest(); 42 | #endif 43 | } 44 | 45 | if (!(this.Request.Headers.TryGetValue(nameof(Chinese), out StringValues codePointString) 46 | && int.TryParse(codePointString.ToString(), out int codePoint) 47 | && codePoint == char.ConvertToUtf32(chinese, 0))) 48 | { 49 | #if DEBUG 50 | return this.BadRequest("Received code point is invalid."); 51 | #else 52 | return this.BadRequest(); 53 | #endif 54 | } 55 | 56 | this.logger.LogInformation("Received {chinese} to analyze.", chinese); 57 | 58 | AnalyzeResult[] results; 59 | Stopwatch stopwatch = Stopwatch.StartNew(); 60 | try 61 | { 62 | if (this.etymologyCache.TryGetValue(codePoint, out AnalyzeResult[] cachedResults)) 63 | { 64 | this.logger.LogInformation("Query result is found in cache for {chinese}.", chinese); 65 | results = cachedResults; 66 | } 67 | else 68 | { 69 | this.logger.LogInformation("Start to query database for {chinese}.", chinese); 70 | IEnumerable<(string Traditional, int CodePoint)> allTraditional = this.characterCache.AllTraditional(codePoint); 71 | results = await this.context.AnalyzeAsync(chinese, allTraditional); 72 | this.logger.LogInformation("Database query is done successfully for {chinese}.", chinese); 73 | this.etymologyCache.Set(codePoint, results, Cache.ServerCacheOptions); 74 | this.logger.LogInformation("Query result is added to cache for {chinese}.", chinese); 75 | } 76 | } 77 | catch (Exception exception) when (exception.LogErrorWith(this.logger, "Database query fails for {chinese}.", chinese)) 78 | { 79 | return new EmptyResult(); // Never execute because LogErrorWith returns false. 80 | } 81 | finally 82 | { 83 | stopwatch.Stop(); 84 | } 85 | 86 | return this.View(new EtymologyAnalyzeModel(chinese, stopwatch.Elapsed, results)); 87 | } 88 | } -------------------------------------------------------------------------------- /docs/_posts/2018-04-23-decomposition-of-chinese-character.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Decompisition of Chinese Characters 3 | --- 4 | 5 | Richard Sears 6 | 7 | # Decompisition of Chinese Characters 8 | We would like to trace the evolution of the components of any modern character back to its original logical components. 9 | We wish to creat a standard formal linguistic form for describing any character. 10 | 11 | # Character-standard-name 12 | 13 | Every character has a standard name so it is referenced exactly the same nomatter where it is used. 14 | The character name should describe what the character means in general and 15 | what the original pictograph was and the basic pronunciation. 16 | The words are separated by "-" so they appere to the computer to be one word. 17 | 18 | examples: 19 | 20 | 金 = metal-bell-jin 21 | 22 | This tells the basic meaning, the original pictograph, a bell which represents metal, and its basic pronunciation, "jin". 23 | 24 | 土 = potters-clay-tu 25 | 26 | A lump of clay on a potters wheel originally represented any dirt, earth, or land. 27 | 28 | # Character-standard-reference 29 | 30 | All character components are referenced by three parts. 31 | A character-standard-name, character(s), pinying pronunciation. 32 | 33 | example: 34 | 35 | metal-bell-jin 钅釒金 jīn 36 | 37 | In this example I have three characters 钅釒金 which I consider to be "exact equivalents" 38 | 39 | # #Compound and #Component 40 | If in the modern character, you can draw a line between two or more logical parts it is called a compound. 41 | If the modern character can not be broken into two or more parts then it is a component. 42 | By components, I mean basic modern components, not primitive components. 43 | 44 | example: 45 | 46 | Compound 菇 47 | 48 | because it can be broken into 艹 and 姑 49 | 50 | Component 先 51 | 52 | I call this a component, because, without knowledge of its older forms, you can not draw a line between two or more logical components. 53 | 54 | I have analyzed about 15,000 different characters and come up with about 900 modern basic modern components. 55 | Many of these modern components are actually compounds if you look at earlier versions. 56 | 57 | # Rennants #rem- and #rem+ 58 | 59 | A large percentage of modern characters have aparent components that are actually logically wrong representations of earlier logically corredt components. They are in fact mistakes. I refere to these as rem-. 60 | 61 | example: 62 | 63 | Compound 肚 64 | 65 | from (rem- 月 yuè) from 66 | 67 | ribs-rou ⺼肉 ròu and 68 | 69 | phonetic potters-clay-tu 土 tǔ. 70 | 71 | 72 | Some remnants are not mistakin characters, but indicate some meaningful part of the original pictograph. I represent this with a rem+ 73 | 74 | example: 75 | 76 | Component 天 77 | 78 | from person-stand-da 大 dà and 79 | 80 | (rem+ 一 yī sky-above-man). 81 | 82 | (name- sky-heaven-tian 天 tiān) 83 | 84 | # #phonetic and #related phonetic 85 | Sometimes a character component acts as a phonetic. 86 | 87 | Compound 肚 88 | 89 | from (rem- 月 yuè) from 90 | 91 | ribs-rou ⺼肉 ròu and 92 | 93 | phonetic potters-clay-tu 土 tǔ. 94 | 95 | Sometimes the phonetic may also be related in meaning. 96 | 97 | Compound 葉 98 | 99 | from grass-cao 艹艸 cǎo and 100 | 101 | related phonetic leaf-ye 枼 yè. 102 | 103 | # etymogical equivalents #older 104 | 报報𡙈 are in fact evolved forms of a single character. I use the key word #older 105 | 106 | example: 107 | 108 | Compound 報 older 𡙈 109 | 110 | from (rem- 扌 shǒu) from 111 | 112 | (rem- 幸 xìng) from 113 | 114 | hand-cuffs-nie 㚔 niè and 115 | 116 | to-regulate-fu 𠬝 fú 117 | 118 | new-char 报. 119 | 120 | # logical equivalents #variant 121 | 122 | 123 | # Simplification #std-var #new-char #simp 124 | Simplifying a traditional character may involve 4 steps 125 | 1) apply any vairint character rule. This may result in a standard variant that is 126 | 127 | 128 | 129 | 130 | # #reduced 131 | example: 132 | 133 | Compound 篾 134 | 135 | from bamboo-zhu 𥫗竹 zhú and 136 | 137 | reduced phonetic disdain-mie 蔑 miè. 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/_posts/2017-09-19-what-is-a-traditional-chinese-character.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is a Traditional Chinese Character 3 | --- 4 | 5 | Richard Sears 6 | 7 | # What is a Traditional Chinese Character? 8 | 9 | Since about 200 AD, Chinese have been writing characters in either li style 隸書or kai style 楷書 and they look very much like the characters of today. Most of these characters would be recognized by a modern Chinese. 10 | 11 | ## Kai characters vs. Li characters 12 | 13 | Chinese calligraphers like to make a big deal about the difference between so called Kai characters 楷體字 and so called Li characters 隸體字 14 | 15 | Both types of characters derived from seal characters 16 | 17 | Kai 楷書 style (regular style) 200-700 AD to modern times. 18 | 19 | Li 隸書 style (clerical style) 475-221 BC to modern times. 20 | 21 | On the left we have kai characters and on the right we have li characters. 22 | 23 | In modern times it is little more than a font difference. The structure is almost always the same. If you can recognize one, you can almost always recognize the other. 24 | 25 | I always like to say that Li characters look like they were written with a Q-tip and Kai characters look like they were written with a brush. 26 | 27 | We could say that: 28 | 29 | > “Traditional characters are any kai or li character used in the past 2000 years”. 30 | 31 | But there are problems with this definition. One: There have been some changes in many characters over the past 2000 years and two: There are many alternate characters that may not be recognized by a modern. 32 | 33 | The Kangxi dictionary 康熙字典 has been a standard dictionary since 1716. 34 | 35 | We could say that: 36 | 37 | > “Traditional characters are any character from the Kangxi 康熙字典dictionary” 38 | 39 | There are problems with this definition. One: the Kangxi dictionary has 48,000 characters and 40,000 of them are of no practical value to a modern Chinese, or any Chinese for that matter. From a practical standpoint, they are not common Chinese. So the problem becomes, how to define a set of all the usual characters which are recognized by a modern literate Chinese. 40 | 41 | Taiwan Ministry of Education published a list of 4808 characters called the常用國字標準字體表 . If you know all of these characters you can recognize 99.9 percent of modern Chinese. We could say that: 42 | 43 | > “Traditional characters are the 4808 characters from the常用國字標準字體表” 44 | 45 | This is a very practical list, but no matter what kind of a list we have, some people will find characters that are important to them which are not in the list. 46 | 47 | Taiwan published list of standard characters for literacy in “traditional” Chinese. 48 | 49 | [常用國字標準字體表 (4808字) 1979](https://github.com/cjkvi/cjkvi-tables/blob/master/zhangyong1979.txt) 50 | 51 | This list is a very useful list and can be used as a standard for traditional Chinese literacy. 52 | 53 | [次常用國字標準字體表(6341) 1982](https://github.com/cjkvi/cjkvi-tables/blob/master/cizhangyong1982.txt) 54 | 55 | This extended list brings the list to 11,149 is quite large and many of these characters are quite rare. 56 | 57 | ## Intermediate characters: 58 | 59 | In order to understand a many modern traditional characters like 報 which means news paper, we may need to look at some earlier kai versions of the modern traditional character. such as ** in order to transition to the seal and earlier characters. So I would want to add these characters. 60 | 61 | ## Character components: 62 | 63 | Every compound traditional character is composed of components. Many of these component parts are not common modern recognizable characters. In 報, the character for news paper we need to look at earlier Kai character versions such as 64 | 65 | see the right hand component ** which is not a common character, but which is necessisary to understand the meaning of the character. So I want to add these characters. 66 | 67 | ## Simplified traditional characters: 68 | 69 | It is said that all simplified characters were actually traditional characters. That is characters that existed before the simplification. This is not entirely true, but many of the characters like ** which is the simplified form of 個 can be found in the older dictionaries. 70 | -------------------------------------------------------------------------------- /src/Etymology.Tests/Web/Server/EtymologyControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tests.Web.Server; 2 | 3 | using Etymology.Common; 4 | using Etymology.Data.Cache; 5 | using Etymology.Data.Models; 6 | using Etymology.Tests.Common; 7 | using Etymology.Tests.Data.Models; 8 | using Etymology.Web.Server.Controllers; 9 | using Etymology.Web.Server.Models; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Caching.Memory; 13 | using Microsoft.Extensions.Logging.Abstractions; 14 | using Microsoft.Extensions.Options; 15 | using Microsoft.Extensions.Primitives; 16 | using Microsoft.VisualStudio.TestTools.UnitTesting; 17 | 18 | [TestClass] 19 | public class EtymologyControllerTests 20 | { 21 | [TestMethod] 22 | public async Task AnalyzeAsyncTest() 23 | { 24 | IEnumerable testTasks = ChineseTests.ChineseCharacters.Select(async item => 25 | { 26 | EtymologyController controller = CreateController(); 27 | controller.ControllerContext.HttpContext.Request.Headers.Add(nameof(Chinese), new StringValues(char.ConvertToUtf32(item.Text, 0).ToString("D", CultureInfo.InvariantCulture))); 28 | ViewResult? view; 29 | try 30 | { 31 | view = await controller.AnalyzeAsync(item.Text) as ViewResult; 32 | } 33 | catch (Exception exception) 34 | { 35 | Trace.WriteLine(exception); 36 | throw; 37 | } 38 | 39 | Assert.IsNotNull(view); 40 | Assert.IsInstanceOfType(view!.Model, typeof(EtymologyAnalyzeModel)); 41 | (string chinese, TimeSpan duration, AnalyzeResult[] results) = (EtymologyAnalyzeModel)view.Model; 42 | Assert.AreEqual(item.Text, chinese); 43 | Assert.IsTrue(duration > TimeSpan.Zero); 44 | Assert.IsNotNull(results); 45 | }); 46 | await Task.WhenAll(testTasks); 47 | } 48 | 49 | [TestMethod] 50 | public async Task AnalyzeAsyncErrorTest() 51 | { 52 | IEnumerable testTasks = ChineseTests.NonChineseCharacters.Select(async item => 53 | { 54 | EtymologyController controller = CreateController(); 55 | try 56 | { 57 | #pragma warning disable CS8604 // Possible null reference argument. 58 | controller.ControllerContext.HttpContext.Request.Headers.Add(nameof(Chinese), new StringValues(char.ConvertToUtf32(item, 0).ToString("D", CultureInfo.InvariantCulture))); 59 | #pragma warning restore CS8604 // Possible null reference argument. 60 | } 61 | catch (Exception exception) when (exception.IsNotCritical()) 62 | { 63 | Trace.WriteLine(exception); 64 | } 65 | 66 | Trace.WriteLine(item); 67 | #if DEBUG 68 | BadRequestObjectResult badRequest = (BadRequestObjectResult)await controller.AnalyzeAsync(item!); 69 | #else 70 | BadRequestResult badRequest = (BadRequestResult)await controller.AnalyzeAsync(item!); 71 | #endif 72 | Assert.IsNotNull(badRequest); 73 | #if DEBUG 74 | Assert.IsInstanceOfType(badRequest.Value, typeof(ArgumentException)); 75 | #endif 76 | Assert.AreEqual((int)HttpStatusCode.BadRequest, badRequest.StatusCode); 77 | }); 78 | await Task.WhenAll(testTasks); 79 | } 80 | 81 | private static EtymologyController CreateController() 82 | { 83 | return new( 84 | #pragma warning disable CA2000 // Dispose objects before losing scope 85 | EtymologyContextTests.CreateDatabase(), 86 | new NullLogger(), 87 | new MemoryCache(new OptionsWrapper(new MemoryCacheOptions())), 88 | new CharacterCache(EtymologyContextTests.CreateDatabase())) 89 | #pragma warning restore CA2000 // Dispose objects before losing scope 90 | { 91 | ControllerContext = new() 92 | { 93 | HttpContext = new DefaultHttpContext(), 94 | }, 95 | }; 96 | } 97 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/ServiceDependencies/etymology - Web Deploy/profile.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_dependencyType": "appService.windows" 6 | }, 7 | "parameters": { 8 | "resourceGroupName": { 9 | "type": "string", 10 | "defaultValue": "etymology", 11 | "metadata": { 12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 13 | } 14 | }, 15 | "resourceGroupLocation": { 16 | "type": "string", 17 | "defaultValue": "centralus", 18 | "metadata": { 19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." 20 | } 21 | }, 22 | "resourceName": { 23 | "type": "string", 24 | "defaultValue": "etymology", 25 | "metadata": { 26 | "description": "Name of the main resource to be created by this template." 27 | } 28 | }, 29 | "resourceLocation": { 30 | "type": "string", 31 | "defaultValue": "[parameters('resourceGroupLocation')]", 32 | "metadata": { 33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 34 | } 35 | } 36 | }, 37 | "variables": { 38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" 40 | }, 41 | "resources": [ 42 | { 43 | "type": "Microsoft.Resources/resourceGroups", 44 | "name": "[parameters('resourceGroupName')]", 45 | "location": "[parameters('resourceGroupLocation')]", 46 | "apiVersion": "2019-10-01" 47 | }, 48 | { 49 | "type": "Microsoft.Resources/deployments", 50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 51 | "resourceGroup": "[parameters('resourceGroupName')]", 52 | "apiVersion": "2019-10-01", 53 | "dependsOn": [ 54 | "[parameters('resourceGroupName')]" 55 | ], 56 | "properties": { 57 | "mode": "Incremental", 58 | "template": { 59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 60 | "contentVersion": "1.0.0.0", 61 | "resources": [ 62 | { 63 | "location": "[parameters('resourceLocation')]", 64 | "name": "[parameters('resourceName')]", 65 | "type": "Microsoft.Web/sites", 66 | "apiVersion": "2015-08-01", 67 | "tags": { 68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" 69 | }, 70 | "dependsOn": [ 71 | "[variables('appServicePlan_ResourceId')]" 72 | ], 73 | "kind": "app", 74 | "properties": { 75 | "name": "[parameters('resourceName')]", 76 | "kind": "app", 77 | "httpsOnly": true, 78 | "reserved": false, 79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]", 80 | "siteConfig": { 81 | "metadata": [ 82 | { 83 | "name": "CURRENT_STACK", 84 | "value": "dotnetcore" 85 | } 86 | ] 87 | } 88 | }, 89 | "identity": { 90 | "type": "SystemAssigned" 91 | } 92 | }, 93 | { 94 | "location": "[parameters('resourceLocation')]", 95 | "name": "[variables('appServicePlan_name')]", 96 | "type": "Microsoft.Web/serverFarms", 97 | "apiVersion": "2015-08-01", 98 | "sku": { 99 | "name": "S1", 100 | "tier": "Standard", 101 | "family": "S", 102 | "size": "S1" 103 | }, 104 | "properties": { 105 | "name": "[variables('appServicePlan_name')]" 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | ] 113 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Properties/ServiceDependencies/etymology-staging - Web Deploy/profile.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_dependencyType": "appService.windows" 6 | }, 7 | "parameters": { 8 | "resourceGroupName": { 9 | "type": "string", 10 | "defaultValue": "etymology", 11 | "metadata": { 12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 13 | } 14 | }, 15 | "resourceGroupLocation": { 16 | "type": "string", 17 | "defaultValue": "centralus", 18 | "metadata": { 19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." 20 | } 21 | }, 22 | "resourceName": { 23 | "type": "string", 24 | "defaultValue": "etymology-staging", 25 | "metadata": { 26 | "description": "Name of the main resource to be created by this template." 27 | } 28 | }, 29 | "resourceLocation": { 30 | "type": "string", 31 | "defaultValue": "[parameters('resourceGroupLocation')]", 32 | "metadata": { 33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 34 | } 35 | } 36 | }, 37 | "variables": { 38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" 40 | }, 41 | "resources": [ 42 | { 43 | "type": "Microsoft.Resources/resourceGroups", 44 | "name": "[parameters('resourceGroupName')]", 45 | "location": "[parameters('resourceGroupLocation')]", 46 | "apiVersion": "2019-10-01" 47 | }, 48 | { 49 | "type": "Microsoft.Resources/deployments", 50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 51 | "resourceGroup": "[parameters('resourceGroupName')]", 52 | "apiVersion": "2019-10-01", 53 | "dependsOn": [ 54 | "[parameters('resourceGroupName')]" 55 | ], 56 | "properties": { 57 | "mode": "Incremental", 58 | "template": { 59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 60 | "contentVersion": "1.0.0.0", 61 | "resources": [ 62 | { 63 | "location": "[parameters('resourceLocation')]", 64 | "name": "[parameters('resourceName')]", 65 | "type": "Microsoft.Web/sites", 66 | "apiVersion": "2015-08-01", 67 | "tags": { 68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" 69 | }, 70 | "dependsOn": [ 71 | "[variables('appServicePlan_ResourceId')]" 72 | ], 73 | "kind": "app", 74 | "properties": { 75 | "name": "[parameters('resourceName')]", 76 | "kind": "app", 77 | "httpsOnly": true, 78 | "reserved": false, 79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]", 80 | "siteConfig": { 81 | "metadata": [ 82 | { 83 | "name": "CURRENT_STACK", 84 | "value": "dotnetcore" 85 | } 86 | ] 87 | } 88 | }, 89 | "identity": { 90 | "type": "SystemAssigned" 91 | } 92 | }, 93 | { 94 | "location": "[parameters('resourceLocation')]", 95 | "name": "[variables('appServicePlan_name')]", 96 | "type": "Microsoft.Web/serverFarms", 97 | "apiVersion": "2015-08-01", 98 | "sku": { 99 | "name": "S1", 100 | "tier": "Standard", 101 | "family": "S", 102 | "size": "S1" 103 | }, 104 | "properties": { 105 | "name": "[variables('appServicePlan_name')]" 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | ] 113 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Client/css/index.css: -------------------------------------------------------------------------------- 1 | @import "ie10-viewport-bug-workaround.css"; 2 | @import "~ladda/dist/ladda-themeless.min.css"; 3 | 4 | .navbar-collapse { 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | position: relative; 10 | padding-top: 50px; 11 | } 12 | 13 | .row-eq-height { 14 | overflow: hidden; 15 | } 16 | 17 | .row-eq-height [class*="col-"] { 18 | margin-bottom: -99999px; 19 | padding-bottom: 99999px; 20 | } 21 | 22 | .pre-inline { 23 | white-space: pre-line; 24 | } 25 | 26 | .m-0 { 27 | margin: 0; 28 | } 29 | 30 | .p-0 { 31 | padding: 0; 32 | } 33 | 34 | .mx-0 { 35 | margin-left: 0; 36 | margin-right: 0; 37 | } 38 | 39 | .my-1 { 40 | margin-top: 15px; 41 | margin-bottom: 15px; 42 | } 43 | 44 | .px-0 { 45 | padding-right: 0; 46 | padding-left: 0; 47 | } 48 | 49 | .p-1 { 50 | padding: 15px; 51 | } 52 | 53 | .px-1 { 54 | padding-right: 15px; 55 | padding-left: 15px; 56 | } 57 | 58 | .py-1 { 59 | padding-top: 15px; 60 | padding-bottom: 15px; 61 | } 62 | 63 | .pt-1 { 64 | padding-top: 15px; 65 | } 66 | 67 | .pb-1 { 68 | padding-bottom: 15px; 69 | } 70 | 71 | .bg-purple { 72 | background-color: #563d7c; 73 | } 74 | 75 | .bg-black { 76 | background-color: #000000; 77 | } 78 | 79 | .border-purple { 80 | border-color: #563d7c; 81 | } 82 | 83 | .border-top { 84 | border-top: 1px solid; 85 | } 86 | 87 | .text-black { 88 | color: #000000; 89 | } 90 | 91 | .text-lg { 92 | font-size: 50px; 93 | } 94 | 95 | .text-danger-color { 96 | color: #ff4444; 97 | } 98 | 99 | a.text-danger-color, a.text-danger-color:visited { 100 | color: #ff4444 !important; 101 | } 102 | 103 | a.text-danger-color:hover, a.text-danger-color:active { 104 | color: #ff9c9c !important; 105 | } 106 | 107 | .text-white { 108 | color: #eeeeee; 109 | } 110 | 111 | a.text-white, a.text-white:visited { 112 | color: #ffffff !important; 113 | } 114 | 115 | a.text-white:hover, a.text-white:active { 116 | color: #ffffff !important; 117 | } 118 | 119 | @media (min-width: 768px) and (max-width: 991px) { 120 | .navbar-collapse.collapse { 121 | display: none !important; 122 | } 123 | 124 | .navbar-collapse.collapse.in { 125 | display: block !important; 126 | } 127 | 128 | .navbar-header .collapse, .navbar-toggle { 129 | display: block !important; 130 | } 131 | 132 | .navbar-header { 133 | float: none; 134 | } 135 | } 136 | 137 | .etymology-news .media-left a { 138 | width: 50px; 139 | height: 64px; 140 | display: block; 141 | font-size: 40px; 142 | color: #e1bee7; 143 | } 144 | 145 | .etymology-videos .media-left a { 146 | width: 114px; 147 | height: 64px; 148 | display: block; 149 | background-size: cover; 150 | background-repeat: no-repeat; 151 | background-position: 50% 50%; 152 | } 153 | 154 | .etymology-brand { 155 | padding-left: 0; 156 | padding-right: 0; 157 | } 158 | 159 | @media (min-width: 768px) { 160 | .etymology-brand { 161 | padding-left: 15px; 162 | padding-right: 15px; 163 | } 164 | } 165 | 166 | @media (max-width: 351px) { 167 | .etymology-nav-icon { 168 | display: none; 169 | } 170 | } 171 | 172 | .navbar-fixed-top .navbar-collapse { 173 | max-height: none; 174 | } 175 | 176 | .etymology-search button { 177 | margin-bottom: 10px; 178 | } 179 | 180 | .etymology-stat div { 181 | padding: 5px; 182 | } 183 | 184 | .etymology-stat h3 { 185 | margin-top: 10px; 186 | } 187 | 188 | .etymology-alert a{ 189 | margin: 5px 0px; 190 | } 191 | 192 | .etymology-result ul { 193 | margin-top: 15px; 194 | } 195 | 196 | .etymology-result ul li div { 197 | background-color: #fff; 198 | width: 105px; 199 | height: 70px; 200 | background-size: 60px 60px; 201 | background-position: 50% 50%; 202 | background-repeat: no-repeat; 203 | color: transparent; 204 | cursor: pointer; 205 | } 206 | 207 | .etymology-result ul li button { 208 | margin-top: 5px; 209 | margin-bottom: 15px; 210 | width: 105px; 211 | font-weight: bold; 212 | } 213 | 214 | .etymology-research a { 215 | font-weight: bold; 216 | } 217 | 218 | .etymology-news .media-body { 219 | color: #ccc; 220 | } 221 | 222 | .etymology-news .media-heading { 223 | color: #fff; 224 | } 225 | 226 | .modal-dialog { 227 | width: auto; 228 | margin-left: 15px; 229 | margin-right: 15px; 230 | } 231 | .modal-dialog, 232 | .modal-content { 233 | height: 95%; 234 | } 235 | 236 | .modal-content { 237 | background-size: contain; 238 | background-repeat: no-repeat; 239 | background-position: center center; 240 | } 241 | .modal-header, .modal-body, .modal-footer { 242 | border: 0px none; 243 | } 244 | .modal-body { 245 | height: calc(100% - 120px); 246 | } 247 | -------------------------------------------------------------------------------- /src/Etymology.Web/Server/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server; 2 | 3 | using Etymology.Common; 4 | using Etymology.Data.Cache; 5 | using Etymology.Data.Models; 6 | using Microsoft.AspNetCore.Antiforgery; 7 | using Microsoft.AspNetCore.Mvc.Razor; 8 | using Microsoft.Net.Http.Headers; 9 | 10 | public class Startup 11 | { 12 | private const string ServerRoot = "Server"; 13 | 14 | private readonly IConfiguration configuration; 15 | 16 | private readonly IWebHostEnvironment environment; 17 | 18 | public Startup(IWebHostEnvironment environment) 19 | { 20 | IConfigurationBuilder configurationBuilder = new ConfigurationBuilder() 21 | .SetBasePath(environment.ContentRootPath) 22 | .AddJsonFile(Path.Combine(ServerRoot, "settings.json"), optional: false, reloadOnChange: true) 23 | .AddJsonFile(Path.Combine(ServerRoot, $"settings.{environment.EnvironmentName}.json"), optional: true, true) 24 | .AddEnvironmentVariables(); 25 | if (!environment.IsDevelopment()) 26 | { 27 | configurationBuilder.AddApplicationInsightsSettings(developerMode: environment.IsStaging()); 28 | } 29 | 30 | this.configuration = configurationBuilder.Build(); 31 | this.environment = environment; 32 | } 33 | 34 | public void ConfigureServices(IServiceCollection services) // Container. 35 | { 36 | services 37 | .AddSettings(this.configuration, out Settings settings) 38 | .AddAntiforgery(settings) 39 | .AddDataAccess(settings.Connections.TryGetValue(nameof(Etymology), out string? connection) && !string.IsNullOrWhiteSpace(connection) 40 | ? connection 41 | : this.configuration.GetSection(nameof(Etymology)).Value) // Environment variable. 42 | .AddResponseCaching() 43 | .AddCharacterCache() 44 | .AddLogging(loggingBuilder => 45 | { 46 | // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0#bilp 47 | // Built-in providers: Console, Debug, EventSource, EventLog (Windows). 48 | // Not built-in: AzureAppServicesFile, AzureAppServicesBlob, ApplicationInsights. 49 | if (this.environment.IsDevelopment()) 50 | { 51 | loggingBuilder 52 | .ClearProviders() 53 | .AddSystemdConsole(consoleFormatterOptions => consoleFormatterOptions.IncludeScopes = true) 54 | .AddDebug(); 55 | } 56 | else 57 | { 58 | loggingBuilder.AddApplicationInsights(); 59 | } 60 | }) 61 | .AddHostFiltering(hostFiltering => hostFiltering.AllowedHosts = settings.AllowedHosts) 62 | .AddMvc(options => options.AddAntiforgery()) // services.AddControllersWithViews(options => options.AddAntiforgery()); 63 | .AddRazorRuntimeCompilation() // AddRazorPages() is not needed. 64 | .AddRazorOptions(option => // WithRazorPagesRoot() does not work. 65 | { 66 | string[] viewLocationFormats = option 67 | .ViewLocationFormats 68 | .Select(format => $"/{ServerRoot}{format}") // $"/{ServerRoot}/Views/{{1}}/{{0}}{RazorViewEngine.ViewExtension}" 69 | .Prepend($"/{ServerRoot}/Views/{{1}}{{0}}{RazorViewEngine.ViewExtension}") // Second. 70 | .Prepend($"/{ServerRoot}/Views/{{1}}{{0}}View{RazorViewEngine.ViewExtension}") // First. 71 | .ToArray(); 72 | option.ViewLocationFormats.Clear(); 73 | viewLocationFormats.ForEach(option.ViewLocationFormats.Add); 74 | }); 75 | 76 | if (settings.IsHttpsOnly) 77 | { 78 | services.AddHttpsRedirection(options => options.HttpsPort = 443); 79 | } 80 | 81 | if (!this.environment.IsDevelopment()) 82 | { 83 | services.AddApplicationInsightsTelemetry(this.configuration); 84 | } 85 | } 86 | 87 | // Async Configure method is not supported by ASP.NET. 88 | // https://github.com/aspnet/Hosting/issues/373 89 | public void Configure(IApplicationBuilder application, ILoggerFactory loggerFactory, IAntiforgery antiforgery, Settings settings) // HTTP pipeline. 90 | { 91 | if (loggerFactory is null) 92 | { 93 | throw new ArgumentNullException(nameof(loggerFactory)); 94 | } 95 | 96 | if (settings is null) 97 | { 98 | throw new ArgumentNullException(nameof(settings)); 99 | } 100 | 101 | if (this.environment.IsProduction()) 102 | { 103 | application 104 | .UseExceptionHandler(settings.ErrorPagePath) 105 | .UseStatusCodePagesWithReExecute(settings.ErrorPagePath); 106 | } 107 | else 108 | { 109 | application.UseDeveloperExceptionPage().UseBrowserLink(); 110 | } 111 | 112 | if (settings.IsHttpsOnly) 113 | { 114 | application 115 | .UseHsts() 116 | .UseHttpsRedirection(); 117 | } 118 | 119 | application 120 | .UseEncodings() // Add support for GB18030. 121 | .UseAntiforgery(settings, antiforgery, loggerFactory.CreateLogger(nameof(RequestValidation))) 122 | .UseDefaultFiles() 123 | .UseStaticFiles(new StaticFileOptions() 124 | { 125 | OnPrepareResponse = staticFileResponseContext => staticFileResponseContext.Context.Response.Headers[HeaderNames.CacheControl] = $"public,max-age={Cache.ClientCacheMaxAge}", 126 | }) 127 | .UseResponseCaching() 128 | .UseRouting() 129 | .UseEndpoints(endpoints => settings.Routes.ForEach(route => endpoints.MapControllerRoute(route.Key, route.Value))) 130 | .UseHostFiltering(); 131 | } 132 | } -------------------------------------------------------------------------------- /src/Etymology.Web/Server/RequestValidation.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Web.Server; 2 | 3 | using Etymology.Common; 4 | using Etymology.Data.Models; 5 | using Microsoft.AspNetCore.Antiforgery; 6 | using Microsoft.AspNetCore.Http.Extensions; 7 | using Microsoft.AspNetCore.Http.Features; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Primitives; 10 | 11 | internal static class RequestValidation 12 | { 13 | private const string CookieName = nameof(Oracle); 14 | 15 | private const string FormFieldName = nameof(Bronze); 16 | 17 | private const string HeaderName = nameof(Seal); 18 | 19 | internal static MvcOptions AddAntiforgery(this MvcOptions options) 20 | { 21 | options.Filters.Add(new ValidateAntiForgeryTokenAttribute()); 22 | return options; 23 | } 24 | 25 | internal static IServiceCollection AddAntiforgery(this IServiceCollection services, Settings settings) => 26 | services.AddAntiforgery(options => 27 | { 28 | options.Cookie.Name = CookieName; 29 | options.Cookie.HttpOnly = false; 30 | options.Cookie.SameSite = settings.SameSiteMode; // Default same site mode is Lax, which make the cookie not readable in 360 browser. 31 | options.FormFieldName = FormFieldName; 32 | options.HeaderName = HeaderName; 33 | }); 34 | 35 | internal static IApplicationBuilder UseAntiforgery(this IApplicationBuilder application, Settings settings, IAntiforgery antiforgery, ILogger logger) => 36 | application.Use(async (context, next) => 37 | { 38 | HttpRequest request = context.Request; 39 | try 40 | { 41 | string path = request.Path.Value ?? string.Empty; 42 | if (settings.IndexPagePaths.Contains(path, StringComparer.OrdinalIgnoreCase)) 43 | { 44 | // Requesting index page. 45 | antiforgery.SendTokenToContext(context, settings); 46 | } // Not requesting index page. 47 | else if (!settings.PublicPaths.Any(prefix => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) 48 | { 49 | (bool isValid, string message) = request.IsValid(settings); 50 | if (!isValid) 51 | { 52 | logger.LogError("Request {method} {uri} from {ipAddress} is invalid. {message}", request.Method, request.GetDisplayUrl(), context.GetIPAddress(), message); 53 | context.Response.StatusCode = (int)HttpStatusCode.BadRequest; 54 | #if DEBUG 55 | await context.Response.WriteAsync(message); 56 | #endif 57 | return; 58 | } 59 | } 60 | 61 | await next(); 62 | } 63 | catch (Exception exception) when (exception.IsNotCritical()) 64 | { 65 | logger.LogError("Request {method} {uri} from {ipAddress} fails. {exception}", request.Method, request.GetDisplayUrl(), context.GetIPAddress(), exception); 66 | #if DEBUG 67 | await context.Response.WriteAsync(exception.ToString()); 68 | #endif 69 | throw; 70 | } 71 | }); 72 | 73 | private static void SendTokenToContext(this IAntiforgery antiforgery, HttpContext context, Settings settings) 74 | { 75 | AntiforgeryTokenSet tokens = antiforgery.GetAndStoreTokens(context); 76 | context.Response.Cookies.Append( 77 | tokens.FormFieldName, 78 | tokens.RequestToken ?? string.Empty, 79 | new CookieOptions() { HttpOnly = false, SameSite = settings.SameSiteMode, Secure = settings.IsHttpsOnly }); // Default same site mode is Lax, which make the cookie not readable in 360 browser. 80 | } 81 | 82 | private static (bool IsValid, string Message) IsValid(this HttpRequest request, Settings settings) 83 | { 84 | // Referrer. 85 | IHeaderDictionary headers = request.Headers; 86 | if (!headers.TryGetValue("Referer", out StringValues rawReferrer)) 87 | { 88 | return (false, "Header's referrer is missing."); 89 | } 90 | 91 | string referrer = rawReferrer.ToString(); 92 | if (!Uri.TryCreate(referrer, UriKind.Absolute, out Uri? referrerUri)) 93 | { 94 | return (false, $"Header's referrer {referrer} is not valid."); 95 | } 96 | 97 | string referrerHost = referrerUri.Host; 98 | if (!settings.AllowedHosts.Contains(referrerHost, StringComparer.OrdinalIgnoreCase)) 99 | { 100 | return (false, $"Header's referrer {referrer} host {referrerHost} is not allowed."); 101 | } 102 | 103 | // User agent. 104 | if (!headers.TryGetValue("User-Agent", out StringValues rawUserAgent) || string.IsNullOrWhiteSpace(rawUserAgent.ToString())) 105 | { 106 | return (false, "Header's user agent is missing."); 107 | } 108 | 109 | // Anti forgery token. 110 | // The public APIs validate both cookie and request. The APIs to validate only cookie is not public. 111 | IRequestCookieCollection cookies = request.Cookies; 112 | if (!cookies.TryGetValue(CookieName, out string? cookie) || string.IsNullOrWhiteSpace(cookie)) 113 | { 114 | return (false, $"Cookie {CookieName} is missing."); 115 | } 116 | 117 | if (!cookies.TryGetValue(FormFieldName, out string? formField) || string.IsNullOrWhiteSpace(formField)) 118 | { 119 | return (false, $"Cookie {FormFieldName} is missing."); 120 | } 121 | 122 | return (true, string.Empty); 123 | } 124 | 125 | private static string? GetIPAddress(this HttpContext context) => 126 | context.Connection.RemoteIpAddress?.ToString() 127 | ?? context.Features.Get().RemoteIpAddress?.ToString() 128 | ?? (context.Request.Headers.TryGetValue("X-Forwarded-For", out StringValues forwarded) ? forwarded.ToString() : null) 129 | ?? (context.Request.Headers.TryGetValue("REMOTE_ADDR", out StringValues remote) ? remote.ToString() : null); 130 | } -------------------------------------------------------------------------------- /Etymology.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Etymology.Data", "src\Etymology.Data\Etymology.Data.csproj", "{AB884CCD-6210-43E6-A0DF-3BD2D3D4E37F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Etymology.Web", "src\Etymology.Web\Etymology.Web.csproj", "{067FEF43-B8EE-495C-AED5-C9FAEA45456F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Etymology.Tests", "src\Etymology.Tests\Etymology.Tests.csproj", "{4FD2F94A-D8B9-488E-8068-D31148ABA50C}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{CC291496-B132-4AE8-BB36-CC0B59B7CAA6}" 15 | ProjectSection(SolutionItems) = preProject 16 | docs\_config.yml = docs\_config.yml 17 | docs\index.md = docs\index.md 18 | EndProjectSection 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_posts", "_posts", "{53EE1C43-2F6F-4464-A93D-9FDEB94764D9}" 21 | ProjectSection(SolutionItems) = preProject 22 | docs\_posts\2017-09-17-why-study-chinese-etymology.md = docs\_posts\2017-09-17-why-study-chinese-etymology.md 23 | docs\_posts\2017-09-18-spoken-chinese-and-other-languages.md = docs\_posts\2017-09-18-spoken-chinese-and-other-languages.md 24 | docs\_posts\2017-09-19-what-is-a-simplified-chinese-character.md = docs\_posts\2017-09-19-what-is-a-simplified-chinese-character.md 25 | docs\_posts\2017-09-19-what-is-a-traditional-chinese-character.md = docs\_posts\2017-09-19-what-is-a-traditional-chinese-character.md 26 | docs\_posts\2018-04-23-decomposition-of-chinese-character.md = docs\_posts\2018-04-23-decomposition-of-chinese-character.md 27 | docs\_posts\2018-04-23-simplification-of-chinese-character.md = docs\_posts\2018-04-23-simplification-of-chinese-character.md 28 | EndProjectSection 29 | EndProject 30 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BF1CDA83-FF92-45D9-A84A-50822A37887A}" 31 | ProjectSection(SolutionItems) = preProject 32 | .gitignore = .gitignore 33 | Analyzers.props = Analyzers.props 34 | Analyzers.ruleset = Analyzers.ruleset 35 | appveyor.yml = appveyor.yml 36 | Etymology.sln.DotSettings = Etymology.sln.DotSettings 37 | GlobalUsings.cs = GlobalUsings.cs 38 | README.md = README.md 39 | stylecop.json = stylecop.json 40 | EndProjectSection 41 | EndProject 42 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Etymology.Tool", "src\Etymology.Tool\Etymology.Tool.csproj", "{11B2B287-3690-47A2-B2C9-BCA644A4F477}" 43 | EndProject 44 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Etymology.Common", "src\Etymology.Common\Etymology.Common.csproj", "{A59BA1E1-7EB9-44C1-A61F-083B6B9F4CA2}" 45 | EndProject 46 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Etymology.Data.Console", "src\Etymology.Data.Console\Etymology.Data.Console.csproj", "{F11A3AC2-9F77-4F7D-9729-F2AA39848641}" 47 | EndProject 48 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EE08DA76-CF69-41AB-BCB2-911EFCDFC99F}" 49 | ProjectSection(SolutionItems) = preProject 50 | .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md 51 | EndProjectSection 52 | EndProject 53 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{BB873D60-37E8-4AD1-ABF8-99044CEF7196}" 54 | ProjectSection(SolutionItems) = preProject 55 | .github\workflows\azure-staging.yml = .github\workflows\azure-staging.yml 56 | EndProjectSection 57 | EndProject 58 | Global 59 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 60 | Debug|Any CPU = Debug|Any CPU 61 | Release|Any CPU = Release|Any CPU 62 | EndGlobalSection 63 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 64 | {AB884CCD-6210-43E6-A0DF-3BD2D3D4E37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {AB884CCD-6210-43E6-A0DF-3BD2D3D4E37F}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {AB884CCD-6210-43E6-A0DF-3BD2D3D4E37F}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {AB884CCD-6210-43E6-A0DF-3BD2D3D4E37F}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {067FEF43-B8EE-495C-AED5-C9FAEA45456F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {067FEF43-B8EE-495C-AED5-C9FAEA45456F}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {067FEF43-B8EE-495C-AED5-C9FAEA45456F}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {067FEF43-B8EE-495C-AED5-C9FAEA45456F}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {4FD2F94A-D8B9-488E-8068-D31148ABA50C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {4FD2F94A-D8B9-488E-8068-D31148ABA50C}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {4FD2F94A-D8B9-488E-8068-D31148ABA50C}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {4FD2F94A-D8B9-488E-8068-D31148ABA50C}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {11B2B287-3690-47A2-B2C9-BCA644A4F477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {11B2B287-3690-47A2-B2C9-BCA644A4F477}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {11B2B287-3690-47A2-B2C9-BCA644A4F477}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {11B2B287-3690-47A2-B2C9-BCA644A4F477}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {A59BA1E1-7EB9-44C1-A61F-083B6B9F4CA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 81 | {A59BA1E1-7EB9-44C1-A61F-083B6B9F4CA2}.Debug|Any CPU.Build.0 = Debug|Any CPU 82 | {A59BA1E1-7EB9-44C1-A61F-083B6B9F4CA2}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {A59BA1E1-7EB9-44C1-A61F-083B6B9F4CA2}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {F11A3AC2-9F77-4F7D-9729-F2AA39848641}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 85 | {F11A3AC2-9F77-4F7D-9729-F2AA39848641}.Debug|Any CPU.Build.0 = Debug|Any CPU 86 | {F11A3AC2-9F77-4F7D-9729-F2AA39848641}.Release|Any CPU.ActiveCfg = Release|Any CPU 87 | {F11A3AC2-9F77-4F7D-9729-F2AA39848641}.Release|Any CPU.Build.0 = Release|Any CPU 88 | EndGlobalSection 89 | GlobalSection(SolutionProperties) = preSolution 90 | HideSolutionNode = FALSE 91 | EndGlobalSection 92 | GlobalSection(NestedProjects) = preSolution 93 | {AB884CCD-6210-43E6-A0DF-3BD2D3D4E37F} = {DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1} 94 | {067FEF43-B8EE-495C-AED5-C9FAEA45456F} = {DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1} 95 | {4FD2F94A-D8B9-488E-8068-D31148ABA50C} = {DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1} 96 | {53EE1C43-2F6F-4464-A93D-9FDEB94764D9} = {CC291496-B132-4AE8-BB36-CC0B59B7CAA6} 97 | {11B2B287-3690-47A2-B2C9-BCA644A4F477} = {DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1} 98 | {A59BA1E1-7EB9-44C1-A61F-083B6B9F4CA2} = {DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1} 99 | {F11A3AC2-9F77-4F7D-9729-F2AA39848641} = {DB296004-0FF7-4FB5-8F9C-9DAD3630A5E1} 100 | {BB873D60-37E8-4AD1-ABF8-99044CEF7196} = {EE08DA76-CF69-41AB-BCB2-911EFCDFC99F} 101 | EndGlobalSection 102 | GlobalSection(ExtensibilityGlobals) = postSolution 103 | SolutionGuid = {9A81CF28-631F-422A-8115-5F115AF1DF62} 104 | EndGlobalSection 105 | EndGlobal 106 | -------------------------------------------------------------------------------- /src/Etymology.Common/Chinese.Conversion.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Common; 2 | 3 | public static partial class Chinese 4 | { 5 | private const string SurrogateCodePointPrefix = "000"; 6 | 7 | public static Encoding GB18030 { get; } = Encoding.GetEncoding(nameof(GB18030)); 8 | 9 | public static string ConvertText(this string value, Encoding from, Encoding to) 10 | { 11 | if (from is null) 12 | { 13 | throw new ArgumentNullException(nameof(from)); 14 | } 15 | 16 | if (to is null) 17 | { 18 | throw new ArgumentNullException(nameof(to)); 19 | } 20 | 21 | return to.GetString(Encoding.Convert(from, to, from.GetBytes(value))); 22 | } 23 | 24 | public static byte[] ConvertBytes(this byte[] bytes, Encoding from, Encoding to) => 25 | Encoding.Convert(from, to, bytes); 26 | 27 | public static string CodePointToHex(this int codePoint) => 28 | codePoint.ToString("X4", CultureInfo.InvariantCulture); 29 | 30 | public static int HexToCodePoint(string hex) => 31 | int.Parse(hex, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); 32 | 33 | public static int BytesToInt32(this byte[] bytes) => 34 | bytes.Aggregate(0, (result, @byte) => result * 256 + @byte); 35 | 36 | public static byte[] TextToBytes(this string text, Encoding? encoding = null) => 37 | (encoding ?? Encoding.Unicode).GetBytes(text); 38 | 39 | public static string BytesToText(this byte[] bytes, Encoding? encoding = null) => 40 | (encoding ?? Encoding.Unicode).GetString(bytes); 41 | 42 | // "4E00" => 一 43 | public static byte[] BasicCodePointToBytes(this string hexCodePoint) => 44 | hexCodePoint.IsBasicCodePoint() 45 | ? hexCodePoint.HexToBytes().FormatBasicBytes() 46 | : throw new ArgumentOutOfRangeException(nameof(hexCodePoint)); 47 | 48 | // "2B740" => 𫝀 49 | public static byte[] SurrogateCodePointToUtf16Bytes(this string hexCodePoint) => 50 | !hexCodePoint.IsSurrogateCodePoint() 51 | ? throw new ArgumentOutOfRangeException(nameof(hexCodePoint)) 52 | : Encoding.Convert(Encoding.UTF32, Encoding.Unicode, hexCodePoint.SurrogateCodePointToUtf32Bytes()); 53 | 54 | // "2B740" => 𫝀 55 | public static byte[] SurrogateCodePointToUtf32Bytes(this string hexCodePoint) => 56 | hexCodePoint.IsSurrogateCodePoint() 57 | ? $"{SurrogateCodePointPrefix}{hexCodePoint}".HexToBytes().FormatSurrogateBytes() 58 | : throw new ArgumentOutOfRangeException(nameof(hexCodePoint)); 59 | 60 | // "4E00" => [一], "020000" => [𫝀] 61 | public static byte[] HexToBytes(this string hex) 62 | { 63 | hex = Regex.Replace(hex, @"\s+", string.Empty); 64 | if (hex.Length % 2 != 0) 65 | { 66 | throw new ArgumentOutOfRangeException(nameof(hex)); 67 | } 68 | 69 | int length = hex.Length; 70 | byte[] bytes = new byte[length / 2]; 71 | for (int index = 0; index < length; index += 2) 72 | { 73 | bytes[index / 2] = byte.Parse(hex.Substring(index, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); 74 | } 75 | 76 | return bytes; 77 | } 78 | 79 | // "4E00" => [一], "2B740" => [𫝀] 80 | public static byte[] CodePointToBytes(this string hexCodePoint) 81 | { 82 | if (hexCodePoint.IsBasicCodePoint()) 83 | { 84 | return hexCodePoint.BasicCodePointToBytes(); 85 | } 86 | 87 | if (hexCodePoint.IsSurrogateCodePoint()) 88 | { 89 | return hexCodePoint.SurrogateCodePointToUtf16Bytes(); 90 | } 91 | 92 | throw new ArgumentOutOfRangeException(nameof(hexCodePoint)); 93 | } 94 | 95 | // [一] => "4E00", [𫝀] => "D86DDF40" 96 | public static string BytesToUtf16CodePoint(this byte[] bytes) 97 | { 98 | if (bytes is null) 99 | { 100 | throw new ArgumentNullException(nameof(bytes)); 101 | } 102 | 103 | return BitConverter.ToString(bytes.FormatBasicBytes()).Replace("-", string.Empty, StringComparison.InvariantCulture); 104 | } 105 | 106 | // [一] => "4E00", [𫝀] => "2B740" 107 | public static string BytesToUtf32CodePoint(this byte[] bytes) 108 | { 109 | byte[] utf32Bytes = bytes.ConvertBytes(Encoding.Unicode, Encoding.UTF32); 110 | return BitConverter.ToString(utf32Bytes.FormatSurrogateBytes()).Replace("-", string.Empty, StringComparison.InvariantCulture).Substring(SurrogateCodePointPrefix.Length); 111 | } 112 | 113 | public static bool IsBasicBytes(this byte[] bytes) => bytes.Length == 2; 114 | 115 | public static bool IsSurrogateBytes(this byte[] bytes) => bytes.Length == 4; 116 | 117 | // [一] => "4E00", [𫝀] => "2B740" 118 | public static string BytesToCodePoint(this byte[] bytes) 119 | { 120 | if (bytes.IsBasicBytes()) 121 | { 122 | return bytes.BytesToUtf16CodePoint(); 123 | } 124 | 125 | if (bytes.IsSurrogateBytes()) 126 | { 127 | return bytes.BytesToUtf32CodePoint(); 128 | } 129 | 130 | throw new ArgumentOutOfRangeException(nameof(bytes)); 131 | } 132 | 133 | // "3400" => "㐀", "20000" => "𠀀" 134 | public static string CodePointToText(this string hexCodePoint) => 135 | 136 | // Equivalent to: hexCodePoint.CodePointToBytes().BytesToText(encoding); 137 | char.ConvertFromUtf32(int.Parse(hexCodePoint, NumberStyles.HexNumber, CultureInfo.InvariantCulture)); 138 | 139 | // "㐀" => "3400", "𠀀" => "20000" 140 | public static string TextToCodePoint(this string text) => 141 | 142 | // Equivalent to: text.TextToBytes(encoding).BytesToCodePoint(); 143 | char.ConvertToUtf32(text, 0).ToString("X4", CultureInfo.InvariantCulture); 144 | 145 | public static IEnumerable Characters(this string text) 146 | { 147 | StringInfo parsed = new(text); 148 | for (int index = 0; index < parsed.LengthInTextElements; index++) 149 | { 150 | yield return parsed.SubstringByTextElements(index, 1); 151 | } 152 | } 153 | 154 | private static bool IsBasicCodePoint(this string hexCodePoint) => // Basic Multilingual Plane. 155 | !string.IsNullOrWhiteSpace(hexCodePoint) && hexCodePoint.Length == 4; 156 | 157 | private static bool IsSurrogateCodePoint(this string hexCodePoint) => // Surrogate pair. 158 | !string.IsNullOrWhiteSpace(hexCodePoint) && hexCodePoint.Length == 5; 159 | 160 | private static byte[] FormatBasicBytes(this byte[] bytes) 161 | { 162 | if (bytes.Length % 2 != 0) 163 | { 164 | throw new ArgumentOutOfRangeException(nameof(bytes)); 165 | } 166 | 167 | for (int index = 0; index < bytes.Length; index += 2) 168 | { 169 | (bytes[index], bytes[index + 1]) = (bytes[index + 1], bytes[index]); 170 | } 171 | 172 | return bytes; 173 | } 174 | 175 | private static byte[] FormatSurrogateBytes(this byte[] bytes) 176 | { 177 | Array.Reverse(bytes); 178 | return bytes; 179 | } 180 | } -------------------------------------------------------------------------------- /src/Etymology.Common/Chinese.Validation.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Common; 2 | 3 | public static partial class Chinese 4 | { 5 | private static readonly (int Min, int Max)[] BasicRanges = new (string Min, string Max)[] 6 | { 7 | ("4E00", "9FFF"), // 一 CJK Unified Ideograms Unified Ideographs: (U+4E00 to U+9FFF). 8 | ("3400", "4DBF"), // 㐦 CJK Ideographs Extension A: (U+3400 to U+4DBF). 9 | ("2E80", "2EFF"), // ⺀ CJK Radicals Supplement: (U+2E80 to U+2EFF). 10 | ("2F00", "2FDF"), // ⼀ Kangxi Radicals: (U+2F00 to U+2FDF). 11 | ("2FF0", "2FFF"), // “⿰” Ideographic Description Characters: (U+2FF0 to U+2FFF). 12 | ("3000", "303F"), // 〥 CJK Symbols and Punctuation: (U+3000 to U+303F). 13 | ("3040", "309F"), // い Hiragana: (U+3040 to U+309F). 14 | ("30A0", "30FF"), // ア Katakana: (U+30A0 to U+30FF). 15 | ("3100", "312F"), // ㄆ Bopomofo: (U+3100 to U+312F). 16 | ("31A0", "31BF"), // ㆡ Bopomofo Extended: (U+31A0 to U+31BF). 17 | ("31C0", "31EF"), // ㇏ CJK Strokes: (U+31C0 to U+31EF) Legitimize. Strokes in (31C0, 31E3) are not handled by SQL Server collation: ㇀㇁㇂㇃㇄㇅㇆㇇㇈㇉㇊㇋㇌㇍㇎㇏㇐㇑㇒㇓㇔㇕㇖㇗㇘㇙㇚㇛㇜㇝㇞㇟㇠㇡㇢㇣. 18 | ("31F0", "31FF"), // ㇰ Katakana Phonetic Extensions: (U+31F0 to U+31FF). 19 | ("F900", "FAFF"), // 豈 CJK Compatibility Ideographs: (U+F900 to U+FAFF). 20 | ("FE30", "FE4F"), // ︽ CJK Compatibility Forms: (U+FE30 to U+FE4F). 21 | ("FF00", "FFEF"), // ャ Half width and Full width Forms: (U+FF00 to U+FFEF). 22 | } 23 | .Select(hexCodePoint => (int.Parse(hexCodePoint.Min, NumberStyles.HexNumber, CultureInfo.InvariantCulture), int.Parse(hexCodePoint.Max, NumberStyles.HexNumber, CultureInfo.InvariantCulture))) 24 | .ToArray(); 25 | 26 | private static readonly (int Min, int Max)[] SurrogateRanges = new (string Min, string Max)[] 27 | { 28 | ("20000", "2A6DF"), // 𠀀 CJK Ideographs Extension B: (U+20000 to U+2A6DF). 29 | ("2A700", "2B73F"), // 𪜀 CJK Ideographs Extension C: (U+2A700 to U+2B73F). 30 | ("2B740", "2B81F"), // 𫝀 CJK Ideographs Extension D: (U+2B740 to U+2B81F). 31 | ("2B820", "2CEAF"), // 𫢸 CJK Ideographs Extension E: (U+2B820 to U+2CEAF). 32 | ("2CEB0", "2EBEF"), // 𬺰 CJK Ideographs Extension F: (U+2CEB0 to U+2EBEF). 33 | ("2F800", "2FA1F"), // “丽” CJK Comparability Ideographs Supplement: (U+2F800 to U+2FA1F). 34 | } 35 | .Select(hexCodePoint => (int.Parse(hexCodePoint.Min, NumberStyles.HexNumber, CultureInfo.InvariantCulture), int.Parse(hexCodePoint.Max, NumberStyles.HexNumber, CultureInfo.InvariantCulture))) 36 | .ToArray(); 37 | 38 | public static (Exception? Exception, bool IsSingleSurrogatePair) ValidateSingleCharacter(string? text, string? argument = null) 39 | { 40 | // Equivalent to: !string.IsNullOrEmpty(text) && new StringInfo(text).LengthInTextElements == 1. 41 | if (string.IsNullOrEmpty(text)) 42 | { 43 | return (new ArgumentNullException(argument, "Input is null or empty."), false); 44 | } 45 | 46 | int length = text.Length; 47 | if (length > 2) 48 | { 49 | return (new ArgumentOutOfRangeException(argument, "Input is more than a single character."), false); 50 | } 51 | 52 | // length is either 1 or 2. 53 | bool isSurrogate = char.IsHighSurrogate(text, 0); 54 | if (isSurrogate) 55 | { 56 | // length must be 2. 57 | if (length != 2) 58 | { 59 | return (new ArgumentException("Input is a single surrogate character and missing another character in the pair.", argument), false); 60 | } 61 | } 62 | else 63 | { 64 | // length must be 1. 65 | if (length != 1) 66 | { 67 | return (new ArgumentOutOfRangeException(argument, "Input is more than a single character."), false); 68 | } 69 | } 70 | 71 | return (null, isSurrogate); 72 | } 73 | 74 | public static (Exception? Exception, bool IsSingleSurrogatePair) ValidateSingleChineseCharacter(string? text, string? argument = null) 75 | { 76 | (Exception? exception, bool isSurrogate) = ValidateSingleCharacter(text, argument); 77 | if (exception != null) 78 | { 79 | return (exception, isSurrogate); 80 | } 81 | 82 | // Equivalent to: 83 | // byte[] bytes = isSurrogate 84 | // ? text.TextToBytes().Convert(Encoding.Unicode, Encoding.UTF32).FormatForUtf32() 85 | // : text.TextToBytes().Adjust(); 86 | // int codePoint = bytes.BytesToInt32(); 87 | int codePoint = isSurrogate ? char.ConvertToUtf32(text!, 0) : text![0]; 88 | 89 | (int Min, int Max)[] ranges = isSurrogate ? SurrogateRanges : BasicRanges; 90 | return ranges.Any(range => range.Min <= codePoint && range.Max >= codePoint) 91 | ? (null, isSurrogate) 92 | : (new ArgumentOutOfRangeException(argument, "Input is a single character but not Chinese."), isSurrogate); 93 | } 94 | 95 | public static bool IsSingleChineseCharacter(this string text) 96 | { 97 | if (string.IsNullOrWhiteSpace(text)) 98 | { 99 | return false; 100 | } 101 | 102 | int length = text.Length; 103 | if (length > 2) 104 | { 105 | return false; 106 | } 107 | 108 | bool isSurrogate = char.IsHighSurrogate(text, 0); 109 | if (isSurrogate) 110 | { 111 | // length must be 2. 112 | if (length != 2) 113 | { 114 | return false; 115 | } 116 | } 117 | else 118 | { 119 | // length must be 1. 120 | if (length != 1) 121 | { 122 | return false; 123 | } 124 | } 125 | 126 | int codePoint = isSurrogate ? char.ConvertToUtf32(text, 0) : text[0]; 127 | (int Min, int Max)[] ranges = isSurrogate ? SurrogateRanges : BasicRanges; 128 | return ranges.Any(range => range.Min <= codePoint && range.Max >= codePoint); 129 | } 130 | 131 | public static bool HasChineseCharacter(this string text) 132 | { 133 | if (string.IsNullOrWhiteSpace(text)) 134 | { 135 | return false; 136 | } 137 | 138 | for (int index = 0; index < text.Length; index++) 139 | { 140 | bool isSurrogate = char.IsHighSurrogate(text, index); 141 | if (isSurrogate && index == text.Length - 1) 142 | { 143 | return false; 144 | } 145 | 146 | int codePoint = isSurrogate ? char.ConvertToUtf32(text, index) : text[index]; 147 | (int Min, int Max)[] ranges = isSurrogate ? SurrogateRanges : BasicRanges; 148 | if (ranges.Any(range => range.Min <= codePoint && range.Max >= codePoint)) 149 | { 150 | return true; 151 | } 152 | 153 | if (isSurrogate) 154 | { 155 | index++; 156 | } 157 | } 158 | 159 | return false; 160 | } 161 | } -------------------------------------------------------------------------------- /src/Etymology.Tests/Data/Models/EtymologyContextTests.cs: -------------------------------------------------------------------------------- 1 | namespace Etymology.Tests.Data.Models; 2 | 3 | using Etymology.Data.Cache; 4 | using Etymology.Data.Models; 5 | using Etymology.Web.Server; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | [TestClass] 11 | public class EtymologyContextTests 12 | { 13 | private static DbContextOptions? contextOptions; 14 | 15 | [TestMethod] 16 | public async Task AnalyzeAsyncTest() 17 | { 18 | await using EtymologyContext database = CreateDatabase(); 19 | const string Chinese = "车"; 20 | CharacterCache characterCache = new(database); 21 | IEnumerable<(string Traditional, int CodePoint)> allTraditional = characterCache.AllTraditional(char.ConvertToUtf32(Chinese, 0)); 22 | AnalyzeResult[] results = await database.AnalyzeAsync(Chinese, allTraditional); 23 | Assert.AreEqual(1, results.Length); 24 | (string? queriedChinese, Etymology? etymology, IList oracles, IList bronzes, IList seals, IList liushutongs) = results.Single(); 25 | Assert.AreEqual(Chinese, queriedChinese); 26 | Assert.IsNotNull(etymology); 27 | Assert.IsNotNull(etymology); 28 | Assert.IsNotNull(oracles); 29 | Assert.IsTrue(oracles.Any()); 30 | Assert.IsNotNull(oracles.First()); 31 | Assert.IsNotNull(bronzes); 32 | Assert.IsTrue(bronzes.Any()); 33 | Assert.IsNotNull(bronzes.First()); 34 | Assert.IsNotNull(seals); 35 | Assert.IsTrue(seals.Any()); 36 | Assert.IsNotNull(seals.First()); 37 | Assert.IsNotNull(liushutongs); 38 | Assert.IsTrue(liushutongs.Any()); 39 | Assert.IsNotNull(liushutongs.First()); 40 | Trace.WriteLine($"{Chinese} {oracles.Count} {bronzes.Count} {seals.Count} {liushutongs.Count}"); 41 | } 42 | 43 | [TestMethod] 44 | [Ignore] 45 | public void AnalyzeAllImagesTest() 46 | { 47 | using EtymologyContext database = CreateDatabase(); 48 | Etymology[] etymologies = database.Etymology 49 | .Where(entity => 50 | database.Oracle.Any(character => character.Traditional == entity.Traditional && character.ImageVectorBase64 != null) 51 | && database.Bronze.Any(character => character.Traditional == entity.Traditional && character.ImageVectorBase64 != null) 52 | && database.Seal.Any(character => character.Traditional == entity.Traditional && character.ImageVectorBase64 != null) 53 | && database.Liushutong.Any(character => character.Traditional == entity.Traditional && character.ImageVectorBase64 != null)) 54 | .ToArray(); 55 | Trace.WriteLine(string.Concat(etymologies.Select(entity => entity.Traditional))); 56 | etymologies.Where(entity => entity.Simplified.Length >= 3).ToList().ForEach(entity => Trace.WriteLine(entity.Simplified)); 57 | etymologies = etymologies.Where(entity => entity.Simplified.Length < 3).ToArray(); 58 | Trace.WriteLine(string.Concat(etymologies.Select(entity => entity.Traditional))); 59 | } 60 | 61 | [TestMethod] 62 | public async Task ExtensionBTest() 63 | { 64 | await using EtymologyContext database = CreateDatabase(); 65 | const string ExtensionB = "𠂇"; 66 | CharacterCache characterCache = new(database); 67 | IEnumerable<(string Traditional, int CodePoint)> allTraditional = characterCache.AllTraditional(char.ConvertToUtf32(ExtensionB, 0)); 68 | (string chinese, Etymology etymology, IList oracles, IList bronzes, IList seals, IList liushutongs) = (await database.AnalyzeAsync(ExtensionB, allTraditional)).Single(); 69 | Assert.IsNotNull(etymology); 70 | Assert.IsNotNull(etymology); 71 | Assert.AreEqual(ExtensionB, etymology.Traditional); 72 | Trace.WriteLine($"{chinese} {oracles.Count} {bronzes.Count} {seals.Count} {liushutongs.Count}"); 73 | } 74 | 75 | [TestMethod] 76 | public void CommonCharactersTest() 77 | { 78 | const string CommonCharacters = "安八白百敗般邦保寶北貝匕比妣畢賓兵丙秉并伯帛亳卜不步采倉曹曾長厂鬯車徹臣辰晨成承乘遲齒赤沖舂丑出初楚畜傳春祠此次大丹單旦稻得登帝典奠丁鼎冬東動斗豆斷隊對多而兒耳二伐反方妃分封豐酆逢缶夫弗服福甫斧簠黼父复婦復干剛高羔告戈格鬲庚工弓公攻古谷鼓官觀盥光龜歸鬼癸國果亥行蒿好禾合何河侯后厚乎呼壺虎化畫淮黃惠會或獲鑊基箕及吉即亟疾己季既祭家甲斝監見姜彊降角教解巾斤今進盡京兢井競九酒舊沮句絕爵君畯考可克客口寇來牢老醴立利麗良林陵令柳六龍聾盧魯陸鹿麓洛旅率馬買麥卯枚眉每媚門蒙盟皿敏名明鳴命莫母牡木目牧乃男南內逆年廿鳥寧牛農奴諾女旁轡彭辟品僕圃七戚祈齊杞啟气千前遣羌且妾侵秦卿丘區曲取去犬人壬日戎如入若卅三散嗇山商上少紹射涉申沈生省尸師十石食時史矢豕使始士氏事室視首受獸叔黍蜀戍束水司絲死巳四祀宋綏歲孫它唐天田聽同土退屯豚鼉外亡王韋唯未位畏衛文聞問我吳五午武舞勿戊夕兮昔析奚襲徙喜系下先咸獻相祥饗向象小辛新星興兄休羞宿戌須徐宣旋學旬亞言炎衍甗央羊昜陽揚夭爻頁一伊衣依匜夷宜疑彝乙亦邑易異肄義因禋寅尹郢永用攸幽猶游友有酉又幼于余盂雩魚虞漁羽雨圉玉聿育昱御禦元爰員曰月樂龠允宰載再在葬昃增乍宅旃召折貞朕征正鄭之執止旨黹至豸彘中終仲舟州周帚朱逐祝貯追茲子自宗足卒族俎祖尊作"; 79 | Trace.WriteLine(CommonCharacters.Length); 80 | 81 | List<(char chinese, AnalyzeResult[] Result)> testedResults = CommonCharacters 82 | .Select(chinese => 83 | { 84 | using EtymologyContext database = CreateDatabase(); 85 | CharacterCache characterCache = new(database); 86 | IEnumerable<(string Traditional, int CodePoint)> allTraditional = characterCache.AllTraditional(char.ConvertToUtf32(new string(chinese, 1), 0)); 87 | return (chinese, database.AnalyzeAsync(new string(chinese, 1), allTraditional).Result); 88 | }) 89 | .Where(item => 90 | { 91 | (char text, AnalyzeResult[] results) = item; 92 | if (!results.Any()) 93 | { 94 | Trace.WriteLine($"Error: {text}"); 95 | return false; 96 | } 97 | 98 | bool hasCharacters = results.Any(result => 99 | { 100 | (_, Etymology etymology, IList oracles, IList bronzes, IList seals, IList liushutongs) = result; 101 | return etymology is not null && etymology.HasSimplified() && oracles.Any() && bronzes.Any() && seals.Any() && liushutongs.Any(); 102 | }); 103 | if (!hasCharacters) 104 | { 105 | Trace.WriteLine($"Error: {text}"); 106 | } 107 | 108 | return hasCharacters; 109 | }) 110 | .ToList(); 111 | Assert.AreEqual(CommonCharacters.Length, testedResults.Count); 112 | Trace.WriteLine(string.Concat(testedResults.Select(result => result.chinese))); 113 | } 114 | 115 | internal static EtymologyContext CreateDatabase() 116 | { 117 | if (contextOptions == null) 118 | { 119 | IConfiguration configuration = new ConfigurationBuilder() 120 | .AddJsonFile("settings.json") 121 | .AddJsonFile($"settings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true) 122 | .Build(); 123 | Settings settings = configuration.Get(); 124 | contextOptions = new DbContextOptionsBuilder() 125 | .UseSqlServer( 126 | settings.Connections.TryGetValue(nameof(Etymology), out string? connection) && !string.IsNullOrWhiteSpace(connection) 127 | ? connection 128 | : Environment.GetEnvironmentVariable(nameof(Etymology)) ?? throw new InvalidOperationException("Failed to get connection string."), 129 | options => options.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null)) 130 | .Options; 131 | } 132 | 133 | return new EtymologyContext(contextOptions); 134 | } 135 | } -------------------------------------------------------------------------------- /src/Etymology.Web/wwwroot/error/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Chinese Etymology 字源 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 113 | 114 | 115 | 116 | 120 | 121 | 122 |
123 | 124 |
125 | 126 |
127 | 128 | 138 | 139 |
140 |

Error

141 | 142 |

143 | Thank you for visiting Chinese Etymology. Something went wrong.
欢迎访问字源. 有错误产生. 144 |

145 |

Home 首页

146 |
147 |
148 |
149 |

Copyright © 1994 - 2017 Richard Sears hanziyuan.net | ChineseEtymology.org All rights received.

150 |
151 |
152 |
153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd 363 | 364 | # Access 365 | *.accdb 366 | *.mdb 367 | -------------------------------------------------------------------------------- /docs/_posts/2018-04-23-simplification-of-chinese-character.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Simplification of Chinese Characters 3 | --- 4 | 5 | Richard Sears 6 | 7 | # Simplification of Chinese Characters 8 | 9 | ## The Problem in 1950 10 | 11 | 1. For 2000 years there had been no real control over the proliferation of Chinese characters. 12 | 1. The standard Kangxi dictionary had 47,000 characters of which only about 7000 were useful or common. 13 | 1. There were many variant characters that meant the same thing, or nearly the same thing 14 | 1. The characters were very complicated and hard to write 15 | 1. Most people could not read 16 | 17 | ## The Fix 18 | 19 | 1. Analyze common variant characters and mandate which ones will be standard and which ones will be banned. 20 | 1. Analyze which characters are useful and ban all the rest 21 | 1. Simplify and standardize the written form of complicated characters 22 | 23 | ## The Questions to ask 24 | 25 | 1. Who made these rules 26 | 1. What were the rules 27 | 1. What was the logic behind these rules 28 | 1. What changes were made to these rules over history. 29 | 30 | ## Traditional to Simplified 31 | 32 | 1. First apply any variant rule that may apply (discussed below) 33 | 1. Second apply any simplification rule that may apply (discussed below) 34 | 1. Third apply any font rule that may apply 35 | 36 | ## Simplified to Traditional 37 | 38 | To go from simplified to traditional you may have to figure out which of several traditional characters is it supposed to be. 39 | 40 | You may have to consult an associated rule: 41 | 1. Variant Rule 42 | 1. Simplification Rule 43 | 1. Applied Rule 44 | 1. Font Rule 45 | 1. Total Character List 46 | 47 | (discussed below) 48 | 49 | # Variant Rule V001 to V810 50 | 51 | Standard table of Variants 第一批异体字整理表 52 | 53 | ## Definitions: 54 | 55 | **Variants**: Two or more characters which have 56 | 57 |  1. exact same pronunciation 58 |  1. exact same meaning 59 |  1. equivalent logic. 60 | 61 | **Equivalent Logic**: 碴 and 𦉆 are both pronounced “cha” and both mean “chip” 62 | 63 | 1. In one the logic means it may be a chip from a rock 石 shí 64 | 65 | 1. In the other the logic means it may be a chip from a piece of pottery 66 | 67 | 缶 fǒu These two characters have “equivalent logic” 68 | 69 | **Same Character**: 报報 and 𡙈 are derived from the same ancient character. 㚔 is derived from the original pictograph. 幸 is derived from 㚔 and 扌is derived from 幸. They are the same character. 70 | 71 | Substituted Character: 72 | 73 | ## History 74 | 75 | In 1956 the government published a list of 810 acceptable variant characters and 1055 associated variant characters which were to be banned. 76 | 77 | ## Problems: 78 | 79 | 1. A variant should be the exact same meaning and pronunciation. But some of the characters only had the same pronunciation but had different meanings. They were in fact substitutions. 80 | 1. There were some special rules associated with this list, where you could use the banned version in some situations depending on its meaning or pronunciation. 81 | 1. This resulted in modern generation of Chinese not being able to recognize many common traditional variant characters. 82 | 1. Over the years, some modifications were made and some banned characters were resurrected. The most recent table has 83 | 84 | 796 accepted characters. 85 | 86 | 1024 banned characters. 87 | 88 | # Simplification Rule A001-A350 89 | 90 | Stand Alone Simplification Only 个不作简化偏旁用的简化字 91 | 92 | ## The History 93 | 94 | In 1955, a list of 350 characters was published which were given simplified forms. 95 | 96 | These characters were to be simplified in their stand alone form, but not if they are part of another character. 97 | 98 | Actually here are cases where they are simplified as components. 99 | 100 | ## The Problems 101 | 102 | 1. In several cases multiple characters are converged to one character. 103 | 1. In several cases the character convergences have been resurrected. 104 | 1. Sometimes the character is substituted for a rare traditional character. 105 | - This complicates the definition of what is a traditional character and what is a traditional character. 106 | 1. Sometimes new characters are formed because of the simplification logic. 107 | - These new characters never existed in any dictionary previously and so I call them “new” characters, even though they may have existed in hand written Chinese for a long time. 108 | 1. In some characters, even the radical component is simplified 109 | 1. In some cases, the character is not simplified because of some special use. 110 | 111 | # Simplification Rule B001-B123 112 | 113 | Simplified as Stand Alone or as Radical 个可作简化偏旁用的简化字和简化偏旁 114 | 115 | ## The History 116 | 117 | In 1955, a list of 132 characters was published which are simplified in their standalone form and are also simplified when used as components of another character. 118 | 119 | There are 1 to n convergences due to table 5 120 | 121 | # Simplification Rule R01-R14 122 | 123 | Simplified radicals 简化偏旁14个 124 | 125 | ## The History 126 | 127 | In 1955 a set of 14 components that if they have stand alone forms, the standalone forms  are not simplified. Some of these characters never have stand alone forms. They are just components of characters. There are also places where characters are components, but not simplified.  128 | 129 | # Applied Rule: 130 | 131 | Rule based simplification 1753个应用第二表所列简化字和简化偏旁得出来的简化字 132 | 133 | ## The History 134 | 135 | There were 1753 Characters that were simplified according to table 1, 2, 3 and 4 136 | 137 | There are characters that are simplified using other rules that are not included in Table 1,2 and 3 138 | 139 | Some of these characters are quite uncommon characters. 140 | 141 | ## The Problems 142 | 143 | 1. In some cases the rules are not strictly applied and the simplified character is an anomaly. 144 | 1. Sometimes the rules are applied to banned alternates and you come up with strange situations. 145 | 1. The number 1753 was applied to the original list of printed characters, so that list is not complete now. 146 | 1. This list is out of date. There are characters from this list that are not included in the 2013 standard list of characters and there are simplifications in the 2013 list of characters that are beyond this list. 147 | 148 | # Complete Simplified Character List and the New Character Font 149 | 150 | ## Purpose of a Complete Character list 151 | 152 | 1. Limit the number of useless characters 153 | 1. Establish Standard Printed Forms using the above “variant rules” and “simplification rules” 154 | 1. Establish exact printed form for characters called the “new character forms” 155 | 1. Establish Literacy Standard 156 | 157 | ## The History 158 | 159 | There have been four publications that can be considered complete lists of simplified characters 160 | 1.  印刷通用汉字字形表 1965 list of 6196 simplified characters for publishers 161 | 1.  1980 list of standard simplified computer characters GB2312-80 162 | 1.  現代漢語常用字表 1988 list of 7000 simplified characters 163 | 1.  通用規範漢字表 2013 list of 8105 simplified characters 164 | 165 | ## The Problems 166 | 167 | 1. There are always characters not in the list that you think you need that you don’t have 168 | 1. The “New Character Forms” 169 | 1. The lists were published with no associated traditional characters. 170 | 171 | # 新字形 New Character Forms and the 標楷體 Standard Kai Form 172 | 173 | ## Font Rule 174 | 175 | Besides following all the rules for simplification of characters, the exact character form was specified in what is called the “new character form”.  These forms were probably barely noticed by most people but when computers came along, it caused a number of characters to have different code points because of the difference in how governments thought characters were supposed to be written. 176 | 177 | ## Example 178 | 179 | 黄 and 黃 are two different code points and ways to write the character for “yellow” 180 | 181 | 黄 is the new character form and 182 | 183 | 黃 is the standard Kai form or Old Character forms 184 | 185 | # The Unicode Standard and Han Unification 186 | 187 | ## The Problem 188 | 189 | Over the years there had evolved some differences in how Korea, Japan, China, Taiwan, Hongkong and Vietnam wrote the same Chinese character.   190 | 191 | ## The Solution 192 | 193 | 1. Korea, Japan, China, Taiwan, Hongkong and Vietnam decided to get together and decide on code points and character structure for all the useful Han characters. If they could agree that the characters were the same, they were all put in one code point, if not, there were different code points for instance 黄 and 黃 are two different code points, even though they are the same character. 194 | 1. Today almost all Han characters on computers are encoded in Unicode. 195 | -------------------------------------------------------------------------------- /docs/_posts/2017-09-18-spoken-chinese-and-other-languages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Spoken Chinese and Other Languages 3 | --- 4 | 5 | Richard Sears 6 | 7 | # Spoken Chinese and Other Languages 8 | 9 | ## Table of Contents 10 | 11 | - [Spoken Chinese and Other Languages](#spoken-chinese-and-other-languages) 12 | - [Table of Contents](#table-of-contents) 13 | - [Mandarin Language and Script](#mandarin-language-and-script) 14 | - [Taiwanese Language and Script](#taiwanese-language-and-script) 15 | - [Book References:](#book-references) 16 | - [Cantonese Language and Characters](#cantonese-language-and-characters) 17 | - [Book References:](#book-references-1) 18 | - [Japanese Language and Script](#japanese-language-and-script) 19 | - [Korean Language and Script](#korean-language-and-script) 20 | - [Vietnamese Language and Script](#vietnamese-language-and-script) 21 | 22 | ## Mandarin Language and Script 23 | 24 | Mandarin originally refers to the language spoken by Chinese officials who were mainly from Beijing. This language was called Guan-Yu 官語 Official-Language. The Sanskrit word Mandari comes through Portuguese and means commander related to English Mand-ate The early Portuguese referred to these people and their language as Mandarin. The BeiJingHua 北京話 Beijing-Talk Spoken in Beijing PuTongHua 普通話 Common Talk spoken in Canton, the HuaYu 華語 Chinese Language spoken in South East Asia and the GuoYu 國語 National Language spoken in Taipei are the same language with only very minor differences. 25 | 26 | PuTongHua is spoken by almost all Chinese although 80% of them will speak some other dialect at home. When speaking of Chinese Dialects we usually mean different languages. Often although the dialects will be closely related, If you have not had experience with it, you will understand almost nothing. 27 | 28 | Modern written Chinese is a direct rendition of spoken Mandarin. In English we always refer to it as Chinese.In Chinese it is usually called HanYu 漢語 Chinese Language This is Mandarin as defined in the dictionary. Almost No one speaks exactly like the Dictionary. But most well educated Chinese have studied tones, and pinying and can pronounce correctly if reading from the dictionary. See Cantonese, Taiwanese. 29 | 30 | ## Taiwanese Language and Script 31 | 32 | Taiwanese is an important language as far as Chinese Etymology is concerned and as far as China is concerned. 33 | 34 | In Chinese it is referred to technically as MinNanHua 閩南話 Southern Min Language It is spoken in Southern FuJian 福建 province and in Taiwan. It is often referred to as TaiWanHua 台灣話 Taiwanese Language or XiaMenHua 厦門話 Amoy Language. Amoy (XiaMen) is the main Chinese coastal city in FuJian where this language is spoken. It is also called TaiYu 台語 Taiwanese. It is not understandable by Mandarin speakers who have never been exposed to it. I estimates that about 80% of Taiwanese has the same etymology as Mandarin, but with very significant phonetic shifts. Written Mandarin can be read in Taiwanese, but it is a very stilted and does not reflect the grammatical structure of real spoken Taiwanese. Unlike Cantonese, Taiwanese in most cases did not invent new characters. When there is a Taiwanese word which has no Mandarin equivalent, they usually took Mandarin characters which when pronounced in Taiwanese would sounded like the Taiwanese word in question. Some characters will be used in places with the usual Mandarin meaning and other characters will be used for the sound. The average Mandarin will not understand written Taiwanese. MinBeiHua 閩北話 Northern Min Language is the other dialect spoken in FuJian, and is quite different from MinNan. FuZhouHua 福州話 is spoken in FuZhou and is also very different. Many of the Chinese emigrants to South East Asia came from ChaoZhou in southern Fukin and speak a language called ChaoJouHua 潮州話 CaoJou is similar to and for the most part understandable by Taiwanese. 35 | 36 | Taiwanese is important etymologically because when we compare the pronunciation of character phonetics in Taiwanese we sometimes find that they are closer than in Mandarin. 37 | 38 | Taiwanese has 7 tones. The teaching materials say 8, but this is so that the saying of all the tones will sound more fluent. There are 2337 unique syllabic utterances in Taiwanese. The database of syllabic utterances was done by Sharry Wu 39 | 40 | ### Book References: 41 | 42 | - 台灣話大詞典 閩南話漳泉二腔系部份 by ChenShou 陳修 主編 43 | 44 | Probably the most extensive Taiwanese to Chinese dictionary 45 | 46 | used for my Taiwanese syllabic database. 47 | 48 | ## Cantonese Language and Characters 49 | 50 | GuangDongHua (GongDongWa) 廣東話 Cantonese is the most common dialect spoken by over-seas Chinese. Its formal name is YueYu (YutYu) 粤語 It is in fact a different language from Manderin, although closely related to it. A person who has grown up in BeiJing and has never heard Cantonese, would understand almost nothing. About 80% of Cantonese words have the same root as Mandarin although the pronunciation may be shifted quite drastically. The other 20 percent is strictly Cantonese and has no Mandarin equivalent. These words are called JukJi Cantonese is spoken in GuangDong province, HongKong, Macao and all around the world. The TaiShanHua (HoiSanWa) 台山話 TaiShan dialect of Cantonese is spoken in SanFrancisco the largest collection of JukJi is a set of about 4500 characters published by the city government of HongKong, The average Cantonese probably only uses a few hundred. These characters can not be found in a HanYuDaZiDian dictionary. 51 | 52 | ### Book References: 53 | 54 | - A Practical Cantonese-English Dictionary 實用粤英詞典 by Sidney Lao 劉緆祥 55 | The Cantonese to English dictionary which I used for my Cantonese syllabic database. 56 | 57 | ## Japanese Language and Script 58 | 59 | The Chinese word HanZi 漢字 means “Chinese Character”. When borrowed by the Japanese the pronunciation became KanJi. 漢字 The Japanese borrowed the Chinese writing system starting in the Tang dynasty about 1400 years ago. Japanese grammar is quite different from Chinese. Japanese is a Ural Altaic language more closely related to Turkish than to Chinese, The Chinese writing did not fit well with Japanese. As a result, several things happened. (1) They borrowed the Chinese characters and used them to represent Japanese words and gave them the Japanese pronunciations called Kunyomi 訓読, (2) In many cases they borrowed the Chinese pronunciation too in this case they used the Onyomi 音読 pronunciation which corresponds to the original Chinese pronunciation. (3) Some Chinese characters were used as phonetics for Japanese words. Originally these phonetics were used mainly by women and other semi literate Japanese. Ultimately the cursive form of these phonetics developed into two precise phonetic alphabets. One called Hiragana ひらがな which is used to write the Japanese words which have no Chinese or other foreign etymology. The Kai forms of these phonetics developed into a precise phonetic alphabet called Katakana カタカナ which was used to write the words in Japanese which are derived from languages other than Japanese or Chinese, mostly English. (4) Since the kanji was borrowed so long ago, in many cases the written form has changed somewhat, the meaning has changed, sometimes a lot. One example is the 読 in Onyomi which in Chinese is 讀 (5) In modern Japanese there has been a move to reduce the number of Chinese characters to around 2200 and some of those characters are rare or non existent in modern Chinese. As a rough eye ball estimate, I would say 80% of kanji is pronounced similar to and has a similar meaning to the Chinese. 60 | 61 | ## Korean Language and Script 62 | 63 | Korean is also Ural Altaic and originally borrowed Chinese characters like the Japanese. Korean never derived special Hiragana and Katakana type alphabets. Instead in 1426 King SeJong 世宗 invented an alphabet for initial central and final sounds of the Korean syllables. The letters of this alphabet are called Hangul 한글 and do not appear to have any connection to Chinese characters. The way these letters are stacked into square boxes that correspond to syllabic utterances and in Korean was obviously influenced by Chinese. These spellings were used for several hundred years for syllables of strictly Korean origin and syllables of Chinese etymology were written in Chinese called Hanja 漢字. In the past 30 years Korean news papers at least have gone completely Hangul. 64 | 65 | ## Vietnamese Language and Script 66 | 67 | Vietnamese has a similar grammatical and morphological structure to Chinese but percentage wise very few syllables are etymologically related to Chinese. The Vietnamese took an approach similar to the Cantonese starting in about the 10th century, but they had to design a large number of new characters. These characters are called ChuNom. When you see old Vietnamese written it is obviously derived from Chinese type characters, but a Chinese literate person will understand almost none of the characters. 68 | --------------------------------------------------------------------------------