From 130b1025de53b24fdb02e4044680003e6bd4d9e2 Mon Sep 17 00:00:00 2001 From: mui-z <93278577+mh-idea@users.noreply.github.com> Date: Sun, 25 Sep 2022 15:09:29 +0900 Subject: [PATCH] Initial Commit --- .github/workflows/swift.yaml | 20 +++ .gitignore | 11 ++ Package.swift | 32 ++++ README.md | 82 +++++++++ .../Controller/Controller.swift | 163 ++++++++++++++++++ .../Parser/ScriptParser.swift | 40 +++++ .../Syntax/NovelSyntax.swift | 38 ++++ .../ControllerTest.swift | 152 ++++++++++++++++ .../ScriptParserTests.swift | 49 ++++++ 9 files changed, 587 insertions(+) create mode 100644 .github/workflows/swift.yaml create mode 100644 .gitignore create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/EffectiveNovelCore/Controller/Controller.swift create mode 100644 Sources/EffectiveNovelCore/Parser/ScriptParser.swift create mode 100644 Sources/EffectiveNovelCore/Syntax/NovelSyntax.swift create mode 100644 Tests/EffectiveNovelCoreTests/ControllerTest.swift create mode 100644 Tests/EffectiveNovelCoreTests/ScriptParserTests.swift diff --git a/.github/workflows/swift.yaml b/.github/workflows/swift.yaml new file mode 100644 index 0000000..99304ae --- /dev/null +++ b/.github/workflows/swift.yaml @@ -0,0 +1,20 @@ +name: Swift + +on: [push] + +jobs: + build: + + runs-on: macos-latest + + steps: + - uses: swift-actions/setup-swift@v1 + with: + swift-version: "5.7.0" + - name: Get swift version + run: swift --version + - uses: actions/checkout@v3 + - name: Build + run: swift build | xcpretty + - name: Run tests + run: swift test | xcpretty \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e639929 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +.idea/ +*.sn \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..8123886 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "EffectiveNovelCore", + platforms: [ + .macOS(.v10_15), + .iOS(.v16) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "EffectiveNovelCore", + targets: ["EffectiveNovelCore"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "EffectiveNovelCore", + dependencies: []), + .testTarget( + name: "EffectiveNovelCoreTests", + dependencies: ["EffectiveNovelCore"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..934f44a --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Effective Novel Core + +This is novel text parse and stream provide package. + +## About +This is novel engine. + +This effectively helps to display the characters of the novel.   + +This lib doesn't include UI layer. +This only provides parse and output stream display event. (I'm going to will link CLI and iOS Example.) + +Also, this lib is not optimized novel game. +Because this doesn't have if functioned, macro, subroutine. + +## Syntax +| command | DisplayEvent | mean | +|------------------|-------------------------|---------------------------------------------------------------------| +| n | `.newline` | newline | +| tw | `.tapWait` | tap wait | +| twn | `tapWaitAndNewline` | tap wait and newline | +| cl | `.clear` | clear | +| delay speed=xxxx | `.delay(speed: Double)` | change delay character displayed speed. speed unit is milliseconds. | +| resetdelay | `.resetDelay` | reset delay speed | +| e | `.end` | stop script novel end point | + +``` +start text[n] + +tap waiting and newline[twn] + +[cl] cleared text. + +very fast stream after this text[delay speed=2][n] + +[resetdelay]reset delay speed.[n] + +end. [e] +``` + +## Usage + +```swift + +// 1. get `NovelController` instance +let controller = NovelController() + +// 2. pass to raw novel text +controller.load(rawText: rawText) + +// 3. start() and listening stream +controller.start() + .sink { event in + switch event { + case .character(let char): + displayCharacter(char) + // and any command handling + } + } + .store(in: &cancellables) + +// (4.) show text until wait tag +controller.showTextUntilWaitTag() + +// (5.) resume tap wait +// If you want to start from any index number, you can use `controller.resume(at: 100)` +controller.resume() + +// (6.) interrupt +controller.interrupt() + + +``` + + + +## Examples +- [ ] CLI novel reader +- [ ] iOS novel reader + +## Todo +- value input diff --git a/Sources/EffectiveNovelCore/Controller/Controller.swift b/Sources/EffectiveNovelCore/Controller/Controller.swift new file mode 100644 index 0000000..916e401 --- /dev/null +++ b/Sources/EffectiveNovelCore/Controller/Controller.swift @@ -0,0 +1,163 @@ +// +// Created by osushi on 2022/09/23. +// + +import Foundation +import Combine + +enum NovelState { + case loadWait, prepare, running, pause +} + +let defaultSpeed: Double = 250 + +public class NovelController { + + public init() { + } + + private var internalOutputStream = PassthroughSubject() + + private var displayEvents: [DisplayEvent] = [] + + private var cancellable: Set = [] + + private(set) var index: Int = 0 + + private(set) var speed: Double = defaultSpeed + + private(set) var state = NovelState.loadWait + + public func load(raw: String) { + let parser = ScriptParser() + state = .prepare + index = 0 + + displayEvents = parser.parse(rawAllString: raw) + } + + public func start() -> AnyPublisher { + switch state { + case .prepare: + state = .running + startLoop() + default: + print("now state is not prepare. now state: \(state)") + } + + return internalOutputStream.eraseToAnyPublisher() + } + + public func interrupt() { + switch state { + case .running, .pause: + reset() + default: + print("now state is not running or pause. now state: \(state)") + } + } + + public func resume() { + switch state { + case .pause: + state = .running + default: + print("now state is not pause. now state: \(state)") + } + } + + public func resume(at resumeIndex: Int) { + switch state { + case .pause: + index = resumeIndex + state = .running + default: + print("now state is not pause. now state: \(state)") + } + } + + public func reset() { + state = .loadWait + displayEvents = [] + index = 0 + } + + public func showTextUntilWaitTag() { + let offset: Int = index + + let checkListRange = displayEvents[offset.. [DisplayEvent] +} + +struct ScriptParser: Parser { + func parse(rawAllString raw: String) -> [DisplayEvent] { + var rawAllString = raw + rawAllString.removeAll(where: { $0 == "\n" }) + + return rawAllString.components(separatedBy: "[") + .filter { !$0.isEmpty } + .map { (raw: $0, isCommandInclude: $0.contains("]")) } + .map { $0.isCommandInclude ? splitCommandIncludingText(raw: $0.raw) : stringToCharacter(string: $0.raw) } + .flatMap { $0 } + } + + // cm] or cm]text + private func splitCommandIncludingText(raw: String) -> [DisplayEvent] { + var result: [DisplayEvent] = [] + let commandAndText = raw.components(separatedBy: "]") + + result.append(DisplayEvent.parseCommand(rawCommand: commandAndText.first!)) + + if let text = commandAndText.last, !text.isEmpty { + result += stringToCharacter(string: text) + } + + return result + } + + private func stringToCharacter(string: String) -> [DisplayEvent] { + string.map { c in DisplayEvent.character(char: c) } + } +} diff --git a/Sources/EffectiveNovelCore/Syntax/NovelSyntax.swift b/Sources/EffectiveNovelCore/Syntax/NovelSyntax.swift new file mode 100644 index 0000000..6d910ce --- /dev/null +++ b/Sources/EffectiveNovelCore/Syntax/NovelSyntax.swift @@ -0,0 +1,38 @@ +// +// Created by osushi on 2022/09/23. +// + +public enum DisplayEvent: Equatable { + case character(char: Character) + case newline + case tapWait + case tapWaitAndNewline + case clear + + case resetDelay + case delay(speed: Double) + + case end + + static func parseCommand(rawCommand: String) -> DisplayEvent { + switch rawCommand { + case "n": + return .newline + case "tw": + return .tapWait + case "twn": + return .tapWaitAndNewline + case "cl": + return .clear + case "resetdelay": + return .resetDelay + case (let command) where command.contains("delay"): + let speed = Double(command.split(separator: "=").last!)! + return .delay(speed: speed) + case "e": + return .end + default: + fatalError(file: #file) + } + } +} diff --git a/Tests/EffectiveNovelCoreTests/ControllerTest.swift b/Tests/EffectiveNovelCoreTests/ControllerTest.swift new file mode 100644 index 0000000..49ed7d5 --- /dev/null +++ b/Tests/EffectiveNovelCoreTests/ControllerTest.swift @@ -0,0 +1,152 @@ +// +// Created by osushi on 2022/09/23. +// + +import XCTest +import Combine +@testable import EffectiveNovelCore + +final class ControllerTest: XCTestCase { + + var cancellables: Set = [] + + override func tearDown() { + cancellables = [] + super.tearDown() + } + + func testLoad() { + let controller = NovelController() + + controller.load(raw: "abc[e]") + + XCTAssertEqual(controller.state, .prepare) + } + + func testStart() { + let expectation = expectation(description: #function) + let controller = NovelController() + + controller.load(raw: "abc[e]") + + expectation.expectedFulfillmentCount = 4 + + let stream = controller.start() + stream + .sink { event in + expectation.fulfill() + } + .store(in: &cancellables) + + XCTAssertEqual(controller.state, .running) + + waitForExpectations(timeout: 3) + XCTAssertEqual(controller.state, .prepare) + } + + func testInterrupt() { + let expectation = expectation(description: #function) + let controller = NovelController() + + controller.load(raw: "abc[e]") + + controller.start() + .sink { event in + expectation.fulfill() + controller.interrupt() + } + .store(in: &cancellables) + + waitForExpectations(timeout: 1) + XCTAssertEqual(controller.state, .loadWait) + } + + func testResume() { + let expectation = expectation(description: #function) + let controller = NovelController() + + expectation.expectedFulfillmentCount = 4 + + controller.load(raw: "a[tw]b[e]") + + controller.start() + .delay(for: 0.001, scheduler: RunLoop.main) + .sink { event in + expectation.fulfill() + + if event == .tapWait { + controller.resume() + } + } + .store(in: &cancellables) + + waitForExpectations(timeout: 2) + + XCTAssertEqual(controller.state, .prepare) + } + + func testResumeSetIndex() { + let expectation = expectation(description: #function) + let controller = NovelController() + + expectation.expectedFulfillmentCount = 8 + + controller.load(raw: "0123[tw]5678[e]") + + controller.start() + .delay(for: 0.001, scheduler: RunLoop.main) + .sink { event in + expectation.fulfill() + + if event == .tapWait { + controller.resume(at: 7) + } + } + .store(in: &cancellables) + + waitForExpectations(timeout: 2) + + XCTAssertEqual(controller.state, .prepare) + } + + func testShowUntilWaitTag() { + let expectation = expectation(description: #function) + let controller = NovelController() + + expectation.expectedFulfillmentCount = 32 + + controller.load(raw: "s012345678901234567890123456789[tw]123[e]") + + controller.start() + .sink { event in + expectation.fulfill() + + if event == .character(char: "s") { + controller.showTextUntilWaitTag() + } + } + .store(in: &cancellables) + + waitForExpectations(timeout: 1) + + XCTAssertEqual(controller.state, .pause) + } + + func testDelay() { + let expectation = expectation(description: #function) + let controller = NovelController() + + expectation.expectedFulfillmentCount = 43 + + controller.load(raw: "s[delay speed=1]0123456789012345678901234567890123456789[e]") + controller.start() + .sink { event in + expectation.fulfill() + } + .store(in: &cancellables) + + waitForExpectations(timeout: 1) + + XCTAssertEqual(controller.state, .prepare) + } +} diff --git a/Tests/EffectiveNovelCoreTests/ScriptParserTests.swift b/Tests/EffectiveNovelCoreTests/ScriptParserTests.swift new file mode 100644 index 0000000..bcb77e9 --- /dev/null +++ b/Tests/EffectiveNovelCoreTests/ScriptParserTests.swift @@ -0,0 +1,49 @@ +import XCTest +@testable import EffectiveNovelCore + +final class ScriptParserTests: XCTestCase { + + func testParseText() { + let parser = ScriptParser() + + XCTAssertEqual(parser.parse(rawAllString: "ab"), [.character(char: "a"), .character(char: "b")]) + + let multiLineText = """ + aa + b + """ + XCTAssertEqual(parser.parse(rawAllString: multiLineText), [.character(char: "a"), .character(char: "a"), .character(char: "b")]) + } + + func testParseAllCommands() { + let parser = ScriptParser() + + XCTAssertEqual(parser.parse(rawAllString: "[n]"), [.newline]) + XCTAssertEqual(parser.parse(rawAllString: "[tw]"), [.tapWait]) + XCTAssertEqual(parser.parse(rawAllString: "[twn]"), [.tapWaitAndNewline]) + XCTAssertEqual(parser.parse(rawAllString: "[cl]"), [.clear]) + XCTAssertEqual(parser.parse(rawAllString: "[delay speed=1000]"), [.delay(speed: 1000)]) + XCTAssertEqual(parser.parse(rawAllString: "[resetdelay]"), [.resetDelay]) + XCTAssertEqual(parser.parse(rawAllString: "[e]"), [.end]) + } + + func testParseEvents() { + let parser = ScriptParser() + + XCTAssertEqual(parser.parse(rawAllString: "[cl]"), [.clear]) + XCTAssertEqual(parser.parse(rawAllString: "[cl][n]"), [.clear, .newline]) + XCTAssertEqual(parser.parse(rawAllString: "s[tw]"), [.character(char: "s"), .tapWait]) + XCTAssertEqual(parser.parse(rawAllString: "[cl]e"), [.clear, .character(char: "e")]) + XCTAssertEqual(parser.parse(rawAllString: "s[cl]e"), [.character(char: "s"), .clear, .character(char: "e")]) + } + + func testContainValueEvent() { + let parser = ScriptParser() + + XCTAssertEqual(parser.parse(rawAllString: "[delay speed=1000]"), [.delay(speed: 1000)]) + XCTAssertEqual(parser.parse(rawAllString: "[delay speed=1000][delay speed=2000]"), [.delay(speed: 1000), .delay(speed: 2000)]) + XCTAssertEqual(parser.parse(rawAllString: "t[delay speed=1000]"), [.character(char: "t"), .delay(speed: 1000)]) + XCTAssertEqual(parser.parse(rawAllString: "[delay speed=1000]t"), [.delay(speed: 1000), .character(char: "t")]) + XCTAssertEqual(parser.parse(rawAllString: "s[delay speed=1000]e"), [.character(char: "s"), .delay(speed: 1000), .character(char: "e")]) + } +} \ No newline at end of file