Repository: vuejs/vue
Pull request: feat(compiler): allow unicode characters for component name as described in #8564

resolve #8564

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [x] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**


Changed files:
filename: src/compiler/parser/html-parser.js
patch: @@ -11,12 +11,11 @@
 
 import { makeMap, no } from 'shared/util'
 import { isNonPhrasingTag } from 'web/compiler/util'
+import { unicodeLetters } from 'core/util/lang'
 
 // Regular Expressions for parsing tags and attributes
 const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
-// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
-// but for Vue templates we can enforce a simple charset
-const ncname = '[a-zA-Z_][\\w\\-\\.]*'
+const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeLetters}]*`
 const qnameCapture = `((?:${ncname}\\:)?${ncname})`
 const startTagOpen = new RegExp(`^<${qnameCapture}`)
 const startTagClose = /^\s*(\/?)>/

filename: src/core/util/lang.js
patch: @@ -1,5 +1,12 @@
 /* @flow */
 
+/**
+ * unicode letters used for parsing html tags, component names and property paths.
+ * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname
+ * skipping \u10000-\uEFFFF due to it freezing up PhantomJS
+ */
+export const unicodeLetters = 'a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD'
+
 /**
  * Check if a string starts with $ or _
  */
@@ -23,7 +30,7 @@ export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
 /**
  * Parse simple path.
  */
-const bailRE = /[^\w.$]/
+const bailRE = new RegExp(`[^${unicodeLetters}.$]`)
 export function parsePath (path: string): any {
   if (bailRE.test(path)) {
     return

filename: src/core/util/options.js
patch: @@ -2,9 +2,9 @@
 
 import config from '../config'
 import { warn } from './debug'
-import { nativeWatch } from './env'
 import { set } from '../observer/index'
-import { hasSymbol } from '../util/index'
+import { unicodeLetters } from './lang'
+import { nativeWatch, hasSymbol } from './env'
 
 import {
   ASSET_TYPES,
@@ -264,11 +264,10 @@ function checkComponents (options: Object) {
 }
 
 export function validateComponentName (name: string) {
-  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
+  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$`).test(name)) {
     warn(
       'Invalid component name: "' + name + '". Component names ' +
-      'can only contain alphanumeric characters and the hyphen, ' +
-      'and must start with a letter.'
+      'should conform to valid custom element name in html5 specification.'
     )
   }
   if (isBuiltInTag(name) || config.isReservedTag(name)) {

filename: test/unit/features/instance/methods-data.spec.js
patch: @@ -26,6 +26,9 @@ describe('Instance methods data', () => {
         data: {
           a: {
             b: 1
+          },
+          유니코드: {
+            なまえ: 'ok'
           }
         },
         methods: {
@@ -108,6 +111,15 @@ describe('Instance methods data', () => {
       expect(spy).toHaveBeenCalledWith(1)
     })
 
+    it('handler option in string', () => {
+      vm.$watch('유니코드.なまえ', {
+        handler: 'foo',
+        immediate: true
+      })
+      expect(spy.calls.count()).toBe(1)
+      expect(spy).toHaveBeenCalledWith('ok')
+    })
+
     it('warn expression', () => {
       vm.$watch('a + b', spy)
       expect('Watcher only accepts simple dot-delimited paths').toHaveBeenWarned()

filename: test/unit/features/options/name.spec.js
patch: @@ -15,7 +15,7 @@ describe('Options name', () => {
     })
 
     /* eslint-disable */
-    expect(`Invalid component name: "Hyper*Vue". Component names can only contain alphanumeric characters and the hyphen, and must start with a letter.`)
+    expect(`Invalid component name: "Hyper*Vue".`)
       .toHaveBeenWarned()
     /* eslint-enable */
 
@@ -24,7 +24,7 @@ describe('Options name', () => {
     })
 
     /* eslint-disable */
-    expect(`Invalid component name: "2Cool2BValid". Component names can only contain alphanumeric characters and the hyphen, and must start with a letter.`)
+    expect(`Invalid component name: "2Cool2BValid".`)
       .toHaveBeenWarned()
     /* eslint-enable */
   })
@@ -37,4 +37,12 @@ describe('Options name', () => {
     expect(SuperComponent.options.components['SuperVue']).toEqual(SuperComponent)
     expect(SuperComponent.options.components['super-component']).toEqual(SuperComponent)
   })
+
+  it('should allow all potential custom element name for component name including non-alphanumeric characters', () => {
+    Vue.extend({
+      name: 'my-컴포넌트'
+    })
+
+    expect(`Invalid component name`).not.toHaveBeenWarned()
+  })
 })