Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@
"max-len": "off",
"max-lines": "off",
"max-nested-callbacks": "error",
"max-params": "error",
"max-params": [
"error",
{
"max": 4
}
],
"max-statements": "off",
"max-statements-per-line": "off",
"multiline-ternary": "off",
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Change Log
All notable changes to this project will be documented in this file. For change log formatting, see http://keepachangelog.com/

## master

- Added a `formatToken` option to `compile`, `getWayName`, and `tokenize` that allows you to manipulate any token value after any grammar or capitalization rules are applied but before the value is inserted into the instruction. [#170](https://github.com/Project-OSRM/osrm-text-instructions/pull/170)
- Removed the `options` parameter to this module, including the `tokenizedInstruction` hook. Use the `formatToken` option instead. [#170](https://github.com/Project-OSRM/osrm-text-instructions/pull/170)

## 0.9.0 2017-10-05

- Added `getBestMatchingLanguage` for determining the closest available language. Pass a user locale into this method before passing the return value into `compile`. [#168](https://github.com/Project-OSRM/osrm-text-instructions/pull/168)
Expand Down
20 changes: 17 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,34 @@ response.legs.forEach(function(leg) {
});
```

#### Parameters `require('osrm-text-instructions')(version, options)`
#### Parameters `require('osrm-text-instructions')(version)`

parameter | required? | values | description
---|----|----|---
`version` | required | `v5` | Major OSRM version
`options.hooks.tokenizedInstruction` | optional | `function(instruction)` | A function to change the raw instruction string before tokens are replaced. Useful to inject custom markup for tokens

#### Parameters `compile(language, step, options)`

parameter | required? | values | description
---|----|----|---
`language` | required | `en` `de` `zh-Hans` `fr` `nl` `ru` [and more](https://github.com/Project-OSRM/osrm-text-instructions/tree/master/languages/translations/) | Compiling instructions for the selected language code.
`step` | required | [OSRM route step object](https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#routestep-object) | The RouteStep as it comes out of OSRM
`options` | optional | Object with 2 keys: `legIndex` and `legCount`, both having integer values. Used for giving instructions for arriving at waypoints.
`options` | optional | Object | See [below](#options)

##### Options

key | type | description
---|----|----|---
`legCount` | integer | Number of legs in the route
`legIndex` | integer | Zero-based index of the leg containing the step; together with `legIndex`, this option determines whether an arrival instruction indicates which waypoint the user has arrived at
`formatToken` | function | Function that formats the given token value after grammaticalization and capitalization but before the value is inserted into the instruction string; useful for wrapping tokens in markup

`formatToken` takes two parameters:

* `token`: A string that indicates the kind of token, such as `way_name` or `direction`
* `value`: A grammatical string for this token, capitalized if the token appears at the beginning of the instruction

and returns a string.

### Development
#### Architecture
Expand Down
44 changes: 25 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ var languages = require('./languages');
var instructions = languages.instructions;
var grammars = languages.grammars;

module.exports = function(version, _options) {
var opts = {};
opts.hooks = {};
opts.hooks.tokenizedInstruction = ((_options || {}).hooks || {}).tokenizedInstruction;

module.exports = function(version) {
Object.keys(instructions).forEach(function(code) {
if (!instructions[code][version]) { throw 'invalid version ' + version + ': ' + code + ' not supported'; }
});
Expand Down Expand Up @@ -98,7 +94,7 @@ module.exports = function(version, _options) {
wayName = this.tokenize(language, phrase, {
name: name,
ref: ref
});
}, options);
} else if (name && ref && wayMotorway && (/\d/).test(ref)) {
wayName = ref;
} else if (!name && ref) {
Expand Down Expand Up @@ -184,10 +180,6 @@ module.exports = function(version, _options) {
instruction = instructionObject.default;
}

if (opts.hooks.tokenizedInstruction) {
instruction = opts.hooks.tokenizedInstruction(instruction);
}

var nthWaypoint = options && options.legIndex >= 0 && options.legIndex !== options.legCount - 1 ? this.ordinalize(language, options.legIndex + 1) : '';

// Replace tokens
Expand All @@ -204,7 +196,7 @@ module.exports = function(version, _options) {
'nth': nthWaypoint
};

return this.tokenize(language, instruction, replaceTokens);
return this.tokenize(language, instruction, replaceTokens, options);
},
grammarize: function(language, name, grammar) {
if (!language) throw new Error('No language code provided');
Expand All @@ -226,22 +218,36 @@ module.exports = function(version, _options) {

return name;
},
tokenize: function(language, instruction, tokens) {
tokenize: function(language, instruction, tokens, options) {
if (!language) throw new Error('No language code provided');
// Keep this function context to use in inline function below (no arrow functions in ES4)
var that = this;
var output = instruction.replace(/\{(\w+):?(\w+)?\}/g, function(token, tag, grammar) {
var name = tokens[tag];
if (typeof name !== 'undefined') {
return that.grammarize(language, name, grammar);
}
var startedWithToken = false;
var output = instruction.replace(/\{(\w+)(?::(\w+))?\}/g, function(token, tag, grammar, offset) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, non-capturing group 👍

var value = tokens[tag];

// Return unknown token unchanged
return token;
if (typeof value === 'undefined') {
return token;
}

value = that.grammarize(language, value, grammar);

// If this token appears at the beginning of the instruction, capitalize it.
if (offset === 0 && instructions[language].meta.capitalizeFirstLetter) {
startedWithToken = true;
value = that.capitalizeFirstLetter(language, value);
}

if (options && options.formatToken) {
value = options.formatToken(tag, value);
}

return value;
})
.replace(/ {2}/g, ' '); // remove excess spaces

if (instructions[language].meta.capitalizeFirstLetter) {
if (!startedWithToken && instructions[language].meta.capitalizeFirstLetter) {
return this.capitalizeFirstLetter(language, output);
}

Expand Down
61 changes: 42 additions & 19 deletions test/index_test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint max-lines: "off" */
var path = require('path');
var fs = require('fs');
var tape = require('tape');
Expand Down Expand Up @@ -34,6 +35,47 @@ tape.test('v5 tokenize', function(assert) {
});
assert.equal(missingSecond, 'Can osrm {second}', 'does not replace tokens which are not provided');

var formatsTokens = v5Compiler.tokenize('en', 'Take me {destination}, {way_name}', {
destination: 'home',
'way_name': 'Country Road'
}, {
formatToken: function (token, value) {
if (token === 'destination') {
return '<prosody rate="slow">' + value + '</prosody>';
}
if (token === 'name' || token === 'way_name' || token === 'rotary_name') {
return value.replace('Road', '<prosody rate="slow">Road</prosody>');
}

return value;
}
});
assert.equal(formatsTokens, 'Take me <prosody rate="slow">home</prosody>, Country <prosody rate="slow">Road</prosody>',
'Formats tokens');

var capitalizesTokens = v5Compiler.tokenize('en', '{modifier} turns are prohibited here', {
modifier: 'left'
}, {
formatToken: function (token, value) {
if (token === 'modifier') {
return '<strong>' + value + '</strong>';
}

return value;
}
});
assert.equal(capitalizesTokens, '<strong>Left</strong> turns are prohibited here',
'Capitalizes tokens before formatting');

var formatsGrammaticalTokens = v5Compiler.tokenize('ru', 'Плавно поверните налево на {way_name:accusative}', {
'way_name': 'Бармалеева улица'
}, {
formatToken: function (token, value) {
return token === 'way_name' ? value.toLocaleUpperCase('ru') : value;
}
});
assert.equal(formatsGrammaticalTokens, 'Плавно поверните налево на БАРМАЛЕЕВУ УЛИЦУ',
'Formats tokens after grammaticalization but before insertion');

assert.end();
});
Expand Down Expand Up @@ -285,25 +327,6 @@ tape.test('v5 compile', function(t) {
t.end();
});

t.test('respects options.instructionStringHook', function(assert) {
var v5Compiler = compiler('v5', {
hooks: {
tokenizedInstruction: function(instruction) {
return instruction.replace('{way_name}', '<blink>{way_name}</blink>');
}
}
});

assert.equal(v5Compiler.compile('en', {
maneuver: {
type: 'turn',
modifier: 'left'
},
name: 'Way Name'
}), 'Turn left onto <blink>Way Name</blink>');
assert.end();
});

t.test('fixtures match generated instructions', function(assert) {
// pre-load instructions
var version = 'v5';
Expand Down