Typescript rewrite #9

Merged
rudism merged 3 commits from typescript into master 2019-04-28 17:53:46 -05:00
22 changed files with 410 additions and 1227 deletions
Showing only changes of commit fe8692bd05 - Show all commits

4
.gitignore vendored
View file

@ -1,4 +1,2 @@
*.swp
bin/
build/
node_modules/
.DS_Store

View file

@ -1,72 +0,0 @@
module.exports = (grunt) ->
pkg = require './package.json'
grunt.initConfig
pkg: pkg
coffee:
compile:
options:
join: true
bare: true
files: [
'bin/ficdown.js': ['src/*.coffee']
]
stylus:
compile:
options:
compress: true
expand: true
files: [
'bin/example/player.css': ['src/example/*.styl']
]
uglify:
js:
files:
'bin/ficdown.min.js': [
'bin/ficdown.js'
]
copy:
static:
files: [
expand: true
flatten: true
src: ['src/*.html']
dest: 'bin/'
]
example:
files: [
expand: true
flatten: true
src: ['src/example/*.html', 'src/example/*.png', 'src/example/*.md', 'bin/ficdown.min.js']
dest: 'bin/example/'
]
watch:
js:
files: ['src/**/*.coffee']
tasks: ['build:js', 'copy:example']
css:
files: ['src/**/*.styl']
tasks: ['stylus:compile']
static:
files: ['src/**/*.html','src/**/*.js','src/**/*.md']
tasks: ['copy:static', 'copy:example']
for name of pkg.devDependencies when name.substring(0, 6) is 'grunt-'
grunt.loadNpmTasks name
grunt.registerTask 'build:js', [
'coffee:compile'
'uglify:js'
]
grunt.registerTask 'default', [
'coffee:compile'
'uglify:js'
'stylus:compile'
'copy:static'
'copy:example'
]

1043
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,14 +2,13 @@
"name": "ficdown.js",
"version": "0.9.1",
"description": "A parser and player for Interactive Fiction written in Ficdown",
"dependencies": {},
"dependencies": {
"jquery": "^3.4.0",
"markdown-it": "^8.4.2"
},
"devDependencies": {
"coffee-script": "^1.7.1",
"grunt": "^0.4.5",
"grunt-contrib-coffee": "^0.10.1",
"grunt-contrib-uglify": "^0.5.0",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-contrib-stylus": "^0.17.0"
"@types/jquery": "^3.3.29",
"@types/markdown-it": "0.0.7",
"typescript": "^3.4.5"
}
}

5
src/Model/Action.ts Normal file
View file

@ -0,0 +1,5 @@
export type Action = {
lineNumber: number,
state: string,
description: string,
}

4
src/Model/AltText.ts Normal file
View file

@ -0,0 +1,4 @@
export type AltText = {
passed: string,
failed: string,
}

6
src/Model/Anchor.ts Normal file
View file

@ -0,0 +1,6 @@
export type Anchor = {
anchor: string,
text: string,
href: string,
title: string,
}

14
src/Model/Block.ts Normal file
View file

@ -0,0 +1,14 @@
import { Line } from './';
export enum BlockType {
Action,
Scene,
Story,
}
export type Block = {
lineNumber: number,
type: BlockType,
name: string,
lines: Line[],
}

1
src/Model/BoolHash.ts Normal file
View file

@ -0,0 +1 @@
export type BoolHash = { [name: string]: boolean }

5
src/Model/Href.ts Normal file
View file

@ -0,0 +1,5 @@
export type Href = {
target: string,
conditions: string,
toggles: string,
}

4
src/Model/Line.ts Normal file
View file

@ -0,0 +1,4 @@
export type Line = {
lineNumber: number,
text: string,
}

5
src/Model/ParseError.ts Normal file
View file

@ -0,0 +1,5 @@
export class ParseError extends Error {
constructor(message: string, public lineNumber: number) {
super(message);
}
}

View file

@ -0,0 +1,6 @@
export type PlayerOptions = {
source: string,
id: string,
startText?: string,
endText?: string,
}

10
src/Model/Scene.ts Normal file
View file

