diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d4432db..656b09aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,16 +11,45 @@ on: jobs: test: runs-on: ubuntu-24.04 + strategy: + matrix: + node-version: ['18', '20', '22'] steps: - name: Checkout code uses: actions/checkout@v3 - - name: Set up Node.js + - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: - node-version: '22' + node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install - name: Run lint run: npm run lint - name: Run tests - run: npm run test \ No newline at end of file + run: npm run test + + typescript: + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '22' + - name: Install dependencies + run: npm install + - name: Validate TypeScript declarations + run: npm run validate-types + - name: Type check TypeScript example + run: npm run type-check + - name: Test TypeScript example compilation and execution + run: | + npx tsc example.ts + node example.js + rm example.js + - name: Verify TypeScript can consume the package + run: | + echo "import osrmTextInstructions = require('./index'); const compiler = osrmTextInstructions('v5'); console.log('TypeScript import works!');" > test-import.ts + npx tsc --noEmit test-import.ts + rm test-import.ts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c326ce..769e9bf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Change Log All notable changes to this project will be documented in this file. For change log formatting, see http://keepachangelog.com/ +## Unreleased +- Add TypeScript support. [#321](https://github.com/Project-OSRM/osrm-text-instructions/pull/321) + ## 0.15.0 2024-03-03 - This package now requires Node 16 and above. [#312](https://github.com/Project-OSRM/osrm-text-instructions/pull/312) diff --git a/README.md b/README.md index 7008df02..13a94cca 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ OSRM Text Instructions is a Node.js library that transforms route data generated * **Customizable**: Flexible options allow you to format and tweak the results to your liking. * **Cross-platform**: A data-driven approach facilitates implementations in other programming languages. OSRM Text Instructions is also available [in Swift and Objective-C](https://github.com/Project-OSRM/osrm-text-instructions.swift/) (for iOS, macOS, tvOS, and watchOS) and [in Java](https://github.com/Project-OSRM/osrm-text-instructions.java/) (for Android and Java SE). * **Well-tested**: A data-driven test suite ensures compatibility across languages and platforms. +* **TypeScript Support**: Full TypeScript type definitions are included for enhanced development experience. ## Usage @@ -24,6 +25,32 @@ response.legs.forEach(function(leg) { }); ``` +### TypeScript Usage + +```typescript +import osrmTextInstructions = require('osrm-text-instructions'); + +const compiler = osrmTextInstructions('v5'); + +const step: osrmTextInstructions.RouteStep = { + maneuver: { + type: 'turn', + modifier: 'left' + }, + name: 'Main Street' +}; + +const options: osrmTextInstructions.CompileOptions = { + legCount: 2, + legIndex: 0, + formatToken: (token: string, value: string) => { + return token === 'way_name' ? `${value}` : value; + } +}; + +const instruction = compiler.compile('en', step, options); +``` + If you are unsure if the user's locale is supported by osrm-text-inustrctions, use [@mapbox/locale-utils](https://github.com/mapbox/locale-utils) for finding the best fitting language. #### Parameters `require('osrm-text-instructions')(version)` diff --git a/example.ts b/example.ts new file mode 100644 index 00000000..fb702884 --- /dev/null +++ b/example.ts @@ -0,0 +1,107 @@ +// Example usage of osrm-text-instructions with TypeScript + +import osrmTextInstructions = require('./index'); + +// Initialize the compiler for OSRM v5 +const compiler = osrmTextInstructions('v5'); + +// Example route step data +const step: osrmTextInstructions.RouteStep = { + maneuver: { + type: 'turn', + modifier: 'left', + bearing_after: 90 + }, + name: 'Main Street', + ref: 'A1', + mode: 'driving', + intersections: [ + { + lanes: [ + { valid: false }, + { valid: true } + ] + } + ] +}; + +// Compile options +const options: osrmTextInstructions.CompileOptions = { + legCount: 2, + legIndex: 0, + classes: ['primary'], + formatToken: (token: string, value: string) => { + if (token === 'way_name') { + return `${value}`; + } + return value; + } +}; + +try { + // Generate instruction + const instruction = compiler.compile('en', step, options); + console.log('Instruction:', instruction); + + // Get way name + const wayName = compiler.getWayName('en', step, options); + console.log('Way name:', wayName); + + // Get direction from degree + const direction = compiler.directionFromDegree('en', 90); + console.log('Direction:', direction); + + // Ordinalize number + const ordinal = compiler.ordinalize('en', 1); + console.log('Ordinal:', ordinal); + + // Generate lane configuration + const laneConfig = compiler.laneConfig(step); + console.log('Lane config:', laneConfig); + + // Capitalize first letter + const capitalized = compiler.capitalizeFirstLetter('en', 'hello world'); + console.log('Capitalized:', capitalized); + + // Access abbreviations + const abbreviations = compiler.abbreviations; + console.log('Available abbreviations:', Object.keys(abbreviations)); + +} catch (error) { + console.error('Error:', error); +} + +// Example with different maneuver types +const departStep: osrmTextInstructions.RouteStep = { + maneuver: { + type: 'depart', + bearing_after: 0 + }, + name: 'Start Street' +}; + +const arriveStep: osrmTextInstructions.RouteStep = { + maneuver: { + type: 'arrive', + modifier: 'right' + }, + name: 'Destination Avenue' +}; + +const roundaboutStep: osrmTextInstructions.RouteStep = { + maneuver: { + type: 'roundabout', + modifier: 'right', + exit: 2 + }, + rotary_name: 'Main Roundabout' +}; + +// Compile different instruction types +const departInstruction = compiler.compile('en', departStep); +const arriveInstruction = compiler.compile('en', arriveStep, { waypointName: 'Home' }); +const roundaboutInstruction = compiler.compile('en', roundaboutStep); + +console.log('Depart:', departInstruction); +console.log('Arrive:', arriveInstruction); +console.log('Roundabout:', roundaboutInstruction); \ No newline at end of file diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..be4349ba --- /dev/null +++ b/index.d.ts @@ -0,0 +1,318 @@ +// Type definitions for osrm-text-instructions +// Project: https://github.com/Project-OSRM/osrm-text-instructions +// Definitions by: Generated + +export = osrmTextInstructions; + +declare function osrmTextInstructions(version: string): osrmTextInstructions.OSRMTextInstructions; + +declare namespace osrmTextInstructions { + /** + * Main interface for OSRM Text Instructions compiler + */ + interface OSRMTextInstructions { + /** + * Capitalizes the first letter of a string according to language rules + * @param language - Language code (e.g., 'en', 'fr', 'de') + * @param string - String to capitalize + * @returns Capitalized string + */ + capitalizeFirstLetter(language: string, string: string): string; + + /** + * Converts a number to its ordinalized form in the given language + * @param language - Language code + * @param number - Number to ordinalize + * @returns Ordinalized string (e.g., "1st", "2nd", "3rd") + */ + ordinalize(language: string, number: number): string; + + /** + * Converts degrees to compass direction in the given language + * @param language - Language code + * @param degree - Bearing in degrees (0-360) + * @returns Compass direction string + */ + directionFromDegree(language: string, degree: number): string; + + /** + * Generates a lane configuration string from step data + * @param step - Route step with intersection data + * @returns Lane configuration string (e.g., "xo", "ox") + */ + laneConfig(step: RouteStep): string; + + /** + * Extracts and formats way name from step data + * @param language - Language code + * @param step - Route step data + * @param options - Formatting options + * @returns Formatted way name + */ + getWayName(language: string, step: RouteStep, options?: CompileOptions): string; + + /** + * Compiles a localized text instruction from a route step + * @param language - Language code + * @param step - Route step including maneuver property + * @param options - Additional compilation options + * @returns Localized text instruction + */ + compile(language: string, step: RouteStep, options?: CompileOptions): string; + + /** + * Applies grammar rules to a name based on language-specific rules + * @param language - Language code + * @param name - Name to grammaticalize + * @param grammar - Grammar rule identifier + * @returns Grammaticalized name + */ + grammarize(language: string, name: string, grammar?: string): string; + + /** + * Tokenizes an instruction string by replacing tokens with values + * @param language - Language code + * @param instruction - Instruction template with tokens + * @param tokens - Token values to replace + * @param options - Tokenization options + * @returns Processed instruction string + */ + tokenize(language: string, instruction: string, tokens: TokenValues, options?: CompileOptions): string; + + /** + * Available abbreviations for all supported languages + */ + abbreviations: LanguageAbbreviations; + } + + /** + * Options for compiling instructions + */ + interface CompileOptions { + /** + * Number of legs in the route + */ + legCount?: number; + + /** + * Zero-based index of the leg containing the step + */ + legIndex?: number; + + /** + * List of road classes (e.g., ['motorway']) + */ + classes?: string[]; + + /** + * Custom name for the leg's destination + */ + waypointName?: string; + + /** + * Function to format token values before insertion + * @param token - Token type (e.g., 'way_name', 'direction') + * @param value - Token value after grammaticalization + * @returns Formatted token value + */ + formatToken?: (token: string, value: string) => string; + } + + /** + * Route step object as defined by OSRM + */ + interface RouteStep { + /** + * Maneuver instruction + */ + maneuver: StepManeuver; + + /** + * Travel mode (e.g., 'driving', 'walking') + */ + mode?: string; + + /** + * Driving side ('left' or 'right') + */ + driving_side?: 'left' | 'right'; + + /** + * Way name + */ + name?: string; + + /** + * Way reference + */ + ref?: string; + + /** + * Destinations (semicolon-separated) + */ + destinations?: string; + + /** + * Exits (semicolon-separated) + */ + exits?: string; + + /** + * Rotary/roundabout name + */ + rotary_name?: string; + + /** + * Junction name + */ + junction_name?: string; + + /** + * Intersections array + */ + intersections?: Intersection[]; + } + + /** + * Step maneuver information + */ + interface StepManeuver { + /** + * Maneuver type + */ + type: ManeuverType; + + /** + * Maneuver modifier + */ + modifier?: ManeuverModifier; + + /** + * Exit number for roundabouts + */ + exit?: number; + + /** + * Bearing after the maneuver in degrees + */ + bearing_after?: number; + } + + /** + * Intersection with lane information + */ + interface Intersection { + /** + * Lane information + */ + lanes?: Lane[]; + } + + /** + * Individual lane information + */ + interface Lane { + /** + * Whether the lane is valid for the maneuver + */ + valid: boolean; + } + + /** + * Token values for instruction templating + */ + interface TokenValues { + way_name?: string; + destination?: string; + exit?: string; + exit_number?: string; + rotary_name?: string; + lane_instruction?: string; + modifier?: string; + direction?: string; + nth?: string; + waypoint_name?: string; + junction_name?: string; + [key: string]: string | undefined; + } + + /** + * Supported maneuver types + */ + type ManeuverType = + | 'turn' + | 'new name' + | 'depart' + | 'arrive' + | 'merge' + | 'on ramp' + | 'off ramp' + | 'fork' + | 'end of road' + | 'continue' + | 'roundabout' + | 'rotary' + | 'roundabout turn' + | 'notification' + | 'exit roundabout' + | 'exit rotary' + | 'use lane'; + + /** + * Supported maneuver modifiers + */ + type ManeuverModifier = + | 'uturn' + | 'sharp right' + | 'right' + | 'slight right' + | 'straight' + | 'slight left' + | 'left' + | 'sharp left'; + + /** + * Supported language codes + */ + type SupportedLanguage = + | 'ar' + | 'ca' + | 'da' + | 'de' + | 'en' + | 'eo' + | 'es' + | 'es-ES' + | 'fi' + | 'fr' + | 'he' + | 'hu' + | 'id' + | 'it' + | 'ja' + | 'ko' + | 'my' + | 'nl' + | 'no' + | 'pl' + | 'pt-BR' + | 'pt-PT' + | 'ro' + | 'ru' + | 'sl' + | 'sv' + | 'tr' + | 'uk' + | 'vi' + | 'yo' + | 'zh-Hans'; + + /** + * Language abbreviations mapping + */ + interface LanguageAbbreviations { + [languageCode: string]: { + [term: string]: string; + }; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0e713c15..730f09f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "BSD-2-Clause", "devDependencies": { "@transifex/api": "^7.1.0", + "@types/node": "^20.0.0", "eslint": "^8.57.0", "mkdirp": "^0.5.1", "request": "^2.79.0", - "tape": "^4.9.2" + "tape": "^4.9.2", + "typescript": "^5.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -161,6 +163,16 @@ "node": ">=16.0.0" } }, + "node_modules/@types/node": { + "version": "20.17.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", + "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1754,6 +1766,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1924,6 +1957,15 @@ "core-js": "^3.35.0" } }, + "@types/node": { + "version": "20.17.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", + "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -3144,6 +3186,18 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 55c023e2..17ecbedc 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "homepage": "http://project-osrm.org", "version": "0.15.0", "main": "./index.js", + "types": "./index.d.ts", "license": "BSD-2-Clause", "bugs": { "url": "https://github.com/Project-OSRM/osrm-text-instructions.js/issues" @@ -15,15 +16,19 @@ }, "devDependencies": { "@transifex/api": "^7.1.0", + "@types/node": "^20.0.0", "eslint": "^8.57.0", "mkdirp": "^0.5.1", "request": "^2.79.0", - "tape": "^4.9.2" + "tape": "^4.9.2", + "typescript": "^5.0.0" }, "scripts": { "lint": "eslint *.js test/*.js scripts/*.js", "pretest": "npm run lint", "test": "tape test/*_test.js", - "transifex": "node scripts/transifex.js" + "transifex": "node scripts/transifex.js", + "type-check": "tsc --noEmit example.ts", + "validate-types": "tsc --noEmit index.d.ts" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b6344a72 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "lib": ["es2017"], + "types": ["node"], + "declaration": true, + "outDir": "./dist", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "*.ts", + "*.d.ts" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +} \ No newline at end of file