mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 17:50:22 +00:00
Disambiguate TypeScript with tsx extension. (#3464)
Using the technique as discussed in #2761.
This commit is contained in:
committed by
Colin Seymour
parent
b66fcb2529
commit
f1be771611
@@ -465,5 +465,13 @@ module Linguist
|
||||
Language["Scilab"]
|
||||
end
|
||||
end
|
||||
|
||||
disambiguate ".tsx" do |data|
|
||||
if /^\s*(import.+(from\s+|require\()['"]react|\/\/\/\s*<reference\s)/.match(data)
|
||||
Language["TypeScript"]
|
||||
elsif /^\s*<\?xml\s+version/i.match(data)
|
||||
Language["XML"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
384
samples/TypeScript/import.tsx
Normal file
384
samples/TypeScript/import.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
// Fixture taken from https://github.com/graphcool/console/blob/dev/src/components/onboarding/PlaygroundCPopup/PlaygroundCPopup.tsx
|
||||
|
||||
import * as React from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {nextStep, selectExample} from '../../../actions/gettingStarted'
|
||||
import {classnames} from '../../../utils/classnames'
|
||||
import Loading from '../../Loading/Loading'
|
||||
import {GettingStartedState} from '../../../types/gettingStarted'
|
||||
import {Example} from '../../../types/types'
|
||||
const classes: any = require('./PlaygroundCPopup.scss')
|
||||
import {$p} from 'graphcool-styles'
|
||||
import * as cx from 'classnames'
|
||||
|
||||
interface Tutorial {
|
||||
title: string
|
||||
description: string
|
||||
image: string
|
||||
link: string
|
||||
}
|
||||
|
||||
const guides: Tutorial[] = [
|
||||
{
|
||||
title: 'Learnrelay.org',
|
||||
description: 'A comprehensive, interactive introduction to Relay',
|
||||
link: 'https://learnrelay.org/',
|
||||
image: require('../../../assets/graphics/relay.png'),
|
||||
},
|
||||
{
|
||||
title: 'GraphQL and the amazing Apollo Client',
|
||||
description: 'Explore an Application built using React and Angular 2',
|
||||
link: 'https://medium.com/google-developer-experts/graphql-and-the-amazing-apollo-client-fe57e162a70c',
|
||||
image: require('../../../assets/graphics/apollo.png'),
|
||||
},
|
||||
{
|
||||
title: 'Introducing Lokka',
|
||||
description: 'A Simple JavaScript Client for GraphQL',
|
||||
link: 'https://voice.kadira.io/introducing-lokka-a-simple-javascript-client-for-graphql-e0802695648c',
|
||||
image: require('../../../assets/graphics/lokka.png'),
|
||||
},
|
||||
]
|
||||
|
||||
const examples = {
|
||||
ReactRelay: {
|
||||
path: 'react-relay-instagram-example',
|
||||
description: 'React + Relay',
|
||||
},
|
||||
ReactApollo: {
|
||||
path: 'react-apollo-instagram-example',
|
||||
description: 'React + Apollo',
|
||||
},
|
||||
ReactNativeApollo: {
|
||||
path: 'react-native-apollo-instagram-example',
|
||||
description: 'React Native + Apollo',
|
||||
},
|
||||
AngularApollo: {
|
||||
path: 'angular-apollo-instagram-example',
|
||||
description: 'Angular + Apollo',
|
||||
},
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
projectId: string
|
||||
nextStep: () => Promise<void>
|
||||
selectExample: (selectedExample: Example) => any
|
||||
gettingStartedState: GettingStartedState
|
||||
}
|
||||
|
||||
interface State {
|
||||
mouseOver: boolean
|
||||
}
|
||||
|
||||
class PlaygroundCPopup extends React.Component<Props, State> {
|
||||
|
||||
state = {
|
||||
mouseOver: false,
|
||||
}
|
||||
|
||||
refs: {
|
||||
[key: string]: any
|
||||
exampleAnchor: HTMLDivElement
|
||||
congratsAnchor: HTMLDivElement
|
||||
scroller: HTMLDivElement,
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (prevProps.gettingStartedState.selectedExample !== this.props.gettingStartedState.selectedExample) {
|
||||
this.refs.scroller.scrollTop += this.refs.exampleAnchor.getBoundingClientRect().top
|
||||
}
|
||||
|
||||
if (prevProps.gettingStartedState.isCurrentStep('STEP5_WAITING')
|
||||
&& this.props.gettingStartedState.isCurrentStep('STEP5_DONE')) {
|
||||
this.refs.scroller.scrollTop += this.refs.congratsAnchor.getBoundingClientRect().top
|
||||
|
||||
const snd = new Audio(require('../../../assets/success.mp3') as string)
|
||||
snd.volume = 0.5
|
||||
snd.play()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {mouseOver} = this.state
|
||||
const {selectedExample} = this.props.gettingStartedState
|
||||
const hovering = !this.props.gettingStartedState.isCurrentStep('STEP4_CLICK_TEASER_STEP5')
|
||||
const downloadUrl = (example) => `${__BACKEND_ADDR__}/resources/getting-started-example?repository=${examples[example].path}&project_id=${this.props.projectId}&user=graphcool-examples` // tslint:disable-line
|
||||
return (
|
||||
<div
|
||||
className='flex justify-center items-start w-100 h-100'
|
||||
style={{
|
||||
transition: 'background-color 0.3s ease',
|
||||
backgroundColor: hovering ? 'rgba(255,255,255,0.7)' : 'transparent',
|
||||
width: 'calc(100% - 266px)',
|
||||
pointerEvents: 'none',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref='scroller'
|
||||
className='flex justify-center w-100'
|
||||
style={{
|
||||
transition: 'height 0.5s ease',
|
||||
height: hovering ? '100%' : mouseOver ? '190%' : '210%',
|
||||
pointerEvents: hovering ? 'all' : 'none',
|
||||
cursor: hovering ? 'auto' : 'pointer',
|
||||
overflow: hovering ? 'auto' : 'hidden',
|
||||
alignItems: selectedExample ? 'flex-start' : 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='bg-white br-2 shadow-2 mv-96'
|
||||
style={{
|
||||
minWidth: 600,
|
||||
maxWidth: 800,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
onMouseLeave={() => this.setState({ mouseOver: false })}
|
||||
onMouseEnter={() => {
|
||||
this.setState({ mouseOver: true })
|
||||
}}
|
||||
onMouseOver={(e: any) => {
|
||||
if (this.props.gettingStartedState.isCurrentStep('STEP4_CLICK_TEASER_STEP5')) {
|
||||
this.props.nextStep()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='ma-16 tc pb-25'>
|
||||
<div className='fw3 ma-38 f-38'>
|
||||
You did it! Time to run an example.
|
||||
</div>
|
||||
<div className='fw2 f-16 mh-96 lh-1-4'>
|
||||
You have successfully set up your own Instagram backend.{' '}
|
||||
When building an app with Graphcool you can easily explore queries in the{' '}
|
||||
playground and "copy & paste" selected queries into your code.{' '}
|
||||
Of course, to do so, you need to implement the frontend first.
|
||||
</div>
|
||||
<div className='fw2 f-16 mh-96 lh-1-4 mt-16'>
|
||||
<div>We put together a simple app to show and add posts</div>
|
||||
<div>using the backend you just built, to test and run it locally.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ma-16 tc pb-25'>
|
||||
<div className='fw3 ma-38 f-25'>
|
||||
Select your preferred technology to download the example.
|
||||
</div>
|
||||
<div className='flex justify-between items-center w-100' ref='exampleAnchor'>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.exampleButton,
|
||||
selectedExample === 'ReactRelay' ? classes.active : '',
|
||||
)}
|
||||
onClick={() => this.props.selectExample('ReactRelay')}
|
||||
>
|
||||
React + Relay
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.exampleButton,
|
||||
selectedExample === 'ReactApollo' ? classes.active : '',
|
||||
)}
|
||||
onClick={() => this.props.selectExample('ReactApollo')}
|
||||
>
|
||||
React + Apollo
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.exampleButton,
|
||||
selectedExample === 'ReactNativeApollo' ? classes.active : '',
|
||||
)}
|
||||
onClick={() => this.props.selectExample('ReactNativeApollo')}
|
||||
>
|
||||
React Native + Apollo
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.exampleButton,
|
||||
selectedExample === 'AngularApollo' ? classes.active : '',
|
||||
)}
|
||||
onClick={() => this.props.selectExample('AngularApollo')}
|
||||
>
|
||||
Angular + Apollo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedExample &&
|
||||
<div>
|
||||
<div className='w-100'>
|
||||
<iframe
|
||||
className='w-100'
|
||||
height='480'
|
||||
allowFullScreen
|
||||
frameBorder='0'
|
||||
src={`https://www.youtube.com/embed/${this.getExampleVideoUrl(selectedExample)}`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='w-100 pa-25'
|
||||
style={{
|
||||
backgroundColor: '#FEF5D2',
|
||||
}}
|
||||
>
|
||||
<div className='mt-25 mb-38 w-100 flex justify-center'>
|
||||
<a
|
||||
href={downloadUrl(selectedExample)}
|
||||
className='pa-16 white'
|
||||
style={{
|
||||
backgroundColor: '#4A90E2',
|
||||
}}
|
||||
>
|
||||
Download example
|
||||
</a>
|
||||
</div>
|
||||
<div className='code dark-gray'>
|
||||
<div className='black-50'>
|
||||
# To see the example in action, run the following commands:
|
||||
</div>
|
||||
<div className='mv-16'>
|
||||
<div className='black'>
|
||||
npm install
|
||||
</div>
|
||||
<div className='black'>
|
||||
npm start
|
||||
</div>
|
||||
</div>
|
||||
<div className='black-50'>
|
||||
# You can now open the app on localhost:3000
|
||||
</div>
|
||||
<div className='black-50'>
|
||||
# Please come back to this page once you're done. We're waiting here. 💤
|
||||
</div>
|
||||
<div className={cx($p.w100, $p.flex, $p.flexRow, $p.justifyCenter, $p.mt25)}>
|
||||
<a href='#' onClick={
|
||||
(e: any) => {
|
||||
e.preventDefault()
|
||||
// we need to skip the 'STEP5_WAITING' step
|
||||
this.props.nextStep()
|
||||
this.props.nextStep()
|
||||
this.props.nextStep()
|
||||
}
|
||||
}>
|
||||
Skip
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{this.props.gettingStartedState.isCurrentStep('STEP5_WAITING') &&
|
||||
<div className='w-100 mv-96 flex justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{this.props.gettingStartedState.isCurrentStep('STEP5_DONE') &&
|
||||
<div className='w-100 mb-96' ref='congratsAnchor'>
|
||||
<div className='flex items-center flex-column pv-60 fw1'>
|
||||
<div className='f-96'>
|
||||
🎉
|
||||
</div>
|
||||
<div className='f-38 mt-38'>
|
||||
Congratulations!
|
||||
</div>
|
||||
<div className='f-38 mt-16'>
|
||||
We knew you had it in you.
|
||||
</div>
|
||||
<div className='f-16 mv-38'>
|
||||
Now go out there and build amazing things!
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between ph-25 pv-16'>
|
||||
<div className='w-50 pr-16'>
|
||||
<div className='ttu ls-2 f-16 fw1 lh-1-4'>
|
||||
Get started on your own<br />with those excellent tutorials
|
||||
</div>
|
||||
<div className='mv-38'>
|
||||
{guides.map(guide => this.renderBox(guide))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-50 pl-16'>
|
||||
<div className='ttu ls-2 f-16 fw1 lh-1-4'>
|
||||
Get more out of Graphcool<br />with our guides
|
||||
</div>
|
||||
<div className={`h-100 justify-start flex flex-column mv-38 ${classes.guides}`}>
|
||||
<a
|
||||
href='https://graph.cool/docs/tutorials/quickstart-2-daisheeb9x'
|
||||
className={`${classes.one} fw4 black db flex items-center mb-25`}
|
||||
target='_blank'
|
||||
>
|
||||
Declaring Relations
|
||||
</a>
|
||||
<a
|
||||
href='https://graph.cool/docs/tutorials/quickstart-3-saigai7cha'
|
||||
className={`${classes.two} fw4 black db flex items-center mb-25`}
|
||||
target='_blank'
|
||||
>
|
||||
Implementing Business Logic
|
||||
</a>
|
||||
<a
|
||||
href='https://graph.cool/docs/tutorials/thinking-in-terms-of-graphs-ahsoow1ool'
|
||||
target='_blank'
|
||||
className={`${classes.three} fw4 black db flex items-center mb-25`}
|
||||
>
|
||||
Thinking in terms of graphs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-100 justify-center'>
|
||||
<div
|
||||
className='f-25 mv-16 pv-16 ph-60 ls-1 ttu pointer bg-accent white dim'
|
||||
onClick={this.props.nextStep}
|
||||
>
|
||||
Finish Onboarding
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderBox = (tutorial: Tutorial) => {
|
||||
return (
|
||||
<div key={tutorial.title} className='pa-16 mb-16 lh-1-4' style={{background: 'rgba(0,0,0,0.03)'}}>
|
||||
<a className='flex items-center' href={tutorial.link} target='_blank'>
|
||||
<div className='flex items-center justify-center' style={{ flex: '0 0 60px', height: 60 }}>
|
||||
<img src={tutorial.image}/>
|
||||
</div>
|
||||
<div className='flex flex-column space-between ml-38'>
|
||||
<div className='mb-6 dark-gray f-16'>
|
||||
{tutorial.title}
|
||||
</div>
|
||||
<div className='fw1 mid-gray'>
|
||||
{tutorial.description}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private getExampleVideoUrl = (example: Example): string => {
|
||||
switch (example) {
|
||||
case 'ReactRelay': return '_dj9Os2ev4M'
|
||||
case 'ReactApollo': return '9nlwyPUPXjQ'
|
||||
case 'ReactNativeApollo': return '9nlwyPUPXjQ'
|
||||
case 'AngularApollo': return 'EzD5fJ-uniI'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
gettingStartedState: state.gettingStarted.gettingStartedState,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return bindActionCreators({nextStep, selectExample}, dispatch)
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(PlaygroundCPopup))
|
||||
366
samples/TypeScript/react-native.tsx
Normal file
366
samples/TypeScript/react-native.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
// Fixture taken from https://github.com/bgrieder/RNTSExplorer/blob/master/typescript/components/TextExample.ios.tsx
|
||||
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* Typescript rewrite by Bruno Grieder
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
import * as React from 'react-native';
|
||||
import RNTSExample from '../RNTSExample'
|
||||
import RNTSExampleModule from '../RNTSExampleModule'
|
||||
|
||||
const {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React
|
||||
|
||||
|
||||
const styles = StyleSheet.create(
|
||||
{
|
||||
backgroundColorText: {
|
||||
left: 5,
|
||||
backgroundColor: 'rgba(100, 100, 100, 0.3)'
|
||||
},
|
||||
entity: {
|
||||
fontWeight: '500',
|
||||
color: '#527fe4',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
class Entity extends React.Component<any,any> {
|
||||
render() {
|
||||
return (
|
||||
<Text style={styles.entity}>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface AttrTogglerState {
|
||||
fontWeight?: string
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
class AttributeToggler extends React.Component<any, AttrTogglerState> {
|
||||
componentWillMount() {
|
||||
this.setState(
|
||||
{
|
||||
fontWeight: '500',
|
||||
fontSize: 15
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private increaseSize = (): void => {
|
||||
this.setState( {
|
||||
fontSize: this.state.fontSize + 1
|
||||
} )
|
||||
}
|
||||
|
||||
render() {
|
||||
const curStyle: React.TextStyle = { fontSize: this.state.fontSize }
|
||||
return (
|
||||
<Text>
|
||||
<Text style={curStyle}>
|
||||
Tap the controls below to change attributes.
|
||||
</Text>
|
||||
<Text>
|
||||
See how it will even work on{' '}
|
||||
<Text style={curStyle}>
|
||||
this nested text
|
||||
</Text>
|
||||
<Text onPress={this.increaseSize}>
|
||||
{'>> Increase Size <<'}
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
title: '<Text>',
|
||||
description: 'Base component for rendering styled text.',
|
||||
displayName: 'TextExample',
|
||||
examples: [
|
||||
{
|
||||
title: 'Wrap',
|
||||
render: function () {
|
||||
return (
|
||||
<Text>
|
||||
The text should wrap if it goes on multiple lines. See, this is going to
|
||||
the next line.
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Padding',
|
||||
render: function () {
|
||||
return (
|
||||
<Text style={{padding: 10}}>
|
||||
This text is indented by 10px padding on all sides.
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Font Family',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontFamily: 'Cochin'}}>
|
||||
Cochin
|
||||
</Text>
|
||||
<Text style={{fontFamily: 'Cochin', fontWeight: 'bold'}}>
|
||||
Cochin bold
|
||||
</Text>
|
||||
<Text style={{fontFamily: 'Helvetica'}}>
|
||||
Helvetica
|
||||
</Text>
|
||||
<Text style={{fontFamily: 'Helvetica', fontWeight: 'bold'}}>
|
||||
Helvetica bold
|
||||
</Text>
|
||||
<Text style={{fontFamily: 'Verdana'}}>
|
||||
Verdana
|
||||
</Text>
|
||||
<Text style={{fontFamily: 'Verdana', fontWeight: 'bold'}}>
|
||||
Verdana bold
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Font Size',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontSize: 23}}>
|
||||
Size 23
|
||||
</Text>
|
||||
<Text style={{fontSize: 8}}>
|
||||
Size 8
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Color',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{color: 'red'}}>
|
||||
Red color
|
||||
</Text>
|
||||
<Text style={{color: 'blue'}}>
|
||||
Blue color
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Font Weight',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontWeight: '100'}}>
|
||||
Move fast and be ultralight
|
||||
</Text>
|
||||
<Text style={{fontWeight: '200'}}>
|
||||
Move fast and be light
|
||||
</Text>
|
||||
<Text style={{fontWeight: 'normal'}}>
|
||||
Move fast and be normal
|
||||
</Text>
|
||||
<Text style={{fontWeight: 'bold'}}>
|
||||
Move fast and be bold
|
||||
</Text>
|
||||
<Text style={{fontWeight: '900'}}>
|
||||
Move fast and be ultrabold
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Font Style',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{fontStyle: 'normal'}}>
|
||||
Normal text
|
||||
</Text>
|
||||
<Text style={{fontStyle: 'italic'}}>
|
||||
Italic text
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Nested',
|
||||
description: 'Nested text components will inherit the styles of their ' +
|
||||
'parents (only backgroundColor is inherited from non-Text parents). ' +
|
||||
'<Text> only supports other <Text> and raw text (strings) as children.',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text>
|
||||
(Normal text,
|
||||
<Text style={{fontWeight: 'bold'}}>
|
||||
(and bold
|
||||
<Text style={{fontSize: 11, color: '#527fe4'}}>
|
||||
(and tiny inherited bold blue)
|
||||
</Text>
|
||||
)
|
||||
</Text>
|
||||
)
|
||||
</Text>
|
||||
<Text style={{fontSize: 12}}>
|
||||
<Entity>Entity Name</Entity>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Text Align',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{textAlign: 'left'}}>
|
||||
left left left left left left left left left left left left left left left
|
||||
</Text>
|
||||
<Text style={{textAlign: 'center'}}>
|
||||
center center center center center center center center center center center
|
||||
</Text>
|
||||
<Text style={{textAlign: 'right'}}>
|
||||
right right right right right right right right right right right right right
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Spaces',
|
||||
render: function () {
|
||||
return (
|
||||
<Text>
|
||||
A {'generated'} {' '} {'string'} and some     spaces
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Line Height',
|
||||
render: function () {
|
||||
return (
|
||||
<Text>
|
||||
<Text style={{lineHeight: 35}}>
|
||||
A lot of space between the lines of this long passage that should
|
||||
wrap once.
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Empty Text',
|
||||
description: 'It\'s ok to have Text with zero or null children.',
|
||||
render: function () {
|
||||
return (
|
||||
<Text />
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Toggling Attributes',
|
||||
render: (): React.ReactElement<any> => {
|
||||
return <AttributeToggler />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'backgroundColor attribute',
|
||||
description: 'backgroundColor is inherited from all types of views.',
|
||||
render: function () {
|
||||
return (
|
||||
<View style={{backgroundColor: 'yellow'}}>
|
||||
<Text>
|
||||
Yellow background inherited from View parent,
|
||||
<Text style={{backgroundColor: '#ffaaaa'}}>
|
||||
{' '}red background,
|
||||
<Text style={{backgroundColor: '#aaaaff'}}>
|
||||
{' '}blue background,
|
||||
<Text>
|
||||
{' '}inherited blue background,
|
||||
<Text style={{backgroundColor: '#aaffaa'}}>
|
||||
{' '}nested green background.
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'containerBackgroundColor attribute',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<View style={{flexDirection: 'row', height: 85}}>
|
||||
<View style={{backgroundColor: '#ffaaaa', width: 150}}/>
|
||||
<View style={{backgroundColor: '#aaaaff', width: 150}}/>
|
||||
</View>
|
||||
<Text style={[styles.backgroundColorText, {top: -80}]}>
|
||||
Default containerBackgroundColor (inherited) + backgroundColor wash
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.backgroundColorText,
|
||||
{top: -70, containerBackgroundColor: 'transparent'}
|
||||
]}>
|
||||
{"containerBackgroundColor: 'transparent' + backgroundColor wash"}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'numberOfLines attribute',
|
||||
render: function () {
|
||||
return (
|
||||
<View>
|
||||
<Text numberOfLines={1}>
|
||||
Maximum of one line no matter now much I write here. If I keep writing it{"'"}ll just truncate after one line
|
||||
</Text>
|
||||
<Text numberOfLines={2} style={{marginTop: 20}}>
|
||||
Maximum of two lines no matter now much I write here. If I keep writing it{"'"}ll just truncate after two lines
|
||||
</Text>
|
||||
<Text style={{marginTop: 20}}>
|
||||
No maximum lines specified no matter now much I write here. If I keep writing it{"'"}ll just keep going and going
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
} ] as RNTSExample[]
|
||||
} as RNTSExampleModule
|
||||
240
samples/TypeScript/require.tsx
Normal file
240
samples/TypeScript/require.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
// Fixture taken from https://github.com/jcingroup/C551608_Roki/blob/master/Work.WebProj/Scripts/src/tsx/m-parm.tsx
|
||||
|
||||
import $ = require('jquery');
|
||||
import React = require('react');
|
||||
import ReactDOM = require('react-dom');
|
||||
import Moment = require('moment');
|
||||
import ReactBootstrap = require("react-bootstrap");
|
||||
import CommCmpt = require('comm-cmpt');
|
||||
import CommFunc = require('comm-func');
|
||||
|
||||
namespace Parm {
|
||||
interface ParamData {
|
||||
Email?: string;
|
||||
PurchaseTotal?: number;
|
||||
HomoiothermyFee?: number;
|
||||
RefrigerFee?: number;
|
||||
AccountName?: string;
|
||||
BankName?: string;
|
||||
BankCode?: string;
|
||||
AccountNumber?: string;
|
||||
Fee?: number;
|
||||
}
|
||||
export class GridForm extends React.Component<any, { param?: ParamData }>{
|
||||
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
this.queryInitData = this.queryInitData.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.componentDidMount = this.componentDidMount.bind(this);
|
||||
this.setInputValue = this.setInputValue.bind(this);
|
||||
this.render = this.render.bind(this);
|
||||
this.state = {
|
||||
param: {
|
||||
Email: null,
|
||||
PurchaseTotal: 0,
|
||||
HomoiothermyFee: 0,
|
||||
RefrigerFee:0,
|
||||
AccountName: null,
|
||||
BankName: null,
|
||||
BankCode: null,
|
||||
AccountNumber: null,
|
||||
Fee: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
static defaultProps = {
|
||||
apiInitPath: gb_approot + 'Active/ParmData/aj_ParamInit',
|
||||
apiPath: gb_approot + 'api/GetAction/PostParamData'
|
||||
}
|
||||
componentDidMount() {
|
||||
this.queryInitData();
|
||||
}
|
||||
queryInitData() {
|
||||
CommFunc.jqGet(this.props.apiInitPath, {})
|
||||
.done((data, textStatus, jqXHRdata) => {
|
||||
this.setState({ param: data });
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
CommFunc.showAjaxError(errorThrown);
|
||||
});
|
||||
}
|
||||
handleSubmit(e: React.FormEvent) {
|
||||
|
||||
e.preventDefault();
|
||||
CommFunc.jqPost(this.props.apiPath, this.state.param)
|
||||
.done((data, textStatus, jqXHRdata) => {
|
||||
if (data.result) {
|
||||
CommFunc.tosMessage(null, '修改完成', 1);
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
CommFunc.showAjaxError(errorThrown);
|
||||
});
|
||||
return;
|
||||
}
|
||||
handleOnBlur(date) {
|
||||
|
||||
}
|
||||
setInputValue(name: string, e: React.SyntheticEvent) {
|
||||
let input: HTMLInputElement = e.target as HTMLInputElement;
|
||||
let obj = this.state.param;
|
||||
obj[name] = input.value;
|
||||
this.setState({ param: obj });
|
||||
}
|
||||
render() {
|
||||
|
||||
var outHtml: JSX.Element = null;
|
||||
|
||||
let param = this.state.param;
|
||||
let InputDate = CommCmpt.InputDate;
|
||||
|
||||
outHtml = (
|
||||
<div>
|
||||
<ul className="breadcrumb">
|
||||
<li><i className="fa-list-alt"></i>
|
||||
{this.props.menuName}
|
||||
</li>
|
||||
</ul>
|
||||
<h4 className="title"> {this.props.caption} 基本資料維護</h4>
|
||||
<form className="form-horizontal" onSubmit={this.handleSubmit}>
|
||||
<div className="col-xs-12">
|
||||
<div className="item-box">
|
||||
{/*--email--*/}
|
||||
<div className="item-title text-center">
|
||||
<h5>Email信箱設定</h5>
|
||||
</div>
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<ol>
|
||||
<li>多筆信箱請用「<strong className="text-danger">, </strong>」逗號分開。<br />ex.<strong>user1 @demo.com.tw, user2 @demo.com.tw</strong></li>
|
||||
<li>Email 前面可填收件人姓名,用「<strong className="text-danger">: </strong>」冒號分隔姓名和信箱,此項非必要,可省略。<br />ex.<strong>收件人A: user1 @demo.com.tw, 收件人B: user2 @demo.com.tw</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-1 control-label">收件信箱</label>
|
||||
<div className="col-xs-9">
|
||||
<input className="form-control" type="text"
|
||||
value={param.Email}
|
||||
onChange={this.setInputValue.bind(this, 'Email') }
|
||||
maxLength={500}
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
{/*--email end--*/}
|
||||
{/*--shoppingCost--*/}
|
||||
<div className="item-title text-center">
|
||||
<h5>訂單運費設定</h5>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-3 control-label">會員下訂單,當訂單金額少於NT$</label>
|
||||
<div className="col-xs-1">
|
||||
<input className="form-control" type="number"
|
||||
value={param.PurchaseTotal}
|
||||
onChange={this.setInputValue.bind(this, 'PurchaseTotal') }
|
||||
min={0}
|
||||
required/>
|
||||
</div>
|
||||
<label className="col-xs-2 control-label">元時須付常溫運費NT$</label>
|
||||
<div className="col-xs-1">
|
||||
<input className="form-control" type="number"
|
||||
value={param.HomoiothermyFee}
|
||||
onChange={this.setInputValue.bind(this, 'HomoiothermyFee') }
|
||||
min={0}
|
||||
required/>
|
||||
</div>
|
||||
<label className="col-xs-2 control-label">元或冷凍(冷藏)運費NT$</label>
|
||||
<div className="col-xs-1">
|
||||
<input className="form-control" type="number"
|
||||
value={param.RefrigerFee}
|
||||
onChange={this.setInputValue.bind(this, 'RefrigerFee') }
|
||||
min={0}
|
||||
required/>
|
||||
</div>
|
||||
<label className="col-xs-1 control-label">元</label>
|
||||
</div>
|
||||
|
||||
{/*--shoppingCost end--*/}
|
||||
{/*--Payment--*/}
|
||||
<div className="item-title text-center">
|
||||
<h5>付款方式</h5>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-4 control-label">當付款方式選擇『ATM轉帳』時,銀行帳號資料為: </label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-2 control-label">戶名: </label>
|
||||
<div className="col-xs-3">
|
||||
<input className="form-control" type="text"
|
||||
value={param.AccountName}
|
||||
onChange={this.setInputValue.bind(this, 'AccountName') }
|
||||
maxLength={16}
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-2 control-label">銀行: </label>
|
||||
<div className="col-xs-3">
|
||||
<input className="form-control" type="text"
|
||||
value={param.BankName}
|
||||
onChange={this.setInputValue.bind(this, 'BankName') }
|
||||
maxLength={16}
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-2 control-label">代碼: </label>
|
||||
<div className="col-xs-3">
|
||||
<input className="form-control" type="text"
|
||||
value={param.BankCode}
|
||||
onChange={this.setInputValue.bind(this, 'BankCode') }
|
||||
maxLength={5}
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-xs-2 control-label">帳號: </label>
|
||||
<div className="col-xs-3">
|
||||
<input className="form-control" type="text"
|
||||
value={param.AccountNumber}
|
||||
onChange={this.setInputValue.bind(this, 'AccountNumber') }
|
||||
maxLength={16}
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
{/*<div className="form-group">
|
||||
<label className="col-xs-4 control-label">當付款方式選擇『貨到付款』時,須加NT$ </label>
|
||||
<div className="col-xs-1">
|
||||
<input className="form-control" type="number"
|
||||
value={param.Fee}
|
||||
onChange={this.setInputValue.bind(this, 'Fee') }
|
||||
min={0}
|
||||
required/>
|
||||
</div>
|
||||
<label className="control-label">元手續費</label>
|
||||
</div>*/}
|
||||
{/*--Payment end--*/}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="form-action">
|
||||
<div className="col-xs-4 col-xs-offset-5">
|
||||
<button type="submit" className="btn-primary"><i className="fa-check"></i> 儲存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return outHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dom = document.getElementById('page_content');
|
||||
ReactDOM.render(<Parm.GridForm caption={gb_caption} menuName={gb_menuname} iconClass="fa-list-alt" />, dom);
|
||||
863
samples/TypeScript/triple-slash-reference.tsx
Normal file
863
samples/TypeScript/triple-slash-reference.tsx
Normal file
@@ -0,0 +1,863 @@
|
||||
/// <reference path="../DefinitelyTyped/react/react-global.d.ts" />
|
||||
|
||||
// Fixture taken from https://github.com/RyanCavanaugh/koany/blob/master/koany.tsx
|
||||
|
||||
interface Garden {
|
||||
colors: Gardens.RockColor[];
|
||||
shapes: Gardens.RockShape[];
|
||||
}
|
||||
|
||||
namespace Gardens {
|
||||
export enum RockShape {
|
||||
Empty,
|
||||
Circle,
|
||||
Triangle,
|
||||
Square,
|
||||
Max
|
||||
}
|
||||
export const RockShapes = [RockShape.Circle, RockShape.Triangle, RockShape.Square];
|
||||
export const RockShapesAndEmpty = RockShapes.concat(RockShape.Empty);
|
||||
|
||||
export enum RockColor {
|
||||
Empty,
|
||||
White,
|
||||
Red,
|
||||
Black,
|
||||
Max
|
||||
}
|
||||
export const RockColors = [RockColor.White, RockColor.Red, RockColor.Black];
|
||||
export const RockColorsAndEmpty = RockColors.concat(RockColor.Empty);
|
||||
|
||||
export const Size = 9;
|
||||
|
||||
// 012
|
||||
// 345
|
||||
// 678
|
||||
export const adjacencies = [
|
||||
[1, 3], [0, 4, 2], [1, 5],
|
||||
[0, 4, 6], [3, 1, 7, 5], [2, 4, 8],
|
||||
[3, 7], [6, 4, 8], [7, 5]
|
||||
];
|
||||
}
|
||||
|
||||
module Koan {
|
||||
export enum DescribeContext {
|
||||
// every "white stone" is ...
|
||||
Singular,
|
||||
// all "white stones" are
|
||||
Plural,
|
||||
// every stone in the top row is "white"
|
||||
Adjectival
|
||||
}
|
||||
|
||||
export enum PartType {
|
||||
Selector,
|
||||
Aspect
|
||||
}
|
||||
|
||||
export enum StateTestResult {
|
||||
Fail = 0,
|
||||
WeakPass = 1,
|
||||
Pass = 2
|
||||
}
|
||||
|
||||
/// A general format for producing a Statement
|
||||
export interface StatementTemplate<T> {
|
||||
holes: PartType[];
|
||||
describe(args: T): string;
|
||||
test(g: Garden, args: T): StateTestResult;
|
||||
}
|
||||
|
||||
/// A completed rule that can be used to test a Garden
|
||||
export interface ProducedStatement<T> {
|
||||
test(g: Garden): StateTestResult;
|
||||
description: string;
|
||||
children: T;
|
||||
|
||||
hasPassedAndFailed(): boolean;
|
||||
}
|
||||
|
||||
function rnd(max: number) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
function randomColor(): Gardens.RockColor {
|
||||
return Math.floor(Math.random() * (Gardens.RockColor.Max - 1)) + 1
|
||||
}
|
||||
|
||||
function randomShape(): Gardens.RockShape {
|
||||
return Math.floor(Math.random() * (Gardens.RockShape.Max - 1)) + 1
|
||||
}
|
||||
|
||||
/* New Impl Here */
|
||||
interface SelectorSpec<T> {
|
||||
childTypes?: PartType[];
|
||||
precedence: number;
|
||||
weight: number;
|
||||
test(args: T, g: Garden, index: number): string|number|boolean;
|
||||
describe(args: T, context: DescribeContext): string;
|
||||
isAllValues(values: Array<string>|Array<number>): boolean;
|
||||
}
|
||||
|
||||
interface ProducedSelector {
|
||||
test(g: Garden, index: number): string|number|boolean;
|
||||
getDescription(plural: DescribeContext): string;
|
||||
seenAllValues(): boolean;
|
||||
}
|
||||
|
||||
export function buildSelector<T>(spec: SelectorSpec<T>, args: T): ProducedSelector {
|
||||
let seenResults: { [s: string]: boolean;} = {};
|
||||
return {
|
||||
test: (g: Garden, index: number) => {
|
||||
var result = spec.test(args, g, index);
|
||||
seenResults[result + ''] = true;
|
||||
return result;
|
||||
},
|
||||
getDescription: (context) => {
|
||||
return spec.describe(args, context);
|
||||
},
|
||||
seenAllValues: () => {
|
||||
return spec.isAllValues(Object.keys(seenResults));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export var SelectorTemplates: Array<SelectorSpec<{}>> = [];
|
||||
module LetsMakeSomeSelectors {
|
||||
// Is rock
|
||||
SelectorTemplates.push({
|
||||
test: (args, g, i) => g.colors[i] !== Gardens.RockColor.Empty,
|
||||
describe: (args, context) => {
|
||||
switch(context) {
|
||||
case DescribeContext.Plural:
|
||||
return 'Stones';
|
||||
case DescribeContext.Adjectival:
|
||||
return 'not empty';
|
||||
case DescribeContext.Singular:
|
||||
return 'Stone';
|
||||
}
|
||||
},
|
||||
isAllValues: items => items.length === 2,
|
||||
precedence: 0,
|
||||
weight: 1
|
||||
});
|
||||
|
||||
// Is of a certain color and/or shape
|
||||
Gardens.RockColorsAndEmpty.forEach(color => {
|
||||
let colorName = Gardens.RockColor[color];
|
||||
let colorWeight = color === Gardens.RockColor.Empty ? 1 : 0.33;
|
||||
Gardens.RockShapesAndEmpty.forEach(shape => {
|
||||
let shapeName = Gardens.RockShape[shape];
|
||||
let shapeWeight = shape === Gardens.RockShape.Empty ? 1 : 0.33;
|
||||
SelectorTemplates.push({
|
||||
test: (args, g, i) => {
|
||||
if(color === Gardens.RockColor.Empty) {
|
||||
if (shape === Gardens.RockShape.Empty) {
|
||||
return g.colors[i] === Gardens.RockColor.Empty;
|
||||
} else {
|
||||
return g.shapes[i] === shape;
|
||||
}
|
||||
} else {
|
||||
if (shape === Gardens.RockShape.Empty) {
|
||||
return g.colors[i] === color;
|
||||
} else {
|
||||
return g.shapes[i] === shape && g.colors[i] === color;
|
||||
}
|
||||
}
|
||||
},
|
||||
describe: (args, context) => {
|
||||
if(color === Gardens.RockColor.Empty) {
|
||||
if (shape === Gardens.RockShape.Empty) {
|
||||
switch(context) {
|
||||
case DescribeContext.Plural:
|
||||
return 'Empty Cells';
|
||||
case DescribeContext.Adjectival:
|
||||
return 'Empty';
|
||||
case DescribeContext.Singular:
|
||||
return 'Empty Cell';
|
||||
}
|
||||
} else {
|
||||
switch(context) {
|
||||
case DescribeContext.Plural:
|
||||
return shapeName + 's';
|
||||
case DescribeContext.Adjectival:
|
||||
return 'a ' + shapeName;
|
||||
case DescribeContext.Singular:
|
||||
return shapeName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (shape === Gardens.RockShape.Empty) {
|
||||
switch(context) {
|
||||
case DescribeContext.Plural:
|
||||
return colorName + ' Stones';
|
||||
case DescribeContext.Adjectival:
|
||||
return colorName;
|
||||
case DescribeContext.Singular:
|
||||
return colorName + ' Stone';
|
||||
}
|
||||
} else {
|
||||
switch(context) {
|
||||
case DescribeContext.Plural:
|
||||
return colorName + ' ' + shapeName + 's';
|
||||
case DescribeContext.Adjectival:
|
||||
return 'a ' + colorName + ' ' + shapeName;
|
||||
case DescribeContext.Singular:
|
||||
return colorName + ' ' + shapeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isAllValues: items => items.length === 2,
|
||||
precedence: 3,
|
||||
weight: (shapeWeight + colorWeight === 2) ? 0.3 : shapeWeight * colorWeight
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// [?] in the [top|middle|bottom] [row|column]
|
||||
[0, 1, 2].forEach(rowCol => {
|
||||
[true, false].forEach(isRow => {
|
||||
var name = (isRow ? ['top', 'middle', 'bottom'] : ['left', 'middle', 'right'])[rowCol] + ' ' + (isRow ? 'row' : 'column');
|
||||
var spec: SelectorSpec<[ProducedSelector]> = {
|
||||
childTypes: [PartType.Selector],
|
||||
test: (args, g, i) => {
|
||||
var c = isRow ? Math.floor(i / 3) : i % 3;
|
||||
if (c === rowCol) {
|
||||
return args[0].test(g, i);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
describe: (args, plural) => args[0].getDescription(plural) + ' in the ' + name,
|
||||
isAllValues: items => items.length === 2,
|
||||
precedence: 4,
|
||||
weight: 1 / 6
|
||||
};
|
||||
SelectorTemplates.push(spec);
|
||||
});
|
||||
});
|
||||
|
||||
// [?] next to a [?]
|
||||
SelectorTemplates.push({
|
||||
childTypes: [PartType.Selector, PartType.Selector],
|
||||
test: (args, g, i) => {
|
||||
if (args[0].test(g, i)) {
|
||||
return Gardens.adjacencies[i].some(x => !!args[1].test(g, x));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
describe: (args, plural) => {
|
||||
return args[0].getDescription(plural) + ' next to a ' + args[1].getDescription(DescribeContext.Singular);
|
||||
},
|
||||
isAllValues: items => items.length === 2,
|
||||
precedence: 4,
|
||||
weight: 1
|
||||
} as SelectorSpec<[ProducedSelector, ProducedSelector]>);
|
||||
}
|
||||
|
||||
export function buildStatement<T>(s: StatementTemplate<T>, args: T): ProducedStatement<T> {
|
||||
let hasPassed = false;
|
||||
let hasFailed = false;
|
||||
|
||||
let result: ProducedStatement<T> = {
|
||||
children: args,
|
||||
description: s.describe(args),
|
||||
test: (g) => {
|
||||
let r = s.test(g, args);
|
||||
if (r === StateTestResult.Pass) {
|
||||
hasPassed = true;
|
||||
} else if(r === StateTestResult.Fail) {
|
||||
hasFailed = true;
|
||||
}
|
||||
return r;
|
||||
},
|
||||
hasPassedAndFailed: () => {
|
||||
return hasPassed && hasFailed && (args as any as ProducedSelector[]).every(c => c.seenAllValues());
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
export let StatementList: StatementTemplate<any>[] = [];
|
||||
module LetsMakeSomeStatements {
|
||||
// Every [?] is a [?]
|
||||
StatementList.push({
|
||||
holes: [PartType.Selector, PartType.Selector],
|
||||
test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
|
||||
let didAnyTests = false;
|
||||
for (var i = 0; i < Gardens.Size; i++) {
|
||||
if (args[0].test(g, i)) {
|
||||
if(!args[1].test(g, i)) return StateTestResult.Fail;
|
||||
didAnyTests = true;
|
||||
}
|
||||
}
|
||||
return didAnyTests ? StateTestResult.Pass : StateTestResult.WeakPass;
|
||||
},
|
||||
describe: args => {
|
||||
return 'Every ' + args[0].getDescription(DescribeContext.Singular) + ' is ' + args[1].getDescription(DescribeContext.Adjectival);
|
||||
}
|
||||
});
|
||||
|
||||
// There is exactly 1 [?]
|
||||
StatementList.push({
|
||||
holes: [PartType.Selector],
|
||||
test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
|
||||
var count = 0;
|
||||
for (var i = 0; i < Gardens.Size; i++) {
|
||||
if (args[0].test(g, i)) count++;
|
||||
}
|
||||
|
||||
return count === 1 ? StateTestResult.Pass : StateTestResult.Fail;
|
||||
},
|
||||
describe: args => {
|
||||
return 'There is exactly one ' + args[0].description;
|
||||
}
|
||||
});
|
||||
|
||||
// There are more [?] than [?]
|
||||
StatementList.push({
|
||||
holes: [PartType.Selector, PartType.Selector],
|
||||
test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
|
||||
var p1c = 0, p2c = 0;
|
||||
for (var i = 0; i < Gardens.Size; i++) {
|
||||
if (args[0].test(g, i)) p1c++;
|
||||
if (args[1].test(g, i)) p2c++;
|
||||
}
|
||||
if(p1c > p2c && p2c > 0) {
|
||||
return StateTestResult.Pass;
|
||||
} else if(p1c > p2c) {
|
||||
return StateTestResult.WeakPass;
|
||||
} else {
|
||||
return StateTestResult.Fail;
|
||||
}
|
||||
},
|
||||
describe: args => {
|
||||
return 'There are more ' + args[0].descriptionPlural + ' than ' + args[1].descriptionPlural;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function randomElementOf<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
return undefined;
|
||||
} else {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
}
|
||||
|
||||
function randomWeightedElementOf<T extends { weight: number }>(arr: T[]): T {
|
||||
var totalWeight = arr.reduce((acc, v) => acc + v.weight, 0);
|
||||
var rnd = Math.random() * totalWeight;
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
rnd -= arr[i].weight;
|
||||
if (rnd <= 0) return arr[i];
|
||||
}
|
||||
// Got destroyed by floating error, just try again
|
||||
return randomWeightedElementOf(arr);
|
||||
}
|
||||
|
||||
export function buildRandomNewSelector(maxPrecedence = 1000000): ProducedSelector {
|
||||
var choices = SelectorTemplates;
|
||||
|
||||
let initial = randomWeightedElementOf(choices.filter(p => p.precedence <= maxPrecedence));
|
||||
// Fill in the holes
|
||||
if (initial.childTypes) {
|
||||
var fills = initial.childTypes.map(h => {
|
||||
if (h === PartType.Selector) {
|
||||
return buildRandomNewSelector(initial.precedence - 1);
|
||||
} else {
|
||||
throw new Error('Only know how to fill Selector holes')
|
||||
}
|
||||
});
|
||||
return buildSelector(initial, fills);
|
||||
} else {
|
||||
return buildSelector(initial, []);
|
||||
}
|
||||
}
|
||||
|
||||
export function makeEmptyGarden(): Garden {
|
||||
var g = {} as Garden;
|
||||
g.colors = [];
|
||||
g.shapes = [];
|
||||
for (var i = 0; i < Gardens.Size; i++) {
|
||||
g.colors.push(Gardens.RockColor.Empty);
|
||||
g.shapes.push(Gardens.RockShape.Empty);
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
export function gardenToString(g: Garden): string {
|
||||
return g.colors.join('') + g.shapes.join('');
|
||||
}
|
||||
|
||||
export function makeRandomGarden(): Garden {
|
||||
var g = makeEmptyGarden();
|
||||
blitRandomGardenPair(g, g);
|
||||
return g;
|
||||
}
|
||||
|
||||
export function cloneGarden(g: Garden): Garden {
|
||||
var result: Garden = {
|
||||
colors: g.colors.slice(0),
|
||||
shapes: g.shapes.slice(0)
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
export function clearGarden(g: Garden) {
|
||||
for (var i = 0; i < Gardens.Size; i++) {
|
||||
g.colors[i] = Gardens.RockColor.Empty;
|
||||
g.shapes[i] = Gardens.RockShape.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
export function blitRandomGardenPair(g1: Garden, g2: Garden): void {
|
||||
let placeCount = 0;
|
||||
for (var i = 0; i < Gardens.Size; i++) {
|
||||
if (rnd(7) === 0) {
|
||||
g1.colors[i] = g2.colors[i] = randomColor();
|
||||
g1.shapes[i] = g2.shapes[i] = randomShape();
|
||||
} else {
|
||||
placeCount++;
|
||||
g1.colors[i] = g2.colors[i] = Gardens.RockColor.Empty;
|
||||
g1.shapes[i] = g2.shapes[i] = Gardens.RockShape.Empty;
|
||||
}
|
||||
}
|
||||
if (placeCount === 0) blitRandomGardenPair(g1, g2);
|
||||
}
|
||||
|
||||
export function blitNumberedGarden(g: Garden, stoneCount: number, n: number): void {
|
||||
clearGarden(g);
|
||||
|
||||
let cellNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
for (let i = 0; i < stoneCount; i++) {
|
||||
let cellNum = getValue(cellNumbers.length);
|
||||
let cell = cellNumbers.splice(cellNum, 1)[0];
|
||||
g.colors[cell] = getValue(3) + 1;
|
||||
g.shapes[cell] = getValue(3) + 1;
|
||||
}
|
||||
|
||||
function getValue(max: number) {
|
||||
let result = n % max;
|
||||
n = (n - result) / max;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function mutateGarden(g: Garden): void {
|
||||
while (true) {
|
||||
var op = rnd(5);
|
||||
let x = rnd(Gardens.Size);
|
||||
let y = rnd(Gardens.Size);
|
||||
switch (op) {
|
||||
case 0: // Swap two non-identical cells
|
||||
if (g.colors[x] !== g.colors[y] || g.shapes[x] !== g.shapes[y]) {
|
||||
var tmp: any = g.colors[x];
|
||||
g.colors[x] = g.colors[y];
|
||||
g.colors[y] = tmp;
|
||||
tmp = g.shapes[x];
|
||||
g.shapes[x] = g.shapes[y];
|
||||
g.shapes[y] = tmp;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 1: // Add a stone
|
||||
if (g.colors[x] === Gardens.RockColor.Empty) {
|
||||
g.colors[x] = randomColor();
|
||||
g.shapes[x] = randomShape();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 2: // Remove a stone
|
||||
if (g.colors.filter(x => x !== Gardens.RockColor.Empty).length === 1) continue;
|
||||
|
||||
if (g.colors[x] !== Gardens.RockColor.Empty) {
|
||||
g.colors[x] = Gardens.RockColor.Empty;
|
||||
g.shapes[x] = Gardens.RockShape.Empty;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 3: // Change a color
|
||||
let c = randomColor();
|
||||
if (g.colors[x] !== Gardens.RockColor.Empty && g.colors[x] !== c) {
|
||||
g.colors[x] = c;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 4: // Change a shape
|
||||
let s = randomShape();
|
||||
if (g.shapes[x] !== Gardens.RockShape.Empty && g.shapes[x] !== s) {
|
||||
g.shapes[x] = s;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Indexion {
|
||||
sizes: number[];
|
||||
constructor(...sizes: number[]) {
|
||||
this.sizes = sizes;
|
||||
}
|
||||
|
||||
public getValues(index: number): number[] {
|
||||
let result = new Array<number>(this.sizes.length);
|
||||
this.fillValues(index, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public fillValues(index: number, result: number[]): void {
|
||||
for (var i = 0; i < this.sizes.length; i++) {
|
||||
result[i] = index % this.sizes[i];
|
||||
index -= result[i];
|
||||
index /= this.sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
public valuesToIndex(values: number[]): number {
|
||||
var result = 0;
|
||||
var factor = 1;
|
||||
for (var i = 0; i < this.sizes.length; i++) {
|
||||
result += values[i] * this.sizes[i] * factor;
|
||||
factor *= this.sizes[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getAdjacentIndices(index: number): number[][] {
|
||||
var baseline = this.getValues(index);
|
||||
var results: number[][] = [];
|
||||
for (var i = 0; i < this.sizes.length; i++) {
|
||||
if(baseline[i] > 0) {
|
||||
baseline[i]--;
|
||||
results.push(baseline.slice());
|
||||
baseline[i]++;
|
||||
}
|
||||
if(baseline[i] < this.sizes[i] - 1) {
|
||||
baseline[i]++;
|
||||
results.push(baseline.slice());
|
||||
baseline[i]--;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public distance(index1: number, index2: number): number {
|
||||
let delta = 0;
|
||||
for (var i = 0; i < this.sizes.length; i++) {
|
||||
var a = index1 % this.sizes[i];
|
||||
var b = index2 % this.sizes[i];
|
||||
delta += Math.abs(b - a);
|
||||
index1 -= a;
|
||||
index2 -= b;
|
||||
index1 /= this.sizes[i];
|
||||
index2 /= this.sizes[i];
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function makeNewExample() {
|
||||
while (true) {
|
||||
var p1 = Koan.buildSelector(Koan.SelectorTemplates[12], []);
|
||||
var p2 = Koan.buildSelector(Koan.SelectorTemplates[14], []);
|
||||
var test = Koan.buildStatement(Koan.StatementList[0], [p1, p2]);
|
||||
|
||||
var examples: Garden[] = [];
|
||||
|
||||
console.log('Attempt to generate examples for "' + test.description + '"');
|
||||
|
||||
var maxGarden = /*(9 * 9) + (9 * 9 * 9 * 8) + */(9 * 9 * 9 * 8 * 9 * 7);
|
||||
let g = Koan.makeEmptyGarden();
|
||||
let passCount = 0, failCount = 0;
|
||||
let resultLookup: boolean[] = [];
|
||||
let lastResult: boolean = undefined;
|
||||
for (var i = 0; i < maxGarden; i++) {
|
||||
Koan.blitNumberedGarden(g, 3, i);
|
||||
let result = test.test(g);
|
||||
if(result === Koan.StateTestResult.Pass) {
|
||||
resultLookup[i] = true;
|
||||
passCount++;
|
||||
|
||||
if (lastResult !== true && examples.length < 10) examples.push(Koan.cloneGarden(g));
|
||||
lastResult = true;
|
||||
} else if (result === Koan.StateTestResult.Fail) {
|
||||
resultLookup[i] = false;
|
||||
failCount++;
|
||||
|
||||
if (lastResult !== false && examples.length < 10) examples.push(Koan.cloneGarden(g));
|
||||
lastResult = false;
|
||||
}
|
||||
|
||||
if (examples.length === 10) break;
|
||||
}
|
||||
|
||||
console.log('Rule passes ' + passCount + ' and fails ' + failCount);
|
||||
|
||||
/*
|
||||
if (!test.hasPassedAndFailed()) {
|
||||
console.log('Rule has unreachable, contradictory, or tautological clauses');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (passCount === 0 || failCount === 0) {
|
||||
console.log('Rule is always true or always false');
|
||||
continue;
|
||||
}
|
||||
*/
|
||||
|
||||
var h = document.createElement('h2');
|
||||
h.innerText = test.description;
|
||||
document.body.appendChild(h);
|
||||
|
||||
return { test: test, examples: examples };
|
||||
}
|
||||
}
|
||||
|
||||
let list: Garden[] = [];
|
||||
let test: Koan.ProducedStatement<any>;
|
||||
window.onload = function() {
|
||||
let rule = makeNewExample();
|
||||
let garden = Koan.makeRandomGarden();
|
||||
list = rule.examples;
|
||||
test = rule.test;
|
||||
|
||||
function renderList() {
|
||||
function makeGarden(g: Garden, i: number) {
|
||||
return <GardenDisplay
|
||||
garden={g}
|
||||
key={i + Koan.gardenToString(g)}
|
||||
test={test}
|
||||
leftButton='✗'
|
||||
rightButton='✎'
|
||||
onLeftButtonClicked={() => {
|
||||
console.log(list.indexOf(g));
|
||||
list.splice(list.indexOf(g), 1);
|
||||
renderList();
|
||||
}}
|
||||
onRightButtonClicked={() => {
|
||||
garden = g;
|
||||
renderEditor();
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
let gardenList = <div>{list.map(makeGarden)}</div>;
|
||||
React.render(gardenList, document.getElementById('results'));
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
function renderEditor() {
|
||||
i++;
|
||||
let editor = <GardenEditor key={i} test={rule.test} garden={garden} onSaveClicked={(garden) => {
|
||||
list.push(garden);
|
||||
renderList();
|
||||
}} />;
|
||||
React.render(editor, document.getElementById('editor'));
|
||||
}
|
||||
|
||||
renderList();
|
||||
renderEditor();
|
||||
}
|
||||
|
||||
function classNames(nameMap: any): string {
|
||||
return Object.keys(nameMap).filter(k => nameMap[k]).join(' ');
|
||||
}
|
||||
|
||||
interface GardenCellProps extends React.Props<{}> {
|
||||
color: Gardens.RockColor;
|
||||
shape: Gardens.RockShape;
|
||||
index: number;
|
||||
|
||||
movable?: boolean;
|
||||
onEdit?(newColor: Gardens.RockColor, newShape: Gardens.RockShape): void;
|
||||
}
|
||||
interface GardenCellState {
|
||||
isDragging?: boolean;
|
||||
}
|
||||
class GardenCell extends React.Component<GardenCellProps, GardenCellState> {
|
||||
state: GardenCellState = {};
|
||||
ignoreNextEdit = false;
|
||||
|
||||
render() {
|
||||
var classes = ['cell', 'index_' + this.props.index];
|
||||
|
||||
if (this.state.isDragging) {
|
||||
// Render as blank
|
||||
} else {
|
||||
classes.push(Gardens.RockColor[this.props.color], Gardens.RockShape[this.props.shape]);
|
||||
}
|
||||
|
||||
if (this.props.movable) classes.push('movable');
|
||||
let events: React.HTMLAttributes = {
|
||||
onDragStart: (e) => {
|
||||
this.ignoreNextEdit = false;
|
||||
e.dataTransfer.dropEffect = 'copyMove';
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('shape', this.props.shape.toString());
|
||||
e.dataTransfer.setData('color', this.props.color.toString());
|
||||
|
||||
let drag = document.getElementById(getGardenName(this.props.color, this.props.shape));
|
||||
let xfer: any = (e.nativeEvent as DragEvent).dataTransfer;
|
||||
xfer.setDragImage(drag, drag.clientWidth * 0.5, drag.clientHeight * 0.5);
|
||||
|
||||
this.setState({ isDragging: true });
|
||||
},
|
||||
onDragEnter: (e) => {
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.preventDefault();
|
||||
},
|
||||
onDragOver: (e) => {
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.preventDefault();
|
||||
},
|
||||
onDragEnd: (e) => {
|
||||
this.setState({ isDragging: false });
|
||||
if (!this.ignoreNextEdit) {
|
||||
this.props.onEdit && this.props.onEdit(undefined, undefined);
|
||||
}
|
||||
},
|
||||
draggable: true
|
||||
}
|
||||
|
||||
let handleDrop = (event: React.DragEvent) => {
|
||||
if(this.props.onEdit) {
|
||||
if (this.state.isDragging) {
|
||||
// Dragged to self, don't do anything
|
||||
this.ignoreNextEdit = true;
|
||||
} else {
|
||||
let shape: Gardens.RockShape = +event.dataTransfer.getData('shape');
|
||||
let color: Gardens.RockColor = +event.dataTransfer.getData('color');
|
||||
this.props.onEdit(color, shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <span className={classes.join(' ')} onDrop={handleDrop} {...this.props.movable ? events : {}} />;
|
||||
}
|
||||
}
|
||||
|
||||
interface GardenDisplayProps extends React.Props<GardenDisplay> {
|
||||
garden?: Garden;
|
||||
test?: Koan.ProducedStatement<any>;
|
||||
|
||||
leftButton?: string;
|
||||
rightButton?: string;
|
||||
onLeftButtonClicked?(): void;
|
||||
onRightButtonClicked?(): void;
|
||||
|
||||
editable?: boolean;
|
||||
onChanged?(newGarden: Garden): void;
|
||||
}
|
||||
interface GardenDisplayState {
|
||||
garden?: Garden;
|
||||
}
|
||||
class GardenDisplay extends React.Component<GardenDisplayProps, GardenDisplayState> {
|
||||
state = {
|
||||
garden: Koan.cloneGarden(this.props.garden)
|
||||
};
|
||||
|
||||
leftClicked = () => {
|
||||
this.props.onLeftButtonClicked && this.props.onLeftButtonClicked();
|
||||
};
|
||||
|
||||
rightClicked = () => {
|
||||
this.props.onRightButtonClicked && this.props.onRightButtonClicked();
|
||||
};
|
||||
|
||||
render() {
|
||||
let g = this.state.garden;
|
||||
let pass = (this.props.test && this.props.test.test(this.state.garden));
|
||||
|
||||
let classes = {
|
||||
garden: true,
|
||||
unknown: pass === undefined,
|
||||
pass: pass === Koan.StateTestResult.Pass || pass === Koan.StateTestResult.WeakPass,
|
||||
fail: pass === Koan.StateTestResult.Fail,
|
||||
editable: this.props.editable
|
||||
};
|
||||
|
||||
var children = g.colors.map((_, i) => (
|
||||
<GardenCell
|
||||
key={i}
|
||||
color={g.colors[i]}
|
||||
shape={g.shapes[i]}
|
||||
index={i}
|
||||
movable={this.props.editable}
|
||||
onEdit={(newColor, newShape) => {
|
||||
if(this.props.editable) {
|
||||
let newGarden = Koan.cloneGarden(this.state.garden);
|
||||
newGarden.colors[i] = newColor;
|
||||
newGarden.shapes[i] = newShape;
|
||||
this.setState({ garden: newGarden });
|
||||
this.props.onChanged && this.props.onChanged(newGarden);
|
||||
}
|
||||
}}
|
||||
/>));
|
||||
|
||||
return <div className="gardenDisplay">
|
||||
<div className={classNames(classes)}>{children}</div>
|
||||
<span className="infoRow">
|
||||
{this.props.leftButton && <div className="button left" onClick={this.leftClicked}>{this.props.leftButton}</div>}
|
||||
<div className={"passfail " + (pass ? 'pass' : 'fail')}>{pass ? '✓' : '🚫'}</div>
|
||||
{this.props.rightButton && <div className="button right" onClick={this.rightClicked}>{this.props.rightButton}</div>}
|
||||
</span>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
interface GardenEditorProps extends React.Props<GardenEditor> {
|
||||
onSaveClicked?(garden: Garden): void;
|
||||
test?: Koan.ProducedStatement<any>;
|
||||
garden?: Garden;
|
||||
}
|
||||
interface GardenEditorState {
|
||||
garden?: Garden;
|
||||
pass?: boolean;
|
||||
}
|
||||
class GardenEditor extends React.Component<GardenEditorProps, {}> {
|
||||
state = { garden: this.props.garden };
|
||||
|
||||
save = () => {
|
||||
this.props.onSaveClicked && this.props.onSaveClicked(this.state.garden);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div className="editor">
|
||||
<GardenDisplay garden={this.state.garden} test={this.props.test} editable onChanged={g => this.setState({ garden: g }) } />
|
||||
<StonePalette />
|
||||
<div className="button save" onClick={this.save}>{'💾'}</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class StonePalette extends React.Component<{}, {}> {
|
||||
render() {
|
||||
let items: JSX.Element[] = [];
|
||||
Gardens.RockColors.forEach(color => {
|
||||
Gardens.RockShapes.forEach(shape => {
|
||||
let name = getGardenName(color, shape);
|
||||
let extraProps = { id: name, key: name };
|
||||
let index = items.length;
|
||||
items.push(<GardenCell
|
||||
color={color}
|
||||
shape={shape}
|
||||
index={index}
|
||||
movable
|
||||
{...extraProps} />)
|
||||
});
|
||||
});
|
||||
return <div className="palette">{items}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function getGardenName(color: Gardens.RockColor, shape: Gardens.RockShape) {
|
||||
return 'draggable.' + Gardens.RockShape[shape] + '.' + Gardens.RockColor[color];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//@jsx: preserve
|
||||
//@module: amd
|
||||
|
||||
//@filename: react.d.ts
|
||||
declare module JSX {
|
||||
interface Element { }
|
||||
interface IntrinsicElements {
|
||||
}
|
||||
interface ElementAttributesProperty {
|
||||
props;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
foo: string;
|
||||
}
|
||||
|
||||
//@filename: file.tsx
|
||||
export class MyComponent {
|
||||
render() {
|
||||
}
|
||||
|
||||
props: { foo: string; }
|
||||
}
|
||||
|
||||
<MyComponent foo="bar" />; // ok
|
||||
<MyComponent foo={0} />; // should be an error
|
||||
@@ -264,4 +264,11 @@ class TestHeuristcs < Minitest::Test
|
||||
"XML" => all_fixtures("XML", "*.ts")
|
||||
})
|
||||
end
|
||||
|
||||
def test_tsx_by_heuristics
|
||||
assert_heuristics({
|
||||
"TypeScript" => all_fixtures("TypeScript", "*.tsx"),
|
||||
"XML" => all_fixtures("XML", "*.tsx")
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user