@ -0,0 +1,10 @@
import { BoolHash } from './';
export type Scene = {
lineNumber: number,
name: string,
key: string,
description: string,
conditions?: BoolHash,
id?: string;
}

9
src/Model/Story.ts Normal file
View file

@ -0,0 +1,9 @@
import { Scene, Action } from './';
export type Story = {
name: string,
description: string,
firstScene: string,
scenes: { [key: string]: Scene[] },
actions: { [key: string]: Action },
}

11
src/Model/index.ts Normal file
View file

@ -0,0 +1,11 @@
export * from './Action';
export * from './AltText';
export * from './Anchor';
export * from './Block';
export * from './BoolHash';
export * from './Href';
export * from './Line';
export * from './ParseError';
export * from './PlayerOptions';
export * from './Scene';
export * from './Story';

117
src/Parser.ts Normal file
View file

@ -0,0 +1,117 @@
import {
Action,
Block,
BlockType,
BoolHash,
Line,
ParseError,
Scene,
Story,
} from './Model';
import { Util } from './Util';
export class Parser {
public static parse(source: string): Story {
const lines = source.split(/\n|\r\n/);
const blocks = this.extractBlocks(lines);
return this.parseBlocks(blocks);
}
private static getBlockType(hashCount: 1 | 2 | 3): BlockType {
switch(hashCount) {
case 1: return BlockType.Story;
case 2: return BlockType.Scene;
case 3: return BlockType.Action;
}
}
private static extractBlocks(lines: string[]): Block[] {
const blocks: Block[] = [];
let currentBlock: Block | undefined = undefined;
for(let i = 0; i < lines.length; i++) {
const match = lines[i].match(/^(#{1,3})\s+([^#].*)$/);
if(match) {
if(currentBlock) blocks.push(currentBlock);
currentBlock = {
lineNumber: i,
type: this.getBlockType(<1 | 2 | 3>match[1].length),
name: match[2],
lines: [],
};
} else if(currentBlock) {
currentBlock.lines.push({ lineNumber: i, text: lines[i] });
}
}
if(currentBlock) blocks.push(currentBlock);
return blocks;
}
private static parseBlocks(blocks: Block[]): Story {
let storyBlock: Block | undefined =
blocks.find(b => b.type === BlockType.Story);
if(!storyBlock) throw new ParseError('no story block', 0);
const storyName = Util.matchAnchor(storyBlock.name);
if(!storyName) throw new ParseError('no story name', storyBlock.lineNumber);
const storyHref = Util.matchHref(storyName.href);
if(!storyHref) throw new ParseError('no link to first scene', storyBlock.lineNumber);
const story: Story = {
name: storyName.text,
description: Util.trimText(storyBlock.lines.join("\n")),
firstScene: storyHref.target,
scenes: {},
actions: {},
};
for(let block of blocks) {
switch(block.type) {
case BlockType.Scene:
const scene = this.blockToScene(block);
if(!story.scenes[scene.key]) story.scenes[scene.key] = [];
story.scenes[scene.key].push(scene);
break;
case BlockType.Action:
const action = this.blockToAction(block);
story.actions[action.state] = action;
break;
}
}
return story;
}
private static blockToScene(block: Block): Scene {
const sceneName = Util.matchAnchor(block.name);
let name: string | undefined = undefined;
let key: string;
let conditions: BoolHash | undefined = undefined;
if(sceneName) {
name = sceneName.title
? Util.trimText(sceneName.title)
: Util.trimText(sceneName.text);
key = Util.normalize(sceneName.text);
const href = Util.matchHref(sceneName.href);
if(href && href.conditions) conditions =
Util.toBoolHash(href.conditions.split('&'));
} else {
name = Util.trimText(block.name);
key = Util.normalize(block.name);
}
return {
name,
key,
conditions,
description: Util.trimText(block.lines.join("\n")),
lineNumber: block.lineNumber,
};
}
private static blockToAction(block: Block): Action {
return {
state: Util.normalize(block.name),
description: Util.trimText(block.lines.join("\n")),
lineNumber: block.lineNumber,
};
}
}

30
src/Player.ts Normal file
View file

@ -0,0 +1,30 @@
import {
BoolHash,
PlayerOptions,
Scene,
Story,
} from './Model';
import { Parser } from './Parser';
import * as Markdown from 'markdown-it';
import * as $ from 'jquery';
export class Player {
private static converter = new Markdown();
private container: JQuery<any>;
private playerState: BoolHash = {};
private visitedScenes: BoolHash = {};
private currentScene?: Scene;
private moveCounter: number = 0;
private story: Story;
constructor(private options: PlayerOptions) {
this.story = Parser.parse(options.source);
let i = 0;
for(let [key, scenes] of Object.entries(this.story.scenes)) {
for(let scene of scenes) scene.id = `#s${ ++i }`;
}
this.container = $(`#${ options.id }`);
this.container.addClass('ficdown').data('player', this);
}
}

97
src/Util.ts Normal file
View file

@ -0,0 +1,97 @@
import { Anchor, Href, BoolHash, AltText } from './Model';
export class Util {
private static regexLib: { [name: string]: RegExp } = {
anchors: /(\[((?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[\])*\])*\])*\])*\])*\])*)\]\([ ]*((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\(\))*\))*\))*\))*\))*\))*)[ ]*((['"])(.*?)\5[ ]*)?\))/gm,
href: /^(?:\/([a-zA-Z](?:-?[a-zA-Z0-9])*))?(?:\?((?:!?[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:&!?[a-zA-Z](?:-?[a-zA-Z0-9])*)*)?)?(?:#((?:[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:\+[a-zA-Z](?:-?[a-zA-Z0-9])*)*))?$/,
trim: /^\s+|\s+$/g,
altText: /^((?:[^|\\]|\\.)*)(?:\|((?:[^|\\]|\\.)+))?$/,
escapeChar: /\\(?=[^\\])/g,
emptyListItem: /^[ ]*-\s*([\r\n]+|$)/gm,
};
private static matchToAnchor(match: RegExpExecArray): Anchor {
const result: Anchor = {
anchor: match[1],
text: match[2],
href: match[3],
title: match[6],
};
if(result.href.indexOf('"') === 0) {
result.title = result.href.substring(1, result.href.length - 1);
result.href = '';
}
return result;
}
public static matchAnchor(text: string): Anchor | undefined {
const match = this.regexLib.anchors.exec(text);
if(match) return this.matchToAnchor(match);
}
public static matchAnchors(text: string): Anchor[] {
let match: RegExpExecArray | null;
const anchors: Anchor[] = [];
while(match = this.regexLib.anchors.exec(text)) {
anchors.push(this.matchToAnchor(match));
}
return anchors;
}
public static matchHref(text: string): Href | undefined {
const match = this.regexLib.href.exec(text);
if(match) {
return {
target: match[1],
conditions: match[2],
toggles: match[3],
};
}
}
public static trimText(text: string): string {
return text.replace(this.regexLib.trim, '');
}
public static normalize(text: string): string {
return text.toLowerCase().replace(/^\W+|\W+$/g, '').replace(/\W+/g, '-');
}
public static toBoolHash(names: string[]): BoolHash | undefined {
if(names) {
const hash: BoolHash = {};
for(let name of names) {
if(name.indexOf('!') === 0) {
hash[name.substring(1, name.length)] = false;
} else {
hash[name] = true;
}
}
return hash;
}
}
public static conditionsMet(state: BoolHash, conditions: BoolHash): boolean {
for(let [cond, val] of Object.entries(conditions)) {
if((val && !state[cond]) || (!val && state[cond])) {
return false;
}
}
return true;
}
public static splitAltText(text: string): AltText | undefined {
const match = this.regexLib.altText.exec(text);
if(match) {
return {
passed: match[0],
failed: match[1],
};
}
}
}

View file

@ -1,76 +0,0 @@
parseText = (text) ->
lines = text.split /\n|\r\n/
blocks = extractBlocks lines
story = parseBlocks blocks
getBlockType = (hashCount) ->
switch hashCount
when 1 then 'story'
when 2 then 'scene'
when 3 then 'action'
extractBlocks = (lines) ->
blocks = []
currentBlock = null
for line in lines
match = line.match /^(#{1,3})\s+([^#].*)$/
if match?
if currentBlock != null
blocks.push currentBlock
currentBlock =
type: getBlockType match[1].length
name: match[2]
lines: []
else
currentBlock.lines.push line
if currentBlock != null
blocks.push currentBlock
return blocks
parseBlocks = (blocks) ->
storyBlock = null
for block in blocks
if block.type == 'story'
storyBlock = block
break
storyName = matchAnchor storyBlock.name
storyHref = matchHref storyName.href
story =
name: storyName.text
description: trimText storyBlock.lines.join "\n"
firstScene: storyHref.target
scenes: {}
actions: {}
for block in blocks
switch block.type
when 'scene'
scene = blockToScene block
if !story.scenes[scene.key]
story.scenes[scene.key] = []
story.scenes[scene.key].push scene
when 'action'
action = blockToAction block
story.actions[action.state] = action
return story
blockToScene = (block) ->
sceneName = matchAnchor block.name
if sceneName?
title = if sceneName.title? then trimText sceneName.title else trimText sceneName.text
key = normalize sceneName.text
href = matchHref sceneName.href
if href?.conditions?
conditions = toBoolHash href.conditions.split '&'
else
title = trimText block.name
key = normalize block.name
scene =
name: if title != '' then title else null
key: key
description: trimText block.lines.join "\n"
conditions: if conditions? then conditions else null
blockToAction = (block) ->
action =
state: normalize block.name
description: trimText block.lines.join "\n"

View file

@ -1,89 +0,0 @@
regexLib =
anchors: /(\[((?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[\])*\])*\])*\])*\])*\])*)\]\([ ]*((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\(\))*\))*\))*\))*\))*\))*)[ ]*((['"])(.*?)\5[ ]*)?\))/gm
href: /^(?:\/([a-zA-Z](?:-?[a-zA-Z0-9])*))?(?:\?((?:!?[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:&!?[a-zA-Z](?:-?[a-zA-Z0-9])*)*)?)?(?:#((?:[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:\+[a-zA-Z](?:-?[a-zA-Z0-9])*)*))?$/
trim: /^\s+|\s+$/g
altText: /^((?:[^|\\]|\\.)*)(?:\|((?:[^|\\]|\\.)+))?$/
escapeChar: /\\(?=[^\\])/g
emptyListItem: /^[ ]*-\s*([\r\n]+|$)/gm
matchAnchor = (text) ->
re = new RegExp regexLib.anchors
match = re.exec text
if match?
result =
anchor: match[1]
text: match[2]
href: match[3]
title: match[6]
if result.href.indexOf('"') == 0
result.title = result.href.substring 1, result.href.length - 1
result.href = ''
return result
return match
matchAnchors = (text) ->
re = new RegExp regexLib.anchors
anchors = []
while match = re.exec text
anchor =
anchor: match[1]
text: match[2]
href: match[3]
title: match[6]
if anchor.href.indexOf('"') == 0
anchor.title = anchor.href.substring 1, anchor.href.length - 1
anchor.href = ''
anchors.push anchor
return anchors
trimText = (text) ->
text.replace regexLib.trim, ''
matchHref = (href) ->
re = new RegExp regexLib.href
match = re.exec href
if match?
result =
target: match[1]
conditions: match[2]
toggles: match[3]
return result
return match
normalize = (text) ->
text.toLowerCase().replace(/^\W+|\W+$/g, '').replace /\W+/g, '-'
toBoolHash = (names) ->
if !names?
return null
hash = {}
for name in names
if name.indexOf('!') == 0
hash[name.substring 1, name.length] = false
else
hash[name] = true
return hash
conditionsMet = (state, conditions) ->
met = true
for cond, val of conditions
if (val and !state[cond]) or (!val and state[cond])
met = false
break
return met
splitAltText = (text) ->
re = new RegExp regexLib.altText
match = re.exec text
if match?
result =
passed: match[1]
failed: match[2]
return result
return

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": [ "es2017" ],
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./build",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strict": true,
"sourceMap": true
}
}