Просмотр исходного кода

Refactor and add specs for Scanner/MarkdownParser

Chris Watson 5 лет назад
Родитель
Сommit
18a8b47c9e

+ 1 - 0
.gitignore

@@ -9,6 +9,7 @@
 /gramjs/tl/AllTLObjects.js
 /gramjs/errors/RPCErrorList.js
 /dist/
+/coverage/
 
 # User session
 *.session

+ 2 - 2
__tests__/AES.spec.js → __tests__/crypto/AES.spec.js

@@ -1,5 +1,5 @@
-const AES = require('../gramjs/crypto/AES')
-const AESModeCTR = require('../gramjs/crypto/AESCTR')
+const AES = require('../../gramjs/crypto/AES')
+const AESModeCTR = require('../../gramjs/crypto/AESCTR')
 describe('IGE encrypt function', () => {
     test('it should return 4a657a834edc2956ec95b2a42ec8c1f2d1f0a6028ac26fd830ed23855574b4e69dd1a2be2ba18a53a49b879b2' +
         '45e1065e14b6e8ac5ba9b24befaff3209b77b5f', () => {

+ 1 - 1
__tests__/calcKey.spec.js → __tests__/crypto/calcKey.spec.js

@@ -1,4 +1,4 @@
-const MTProtoState = require('../gramjs/network/MTProtoState')
+const MTProtoState = require('../../gramjs/network/MTProtoState')
 
 describe('calcKey function', () => {
     test('it should return 0x93355e3f1f50529b6fb93eaf97f29b69c16345f53621e9d45cd9a11ddfbebac9 and' +

+ 1 - 1
__tests__/factorizator.spec.js → __tests__/crypto/factorizator.spec.js

@@ -1,4 +1,4 @@
-const Factorizator = require('../gramjs/crypto/Factorizator')
+const Factorizator = require('../../gramjs/crypto/Factorizator')
 
 describe('calcKey function', () => {
     test('it should return 0x20a13b25e1726bfc', () => {

+ 1 - 1
__tests__/readBuffer.spec.js → __tests__/crypto/readBuffer.spec.js

@@ -1,4 +1,4 @@
-const Helpers = require('../gramjs/Helpers')
+const Helpers = require('../../gramjs/Helpers')
 
 describe('readBufferFromBigInt 8 bytes function', () => {
     test('it should return 0x20a13b25e1726bfc', () => {

+ 95 - 0
__tests__/extensions/Markdown.spec.js

@@ -0,0 +1,95 @@
+const { MarkdownParser } = require('../../gramjs/extensions/Markdown')
+const types = require('../../gramjs/tl/types')
+
+describe('MarkdownParser', () => {
+    test('it should construct a new MarkdownParser', () => {
+        const parser = new MarkdownParser('Hello world')
+        expect(parser.text).toEqual('')
+        expect(parser.entities).toEqual([])
+    })
+
+    describe('.parse', () => {
+        test('it should parse bold entities', () => {
+            const parser = new MarkdownParser('Hello **world**')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityBold)
+        })
+
+        test('it should parse italic entities', () => {
+            const parser = new MarkdownParser('Hello __world__')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic)
+        })
+
+        test('it should parse code entities', () => {
+            const parser = new MarkdownParser('Hello `world`')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityCode)
+        })
+
+        test('it should parse pre entities', () => {
+            const parser = new MarkdownParser('Hello ```world```')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityPre)
+        })
+
+        test('it should parse strike entities', () => {
+            const parser = new MarkdownParser('Hello ~~world~~')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityStrike)
+        })
+
+        test('it should parse link entities', () => {
+            const parser = new MarkdownParser('Hello [world](https://hello.world)')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityTextUrl)
+            expect(entities[0].url).toEqual('https://hello.world')
+        })
+
+        test('it should not parse nested entities', () => {
+            const parser = new MarkdownParser('Hello **__world__**')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello __world__')
+            expect(entities.length).toEqual(1)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityBold)
+        })
+
+        test('it should parse multiple entities', () => {
+            const parser = new MarkdownParser('__Hello__ **world**')
+            const [text, entities] = parser.parse()
+            expect(text).toEqual('Hello world')
+            expect(entities.length).toEqual(2)
+            expect(entities[0]).toBeInstanceOf(types.MessageEntityItalic)
+            expect(entities[1]).toBeInstanceOf(types.MessageEntityBold)
+        })
+    })
+
+    describe('.unparse', () => {
+        test('it should create a markdown string from raw text and entities', () => {
+            const unparsed = '**hello** __hello__ ~~hello~~ `hello` ```hello``` [hello](https://hello.world)'
+            const strippedText = 'hello hello hello hello hello hello'
+            const rawEntities = [
+                new types.MessageEntityBold({ offset: 0, length: 5 }),
+                new types.MessageEntityItalic({ offset: 6, length: 5 }),
+                new types.MessageEntityStrike({ offset: 12, length: 5 }),
+                new types.MessageEntityCode({ offset: 18, length: 5 }),
+                new types.MessageEntityPre({ offset: 24, length: 5 }),
+                new types.MessageEntityTextUrl({ offset: 30, length: 5, url: 'https://hello.world' }),
+            ]
+            const text = MarkdownParser.unparse(strippedText, rawEntities)
+            expect(text).toEqual(unparsed)
+        })
+    })
+})

+ 100 - 0
__tests__/extensions/Scanner.spec.js

@@ -0,0 +1,100 @@
+const Scanner = require('../../gramjs/extensions/Scanner')
+
+const helloScanner = new Scanner('Hello world')
+
+describe('Scanner', () => {
+    beforeEach(() => helloScanner.reset())
+
+    test('it should construct a new Scanner', () => {
+        expect(helloScanner.str).toEqual('Hello world')
+        expect(helloScanner.pos).toEqual(0)
+        expect(helloScanner.lastMatch).toBeNull()
+    })
+
+    describe('.chr', () => {
+        test('it should return the character at the current pos', () => {
+            expect(helloScanner.chr).toEqual('H')
+        })
+    })
+
+    describe('.peek', () => {
+        test('it should return the character at the current pos', () => {
+            expect(helloScanner.peek()).toEqual('H')
+        })
+
+        test('it should return the next n characters', () => {
+            expect(helloScanner.peek(3)).toEqual('Hel')
+            expect(helloScanner.peek(5)).toEqual('Hello')
+        })
+    })
+
+    describe('.consume', () => {
+        test('it should consume the current character', () => {
+            const char = helloScanner.consume()
+            expect(char).toEqual('H')
+            expect(helloScanner.pos).toEqual(1)
+        })
+
+        test('it should consume the next n characters', () => {
+            const chars = helloScanner.consume(5)
+            expect(chars).toEqual('Hello')
+            expect(helloScanner.pos).toEqual(5)
+        })
+    })
+
+    describe('.reverse', () => {
+        test('it should set pos back n characters', () => {
+            helloScanner.consume(5)
+            helloScanner.reverse(5)
+            expect(helloScanner.pos).toEqual(0)
+        })
+
+        test('it should not go back further than 0', () => {
+            helloScanner.reverse(10)
+            expect(helloScanner.pos).toEqual(0)
+        })
+    })
+
+    describe('.scanUntil', () => {
+        test('it should scan the string for a regular expression starting at the current pos', () => {
+            helloScanner.scanUntil(/w/)
+            expect(helloScanner.pos).toEqual(6)
+        })
+
+        test('it should do nothing if the pattern is not found', () => {
+            helloScanner.scanUntil(/G/)
+            expect(helloScanner.pos).toEqual(0)
+        })
+    })
+
+    describe('.rest', () => {
+        test('it should return the unconsumed input', () => {
+            helloScanner.consume(6)
+            expect(helloScanner.rest).toEqual('world')
+        })
+    })
+
+    describe('.reset', () => {
+        test('it should reset the pos to 0', () => {
+            helloScanner.consume(5)
+            helloScanner.reset()
+            expect(helloScanner.pos).toEqual(0)
+        })
+    })
+
+    describe('.eof', () => {
+        test('it should return true if the scanner has reached the end of the input', () => {
+            expect(helloScanner.eof()).toBe(false)
+            helloScanner.consume(11)
+            expect(helloScanner.eof()).toBe(true)
+        })
+    })
+
+    describe('.bof', () => {
+        test('it should return true if pos is 0', () => {
+            expect(helloScanner.bof()).toBe(true)
+            helloScanner.consume(11)
+            expect(helloScanner.bof()).toBe(false)
+        })
+    })
+})

+ 8 - 11
gramjs/extensions/Markdown.js

@@ -11,7 +11,7 @@ const URL_RE = /\[([\S\s]+?)\]\((.+?)\)/
 const DELIMITERS = {
     'MessageEntityBold': '**',
     'MessageEntityItalic': '__',
-    'MessageEntityCode': '``',
+    'MessageEntityCode': '`',
     'MessageEntityPre': '```',
     'MessageEntityStrike': '~~',
 }
@@ -23,10 +23,6 @@ class MarkdownParser extends Scanner {
         this.entities = []
     }
 
-    get textPos() {
-        return this.text.length - 1
-    }
-
     parse() {
         // Do a little reset
         this.text = ''
@@ -49,8 +45,8 @@ class MarkdownParser extends Scanner {
             case '`':
                 if (this.peek(3) == '```') {
                     if (this.parseEntity(MessageEntityPre, '```')) break
-                } else if (this.peek(2) == '``') {
-                    if (this.parseEntity(MessageEntityCode, '``')) break
+                } else if (this.peek(1) == '`') {
+                    if (this.parseEntity(MessageEntityCode, '`')) break
                 }
             case '[':
                 if (this.parseURL()) break
@@ -71,15 +67,15 @@ class MarkdownParser extends Scanner {
         for (const entity of entities) {
             const s = entity.offset
             const e = entity.offset + entity.length
-            const delimiter = DELIMITERS[typeof(entity)]
+            const delimiter = DELIMITERS[entity.constructor.name]
             if (delimiter) {
                 insertAt.push([s, delimiter])
                 insertAt.push([e, delimiter])
             } else {
                 let url = null
-
                 if (entity instanceof MessageEntityTextUrl) {
                     url = entity.url
+                    console.log(url)
                 } else if (entity instanceof MessageEntityMentionName) {
                     url = `tg://user?id=${entity.userId}`
                 }
@@ -108,7 +104,7 @@ class MarkdownParser extends Scanner {
     parseEntity(EntityType, delimiter) {
         // The offset for this entity should be the end of the
         // text string
-        const offset = this.textPos
+        const offset = this.text.length
 
         // Consume the delimiter
         this.consume(delimiter.length)
@@ -140,11 +136,12 @@ class MarkdownParser extends Scanner {
 
         const [full, txt, url] = match
         const len = full.length
+        const offset = this.text.length
 
         this.text += txt
 
         const entity = new MessageEntityTextUrl({
-            offset: this.pos,
+            offset: offset,
             length: txt.length,
             url: url,
         })

+ 9 - 3
gramjs/extensions/Scanner.js

@@ -15,7 +15,7 @@ class Scanner {
 
     reverse(n = 1) {
         const pos = this.pos - n
-        return pos < 0 ? 0 : pos
+        this.pos = pos < 0 ? 0 : pos
     }
 
     consume(n = 1) {
@@ -23,7 +23,13 @@ class Scanner {
     }
 
     scanUntil(re, consumeMatch = false) {
-        const match = this.lastMatch = this.rest.match(re)
+        let match
+        try {
+            match = this.lastMatch = this.rest.match(re)
+        } catch {
+            match = null
+        }
+
         if (!match) return null
 
         let len = match.index
@@ -33,7 +39,7 @@ class Scanner {
     }
 
     get rest() {
-        return this.str.slice(this.pos, this.str.length)
+        return this.str.slice(this.pos, this.str.length) || null
     }
 
     reset() {

+ 132 - 132
jest.config.js

@@ -2,187 +2,187 @@
 // https://jestjs.io/docs/en/configuration.html
 
 module.exports = {
-  // All imported modules in your tests should be mocked automatically
-  // automock: false,
+    // All imported modules in your tests should be mocked automatically
+    // automock: false,
 
-  // Stop running tests after `n` failures
-  // bail: 0,
+    // Stop running tests after `n` failures
+    // bail: 0,
 
-  // Respect "browser" field in package.json when resolving modules
-  // browser: false,
+    // Respect "browser" field in package.json when resolving modules
+    // browser: false,
 
-  // The directory where Jest should store its cached dependency information
-  // cacheDirectory: "C:\\Users\\painor\\AppData\\Local\\Temp\\jest",
+    // The directory where Jest should store its cached dependency information
+    // cacheDirectory: "C:\\Users\\painor\\AppData\\Local\\Temp\\jest",
 
-  // Automatically clear mock calls and instances between every test
-  clearMocks: true,
+    // Automatically clear mock calls and instances between every test
+    clearMocks: true,
 
-  // Indicates whether the coverage information should be collected while executing the test
-  // collectCoverage: false,
+    // Indicates whether the coverage information should be collected while executing the test
+    // collectCoverage: false,
 
-  // An array of glob patterns indicating a set of files for which coverage information should be collected
-  // collectCoverageFrom: null,
+    // An array of glob patterns indicating a set of files for which coverage information should be collected
+    // collectCoverageFrom: null,
 
-  // The directory where Jest should output its coverage files
-  coverageDirectory: "coverage",
+    // The directory where Jest should output its coverage files
+    coverageDirectory: 'coverage',
 
-  // An array of regexp pattern strings used to skip coverage collection
-  // coveragePathIgnorePatterns: [
-  //   "\\\\node_modules\\\\"
-  // ],
+    // An array of regexp pattern strings used to skip coverage collection
+    // coveragePathIgnorePatterns: [
+    //   "\\\\node_modules\\\\"
+    // ],
 
-  // A list of reporter names that Jest uses when writing coverage reports
-  // coverageReporters: [
-  //   "json",
-  //   "text",
-  //   "lcov",
-  //   "clover"
-  // ],
+    // A list of reporter names that Jest uses when writing coverage reports
+    // coverageReporters: [
+    //   "json",
+    //   "text",
+    //   "lcov",
+    //   "clover"
+    // ],
 
-  // An object that configures minimum threshold enforcement for coverage results
-  // coverageThreshold: null,
+    // An object that configures minimum threshold enforcement for coverage results
+    // coverageThreshold: null,
 
-  // A path to a custom dependency extractor
-  // dependencyExtractor: null,
+    // A path to a custom dependency extractor
+    // dependencyExtractor: null,
 
-  // Make calling deprecated APIs throw helpful error messages
-  // errorOnDeprecated: false,
+    // Make calling deprecated APIs throw helpful error messages
+    // errorOnDeprecated: false,
 
-  // Force coverage collection from ignored files using an array of glob patterns
-  // forceCoverageMatch: [],
+    // Force coverage collection from ignored files using an array of glob patterns
+    // forceCoverageMatch: [],
 
-  // A path to a module which exports an async function that is triggered once before all test suites
-  // globalSetup: null,
+    // A path to a module which exports an async function that is triggered once before all test suites
+    // globalSetup: null,
 
-  // A path to a module which exports an async function that is triggered once after all test suites
-  // globalTeardown: null,
+    // A path to a module which exports an async function that is triggered once after all test suites
+    // globalTeardown: null,
 
-  // A set of global variables that need to be available in all test environments
-  // globals: {},
+    // A set of global variables that need to be available in all test environments
+    // globals: {},
 
-  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
-  // maxWorkers: "50%",
+    // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
+    // maxWorkers: "50%",
 
-  // An array of directory names to be searched recursively up from the requiring module's location
-  // moduleDirectories: [
-  //   "node_modules"
-  // ],
+    // An array of directory names to be searched recursively up from the requiring module's location
+    // moduleDirectories: [
+    //   "node_modules"
+    // ],
 
-  // An array of file extensions your modules use
-  // moduleFileExtensions: [
-  //   "js",
-  //   "json",
-  //   "jsx",
-  //   "ts",
-  //   "tsx",
-  //   "node"
-  // ],
+    // An array of file extensions your modules use
+    // moduleFileExtensions: [
+    //   "js",
+    //   "json",
+    //   "jsx",
+    //   "ts",
+    //   "tsx",
+    //   "node"
+    // ],
 
-  // A map from regular expressions to module names that allow to stub out resources with a single module
-  // moduleNameMapper: {},
+    // A map from regular expressions to module names that allow to stub out resources with a single module
+    // moduleNameMapper: {},
 
-  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
-  // modulePathIgnorePatterns: [],
+    // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
+    // modulePathIgnorePatterns: [],
 
-  // Activates notifications for test results
-  // notify: false,
+    // Activates notifications for test results
+    // notify: false,
 
-  // An enum that specifies notification mode. Requires { notify: true }
-  // notifyMode: "failure-change",
+    // An enum that specifies notification mode. Requires { notify: true }
+    // notifyMode: "failure-change",
 
-  // A preset that is used as a base for Jest's configuration
-  // preset: null,
+    // A preset that is used as a base for Jest's configuration
+    // preset: null,
 
-  // Run tests from one or more projects
-  // projects: null,
+    // Run tests from one or more projects
+    // projects: null,
 
-  // Use this configuration option to add custom reporters to Jest
-  // reporters: undefined,
+    // Use this configuration option to add custom reporters to Jest
+    // reporters: undefined,
 
-  // Automatically reset mock state between every test
-  // resetMocks: false,
+    // Automatically reset mock state between every test
+    // resetMocks: false,
 
-  // Reset the module registry before running each individual test
-  // resetModules: false,
+    // Reset the module registry before running each individual test
+    // resetModules: false,
 
-  // A path to a custom resolver
-  // resolver: null,
+    // A path to a custom resolver
+    // resolver: null,
 
-  // Automatically restore mock state between every test
-  // restoreMocks: false,
+    // Automatically restore mock state between every test
+    // restoreMocks: false,
 
-  // The root directory that Jest should scan for tests and modules within
-  // rootDir: null,
+    // The root directory that Jest should scan for tests and modules within
+    // rootDir: null,
 
-  // A list of paths to directories that Jest should use to search for files in
-  // roots: [
-  //   "<rootDir>"
-  // ],
+    // A list of paths to directories that Jest should use to search for files in
+    // roots: [
+    //   "<rootDir>"
+    // ],
 
-  // Allows you to use a custom runner instead of Jest's default test runner
-  // runner: "jest-runner",
+    // Allows you to use a custom runner instead of Jest's default test runner
+    // runner: "jest-runner",
 
-  // The paths to modules that run some code to configure or set up the testing environment before each test
-  // setupFiles: [],
+    // The paths to modules that run some code to configure or set up the testing environment before each test
+    // setupFiles: [],
 
-  // A list of paths to modules that run some code to configure or set up the testing framework before each test
-  // setupFilesAfterEnv: [],
+    // A list of paths to modules that run some code to configure or set up the testing framework before each test
+    // setupFilesAfterEnv: [],
 
-  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
-  // snapshotSerializers: [],
+    // A list of paths to snapshot serializer modules Jest should use for snapshot testing
+    // snapshotSerializers: [],
 
-  // The test environment that will be used for testing
-  testEnvironment: "node",
+    // The test environment that will be used for testing
+    testEnvironment: 'node',
 
-  // Options that will be passed to the testEnvironment
-  // testEnvironmentOptions: {},
+    // Options that will be passed to the testEnvironment
+    // testEnvironmentOptions: {},
 
-  // Adds a location field to test results
-  // testLocationInResults: false,
+    // Adds a location field to test results
+    // testLocationInResults: false,
 
-  // The glob patterns Jest uses to detect test files
-  // testMatch: [
-  //   "**/__tests__/**/*.[jt]s?(x)",
-  //   "**/?(*.)+(spec|test).[tj]s?(x)"
-  // ],
+    // The glob patterns Jest uses to detect test files
+    // testMatch: [
+    //   "**/__tests__/**/*.[jt]s?(x)",
+    //   "**/?(*.)+(spec|test).[tj]s?(x)"
+    // ],
 
-  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
-  // testPathIgnorePatterns: [
-  //   "\\\\node_modules\\\\"
-  // ],
+    // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
+    // testPathIgnorePatterns: [
+    //   "\\\\node_modules\\\\"
+    // ],
 
-  // The regexp pattern or array of patterns that Jest uses to detect test files
-  // testRegex: [],
+    // The regexp pattern or array of patterns that Jest uses to detect test files
+    // testRegex: [],
 
-  // This option allows the use of a custom results processor
-  // testResultsProcessor: null,
+    // This option allows the use of a custom results processor
+    // testResultsProcessor: null,
 
-  // This option allows use of a custom test runner
-  // testRunner: "jasmine2",
+    // This option allows use of a custom test runner
+    // testRunner: "jasmine2",
 
-  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
-  // testURL: "http://localhost",
+    // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
+    // testURL: "http://localhost",
 
-  // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
-  // timers: "real",
+    // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
+    // timers: "real",
 
-  // A map from regular expressions to paths to transformers
-  // transform: null,
+    // A map from regular expressions to paths to transformers
+    // transform: null,
 
-  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
-  // transformIgnorePatterns: [
-  //   "\\\\node_modules\\\\"
-  // ],
+    // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
+    // transformIgnorePatterns: [
+    //   "\\\\node_modules\\\\"
+    // ],
 
-  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
-  // unmockedModulePathPatterns: undefined,
+    // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
+    // unmockedModulePathPatterns: undefined,
 
-  // Indicates whether each individual test should be reported during the run
-  // verbose: null,
+    // Indicates whether each individual test should be reported during the run
+    // verbose: null,
 
-  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
-  // watchPathIgnorePatterns: [],
+    // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
+    // watchPathIgnorePatterns: [],
 
-  // Whether to use watchman for file crawling
-  // watchman: true,
-};
+    // Whether to use watchman for file crawling
+    // watchman: true,
+}

Разница между файлами не показана из-за своего большого размера
+ 1287 - 257
package-lock.json


Некоторые файлы не были показаны из-за большого количества измененных файлов