初始提交
CodeQL / Analyze (push) Has been cancelled
Details
CodeQL / Analyze (push) Has been cancelled
Details
This commit is contained in:
commit
e344a3b581
|
@ -0,0 +1,4 @@
|
|||
languages:
|
||||
JavaScript: true
|
||||
exclude_paths:
|
||||
- "build/*"
|
|
@ -0,0 +1,7 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_size = 2
|
||||
indent_style = space
|
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
|
@ -0,0 +1,147 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
---
|
||||
name: "Checkin"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: "Build & Test"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node:
|
||||
- lts/*
|
||||
- node
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Setup node"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
|
||||
- name: "npm ci"
|
||||
run: npm ci
|
||||
|
||||
- name: "Build"
|
||||
run: npm run build
|
||||
|
||||
- name: "Linters"
|
||||
run: npm run lint
|
||||
|
||||
- name: "Node Unit Tests"
|
||||
run: npm run test-unit
|
||||
|
||||
- name: "Node Acceptance Tests"
|
||||
run: npm run test-acceptance
|
||||
|
||||
# Disabled for the moment, I can't get sauce labs to work
|
||||
# - name: "Browser Unit Tests"
|
||||
# env:
|
||||
# SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}}
|
||||
# SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}}
|
||||
# run: npm run test-browser
|
||||
|
||||
- name: "Coverage Push"
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
flag-name: unit-${{ matrix.node }}
|
||||
parallel: true
|
||||
|
||||
- name: "Artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.node == 'node'
|
||||
with:
|
||||
name: distribution
|
||||
path: dist/*.js
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
ghpages:
|
||||
name: "GH Pages"
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Setup node"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: npm
|
||||
|
||||
- name: "npm ci"
|
||||
run: npm ci
|
||||
|
||||
- name: "GH Pages Prepare"
|
||||
run: npm run ghpages
|
||||
|
||||
- name: "GH Pages Push"
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
publish_dir: docs
|
||||
|
||||
timezones:
|
||||
name: "Timezones"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Setup node"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: npm
|
||||
|
||||
- name: "npm ci"
|
||||
run: npm ci
|
||||
|
||||
- name: "Download tzdb"
|
||||
id: tzdb
|
||||
run: |
|
||||
export TZDB_VERSION=$(node tools/scriptutils.js tzdb-version)
|
||||
mkdir -p tools/tzdb
|
||||
cd tools/tzdb
|
||||
wget "https://data.iana.org/time-zones/releases/tzdata${TZDB_VERSION}.tar.gz" -O - | tar xz
|
||||
|
||||
- name: "Build vzic"
|
||||
run: |
|
||||
git clone https://github.com/libical/vzic tools/vzic
|
||||
cd tools/vzic
|
||||
make TZID_PREFIX="" OLSON_DIR="$(readlink -f ../tzdb)"
|
||||
|
||||
- name: "Run vzic"
|
||||
run: tools/vzic/vzic --olson-dir tools/tzdb --output-dir tools/tzdb/zoneinfo
|
||||
|
||||
- name: "Create zones"
|
||||
run: |
|
||||
mkdir -p dist
|
||||
node tools/scriptutils.js generate-zones tools/tzdb > dist/ical.timezones.js
|
||||
|
||||
- name: "Artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: timezones
|
||||
path: dist/ical.timezones.js
|
||||
|
||||
finish:
|
||||
needs: [build-and-test, timezones]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel-finished: true
|
|
@ -0,0 +1,47 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '**/*.js'
|
||||
schedule:
|
||||
- cron: '22 18 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: "javascript"
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
name: Remove needinfo label
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.comment.author_association != 'OWNER' &&
|
||||
github.event.comment.author_association != 'COLLABORATOR'
|
||||
steps:
|
||||
- name: Remove needinfo label
|
||||
uses: octokit/request-action@v2.x
|
||||
continue-on-error: true
|
||||
with:
|
||||
route: DELETE /repos/:repository/issues/:issue/labels/:label
|
||||
repository: ${{ github.repository }}
|
||||
issue: ${{ github.event.issue.number }}
|
||||
label: needinfo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: Close old issues with the needinfo tag
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close old issues with the needinfo tag
|
||||
uses: dwieeb/needs-reply@v2
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-label: needinfo
|
||||
close-message: >
|
||||
It looks like we haven't heard back on this issue, therefore we are
|
||||
closing this issue. If this problem persists in the latest version
|
||||
of ical.js, please re-open this issue.
|
|
@ -0,0 +1,53 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
---
|
||||
|
||||
name: "Publish"
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Setup Node"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: node
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: "npm ci"
|
||||
run: npm ci
|
||||
|
||||
- name: "Build"
|
||||
run: npm run build
|
||||
|
||||
- name: "Linters"
|
||||
run: npm run lint
|
||||
|
||||
- name: "Node Unit Tests"
|
||||
run: npm run test-unit
|
||||
|
||||
- name: "Node Acceptance Tests"
|
||||
run: npm run test-acceptance
|
||||
|
||||
- name: "Attach release assets"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release upload ${{ github.event.release.tag_name }} \
|
||||
dist/ical.js \
|
||||
dist/ical.min.js \
|
||||
dist/ical.es5.cjs \
|
||||
dist/ical.es5.min.cjs
|
||||
|
||||
- name: "npm publish"
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
bower_components
|
||||
*.pyc
|
||||
dist/
|
||||
docs/api/
|
||||
docs/validator.html
|
||||
docs/recur-tester.html
|
||||
tools/vzic/
|
||||
tools/tzdb/
|
||||
tools/libical/
|
||||
tools/jsdoc-symbols-temp.json
|
||||
coverage/
|
|
@ -0,0 +1,55 @@
|
|||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.3.0] - 2018-11-09
|
||||
|
||||
## [1.2.2] - 2016-07-20
|
||||
|
||||
## [1.2.1] - 2016-04-14
|
||||
|
||||
## [1.2.0] - 2016-04-13
|
||||
|
||||
## [1.1.2] - 2015-07-07
|
||||
|
||||
## [1.1.1] - 2015-06-01
|
||||
|
||||
## [1.1.0] - 2015-06-01
|
||||
|
||||
## [1.0.4] - 2015-05-23
|
||||
|
||||
## [1.0.3] - 2015-05-22
|
||||
|
||||
## [1.0.2] - 2015-05-22
|
||||
|
||||
## [1.0.1] - 2015-05-22
|
||||
|
||||
## [1.0.0] - 2015-05-22
|
||||
|
||||
## [0.0.6] - 2014-09-08
|
||||
|
||||
## [0.0.5] - 2014-09-03
|
||||
|
||||
## [0.0.4] - 2014-09-03
|
||||
|
||||
## [0.0.3] - 2014-09-03
|
||||
|
||||
## 0.0.1 - 2014-05-23
|
||||
|
||||
[Unreleased]: https://github.com/kewisch/ical.js/compare/v1.3.0...HEAD
|
||||
[1.3.0]: https://github.com/kewisch/ical.js/compare/v1.2.2...v1.3.0
|
||||
[1.2.2]: https://github.com/kewisch/ical.js/compare/v1.2.1...v1.2.2
|
||||
[1.2.1]: https://github.com/kewisch/ical.js/compare/v1.2.0...v1.2.1
|
||||
[1.2.0]: https://github.com/kewisch/ical.js/compare/v1.1.2...v1.2.0
|
||||
[1.1.2]: https://github.com/kewisch/ical.js/compare/v1.1.1...v1.1.2
|
||||
[1.1.1]: https://github.com/kewisch/ical.js/compare/v1.1.0...v1.1.1
|
||||
[1.1.0]: https://github.com/kewisch/ical.js/compare/v1.0.4...v1.1.0
|
||||
[1.0.4]: https://github.com/kewisch/ical.js/compare/v1.0.3...v1.0.4
|
||||
[1.0.3]: https://github.com/kewisch/ical.js/compare/v1.0.2...v1.0.3
|
||||
[1.0.2]: https://github.com/kewisch/ical.js/compare/v1.0.1...v1.0.2
|
||||
[1.0.1]: https://github.com/kewisch/ical.js/compare/v1.0.0...v1.0.1
|
||||
[1.0.0]: https://github.com/kewisch/ical.js/compare/v0.0.6...v1.0.0
|
||||
[0.0.6]: https://github.com/kewisch/ical.js/compare/v0.0.5...v0.0.6
|
||||
[0.0.5]: https://github.com/kewisch/ical.js/compare/v0.0.4...v0.0.5
|
||||
[0.0.4]: https://github.com/kewisch/ical.js/compare/v0.0.3...v0.0.4
|
||||
[0.0.3]: https://github.com/kewisch/ical.js/compare/v0.0.1...v0.0.3
|
|
@ -0,0 +1,15 @@
|
|||
# Community Participation Guidelines
|
||||
|
||||
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
|
||||
For more details, please read the
|
||||
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
|
||||
|
||||
## How to Report
|
||||
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
|
||||
|
||||
<!--
|
||||
## Project Specific Etiquette
|
||||
|
||||
In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
|
||||
Please update for your project.
|
||||
-->
|
|
@ -0,0 +1,46 @@
|
|||
Woohoo, a new contributor!
|
||||
==========================
|
||||
Thank you so much for looking into ical.js. With your work you are doing good by making it easier to
|
||||
process calendar data on the web.
|
||||
|
||||
To give you a feeling about what you are dealing with, ical.js was originally created as a
|
||||
replacement for [libical], meant to be used in [Lightning], the calendaring extension to
|
||||
Thunderbird. Using binary components in Mozilla extensions often leads to compatibility issues so a
|
||||
pure JavaScript implementation was needed. It was also used in the Firefox OS calendaring
|
||||
application.
|
||||
|
||||
Work on the library prompted creating some standards around it. One of them is jCal ([rfc7265]), an
|
||||
alternative text format for iCalendar data using JSON. The other document is jCard ([rfc7095]),
|
||||
which is the counterpart for vCard data.
|
||||
|
||||
Pull Requests
|
||||
-------------
|
||||
In general we are happy about any form of contribution to ical.js. Note however that since the
|
||||
library is used in at least one larger projects, drastic changes to the API should be discussed in
|
||||
an issue beforehand. If you have a bug fix that doesn't affect the API or just adds methods and you
|
||||
don't want to waste time discussing it, feel free to just send a pull request and we'll see.
|
||||
|
||||
Also, you should check for linter errors and run the tests using `npm run lint` `npm run test`.
|
||||
There are also performance tests and browser tests if you want to be thourough.
|
||||
|
||||
Currently the team working on ical.js consists of a very small number of voluntary contributors. If
|
||||
you don't get a reply in a timely manner please don't feel turned down. If you are getting impatient
|
||||
with us, go ahead and send one or more reminders via email or comment.
|
||||
|
||||
License
|
||||
-------
|
||||
ical.js is licensed under the [Mozilla Public License], version 2.0.
|
||||
|
||||
Last words
|
||||
----------
|
||||
If you have any questions please don't hesitate to get in touch. You can leave a comment on an
|
||||
issue, send [@kewisch] an email, or for ad-hoc questions contact `Fallen` on [chat.mozilla.org].
|
||||
|
||||
[libical]: https://github.com/libical/libical/
|
||||
[Lightning]: http://www.mozilla.org/projects/calendar/
|
||||
[rfc7095]: https://tools.ietf.org/html/rfc7095
|
||||
[rfc7265]: https://tools.ietf.org/html/rfc7265
|
||||
[running tests]: https://github.com/kewisch/ical.js/wiki/Running-Tests
|
||||
[chat.mozilla.org]: https://chat.mozilla.org/
|
||||
[@kewisch]: https://github.com/kewisch/
|
||||
[Mozilla Public License]: https://www.mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,120 @@
|
|||
# ical.js - Javascript parser for iCalendar, jCal, vCard, jCard.
|
||||
|
||||
This is a library to parse the formats defined in the following rfcs and their extensions:
|
||||
* [rfc 5545](http://tools.ietf.org/html/rfc5545) (iCalendar)
|
||||
* [rfc7265](http://tools.ietf.org/html/rfc7265) (jCal)
|
||||
* [rfc6350](http://tools.ietf.org/html/rfc6350) (vCard)
|
||||
* [rfc7095](http://tools.ietf.org/html/rfc7095) (jCard)
|
||||
|
||||
The initial goal was to use it as a replacement for libical in the [Mozilla Calendar
|
||||
Project](http://www.mozilla.org/projects/calendar/), but the library has been written with the web
|
||||
in mind. This library enables you to do all sorts of cool experiments with calendar data and the
|
||||
web. Most algorithms here were taken from [libical](https://github.com/libical/libical). If you are
|
||||
bugfixing this library, please check if the fix can be upstreamed to libical.
|
||||
|
||||
 [](https://coveralls.io/r/kewisch/ical.js) [](http://badge.fury.io/js/ical.js) [](https://cdnjs.com/libraries/ical.js)
|
||||
|
||||
## Sandbox and Validator
|
||||
|
||||
If you want to try out ICAL.js right now, there is a
|
||||
[jsfiddle](http://jsfiddle.net/kewisch/227efboL/) set up and ready to use.
|
||||
|
||||
The ICAL validator demonstrates how to use the library in a webpage, and helps verify iCalendar and
|
||||
jCal. [Try the validator online](http://kewisch.github.io/ical.js/validator.html)
|
||||
|
||||
The recurrence tester calculates occurrences based on a RRULE. It can be used to aid in
|
||||
creating test cases for the recurrence iterator.
|
||||
[Try the recurrence tester online](https://kewisch.github.io/ical.js/recur-tester.html).
|
||||
|
||||
## Installing
|
||||
|
||||
ICAL.js has no dependencies and is written in modern JavaScript. You can install ICAL.js via
|
||||
[npm](https://www.npmjs.com/), if you would like to use it in Node.js:
|
||||
```bash
|
||||
npm install ical.js
|
||||
```
|
||||
Then simply import it for use:
|
||||
```javascript
|
||||
import ICAL from "ical.js";
|
||||
```
|
||||
|
||||
If you are working with a browser, be aware this is an ES6 module:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js";
|
||||
document.querySelector("button").addEventListener("click", () => {
|
||||
ICAL.parse(document.getElementById("txt").value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
If you need to make use of a script tag, you can use the transpiled ES5 version:
|
||||
```html
|
||||
<script src="https://unpkg.com/ical.js/dist/ical.es5.min.cjs"></script>
|
||||
<textarea id="txt"></textarea>
|
||||
<button onclick="ICAL.parse(document.getElementById('txt').value)"></button>
|
||||
```
|
||||
|
||||
The browser examples above use the minified versions of the library, which is probably what you want.
|
||||
However, there are also unminified versions of ICAL.js available on unpkg.
|
||||
|
||||
- Unminified ES6 module: `https://unpkg.com/ical.js/dist/ical.js`
|
||||
- Unminified ES5 version: `https://unpkg.com/ical.js/dist/ical.es5.cjs`
|
||||
|
||||
## Timezones
|
||||
The stock ical.js does not register any timezones, due to the additional size it brings. If you'd
|
||||
like to do timezone conversion, and the timezone definitions are not included in the respective ics
|
||||
files, you'll need to use `ical.timezones.js` or its minified counterpart.
|
||||
|
||||
This file is not included in the distribution since it pulls in IANA timezones that might change
|
||||
regularly. See the github actions on building your own timezones during CI, or grab a recent build
|
||||
from main.
|
||||
|
||||
## Documentation
|
||||
|
||||
For a few guides with code samples, please check out
|
||||
[the wiki](https://github.com/kewisch/ical.js/wiki). If you prefer,
|
||||
full API documentation [is available here](http://kewisch.github.io/ical.js/api/).
|
||||
If you are missing anything, please don't hesitate to create an issue.
|
||||
|
||||
## Developing
|
||||
|
||||
To contribute to ICAL.js you need to set up the development environment. A simple `npm install` will
|
||||
get you set up. If you would like to help out and would like to discuss any API changes, please feel
|
||||
free to create an issue.
|
||||
|
||||
### Tests
|
||||
|
||||
The following test suites are available
|
||||
|
||||
npm run test-unit # Node unit tests
|
||||
npm run test-acceptance # Node acceptance tests
|
||||
npm run test-performance # Performance comparison tests
|
||||
npm run test-browser # Browser unit and acceptance tests
|
||||
|
||||
npm run test # Node unit and acceptance tests (This is fast and covers most aspects)
|
||||
npm run test-all # All of the above
|
||||
|
||||
See [the wiki](https://github.com/kewisch/ical.js/wiki/Running-Tests) for more details.
|
||||
|
||||
Code coverage is automatically generated for the node unit tests. You can [view the coverage
|
||||
results](https://coveralls.io/r/kewisch/ical.js) online, or run them locally to make sure new
|
||||
code is covered.
|
||||
|
||||
### Linters
|
||||
To make sure all ICAL.js code uses a common style, please run the linters using `npm run lint`.
|
||||
Please make sure you fix any issues shown by this command before sending a pull request.
|
||||
|
||||
### Documentation
|
||||
You can generate the documentation locally, this is also helpful to ensure the jsdoc you have
|
||||
written is valid. To do so, run `npm run jsdoc`. You will find the output in the `docs/api/`
|
||||
subdirectory.
|
||||
|
||||
### Packaging
|
||||
When you are done with your work, you can run `npm run build` to create the single-file build for
|
||||
use in the browser, including its minified counterpart and the source map.
|
||||
|
||||
## License
|
||||
ical.js is licensed under the
|
||||
[Mozilla Public License](https://www.mozilla.org/MPL/2.0/), version 2.0.
|
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="refresh" content="0;url=./api/">
|
||||
</head>
|
||||
</html>
|
|
@ -0,0 +1,389 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
import html from "eslint-plugin-html";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"**/*.{js,cjs}",
|
||||
"!lib/ical/**/*.{js,cjs}",
|
||||
"!test/**/*.{js,cjs}",
|
||||
"!tools/scriptutils.js",
|
||||
"!tools/ICALTester/**/*.js",
|
||||
"!tools/jsdoc-ical.cjs",
|
||||
"!eslint.config.js",
|
||||
"!rollup.config.js",
|
||||
"!karma.conf.cjs"
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
"@stylistic": stylistic
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.es2021
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// Enforce one true brace style (opening brace on the same line)
|
||||
// Allow single line (for now) because of the vast number of changes needed
|
||||
"@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }],
|
||||
|
||||
// Enforce newline at the end of file, with no multiple empty lines.
|
||||
"@stylistic/eol-last": "error",
|
||||
|
||||
// Disallow using variables outside the blocks they are defined
|
||||
//"block-scoped-var": "error",
|
||||
|
||||
// Allow trailing commas for easy list extension. Having them does not
|
||||
// impair readability, but also not required either.
|
||||
"@stylistic/comma-dangle": 0,
|
||||
|
||||
// Enforce spacing before and after comma
|
||||
"@stylistic/comma-spacing": ["error", { before: false, after: true }],
|
||||
|
||||
// Enforce one true comma style.
|
||||
"@stylistic/comma-style": ["error", "last"],
|
||||
|
||||
// Enforce curly brace conventions for all control statements.
|
||||
//"curly": "error",
|
||||
|
||||
// Enforce the spacing around the * in generator functions.
|
||||
"@stylistic/generator-star-spacing": ["error", "after"],
|
||||
|
||||
// Require space before/after arrow function's arrow
|
||||
"@stylistic/arrow-spacing": ["error", { before: true, after: true }],
|
||||
|
||||
// Enforces spacing between keys and values in object literal properties.
|
||||
"@stylistic/key-spacing": ["error", { beforeColon: false, afterColon: true, mode: "minimum" }],
|
||||
|
||||
// Disallow the omission of parentheses when invoking a constructor with no
|
||||
// arguments.
|
||||
"@stylistic/new-parens": "error",
|
||||
|
||||
// Disallow use of the Array constructor.
|
||||
"no-array-constructor": "error",
|
||||
|
||||
// disallow use of the Object constructor
|
||||
"no-object-constructor": "error",
|
||||
|
||||
// Disallow Primitive Wrapper Instances
|
||||
"no-new-wrappers": "error",
|
||||
|
||||
// Disallow adding to native types
|
||||
"no-extend-native": "error",
|
||||
|
||||
// Disallow unnecessary semicolons.
|
||||
"@stylistic/no-extra-semi": "error",
|
||||
|
||||
// Disallow mixed spaces and tabs for indentation.
|
||||
"@stylistic/no-mixed-spaces-and-tabs": "error",
|
||||
|
||||
// Disallow comparisons where both sides are exactly the same.
|
||||
"no-self-compare": "error",
|
||||
|
||||
// Disallow trailing whitespace at the end of lines.
|
||||
"@stylistic/no-trailing-spaces": "error",
|
||||
|
||||
// disallow use of octal escape sequences in string literals, such as
|
||||
// var foo = "Copyright \251";
|
||||
"no-octal-escape": "error",
|
||||
|
||||
// disallow use of void operator
|
||||
"no-void": "error",
|
||||
|
||||
// Disallow Yoda conditions (where literal value comes first).
|
||||
"yoda": "error",
|
||||
|
||||
// Require a space immediately following the // in a line comment.
|
||||
//"spaced-comment": ["error", "always"],
|
||||
|
||||
// Require use of the second argument for parseInt().
|
||||
//"radix": "error",
|
||||
|
||||
// Require spaces before/after unary operators (words on by default,
|
||||
// nonwords off by default).
|
||||
//"space-unary-ops": ["error", { "words": true, "nonwords": false }],
|
||||
|
||||
// Enforce spacing after semicolons.
|
||||
"@stylistic/semi-spacing": ["error", { before: false, after: true }],
|
||||
|
||||
// Disallow the use of Boolean literals in conditional expressions.
|
||||
"no-unneeded-ternary": "error",
|
||||
|
||||
// Disallow use of multiple spaces (sometimes used to align const values,
|
||||
// array or object items, etc.). It's hard to maintain and doesn't add that
|
||||
// much benefit.
|
||||
"@stylistic/no-multi-spaces": "error",
|
||||
|
||||
// Require spaces around operators, except for a|0.
|
||||
// Disabled for now given eslint doesn't support default args yet
|
||||
// "space-infix-ops": ["error", {"int32Hint": true}],
|
||||
|
||||
// Require a space around all keywords.
|
||||
"@stylistic/keyword-spacing": "error",
|
||||
|
||||
// Disallow space between function identifier and application.
|
||||
"@stylistic/func-call-spacing": "error",
|
||||
|
||||
// Disallow use of comma operator.
|
||||
"no-sequences": "error",
|
||||
|
||||
// Disallow use of assignment in return statement. It is preferable for a
|
||||
// single line of code to have only one easily predictable effect.
|
||||
"no-return-assign": "error",
|
||||
|
||||
// Require return statements to either always or never specify values
|
||||
//"consistent-return": "error",
|
||||
|
||||
// Disallow padding within blocks.
|
||||
//"padded-blocks": ["error", "never"],
|
||||
|
||||
// Disallow spaces inside parentheses.
|
||||
"@stylistic/space-in-parens": ["error", "never"],
|
||||
|
||||
// Require space after keyword for anonymous functions, but disallow space
|
||||
// after name of named functions.
|
||||
"@stylistic/space-before-function-paren": ["error", { anonymous: "never", named: "never" }],
|
||||
|
||||
// Always require use of semicolons wherever they are valid.
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
|
||||
// Warn about declaration of variables already declared in the outer scope.
|
||||
"no-shadow": "error",
|
||||
|
||||
// Disallow global and local variables that aren't used, but allow unused function arguments.
|
||||
"no-unused-vars": ["error", { vars: "all", args: "none" }],
|
||||
|
||||
// Require padding inside curly braces
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
|
||||
// Disallow spaces inside of brackets
|
||||
"@stylistic/array-bracket-spacing": ["error", "never"],
|
||||
|
||||
// Disallow constant expressions in conditions
|
||||
//"no-constant-condition": ["error", {"checkLoops": false }],
|
||||
|
||||
// Disallow Regexs That Look Like Division
|
||||
"no-div-regex": "error",
|
||||
|
||||
// Disallow Iterator (using __iterator__)
|
||||
"no-iterator": "error",
|
||||
|
||||
// Enforce consistent linebreak style
|
||||
"@stylistic/linebreak-style": ["error", "unix"],
|
||||
|
||||
// Enforces return statements in callbacks of array's methods
|
||||
"array-callback-return": "error",
|
||||
|
||||
// Disallow duplicate imports
|
||||
"no-duplicate-imports": "error",
|
||||
|
||||
// Disallow Labeled Statements
|
||||
"no-labels": "error",
|
||||
|
||||
// Disallow Multiline Strings
|
||||
"no-multi-str": "error",
|
||||
|
||||
// Disallow Initializing to undefined
|
||||
"no-undef-init": "error",
|
||||
|
||||
// Disallow unnecessary computed property keys on objects
|
||||
"no-useless-computed-key": "error",
|
||||
|
||||
// Disallow unnecessary constructor
|
||||
"no-useless-constructor": "error",
|
||||
|
||||
// Disallow renaming import, export, and destructured assignments to the
|
||||
// same name
|
||||
"no-useless-rename": "error",
|
||||
|
||||
// Enforce spacing between rest and spread operators and their expressions
|
||||
"@stylistic/rest-spread-spacing": ["error", "never"],
|
||||
|
||||
// Disallow usage of spacing in template string expressions
|
||||
"@stylistic/template-curly-spacing": ["error", "never"],
|
||||
|
||||
// Disallow the Unicode Byte Order Mark
|
||||
"unicode-bom": ["error", "never"],
|
||||
|
||||
// Enforce spacing around the * in yield* expressions
|
||||
"@stylistic/yield-star-spacing": ["error", "after"],
|
||||
|
||||
// Disallow Implied eval
|
||||
"no-implied-eval": "error",
|
||||
|
||||
// Disallow unnecessary function binding
|
||||
"no-extra-bind": "error",
|
||||
|
||||
// Disallow new For Side Effects
|
||||
"no-new": "error",
|
||||
|
||||
// Require IIFEs to be Wrapped
|
||||
//"wrap-iife": ["error", "inside"],
|
||||
|
||||
// Disallow Unused Expressions
|
||||
"no-unused-expressions": "error",
|
||||
|
||||
// Disallow function or var declarations in nested blocks
|
||||
"no-inner-declarations": "error",
|
||||
|
||||
// Enforce newline before and after dot
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
|
||||
// Disallow Use of caller/callee
|
||||
"no-caller": "error",
|
||||
|
||||
// Disallow Floating Decimals
|
||||
"@stylistic/no-floating-decimal": "error",
|
||||
|
||||
// Require Space Before Blocks
|
||||
"@stylistic/space-before-blocks": "error",
|
||||
|
||||
// Operators always before the line break
|
||||
"@stylistic/operator-linebreak": ["error", "after", { overrides: { ":": "before", "?": "ignore" } }],
|
||||
|
||||
// Restricts the use of parentheses to only where they are necessary
|
||||
//"no-extra-parens": ["error", "all", { "conditionalAssign": false, "returnAssign": false, "nestedBinaryExpressions": false }],
|
||||
|
||||
// Disallow if as the only statement in an else block.
|
||||
//"no-lonely-if": "error",
|
||||
|
||||
// Not more than two empty lines with in the file, and no extra lines at
|
||||
// beginning or end of file.
|
||||
"@stylistic/no-multiple-empty-lines": ["error", { max: 2, maxEOF: 0, maxBOF: 0 }],
|
||||
|
||||
// Make sure all setters have a corresponding getter
|
||||
"accessor-pairs": "error",
|
||||
|
||||
// Enforce spaces inside of single line blocks
|
||||
//"block-spacing": ["error", "always"],
|
||||
|
||||
// Disallow spaces inside of computed properties
|
||||
"@stylistic/computed-property-spacing": ["error", "never"],
|
||||
|
||||
// Require consistent this (using |self|)
|
||||
"consistent-this": ["error", "self"],
|
||||
|
||||
// Disallow unnecessary .call() and .apply()
|
||||
"no-useless-call": "error",
|
||||
|
||||
// Require dot notation when accessing properties
|
||||
"dot-notation": "error",
|
||||
|
||||
// Disallow named function expressions
|
||||
//"func-names": ["error", "never"],
|
||||
|
||||
// Enforce placing object properties on separate lines
|
||||
"@stylistic/object-property-newline": ["error", { allowMultiplePropertiesPerLine: true }],
|
||||
|
||||
// Enforce consistent line breaks inside braces
|
||||
//"object-curly-newline": ["error", { "multiline": true }],
|
||||
|
||||
// Disallow whitespace before properties
|
||||
"@stylistic/no-whitespace-before-property": "error",
|
||||
|
||||
// Disallow mixes of different operators, but allow simple math operations.
|
||||
//"no-mixed-operators": ["error", {
|
||||
// "groups": [
|
||||
// /* ["+", "-", "*", "/", "%", "**"], */
|
||||
// ["&", "|", "^", "~", "<<", ">>", ">>>"],
|
||||
// ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
|
||||
// ["&&", "||"],
|
||||
// ["in", "instanceof"]
|
||||
// ]
|
||||
//}],
|
||||
|
||||
// Disallow unnecessary concatenation of strings
|
||||
"no-useless-concat": "error",
|
||||
|
||||
// Disallow unmodified conditions of loops
|
||||
//"no-unmodified-loop-condition": "error",
|
||||
|
||||
// Suggest using arrow functions as callbacks
|
||||
//"prefer-arrow-callback": ["error", { "allowNamedFunctions": true }],
|
||||
|
||||
// Suggest using the spread operator instead of .apply()
|
||||
"prefer-spread": "error",
|
||||
|
||||
// Quoting style for property names
|
||||
//"@stylistic/quote-props": ["error", "consistent-as-needed", { "keywords": true }],
|
||||
|
||||
// Disallow negated conditions
|
||||
//"no-negated-condition": "error",
|
||||
|
||||
// Enforce a maximum number of statements allowed per line
|
||||
"@stylistic/max-statements-per-line": ["error", { max: 2 }],
|
||||
|
||||
// Disallow arrow functions where they could be confused with comparisons
|
||||
"@stylistic/no-confusing-arrow": "error",
|
||||
|
||||
// Disallow Unnecessary Nested Blocks
|
||||
"no-lone-blocks": "error",
|
||||
|
||||
// Enforce consistent indentation (2-space)
|
||||
//"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
|
||||
// Disallow var, use let or const instead
|
||||
"no-var": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["test/**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.mocha,
|
||||
...globals.node,
|
||||
ICAL: "readonly",
|
||||
assert: "readonly",
|
||||
testSupport: "readonly",
|
||||
perfTest: "readonly"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["test/performance/**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
perfCompareSuite: "readonly"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["test/support/helper.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...globals.mocha
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tools/scriptutils.js", "test/support/perfReporter.cjs", "karma.conf.cjs", "tools/ICALTester/**/*.js"],
|
||||
languageOptions: {
|
||||
globals: globals.node
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["eslint.config.js"],
|
||||
plugins: {
|
||||
"@stylistic": stylistic
|
||||
},
|
||||
rules: {
|
||||
"@stylistic/quote-props": ["error", "consistent-as-needed"]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tools/**/*.html"],
|
||||
plugins: {
|
||||
"@html": html
|
||||
},
|
||||
languageOptions: {
|
||||
globals: globals.browser
|
||||
}
|
||||
}
|
||||
];
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"source": {
|
||||
"include": "lib/ical",
|
||||
"includePattern": ".js$"
|
||||
},
|
||||
"plugins": ["tools/jsdoc-collect-types.cjs", "node_modules/jsdoc-tsimport-plugin/index.js"],
|
||||
"opts": {
|
||||
"destination": "docs/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"source": {
|
||||
"include": "lib/ical",
|
||||
"includePattern": ".js$"
|
||||
},
|
||||
"plugins": ["plugins/markdown", "tools/jsdoc-ical.cjs", "node_modules/jsdoc-tsimport-plugin/index.js"],
|
||||
"opts": {
|
||||
"encoding": "utf8",
|
||||
"readme": "README.md",
|
||||
"destination": "docs/api/",
|
||||
"template": "node_modules/clean-jsdoc-theme",
|
||||
"theme_opts": {
|
||||
"default_theme": "light"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"hardwrap": false,
|
||||
"idInHeadings": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// Karma configuration
|
||||
// Generated on Sun Feb 20 2022 00:57:11 GMT+0100 (Central European Standard Time)
|
||||
|
||||
let pkg = require("./package.json");
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['mocha', 'chai'],
|
||||
plugins: ["karma-chai", "karma-mocha", "karma-spec-reporter"],
|
||||
files: [
|
||||
{ pattern: 'samples/**/*.ics', included: false },
|
||||
{ pattern: 'test/parser/*', included: false },
|
||||
{ pattern: 'lib/ical/*.js', type: 'module', included: false },
|
||||
{ pattern: 'test/*_test.js', included: false },
|
||||
{ pattern: 'test/acceptance/*_test.js', included: false },
|
||||
{ pattern: 'test/support/helper.js', type: "module", included: true },
|
||||
],
|
||||
client: { mocha: Object.assign(pkg.mocha, { timeout: 0 }) },
|
||||
reporters: ['spec'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
autoWatch: false,
|
||||
singleRun: true,
|
||||
concurrency: Infinity,
|
||||
captureTimeout: 240000,
|
||||
browserNoActivityTimeout: 120000,
|
||||
//browsers: ['Firefox'],
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO
|
||||
});
|
||||
|
||||
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
config.set({
|
||||
exitOnFailure: false,
|
||||
customLaunchers: pkg.saucelabs,
|
||||
browsers: Object.keys(pkg.saucelabs),
|
||||
reporters: ['saucelabs', 'spec'],
|
||||
sauceLabs: {
|
||||
testName: 'ICAL.js',
|
||||
startConnect: true
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,173 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
/**
|
||||
* Represents the BINARY value type, which contains extra methods for encoding and decoding.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Binary {
|
||||
/**
|
||||
* Creates a binary value from the given string.
|
||||
*
|
||||
* @param {String} aString The binary value string
|
||||
* @return {Binary} The binary value instance
|
||||
*/
|
||||
static fromString(aString) {
|
||||
return new Binary(aString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Binary instance
|
||||
*
|
||||
* @param {String} aValue The binary data for this value
|
||||
*/
|
||||
constructor(aValue) {
|
||||
this.value = aValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type name, to be used in the jCal object.
|
||||
* @default "binary"
|
||||
* @constant
|
||||
*/
|
||||
icaltype = "binary";
|
||||
|
||||
/**
|
||||
* Base64 decode the current value
|
||||
*
|
||||
* @return {String} The base64-decoded value
|
||||
*/
|
||||
decodeValue() {
|
||||
return this._b64_decode(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the passed parameter with base64 and sets the internal
|
||||
* value to the result.
|
||||
*
|
||||
* @param {String} aValue The raw binary value to encode
|
||||
*/
|
||||
setEncodedValue(aValue) {
|
||||
this.value = this._b64_encode(aValue);
|
||||
}
|
||||
|
||||
_b64_encode(data) {
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Tyler Akins (http://rumkin.com)
|
||||
// + improved by: Bayron Guevara
|
||||
// + improved by: Thunder.m
|
||||
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||
// + bugfixed by: Pellentesque Malesuada
|
||||
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||
// + improved by: Rafał Kukawski (http://kukawski.pl)
|
||||
// * example 1: base64_encode('Kevin van Zonneveld');
|
||||
// * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
|
||||
// mozilla has this native
|
||||
// - but breaks in 2.0.0.12!
|
||||
//if (typeof this.window['atob'] == 'function') {
|
||||
// return atob(data);
|
||||
//}
|
||||
let b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
|
||||
ac = 0,
|
||||
enc = "",
|
||||
tmp_arr = [];
|
||||
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
do { // pack three octets into four hexets
|
||||
o1 = data.charCodeAt(i++);
|
||||
o2 = data.charCodeAt(i++);
|
||||
o3 = data.charCodeAt(i++);
|
||||
|
||||
bits = o1 << 16 | o2 << 8 | o3;
|
||||
|
||||
h1 = bits >> 18 & 0x3f;
|
||||
h2 = bits >> 12 & 0x3f;
|
||||
h3 = bits >> 6 & 0x3f;
|
||||
h4 = bits & 0x3f;
|
||||
|
||||
// use hexets to index into b64, and append result to encoded string
|
||||
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
|
||||
} while (i < data.length);
|
||||
|
||||
enc = tmp_arr.join('');
|
||||
|
||||
let r = data.length % 3;
|
||||
|
||||
return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
|
||||
|
||||
}
|
||||
|
||||
_b64_decode(data) {
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Tyler Akins (http://rumkin.com)
|
||||
// + improved by: Thunder.m
|
||||
// + input by: Aman Gupta
|
||||
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||
// + bugfixed by: Onno Marsman
|
||||
// + bugfixed by: Pellentesque Malesuada
|
||||
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||
// + input by: Brett Zamir (http://brett-zamir.me)
|
||||
// + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||
// * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
|
||||
// * returns 1: 'Kevin van Zonneveld'
|
||||
// mozilla has this native
|
||||
// - but breaks in 2.0.0.12!
|
||||
//if (typeof this.window['btoa'] == 'function') {
|
||||
// return btoa(data);
|
||||
//}
|
||||
let b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
|
||||
ac = 0,
|
||||
dec = "",
|
||||
tmp_arr = [];
|
||||
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data += '';
|
||||
|
||||
do { // unpack four hexets into three octets using index points in b64
|
||||
h1 = b64.indexOf(data.charAt(i++));
|
||||
h2 = b64.indexOf(data.charAt(i++));
|
||||
h3 = b64.indexOf(data.charAt(i++));
|
||||
h4 = b64.indexOf(data.charAt(i++));
|
||||
|
||||
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
|
||||
|
||||
o1 = bits >> 16 & 0xff;
|
||||
o2 = bits >> 8 & 0xff;
|
||||
o3 = bits & 0xff;
|
||||
|
||||
if (h3 == 64) {
|
||||
tmp_arr[ac++] = String.fromCharCode(o1);
|
||||
} else if (h4 == 64) {
|
||||
tmp_arr[ac++] = String.fromCharCode(o1, o2);
|
||||
} else {
|
||||
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
|
||||
}
|
||||
} while (i < data.length);
|
||||
|
||||
dec = tmp_arr.join('');
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this value
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
export default Binary;
|
|
@ -0,0 +1,621 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import Property from "./property.js";
|
||||
import Timezone from "./timezone.js";
|
||||
import ICALParse from "./parse.js";
|
||||
import stringify from "./stringify.js";
|
||||
import design from "./design.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Duration from "./duration.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import UtcOffset from "./utc_offset.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Binary from "./binary.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Period from "./period.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Recur from "./recur.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Time from "./time.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").designSet} designSet
|
||||
* Imports the 'designSet' type from the "types.js" module
|
||||
* @typedef {import("./types.js").Geo} Geo
|
||||
* Imports the 'Geo' type from the "types.js" module
|
||||
*/
|
||||
|
||||
const NAME_INDEX = 0;
|
||||
const PROPERTY_INDEX = 1;
|
||||
const COMPONENT_INDEX = 2;
|
||||
|
||||
/**
|
||||
* Wraps a jCal component, adding convenience methods to add, remove and update subcomponents and
|
||||
* properties.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Component {
|
||||
/**
|
||||
* Create an {@link ICAL.Component} by parsing the passed iCalendar string.
|
||||
*
|
||||
* @param {String} str The iCalendar string to parse
|
||||
*/
|
||||
static fromString(str) {
|
||||
return new Component(ICALParse.component(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Component instance.
|
||||
*
|
||||
* @param {Array|String} jCal Raw jCal component data OR name of new
|
||||
* component
|
||||
* @param {Component=} parent Parent component to associate
|
||||
*/
|
||||
constructor(jCal, parent) {
|
||||
if (typeof(jCal) === 'string') {
|
||||
// jCal spec (name, properties, components)
|
||||
jCal = [jCal, [], []];
|
||||
}
|
||||
|
||||
// mostly for legacy reasons.
|
||||
this.jCal = jCal;
|
||||
|
||||
this.parent = parent || null;
|
||||
|
||||
if (!this.parent && this.name === 'vcalendar') {
|
||||
this._timezoneCache = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrated properties are inserted into the _properties array at the same
|
||||
* position as in the jCal array, so it is possible that the array contains
|
||||
* undefined values for unhydrdated properties. To avoid iterating the
|
||||
* array when checking if all properties have been hydrated, we save the
|
||||
* count here.
|
||||
*
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
_hydratedPropertyCount = 0;
|
||||
|
||||
/**
|
||||
* The same count as for _hydratedPropertyCount, but for subcomponents
|
||||
*
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
_hydratedComponentCount = 0;
|
||||
|
||||
/**
|
||||
* A cache of hydrated time zone objects which may be used by consumers, keyed
|
||||
* by time zone ID.
|
||||
*
|
||||
* @type {Map}
|
||||
* @private
|
||||
*/
|
||||
_timezoneCache = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_components = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_properties = null;
|
||||
|
||||
/**
|
||||
* The name of this component
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
get name() {
|
||||
return this.jCal[NAME_INDEX];
|
||||
}
|
||||
|
||||
/**
|
||||
* The design set for this component, e.g. icalendar vs vcard
|
||||
*
|
||||
* @type {designSet}
|
||||
* @private
|
||||
*/
|
||||
get _designSet() {
|
||||
let parentDesign = this.parent && this.parent._designSet;
|
||||
return parentDesign || design.getDesignSet(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_hydrateComponent(index) {
|
||||
if (!this._components) {
|
||||
this._components = [];
|
||||
this._hydratedComponentCount = 0;
|
||||
}
|
||||
|
||||
if (this._components[index]) {
|
||||
return this._components[index];
|
||||
}
|
||||
|
||||
let comp = new Component(
|
||||
this.jCal[COMPONENT_INDEX][index],
|
||||
this
|
||||
);
|
||||
|
||||
this._hydratedComponentCount++;
|
||||
return (this._components[index] = comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_hydrateProperty(index) {
|
||||
if (!this._properties) {
|
||||
this._properties = [];
|
||||
this._hydratedPropertyCount = 0;
|
||||
}
|
||||
|
||||
if (this._properties[index]) {
|
||||
return this._properties[index];
|
||||
}
|
||||
|
||||
let prop = new Property(
|
||||
this.jCal[PROPERTY_INDEX][index],
|
||||
this
|
||||
);
|
||||
|
||||
this._hydratedPropertyCount++;
|
||||
return (this._properties[index] = prop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds first sub component, optionally filtered by name.
|
||||
*
|
||||
* @param {String=} name Optional name to filter by
|
||||
* @return {?Component} The found subcomponent
|
||||
*/
|
||||
getFirstSubcomponent(name) {
|
||||
if (name) {
|
||||
let i = 0;
|
||||
let comps = this.jCal[COMPONENT_INDEX];
|
||||
let len = comps.length;
|
||||
|
||||
for (; i < len; i++) {
|
||||
if (comps[i][NAME_INDEX] === name) {
|
||||
let result = this._hydrateComponent(i);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.jCal[COMPONENT_INDEX].length) {
|
||||
return this._hydrateComponent(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ensure we return a value (strict mode)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all sub components, optionally filtering by name.
|
||||
*
|
||||
* @param {String=} name Optional name to filter by
|
||||
* @return {Component[]} The found sub components
|
||||
*/
|
||||
getAllSubcomponents(name) {
|
||||
let jCalLen = this.jCal[COMPONENT_INDEX].length;
|
||||
let i = 0;
|
||||
|
||||
if (name) {
|
||||
let comps = this.jCal[COMPONENT_INDEX];
|
||||
let result = [];
|
||||
|
||||
for (; i < jCalLen; i++) {
|
||||
if (name === comps[i][NAME_INDEX]) {
|
||||
result.push(
|
||||
this._hydrateComponent(i)
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
if (!this._components ||
|
||||
(this._hydratedComponentCount !== jCalLen)) {
|
||||
for (; i < jCalLen; i++) {
|
||||
this._hydrateComponent(i);
|
||||
}
|
||||
}
|
||||
|
||||
return this._components || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a named property exists.
|
||||
*
|
||||
* @param {String} name The property name
|
||||
* @return {Boolean} True, when property is found
|
||||
*/
|
||||
hasProperty(name) {
|
||||
let props = this.jCal[PROPERTY_INDEX];
|
||||
let len = props.length;
|
||||
|
||||
let i = 0;
|
||||
for (; i < len; i++) {
|
||||
// 0 is property name
|
||||
if (props[i][NAME_INDEX] === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first property, optionally with the given name.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {?Property} The found property
|
||||
*/
|
||||
getFirstProperty(name) {
|
||||
if (name) {
|
||||
let i = 0;
|
||||
let props = this.jCal[PROPERTY_INDEX];
|
||||
let len = props.length;
|
||||
|
||||
for (; i < len; i++) {
|
||||
if (props[i][NAME_INDEX] === name) {
|
||||
let result = this._hydrateProperty(i);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.jCal[PROPERTY_INDEX].length) {
|
||||
return this._hydrateProperty(0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first property's value, if available.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {Binary | Duration | Period |
|
||||
* Recur | Time | UtcOffset | Geo | string | null} The found property value.
|
||||
*/
|
||||
getFirstPropertyValue(name) {
|
||||
let prop = this.getFirstProperty(name);
|
||||
if (prop) {
|
||||
return prop.getFirstValue();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all properties in the component, optionally filtered by name.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {Property[]} List of properties
|
||||
*/
|
||||
getAllProperties(name) {
|
||||
let jCalLen = this.jCal[PROPERTY_INDEX].length;
|
||||
let i = 0;
|
||||
|
||||
if (name) {
|
||||
let props = this.jCal[PROPERTY_INDEX];
|
||||
let result = [];
|
||||
|
||||
for (; i < jCalLen; i++) {
|
||||
if (name === props[i][NAME_INDEX]) {
|
||||
result.push(
|
||||
this._hydrateProperty(i)
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
if (!this._properties ||
|
||||
(this._hydratedPropertyCount !== jCalLen)) {
|
||||
for (; i < jCalLen; i++) {
|
||||
this._hydrateProperty(i);
|
||||
}
|
||||
}
|
||||
|
||||
return this._properties || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeObjectByIndex(jCalIndex, cache, index) {
|
||||
cache = cache || [];
|
||||
// remove cached version
|
||||
if (cache[index]) {
|
||||
let obj = cache[index];
|
||||
if ("parent" in obj) {
|
||||
obj.parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
cache.splice(index, 1);
|
||||
|
||||
// remove it from the jCal
|
||||
this.jCal[jCalIndex].splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeObject(jCalIndex, cache, nameOrObject) {
|
||||
let i = 0;
|
||||
let objects = this.jCal[jCalIndex];
|
||||
let len = objects.length;
|
||||
let cached = this[cache];
|
||||
|
||||
if (typeof(nameOrObject) === 'string') {
|
||||
for (; i < len; i++) {
|
||||
if (objects[i][NAME_INDEX] === nameOrObject) {
|
||||
this._removeObjectByIndex(jCalIndex, cached, i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (cached) {
|
||||
for (; i < len; i++) {
|
||||
if (cached[i] && cached[i] === nameOrObject) {
|
||||
this._removeObjectByIndex(jCalIndex, cached, i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeAllObjects(jCalIndex, cache, name) {
|
||||
let cached = this[cache];
|
||||
|
||||
// Unfortunately we have to run through all children to reset their
|
||||
// parent property.
|
||||
let objects = this.jCal[jCalIndex];
|
||||
let i = objects.length - 1;
|
||||
|
||||
// descending search required because splice
|
||||
// is used and will effect the indices.
|
||||
for (; i >= 0; i--) {
|
||||
if (!name || objects[i][NAME_INDEX] === name) {
|
||||
this._removeObjectByIndex(jCalIndex, cached, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single sub component.
|
||||
*
|
||||
* @param {Component} component The component to add
|
||||
* @return {Component} The passed in component
|
||||
*/
|
||||
addSubcomponent(component) {
|
||||
if (!this._components) {
|
||||
this._components = [];
|
||||
this._hydratedComponentCount = 0;
|
||||
}
|
||||
|
||||
if (component.parent) {
|
||||
component.parent.removeSubcomponent(component);
|
||||
}
|
||||
|
||||
let idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
|
||||
this._components[idx - 1] = component;
|
||||
this._hydratedComponentCount++;
|
||||
component.parent = this;
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single component by name or the instance of a specific
|
||||
* component.
|
||||
*
|
||||
* @param {Component|String} nameOrComp Name of component, or component
|
||||
* @return {Boolean} True when comp is removed
|
||||
*/
|
||||
removeSubcomponent(nameOrComp) {
|
||||
let removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp);
|
||||
if (removed) {
|
||||
this._hydratedComponentCount--;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all components or (if given) all components by a particular
|
||||
* name.
|
||||
*
|
||||
* @param {String=} name Lowercase component name
|
||||
*/
|
||||
removeAllSubcomponents(name) {
|
||||
let removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name);
|
||||
this._hydratedComponentCount = 0;
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link ICAL.Property} to the component.
|
||||
*
|
||||
* @param {Property} property The property to add
|
||||
* @return {Property} The passed in property
|
||||
*/
|
||||
addProperty(property) {
|
||||
if (!(property instanceof Property)) {
|
||||
throw new TypeError('must be instance of ICAL.Property');
|
||||
}
|
||||
|
||||
if (!this._properties) {
|
||||
this._properties = [];
|
||||
this._hydratedPropertyCount = 0;
|
||||
}
|
||||
|
||||
if (property.parent) {
|
||||
property.parent.removeProperty(property);
|
||||
}
|
||||
|
||||
let idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
|
||||
this._properties[idx - 1] = property;
|
||||
this._hydratedPropertyCount++;
|
||||
property.parent = this;
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to add a property with a value to the component.
|
||||
*
|
||||
* @param {String} name Property name to add
|
||||
* @param {String|Number|Object} value Property value
|
||||
* @return {Property} The created property
|
||||
*/
|
||||
addPropertyWithValue(name, value) {
|
||||
let prop = new Property(name);
|
||||
prop.setValue(value);
|
||||
|
||||
this.addProperty(prop);
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will update or create a property of the given name
|
||||
* and sets its value. If multiple properties with the given name exist,
|
||||
* only the first is updated.
|
||||
*
|
||||
* @param {String} name Property name to update
|
||||
* @param {String|Number|Object} value Property value
|
||||
* @return {Property} The created property
|
||||
*/
|
||||
updatePropertyWithValue(name, value) {
|
||||
let prop = this.getFirstProperty(name);
|
||||
|
||||
if (prop) {
|
||||
prop.setValue(value);
|
||||
} else {
|
||||
prop = this.addPropertyWithValue(name, value);
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single property by name or the instance of the specific
|
||||
* property.
|
||||
*
|
||||
* @param {String|Property} nameOrProp Property name or instance to remove
|
||||
* @return {Boolean} True, when deleted
|
||||
*/
|
||||
removeProperty(nameOrProp) {
|
||||
let removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp);
|
||||
if (removed) {
|
||||
this._hydratedPropertyCount--;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all properties associated with this component, optionally
|
||||
* filtered by name.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {Boolean} True, when deleted
|
||||
*/
|
||||
removeAllProperties(name) {
|
||||
let removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name);
|
||||
this._hydratedPropertyCount = 0;
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Object representation of this component. The returned object
|
||||
* is a live jCal object and should be cloned if modified.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return this.jCal;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this component.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return stringify.component(
|
||||
this.jCal, this._designSet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a time zone definition from the component tree, if any is present.
|
||||
* If the tree contains no time zone definitions or the TZID cannot be
|
||||
* matched, returns null.
|
||||
*
|
||||
* @param {String} tzid The ID of the time zone to retrieve
|
||||
* @return {Timezone} The time zone corresponding to the ID, or null
|
||||
*/
|
||||
getTimeZoneByID(tzid) {
|
||||
// VTIMEZONE components can only appear as a child of the VCALENDAR
|
||||
// component; walk the tree if we're not the root.
|
||||
if (this.parent) {
|
||||
return this.parent.getTimeZoneByID(tzid);
|
||||
}
|
||||
|
||||
// If there is no time zone cache, we are probably parsing an incomplete
|
||||
// file and will have no time zone definitions.
|
||||
if (!this._timezoneCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._timezoneCache.has(tzid)) {
|
||||
return this._timezoneCache.get(tzid);
|
||||
}
|
||||
|
||||
// If the time zone is not already cached, hydrate it from the
|
||||
// subcomponents.
|
||||
const zones = this.getAllSubcomponents('vtimezone');
|
||||
for (const zone of zones) {
|
||||
if (zone.getFirstProperty('tzid').getFirstValue() === tzid) {
|
||||
const hydratedZone = new Timezone({
|
||||
component: zone,
|
||||
tzid: tzid,
|
||||
});
|
||||
|
||||
this._timezoneCache.set(tzid, hydratedZone);
|
||||
|
||||
return hydratedZone;
|
||||
}
|
||||
}
|
||||
|
||||
// Per the standard, we should always have a time zone defined in a file
|
||||
// for any referenced TZID, but don't blow up if the file is invalid.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export default Component;
|
|
@ -0,0 +1,157 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import ICALParse from "./parse.js";
|
||||
import Component from "./component.js";
|
||||
import Event from "./event.js";
|
||||
import Timezone from "./timezone.js";
|
||||
|
||||
/**
|
||||
* The ComponentParser is used to process a String or jCal Object,
|
||||
* firing callbacks for various found components, as well as completion.
|
||||
*
|
||||
* @example
|
||||
* var options = {
|
||||
* // when false no events will be emitted for type
|
||||
* parseEvent: true,
|
||||
* parseTimezone: true
|
||||
* };
|
||||
*
|
||||
* var parser = new ICAL.ComponentParser(options);
|
||||
*
|
||||
* parser.onevent(eventComponent) {
|
||||
* //...
|
||||
* }
|
||||
*
|
||||
* // ontimezone, etc...
|
||||
*
|
||||
* parser.oncomplete = function() {
|
||||
*
|
||||
* };
|
||||
*
|
||||
* parser.process(stringOrComponent);
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class ComponentParser {
|
||||
/**
|
||||
* Creates a new ICAL.ComponentParser instance.
|
||||
*
|
||||
* @param {Object=} options Component parser options
|
||||
* @param {Boolean} options.parseEvent Whether events should be parsed
|
||||
* @param {Boolean} options.parseTimezeone Whether timezones should be parsed
|
||||
*/
|
||||
constructor(options) {
|
||||
if (typeof(options) === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
|
||||
for (let [key, value] of Object.entries(options)) {
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When true, parse events
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
parseEvent = true;
|
||||
|
||||
/**
|
||||
* When true, parse timezones
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
parseTimezone = true;
|
||||
|
||||
|
||||
/* SAX like events here for reference */
|
||||
|
||||
/**
|
||||
* Fired when parsing is complete
|
||||
* @callback
|
||||
*/
|
||||
oncomplete = /* c8 ignore next */ function() {};
|
||||
|
||||
/**
|
||||
* Fired if an error occurs during parsing.
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} err details of error
|
||||
*/
|
||||
onerror = /* c8 ignore next */ function(err) {};
|
||||
|
||||
/**
|
||||
* Fired when a top level component (VTIMEZONE) is found
|
||||
*
|
||||
* @callback
|
||||
* @param {Timezone} component Timezone object
|
||||
*/
|
||||
ontimezone = /* c8 ignore next */ function(component) {};
|
||||
|
||||
/**
|
||||
* Fired when a top level component (VEVENT) is found.
|
||||
*
|
||||
* @callback
|
||||
* @param {Event} component Top level component
|
||||
*/
|
||||
onevent = /* c8 ignore next */ function(component) {};
|
||||
|
||||
/**
|
||||
* Process a string or parse ical object. This function itself will return
|
||||
* nothing but will start the parsing process.
|
||||
*
|
||||
* Events must be registered prior to calling this method.
|
||||
*
|
||||
* @param {Component|String|Object} ical The component to process,
|
||||
* either in its final form, as a jCal Object, or string representation
|
||||
*/
|
||||
process(ical) {
|
||||
//TODO: this is sync now in the future we will have a incremental parser.
|
||||
if (typeof(ical) === 'string') {
|
||||
ical = ICALParse(ical);
|
||||
}
|
||||
|
||||
if (!(ical instanceof Component)) {
|
||||
ical = new Component(ical);
|
||||
}
|
||||
|
||||
let components = ical.getAllSubcomponents();
|
||||
let i = 0;
|
||||
let len = components.length;
|
||||
let component;
|
||||
|
||||
for (; i < len; i++) {
|
||||
component = components[i];
|
||||
|
||||
switch (component.name) {
|
||||
case 'vtimezone':
|
||||
if (this.parseTimezone) {
|
||||
let tzid = component.getFirstPropertyValue('tzid');
|
||||
if (tzid) {
|
||||
this.ontimezone(new Timezone({
|
||||
tzid: tzid,
|
||||
component: component
|
||||
}));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'vevent':
|
||||
if (this.parseEvent) {
|
||||
this.onevent(new Event(component));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//XXX: ideally we should do a "nextTick" here
|
||||
// so in all cases this is actually async.
|
||||
this.oncomplete();
|
||||
}
|
||||
}
|
||||
export default ComponentParser;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,354 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import { isStrictlyNaN, trunc } from "./helpers.js";
|
||||
|
||||
const DURATION_LETTERS = /([PDWHMTS]{1,1})/;
|
||||
const DATA_PROPS_TO_COPY = ["weeks", "days", "hours", "minutes", "seconds", "isNegative"];
|
||||
|
||||
/**
|
||||
* This class represents the "duration" value type, with various calculation
|
||||
* and manipulation methods.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Duration {
|
||||
/**
|
||||
* Returns a new ICAL.Duration instance from the passed seconds value.
|
||||
*
|
||||
* @param {Number} aSeconds The seconds to create the instance from
|
||||
* @return {Duration} The newly created duration instance
|
||||
*/
|
||||
static fromSeconds(aSeconds) {
|
||||
return (new Duration()).fromSeconds(aSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given string is an iCalendar duration value.
|
||||
*
|
||||
* @param {String} value The raw ical value
|
||||
* @return {Boolean} True, if the given value is of the
|
||||
* duration ical type
|
||||
*/
|
||||
static isValueString(string) {
|
||||
return (string[0] === 'P' || string[1] === 'P');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ICAL.Duration} instance from the passed string.
|
||||
*
|
||||
* @param {String} aStr The string to parse
|
||||
* @return {Duration} The created duration instance
|
||||
*/
|
||||
static fromString(aStr) {
|
||||
let pos = 0;
|
||||
let dict = Object.create(null);
|
||||
let chunks = 0;
|
||||
|
||||
while ((pos = aStr.search(DURATION_LETTERS)) !== -1) {
|
||||
let type = aStr[pos];
|
||||
let numeric = aStr.slice(0, Math.max(0, pos));
|
||||
aStr = aStr.slice(pos + 1);
|
||||
|
||||
chunks += parseDurationChunk(type, numeric, dict);
|
||||
}
|
||||
|
||||
if (chunks < 2) {
|
||||
// There must be at least a chunk with "P" and some unit chunk
|
||||
throw new Error(
|
||||
'invalid duration value: Not enough duration components in "' + aStr + '"'
|
||||
);
|
||||
}
|
||||
|
||||
return new Duration(dict);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Duration instance from the given data object.
|
||||
*
|
||||
* @param {Object} aData An object with members of the duration
|
||||
* @param {Number=} aData.weeks Duration in weeks
|
||||
* @param {Number=} aData.days Duration in days
|
||||
* @param {Number=} aData.hours Duration in hours
|
||||
* @param {Number=} aData.minutes Duration in minutes
|
||||
* @param {Number=} aData.seconds Duration in seconds
|
||||
* @param {Boolean=} aData.isNegative If true, the duration is negative
|
||||
* @return {Duration} The createad duration instance
|
||||
*/
|
||||
static fromData(aData) {
|
||||
return new Duration(aData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Duration instance.
|
||||
*
|
||||
* @param {Object} data An object with members of the duration
|
||||
* @param {Number=} data.weeks Duration in weeks
|
||||
* @param {Number=} data.days Duration in days
|
||||
* @param {Number=} data.hours Duration in hours
|
||||
* @param {Number=} data.minutes Duration in minutes
|
||||
* @param {Number=} data.seconds Duration in seconds
|
||||
* @param {Boolean=} data.isNegative If true, the duration is negative
|
||||
*/
|
||||
constructor(data) {
|
||||
this.wrappedJSObject = this;
|
||||
this.fromData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The weeks in this duration
|
||||
* @type {Number}
|
||||
* @default 0
|
||||
*/
|
||||
weeks = 0;
|
||||
|
||||
/**
|
||||
* The days in this duration
|
||||
* @type {Number}
|
||||
* @default 0
|
||||
*/
|
||||
days = 0;
|
||||
|
||||
/**
|
||||
* The days in this duration
|
||||
* @type {Number}
|
||||
* @default 0
|
||||
*/
|
||||
hours = 0;
|
||||
|
||||
/**
|
||||
* The minutes in this duration
|
||||
* @type {Number}
|
||||
* @default 0
|
||||
*/
|
||||
minutes = 0;
|
||||
|
||||
/**
|
||||
* The seconds in this duration
|
||||
* @type {Number}
|
||||
* @default 0
|
||||
*/
|
||||
seconds = 0;
|
||||
|
||||
/**
|
||||
* The seconds in this duration
|
||||
* @type {Boolean}
|
||||
* @default false
|
||||
*/
|
||||
isNegative = false;
|
||||
|
||||
/**
|
||||
* The class identifier.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "icalduration"
|
||||
*/
|
||||
icalclass = "icalduration";
|
||||
|
||||
/**
|
||||
* The type name, to be used in the jCal object.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "duration"
|
||||
*/
|
||||
icaltype = "duration";
|
||||
|
||||
/**
|
||||
* Returns a clone of the duration object.
|
||||
*
|
||||
* @return {Duration} The cloned object
|
||||
*/
|
||||
clone() {
|
||||
return Duration.fromData(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration value expressed as a number of seconds.
|
||||
*
|
||||
* @return {Number} The duration value in seconds
|
||||
*/
|
||||
toSeconds() {
|
||||
let seconds = this.seconds + 60 * this.minutes + 3600 * this.hours +
|
||||
86400 * this.days + 7 * 86400 * this.weeks;
|
||||
return (this.isNegative ? -seconds : seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the passed seconds value into this duration object. Afterwards,
|
||||
* members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up
|
||||
* accordingly.
|
||||
*
|
||||
* @param {Number} aSeconds The duration value in seconds
|
||||
* @return {Duration} Returns this instance
|
||||
*/
|
||||
fromSeconds(aSeconds) {
|
||||
let secs = Math.abs(aSeconds);
|
||||
|
||||
this.isNegative = (aSeconds < 0);
|
||||
this.days = trunc(secs / 86400);
|
||||
|
||||
// If we have a flat number of weeks, use them.
|
||||
if (this.days % 7 == 0) {
|
||||
this.weeks = this.days / 7;
|
||||
this.days = 0;
|
||||
} else {
|
||||
this.weeks = 0;
|
||||
}
|
||||
|
||||
secs -= (this.days + 7 * this.weeks) * 86400;
|
||||
|
||||
this.hours = trunc(secs / 3600);
|
||||
secs -= this.hours * 3600;
|
||||
|
||||
this.minutes = trunc(secs / 60);
|
||||
secs -= this.minutes * 60;
|
||||
|
||||
this.seconds = secs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the current instance using members from the passed data object.
|
||||
*
|
||||
* @param {Object} aData An object with members of the duration
|
||||
* @param {Number=} aData.weeks Duration in weeks
|
||||
* @param {Number=} aData.days Duration in days
|
||||
* @param {Number=} aData.hours Duration in hours
|
||||
* @param {Number=} aData.minutes Duration in minutes
|
||||
* @param {Number=} aData.seconds Duration in seconds
|
||||
* @param {Boolean=} aData.isNegative If true, the duration is negative
|
||||
*/
|
||||
fromData(aData) {
|
||||
for (let prop of DATA_PROPS_TO_COPY) {
|
||||
if (aData && prop in aData) {
|
||||
this[prop] = aData[prop];
|
||||
} else {
|
||||
this[prop] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the duration instance to the default values, i.e. PT0S
|
||||
*/
|
||||
reset() {
|
||||
this.isNegative = false;
|
||||
this.weeks = 0;
|
||||
this.days = 0;
|
||||
this.hours = 0;
|
||||
this.minutes = 0;
|
||||
this.seconds = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the duration instance with another one.
|
||||
*
|
||||
* @param {Duration} aOther The instance to compare with
|
||||
* @return {Number} -1, 0 or 1 for less/equal/greater
|
||||
*/
|
||||
compare(aOther) {
|
||||
let thisSeconds = this.toSeconds();
|
||||
let otherSeconds = aOther.toSeconds();
|
||||
return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the duration instance. For example, a duration with a value
|
||||
* of 61 seconds will be normalized to 1 minute and 1 second.
|
||||
*/
|
||||
normalize() {
|
||||
this.fromSeconds(this.toSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this duration.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
if (this.toSeconds() == 0) {
|
||||
return "PT0S";
|
||||
} else {
|
||||
let str = "";
|
||||
if (this.isNegative) str += "-";
|
||||
str += "P";
|
||||
if (this.weeks) str += this.weeks + "W";
|
||||
if (this.days) str += this.days + "D";
|
||||
|
||||
if (this.hours || this.minutes || this.seconds) {
|
||||
str += "T";
|
||||
if (this.hours) str += this.hours + "H";
|
||||
if (this.minutes) str += this.minutes + "M";
|
||||
if (this.seconds) str += this.seconds + "S";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The iCalendar string representation of this duration.
|
||||
* @return {String}
|
||||
*/
|
||||
toICALString() {
|
||||
return this.toString();
|
||||
}
|
||||
}
|
||||
export default Duration;
|
||||
|
||||
/**
|
||||
* Internal helper function to handle a chunk of a duration.
|
||||
*
|
||||
* @private
|
||||
* @param {String} letter type of duration chunk
|
||||
* @param {String} number numeric value or -/+
|
||||
* @param {Object} dict target to assign values to
|
||||
*/
|
||||
function parseDurationChunk(letter, number, object) {
|
||||
let type;
|
||||
switch (letter) {
|
||||
case 'P':
|
||||
if (number && number === '-') {
|
||||
object.isNegative = true;
|
||||
} else {
|
||||
object.isNegative = false;
|
||||
}
|
||||
// period
|
||||
break;
|
||||
case 'D':
|
||||
type = 'days';
|
||||
break;
|
||||
case 'W':
|
||||
type = 'weeks';
|
||||
break;
|
||||
case 'H':
|
||||
type = 'hours';
|
||||
break;
|
||||
case 'M':
|
||||
type = 'minutes';
|
||||
break;
|
||||
case 'S':
|
||||
type = 'seconds';
|
||||
break;
|
||||
default:
|
||||
// Not a valid chunk
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
if (!number && number !== 0) {
|
||||
throw new Error(
|
||||
'invalid duration value: Missing number before "' + letter + '"'
|
||||
);
|
||||
}
|
||||
let num = parseInt(number, 10);
|
||||
if (isStrictlyNaN(num)) {
|
||||
throw new Error(
|
||||
'invalid duration value: Invalid number "' + number + '" before "' + letter + '"'
|
||||
);
|
||||
}
|
||||
object[type] = num;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
|
@ -0,0 +1,569 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import { binsearchInsert } from "./helpers.js";
|
||||
import Component from "./component.js";
|
||||
import Property from "./property.js";
|
||||
import Timezone from "./timezone.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Time from "./time.js";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Duration from "./duration.js";
|
||||
import RecurExpansion from "./recur_expansion.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").frequencyValues} frequencyValues
|
||||
* Imports the 'frequencyValues' type from the "types.js" module
|
||||
* @typedef {import("./types.js").occurrenceDetails} occurrenceDetails
|
||||
* Imports the 'occurrenceDetails' type from the "types.js" module
|
||||
*/
|
||||
|
||||
/**
|
||||
* ICAL.js is organized into multiple layers. The bottom layer is a raw jCal
|
||||
* object, followed by the component/property layer. The highest level is the
|
||||
* event representation, which this class is part of. See the
|
||||
* {@tutorial layers} guide for more details.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Event {
|
||||
/**
|
||||
* Creates a new ICAL.Event instance.
|
||||
*
|
||||
* @param {Component=} component The ICAL.Component to base this event on
|
||||
* @param {Object} [options] Options for this event
|
||||
* @param {Boolean=} options.strictExceptions When true, will verify exceptions are related by
|
||||
* their UUID
|
||||
* @param {Array<Component|Event>=} options.exceptions
|
||||
* Exceptions to this event, either as components or events. If not
|
||||
* specified exceptions will automatically be set in relation of
|
||||
* component's parent
|
||||
*/
|
||||
constructor(component, options) {
|
||||
if (!(component instanceof Component)) {
|
||||
options = component;
|
||||
component = null;
|
||||
}
|
||||
|
||||
if (component) {
|
||||
this.component = component;
|
||||
} else {
|
||||
this.component = new Component('vevent');
|
||||
}
|
||||
|
||||
this._rangeExceptionCache = Object.create(null);
|
||||
this.exceptions = Object.create(null);
|
||||
this.rangeExceptions = [];
|
||||
|
||||
if (options && options.strictExceptions) {
|
||||
this.strictExceptions = options.strictExceptions;
|
||||
}
|
||||
|
||||
if (options && options.exceptions) {
|
||||
options.exceptions.forEach(this.relateException, this);
|
||||
} else if (this.component.parent && !this.isRecurrenceException()) {
|
||||
this.component.parent.getAllSubcomponents('vevent').forEach(function(event) {
|
||||
if (event.hasProperty('recurrence-id')) {
|
||||
this.relateException(event);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static THISANDFUTURE = 'THISANDFUTURE';
|
||||
|
||||
/**
|
||||
* List of related event exceptions.
|
||||
*
|
||||
* @type {Event[]}
|
||||
*/
|
||||
exceptions = null;
|
||||
|
||||
/**
|
||||
* When true, will verify exceptions are related by their UUID.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
strictExceptions = false;
|
||||
|
||||
/**
|
||||
* Relates a given event exception to this object. If the given component
|
||||
* does not share the UID of this event it cannot be related and will throw
|
||||
* an exception.
|
||||
*
|
||||
* If this component is an exception it cannot have other exceptions
|
||||
* related to it.
|
||||
*
|
||||
* @param {Component|Event} obj Component or event
|
||||
*/
|
||||
relateException(obj) {
|
||||
if (this.isRecurrenceException()) {
|
||||
throw new Error('cannot relate exception to exceptions');
|
||||
}
|
||||
|
||||
if (obj instanceof Component) {
|
||||
obj = new Event(obj);
|
||||
}
|
||||
|
||||
if (this.strictExceptions && obj.uid !== this.uid) {
|
||||
throw new Error('attempted to relate unrelated exception');
|
||||
}
|
||||
|
||||
let id = obj.recurrenceId.toString();
|
||||
|
||||
// we don't sort or manage exceptions directly
|
||||
// here the recurrence expander handles that.
|
||||
this.exceptions[id] = obj;
|
||||
|
||||
// index RANGE=THISANDFUTURE exceptions so we can
|
||||
// look them up later in getOccurrenceDetails.
|
||||
if (obj.modifiesFuture()) {
|
||||
let item = [
|
||||
obj.recurrenceId.toUnixTime(), id
|
||||
];
|
||||
|
||||
// we keep them sorted so we can find the nearest
|
||||
// value later on...
|
||||
let idx = binsearchInsert(
|
||||
this.rangeExceptions,
|
||||
item,
|
||||
compareRangeException
|
||||
);
|
||||
|
||||
this.rangeExceptions.splice(idx, 0, item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this record is an exception and has the RANGE=THISANDFUTURE
|
||||
* value.
|
||||
*
|
||||
* @return {Boolean} True, when exception is within range
|
||||
*/
|
||||
modifiesFuture() {
|
||||
if (!this.component.hasProperty('recurrence-id')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let range = this.component.getFirstProperty('recurrence-id').getParameter('range');
|
||||
return range === Event.THISANDFUTURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the range exception nearest to the given date.
|
||||
*
|
||||
* @param {Time} time usually an occurrence time of an event
|
||||
* @return {?Event} the related event/exception or null
|
||||
*/
|
||||
findRangeException(time) {
|
||||
if (!this.rangeExceptions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let utc = time.toUnixTime();
|
||||
let idx = binsearchInsert(
|
||||
this.rangeExceptions,
|
||||
[utc],
|
||||
compareRangeException
|
||||
);
|
||||
|
||||
idx -= 1;
|
||||
|
||||
// occurs before
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rangeItem = this.rangeExceptions[idx];
|
||||
|
||||
/* c8 ignore next 4 */
|
||||
if (utc < rangeItem[0]) {
|
||||
// sanity check only
|
||||
return null;
|
||||
}
|
||||
|
||||
return rangeItem[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the occurrence details based on its start time. If the
|
||||
* occurrence has an exception will return the details for that exception.
|
||||
*
|
||||
* NOTE: this method is intend to be used in conjunction
|
||||
* with the {@link ICAL.Event#iterator iterator} method.
|
||||
*
|
||||
* @param {Time} occurrence time occurrence
|
||||
* @return {occurrenceDetails} Information about the occurrence
|
||||
*/
|
||||
getOccurrenceDetails(occurrence) {
|
||||
let id = occurrence.toString();
|
||||
let utcId = occurrence.convertToZone(Timezone.utcTimezone).toString();
|
||||
let item;
|
||||
let result = {
|
||||
//XXX: Clone?
|
||||
recurrenceId: occurrence
|
||||
};
|
||||
|
||||
if (id in this.exceptions) {
|
||||
item = result.item = this.exceptions[id];
|
||||
result.startDate = item.startDate;
|
||||
result.endDate = item.endDate;
|
||||
result.item = item;
|
||||
} else if (utcId in this.exceptions) {
|
||||
item = this.exceptions[utcId];
|
||||
result.startDate = item.startDate;
|
||||
result.endDate = item.endDate;
|
||||
result.item = item;
|
||||
} else {
|
||||
// range exceptions (RANGE=THISANDFUTURE) have a
|
||||
// lower priority then direct exceptions but
|
||||
// must be accounted for first. Their item is
|
||||
// always the first exception with the range prop.
|
||||
let rangeExceptionId = this.findRangeException(
|
||||
occurrence
|
||||
);
|
||||
let end;
|
||||
|
||||
if (rangeExceptionId) {
|
||||
let exception = this.exceptions[rangeExceptionId];
|
||||
|
||||
// range exception must modify standard time
|
||||
// by the difference (if any) in start/end times.
|
||||
result.item = exception;
|
||||
|
||||
let startDiff = this._rangeExceptionCache[rangeExceptionId];
|
||||
|
||||
if (!startDiff) {
|
||||
let original = exception.recurrenceId.clone();
|
||||
let newStart = exception.startDate.clone();
|
||||
|
||||
// zones must be same otherwise subtract may be incorrect.
|
||||
original.zone = newStart.zone;
|
||||
startDiff = newStart.subtractDate(original);
|
||||
|
||||
this._rangeExceptionCache[rangeExceptionId] = startDiff;
|
||||
}
|
||||
|
||||
let start = occurrence.clone();
|
||||
start.zone = exception.startDate.zone;
|
||||
start.addDuration(startDiff);
|
||||
|
||||
end = start.clone();
|
||||
end.addDuration(exception.duration);
|
||||
|
||||
result.startDate = start;
|
||||
result.endDate = end;
|
||||
} else {
|
||||
// no range exception standard expansion
|
||||
end = occurrence.clone();
|
||||
end.addDuration(this.duration);
|
||||
|
||||
result.endDate = end;
|
||||
result.startDate = occurrence;
|
||||
result.item = this;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a recur expansion instance for a specific point in time (defaults
|
||||
* to startDate).
|
||||
*
|
||||
* @param {Time=} startTime Starting point for expansion
|
||||
* @return {RecurExpansion} Expansion object
|
||||
*/
|
||||
iterator(startTime) {
|
||||
return new RecurExpansion({
|
||||
component: this.component,
|
||||
dtstart: startTime || this.startDate
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the event is recurring
|
||||
*
|
||||
* @return {Boolean} True, if event is recurring
|
||||
*/
|
||||
isRecurring() {
|
||||
let comp = this.component;
|
||||
return comp.hasProperty('rrule') || comp.hasProperty('rdate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the event describes a recurrence exception. See
|
||||
* {@tutorial terminology} for details.
|
||||
*
|
||||
* @return {Boolean} True, if the event describes a recurrence exception
|
||||
*/
|
||||
isRecurrenceException() {
|
||||
return this.component.hasProperty('recurrence-id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the types of recurrences this event may have.
|
||||
*
|
||||
* Returned as an object with the following possible keys:
|
||||
*
|
||||
* - YEARLY
|
||||
* - MONTHLY
|
||||
* - WEEKLY
|
||||
* - DAILY
|
||||
* - MINUTELY
|
||||
* - SECONDLY
|
||||
*
|
||||
* @return {Object.<frequencyValues, Boolean>}
|
||||
* Object of recurrence flags
|
||||
*/
|
||||
getRecurrenceTypes() {
|
||||
let rules = this.component.getAllProperties('rrule');
|
||||
let i = 0;
|
||||
let len = rules.length;
|
||||
let result = Object.create(null);
|
||||
|
||||
for (; i < len; i++) {
|
||||
let value = rules[i].getFirstValue();
|
||||
result[value.freq] = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The uid of this event
|
||||
* @type {String}
|
||||
*/
|
||||
get uid() {
|
||||
return this._firstProp('uid');
|
||||
}
|
||||
|
||||
set uid(value) {
|
||||
this._setProp('uid', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The start date
|
||||
* @type {Time}
|
||||
*/
|
||||
get startDate() {
|
||||
return this._firstProp('dtstart');
|
||||
}
|
||||
|
||||
set startDate(value) {
|
||||
this._setTime('dtstart', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The end date. This can be the result directly from the property, or the
|
||||
* end date calculated from start date and duration. Setting the property
|
||||
* will remove any duration properties.
|
||||
* @type {Time}
|
||||
*/
|
||||
get endDate() {
|
||||
let endDate = this._firstProp('dtend');
|
||||
if (!endDate) {
|
||||
let duration = this._firstProp('duration');
|
||||
endDate = this.startDate.clone();
|
||||
if (duration) {
|
||||
endDate.addDuration(duration);
|
||||
} else if (endDate.isDate) {
|
||||
endDate.day += 1;
|
||||
}
|
||||
}
|
||||
return endDate;
|
||||
}
|
||||
|
||||
set endDate(value) {
|
||||
if (this.component.hasProperty('duration')) {
|
||||
this.component.removeProperty('duration');
|
||||
}
|
||||
this._setTime('dtend', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration. This can be the result directly from the property, or the
|
||||
* duration calculated from start date and end date. Setting the property
|
||||
* will remove any `dtend` properties.
|
||||
* @type {Duration}
|
||||
*/
|
||||
get duration() {
|
||||
let duration = this._firstProp('duration');
|
||||
if (!duration) {
|
||||
return this.endDate.subtractDateTz(this.startDate);
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
set duration(value) {
|
||||
if (this.component.hasProperty('dtend')) {
|
||||
this.component.removeProperty('dtend');
|
||||
}
|
||||
|
||||
this._setProp('duration', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The location of the event.
|
||||
* @type {String}
|
||||
*/
|
||||
get location() {
|
||||
return this._firstProp('location');
|
||||
}
|
||||
|
||||
set location(value) {
|
||||
this._setProp('location', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The attendees in the event
|
||||
* @type {Property[]}
|
||||
*/
|
||||
get attendees() {
|
||||
//XXX: This is way lame we should have a better
|
||||
// data structure for this later.
|
||||
return this.component.getAllProperties('attendee');
|
||||
}
|
||||
|
||||
/**
|
||||
* The event summary
|
||||
* @type {String}
|
||||
*/
|
||||
get summary() {
|
||||
return this._firstProp('summary');
|
||||
}
|
||||
|
||||
set summary(value) {
|
||||
this._setProp('summary', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The event description.
|
||||
* @type {String}
|
||||
*/
|
||||
get description() {
|
||||
return this._firstProp('description');
|
||||
}
|
||||
|
||||
set description(value) {
|
||||
this._setProp('description', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The event color from [rfc7986](https://datatracker.ietf.org/doc/html/rfc7986)
|
||||
* @type {String}
|
||||
*/
|
||||
get color() {
|
||||
return this._firstProp('color');
|
||||
}
|
||||
|
||||
set color(value) {
|
||||
this._setProp('color', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The organizer value as an uri. In most cases this is a mailto: uri, but
|
||||
* it can also be something else, like urn:uuid:...
|
||||
* @type {String}
|
||||
*/
|
||||
get organizer() {
|
||||
return this._firstProp('organizer');
|
||||
}
|
||||
|
||||
set organizer(value) {
|
||||
this._setProp('organizer', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The sequence value for this event. Used for scheduling
|
||||
* see {@tutorial terminology}.
|
||||
* @type {Number}
|
||||
*/
|
||||
get sequence() {
|
||||
return this._firstProp('sequence');
|
||||
}
|
||||
|
||||
set sequence(value) {
|
||||
this._setProp('sequence', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The recurrence id for this event. See {@tutorial terminology} for details.
|
||||
* @type {Time}
|
||||
*/
|
||||
get recurrenceId() {
|
||||
return this._firstProp('recurrence-id');
|
||||
}
|
||||
|
||||
set recurrenceId(value) {
|
||||
this._setTime('recurrence-id', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set/update a time property's value.
|
||||
* This will also update the TZID of the property.
|
||||
*
|
||||
* TODO: this method handles the case where we are switching
|
||||
* from a known timezone to an implied timezone (one without TZID).
|
||||
* This does _not_ handle the case of moving between a known
|
||||
* (by TimezoneService) timezone to an unknown timezone...
|
||||
*
|
||||
* We will not add/remove/update the VTIMEZONE subcomponents
|
||||
* leading to invalid ICAL data...
|
||||
* @private
|
||||
* @param {String} propName The property name
|
||||
* @param {Time} time The time to set
|
||||
*/
|
||||
_setTime(propName, time) {
|
||||
let prop = this.component.getFirstProperty(propName);
|
||||
|
||||
if (!prop) {
|
||||
prop = new Property(propName);
|
||||
this.component.addProperty(prop);
|
||||
}
|
||||
|
||||
// utc and local don't get a tzid
|
||||
if (
|
||||
time.zone === Timezone.localTimezone ||
|
||||
time.zone === Timezone.utcTimezone
|
||||
) {
|
||||
// remove the tzid
|
||||
prop.removeParameter('tzid');
|
||||
} else {
|
||||
prop.setParameter('tzid', time.zone.tzid);
|
||||
}
|
||||
|
||||
prop.setValue(time);
|
||||
}
|
||||
|
||||
_setProp(name, value) {
|
||||
this.component.updatePropertyWithValue(name, value);
|
||||
}
|
||||
|
||||
_firstProp(name) {
|
||||
return this.component.getFirstPropertyValue(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this event.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return this.component.toString();
|
||||
}
|
||||
}
|
||||
export default Event;
|
||||
|
||||
function compareRangeException(a, b) {
|
||||
if (a[0] > b[0]) return 1;
|
||||
if (b[0] > a[0]) return -1;
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,317 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import TimezoneService from "./timezone_service.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Component from "./component.js";
|
||||
import ICALmodule from "./module.js";
|
||||
|
||||
/**
|
||||
* Helper functions used in various places within ical.js
|
||||
* @module ICAL.helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Compiles a list of all referenced TZIDs in all subcomponents and
|
||||
* removes any extra VTIMEZONE subcomponents. In addition, if any TZIDs
|
||||
* are referenced by a component, but a VTIMEZONE does not exist,
|
||||
* an attempt will be made to generate a VTIMEZONE using ICAL.TimezoneService.
|
||||
*
|
||||
* @param {Component} vcal The top-level VCALENDAR component.
|
||||
* @return {Component} The ICAL.Component that was passed in.
|
||||
*/
|
||||
export function updateTimezones(vcal) {
|
||||
let allsubs, properties, vtimezones, reqTzid, i;
|
||||
|
||||
if (!vcal || vcal.name !== "vcalendar") {
|
||||
//not a top-level vcalendar component
|
||||
return vcal;
|
||||
}
|
||||
|
||||
//Store vtimezone subcomponents in an object reference by tzid.
|
||||
//Store properties from everything else in another array
|
||||
allsubs = vcal.getAllSubcomponents();
|
||||
properties = [];
|
||||
vtimezones = {};
|
||||
for (i = 0; i < allsubs.length; i++) {
|
||||
if (allsubs[i].name === "vtimezone") {
|
||||
let tzid = allsubs[i].getFirstProperty("tzid").getFirstValue();
|
||||
vtimezones[tzid] = allsubs[i];
|
||||
} else {
|
||||
properties = properties.concat(allsubs[i].getAllProperties());
|
||||
}
|
||||
}
|
||||
|
||||
//create an object with one entry for each required tz
|
||||
reqTzid = {};
|
||||
for (i = 0; i < properties.length; i++) {
|
||||
let tzid = properties[i].getParameter("tzid");
|
||||
if (tzid) {
|
||||
reqTzid[tzid] = true;
|
||||
}
|
||||
}
|
||||
|
||||
//delete any vtimezones that are not on the reqTzid list.
|
||||
for (let [tzid, comp] of Object.entries(vtimezones)) {
|
||||
if (!reqTzid[tzid]) {
|
||||
vcal.removeSubcomponent(comp);
|
||||
}
|
||||
}
|
||||
|
||||
//create any missing, but registered timezones
|
||||
for (let tzid of Object.keys(reqTzid)) {
|
||||
if (!vtimezones[tzid] && TimezoneService.has(tzid)) {
|
||||
vcal.addSubcomponent(TimezoneService.get(tzid).component);
|
||||
}
|
||||
}
|
||||
|
||||
return vcal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given type is of the number type and also NaN.
|
||||
*
|
||||
* @param {Number} number The number to check
|
||||
* @return {Boolean} True, if the number is strictly NaN
|
||||
*/
|
||||
export function isStrictlyNaN(number) {
|
||||
return typeof(number) === 'number' && isNaN(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string value that is expected to be an integer, when the valid is
|
||||
* not an integer throws a decoration error.
|
||||
*
|
||||
* @param {String} string Raw string input
|
||||
* @return {Number} Parsed integer
|
||||
*/
|
||||
export function strictParseInt(string) {
|
||||
let result = parseInt(string, 10);
|
||||
|
||||
if (isStrictlyNaN(result)) {
|
||||
throw new Error(
|
||||
'Could not extract integer from "' + string + '"'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or returns a class instance of a given type with the initialization
|
||||
* data if the data is not already an instance of the given type.
|
||||
*
|
||||
* @example
|
||||
* var time = new ICAL.Time(...);
|
||||
* var result = ICAL.helpers.formatClassType(time, ICAL.Time);
|
||||
*
|
||||
* (result instanceof ICAL.Time)
|
||||
* // => true
|
||||
*
|
||||
* result = ICAL.helpers.formatClassType({}, ICAL.Time);
|
||||
* (result isntanceof ICAL.Time)
|
||||
* // => true
|
||||
*
|
||||
*
|
||||
* @param {Object} data object initialization data
|
||||
* @param {Object} type object type (like ICAL.Time)
|
||||
* @return {?} An instance of the found type.
|
||||
*/
|
||||
export function formatClassType(data, type) {
|
||||
if (typeof(data) === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (data instanceof type) {
|
||||
return data;
|
||||
}
|
||||
return new type(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to indexOf but will only match values when they are not preceded
|
||||
* by a backslash character.
|
||||
*
|
||||
* @param {String} buffer String to search
|
||||
* @param {String} search Value to look for
|
||||
* @param {Number} pos Start position
|
||||
* @return {Number} The position, or -1 if not found
|
||||
*/
|
||||
export function unescapedIndexOf(buffer, search, pos) {
|
||||
while ((pos = buffer.indexOf(search, pos)) !== -1) {
|
||||
if (pos > 0 && buffer[pos - 1] === '\\') {
|
||||
pos += 1;
|
||||
} else {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index for insertion using binary search.
|
||||
*
|
||||
* @param {Array} list The list to search
|
||||
* @param {?} seekVal The value to insert
|
||||
* @param {function(?,?)} cmpfunc The comparison func, that can
|
||||
* compare two seekVals
|
||||
* @return {Number} The insert position
|
||||
*/
|
||||
export function binsearchInsert(list, seekVal, cmpfunc) {
|
||||
if (!list.length)
|
||||
return 0;
|
||||
|
||||
let low = 0, high = list.length - 1,
|
||||
mid, cmpval;
|
||||
|
||||
while (low <= high) {
|
||||
mid = low + Math.floor((high - low) / 2);
|
||||
cmpval = cmpfunc(seekVal, list[mid]);
|
||||
|
||||
if (cmpval < 0)
|
||||
high = mid - 1;
|
||||
else if (cmpval > 0)
|
||||
low = mid + 1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmpval < 0)
|
||||
return mid; // insertion is displacing, so use mid outright.
|
||||
else if (cmpval > 0)
|
||||
return mid + 1;
|
||||
else
|
||||
return mid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the passed object or primitive. By default a shallow clone will be
|
||||
* executed.
|
||||
*
|
||||
* @param {*} aSrc The thing to clone
|
||||
* @param {Boolean=} aDeep If true, a deep clone will be performed
|
||||
* @return {*} The copy of the thing
|
||||
*/
|
||||
export function clone(aSrc, aDeep) {
|
||||
if (!aSrc || typeof aSrc != "object") {
|
||||
return aSrc;
|
||||
} else if (aSrc instanceof Date) {
|
||||
return new Date(aSrc.getTime());
|
||||
} else if ("clone" in aSrc) {
|
||||
return aSrc.clone();
|
||||
} else if (Array.isArray(aSrc)) {
|
||||
let arr = [];
|
||||
for (let i = 0; i < aSrc.length; i++) {
|
||||
arr.push(aDeep ? clone(aSrc[i], true) : aSrc[i]);
|
||||
}
|
||||
return arr;
|
||||
} else {
|
||||
let obj = {};
|
||||
for (let [name, value] of Object.entries(aSrc)) {
|
||||
if (aDeep) {
|
||||
obj[name] = clone(value, true);
|
||||
} else {
|
||||
obj[name] = value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs iCalendar line folding. A line ending character is inserted and
|
||||
* the next line begins with a whitespace.
|
||||
*
|
||||
* @example
|
||||
* SUMMARY:This line will be fold
|
||||
* ed right in the middle of a word.
|
||||
*
|
||||
* @param {String} aLine The line to fold
|
||||
* @return {String} The folded line
|
||||
*/
|
||||
export function foldline(aLine) {
|
||||
let result = "";
|
||||
let line = aLine || "", pos = 0, line_length = 0;
|
||||
//pos counts position in line for the UTF-16 presentation
|
||||
//line_length counts the bytes for the UTF-8 presentation
|
||||
while (line.length) {
|
||||
let cp = line.codePointAt(pos);
|
||||
if (cp < 128) ++line_length;
|
||||
else if (cp < 2048) line_length += 2;//needs 2 UTF-8 bytes
|
||||
else if (cp < 65536) line_length += 3;
|
||||
else line_length += 4; //cp is less than 1114112
|
||||
if (line_length < ICALmodule.foldLength + 1)
|
||||
pos += cp > 65535 ? 2 : 1;
|
||||
else {
|
||||
result += ICALmodule.newLineChar + " " + line.slice(0, Math.max(0, pos));
|
||||
line = line.slice(Math.max(0, pos));
|
||||
pos = line_length = 0;
|
||||
}
|
||||
}
|
||||
return result.slice(ICALmodule.newLineChar.length + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the given string or number with zeros so it will have at least two
|
||||
* characters.
|
||||
*
|
||||
* @param {String|Number} data The string or number to pad
|
||||
* @return {String} The number padded as a string
|
||||
*/
|
||||
export function pad2(data) {
|
||||
if (typeof(data) !== 'string') {
|
||||
// handle fractions.
|
||||
if (typeof(data) === 'number') {
|
||||
data = parseInt(data);
|
||||
}
|
||||
data = String(data);
|
||||
}
|
||||
|
||||
let len = data.length;
|
||||
|
||||
switch (len) {
|
||||
case 0:
|
||||
return '00';
|
||||
case 1:
|
||||
return '0' + data;
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the given number, correctly handling negative numbers.
|
||||
*
|
||||
* @param {Number} number The number to truncate
|
||||
* @return {Number} The truncated number
|
||||
*/
|
||||
export function trunc(number) {
|
||||
return (number < 0 ? Math.ceil(number) : Math.floor(number));
|
||||
}
|
||||
|
||||
/**
|
||||
* Poor-man's cross-browser object extension. Doesn't support all the
|
||||
* features, but enough for our usage. Note that the target's properties are
|
||||
* not overwritten with the source properties.
|
||||
*
|
||||
* @example
|
||||
* var child = ICAL.helpers.extend(parent, {
|
||||
* "bar": 123
|
||||
* });
|
||||
*
|
||||
* @param {Object} source The object to extend
|
||||
* @param {Object} target The object to extend with
|
||||
* @return {Object} Returns the target.
|
||||
*/
|
||||
export function extend(source, target) {
|
||||
for (let key in source) {
|
||||
let descr = Object.getOwnPropertyDescriptor(source, key);
|
||||
if (descr && !Object.getOwnPropertyDescriptor(target, key)) {
|
||||
Object.defineProperty(target, key, descr);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import Binary from "./binary.js";
|
||||
import Component from "./component.js";
|
||||
import ComponentParser from "./component_parser.js";
|
||||
import design from "./design.js";
|
||||
import Duration from "./duration.js";
|
||||
import Event from "./event.js";
|
||||
import * as helpers from "./helpers.js";
|
||||
import parse from "./parse.js";
|
||||
import Period from "./period.js";
|
||||
import Property from "./property.js";
|
||||
import Recur from "./recur.js";
|
||||
import RecurExpansion from "./recur_expansion.js";
|
||||
import RecurIterator from "./recur_iterator.js";
|
||||
import stringify from "./stringify.js";
|
||||
import Time from "./time.js";
|
||||
import Timezone from "./timezone.js";
|
||||
import TimezoneService from "./timezone_service.js";
|
||||
import UtcOffset from "./utc_offset.js";
|
||||
import VCardTime from "./vcard_time.js";
|
||||
|
||||
/**
|
||||
* The main ICAL module. Provides access to everything else.
|
||||
*
|
||||
* @alias ICAL
|
||||
* @namespace ICAL
|
||||
* @property {ICAL.design} design
|
||||
* @property {ICAL.helpers} helpers
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* The number of characters before iCalendar line folding should occur
|
||||
* @type {Number}
|
||||
* @default 75
|
||||
*/
|
||||
foldLength: 75,
|
||||
|
||||
debug: false,
|
||||
|
||||
/**
|
||||
* The character(s) to be used for a newline. The default value is provided by
|
||||
* rfc5545.
|
||||
* @type {String}
|
||||
* @default "\r\n"
|
||||
*/
|
||||
newLineChar: '\r\n',
|
||||
|
||||
Binary,
|
||||
Component,
|
||||
ComponentParser,
|
||||
Duration,
|
||||
Event,
|
||||
Period,
|
||||
Property,
|
||||
Recur,
|
||||
RecurExpansion,
|
||||
RecurIterator,
|
||||
Time,
|
||||
Timezone,
|
||||
TimezoneService,
|
||||
UtcOffset,
|
||||
VCardTime,
|
||||
|
||||
parse,
|
||||
stringify,
|
||||
|
||||
design,
|
||||
helpers
|
||||
};
|
|
@ -0,0 +1,547 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import design from "./design.js";
|
||||
import { unescapedIndexOf } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
*
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").parserState} parserState
|
||||
* Imports the 'parserState' type from the "types.js" module
|
||||
* @typedef {import("./types.js").designSet} designSet
|
||||
* Imports the 'designSet' type from the "types.js" module
|
||||
*/
|
||||
|
||||
const CHAR = /[^ \t]/;
|
||||
const VALUE_DELIMITER = ':';
|
||||
const PARAM_DELIMITER = ';';
|
||||
const PARAM_NAME_DELIMITER = '=';
|
||||
const DEFAULT_VALUE_TYPE = 'unknown';
|
||||
const DEFAULT_PARAM_TYPE = 'text';
|
||||
const RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" };
|
||||
|
||||
/**
|
||||
* Parses iCalendar or vCard data into a raw jCal object. Consult
|
||||
* documentation on the {@tutorial layers|layers of parsing} for more
|
||||
* details.
|
||||
*
|
||||
* @function ICAL.parse
|
||||
* @memberof ICAL
|
||||
* @variation function
|
||||
* @todo Fix the API to be more clear on the return type
|
||||
* @param {String} input The string data to parse
|
||||
* @return {Object|Object[]} A single jCal object, or an array thereof
|
||||
*/
|
||||
export default function parse(input) {
|
||||
let state = {};
|
||||
let root = state.component = [];
|
||||
|
||||
state.stack = [root];
|
||||
|
||||
parse._eachLine(input, function(err, line) {
|
||||
parse._handleContentLine(line, state);
|
||||
});
|
||||
|
||||
|
||||
// when there are still items on the stack
|
||||
// throw a fatal error, a component was not closed
|
||||
// correctly in that case.
|
||||
if (state.stack.length > 1) {
|
||||
throw new ParserError(
|
||||
'invalid ical body. component began but did not end'
|
||||
);
|
||||
}
|
||||
|
||||
state = null;
|
||||
|
||||
return (root.length == 1 ? root[0] : root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an iCalendar property value into the jCal for a single property
|
||||
*
|
||||
* @function ICAL.parse.property
|
||||
* @param {String} str
|
||||
* The iCalendar property string to parse
|
||||
* @param {designSet=} designSet
|
||||
* The design data to use for this property
|
||||
* @return {Object}
|
||||
* The jCal Object containing the property
|
||||
*/
|
||||
parse.property = function(str, designSet) {
|
||||
let state = {
|
||||
component: [[], []],
|
||||
designSet: designSet || design.defaultSet
|
||||
};
|
||||
parse._handleContentLine(str, state);
|
||||
return state.component[1][0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience method to parse a component. You can use ICAL.parse() directly
|
||||
* instead.
|
||||
*
|
||||
* @function ICAL.parse.component
|
||||
* @see ICAL.parse(function)
|
||||
* @param {String} str The iCalendar component string to parse
|
||||
* @return {Object} The jCal Object containing the component
|
||||
*/
|
||||
parse.component = function(str) {
|
||||
return parse(str);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An error that occurred during parsing.
|
||||
*
|
||||
* @param {String} message The error message
|
||||
* @memberof ICAL.parse
|
||||
* @extends {Error}
|
||||
*/
|
||||
class ParserError extends Error {
|
||||
name = this.constructor.name;
|
||||
}
|
||||
|
||||
// classes & constants
|
||||
parse.ParserError = ParserError;
|
||||
|
||||
|
||||
/**
|
||||
* Handles a single line of iCalendar/vCard, updating the state.
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._handleContentLine
|
||||
* @param {String} line The content line to process
|
||||
* @param {parserState} state The current state of the line parsing
|
||||
*/
|
||||
parse._handleContentLine = function(line, state) {
|
||||
// break up the parts of the line
|
||||
let valuePos = line.indexOf(VALUE_DELIMITER);
|
||||
let paramPos = line.indexOf(PARAM_DELIMITER);
|
||||
|
||||
let lastParamIndex;
|
||||
let lastValuePos;
|
||||
|
||||
// name of property or begin/end
|
||||
let name;
|
||||
let value;
|
||||
// params is only overridden if paramPos !== -1.
|
||||
// we can't do params = params || {} later on
|
||||
// because it sacrifices ops.
|
||||
let params = {};
|
||||
|
||||
/**
|
||||
* Different property cases
|
||||
*
|
||||
*
|
||||
* 1. RRULE:FREQ=foo
|
||||
* // FREQ= is not a param but the value
|
||||
*
|
||||
* 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
|
||||
* // ROLE= is a param because : has not happened yet
|
||||
*/
|
||||
// when the parameter delimiter is after the
|
||||
// value delimiter then it is not a parameter.
|
||||
|
||||
if ((paramPos !== -1 && valuePos !== -1)) {
|
||||
// when the parameter delimiter is after the
|
||||
// value delimiter then it is not a parameter.
|
||||
if (paramPos > valuePos) {
|
||||
paramPos = -1;
|
||||
}
|
||||
}
|
||||
|
||||
let parsedParams;
|
||||
if (paramPos !== -1) {
|
||||
name = line.slice(0, Math.max(0, paramPos)).toLowerCase();
|
||||
parsedParams = parse._parseParameters(line.slice(Math.max(0, paramPos)), 0, state.designSet);
|
||||
if (parsedParams[2] == -1) {
|
||||
throw new ParserError("Invalid parameters in '" + line + "'");
|
||||
}
|
||||
params = parsedParams[0];
|
||||
lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
|
||||
if ((lastValuePos =
|
||||
line.slice(Math.max(0, lastParamIndex)).indexOf(VALUE_DELIMITER)) !== -1) {
|
||||
value = line.slice(Math.max(0, lastParamIndex + lastValuePos + 1));
|
||||
} else {
|
||||
throw new ParserError("Missing parameter value in '" + line + "'");
|
||||
}
|
||||
} else if (valuePos !== -1) {
|
||||
// without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
|
||||
name = line.slice(0, Math.max(0, valuePos)).toLowerCase();
|
||||
value = line.slice(Math.max(0, valuePos + 1));
|
||||
|
||||
if (name === 'begin') {
|
||||
let newComponent = [value.toLowerCase(), [], []];
|
||||
if (state.stack.length === 1) {
|
||||
state.component.push(newComponent);
|
||||
} else {
|
||||
state.component[2].push(newComponent);
|
||||
}
|
||||
state.stack.push(state.component);
|
||||
state.component = newComponent;
|
||||
if (!state.designSet) {
|
||||
state.designSet = design.getDesignSet(state.component[0]);
|
||||
}
|
||||
return;
|
||||
} else if (name === 'end') {
|
||||
state.component = state.stack.pop();
|
||||
return;
|
||||
}
|
||||
// If it is not begin/end, then this is a property with an empty value,
|
||||
// which should be considered valid.
|
||||
} else {
|
||||
/**
|
||||
* Invalid line.
|
||||
* The rational to throw an error is we will
|
||||
* never be certain that the rest of the file
|
||||
* is sane and it is unlikely that we can serialize
|
||||
* the result correctly either.
|
||||
*/
|
||||
throw new ParserError(
|
||||
'invalid line (no token ";" or ":") "' + line + '"'
|
||||
);
|
||||
}
|
||||
|
||||
let valueType;
|
||||
let multiValue = false;
|
||||
let structuredValue = false;
|
||||
let propertyDetails;
|
||||
let splitName;
|
||||
let ungroupedName;
|
||||
|
||||
// fetch the ungrouped part of the name
|
||||
if (state.designSet.propertyGroups && name.indexOf('.') !== -1) {
|
||||
splitName = name.split('.');
|
||||
params.group = splitName[0];
|
||||
ungroupedName = splitName[1];
|
||||
} else {
|
||||
ungroupedName = name;
|
||||
}
|
||||
|
||||
if (ungroupedName in state.designSet.property) {
|
||||
propertyDetails = state.designSet.property[ungroupedName];
|
||||
|
||||
if ('multiValue' in propertyDetails) {
|
||||
multiValue = propertyDetails.multiValue;
|
||||
}
|
||||
|
||||
if ('structuredValue' in propertyDetails) {
|
||||
structuredValue = propertyDetails.structuredValue;
|
||||
}
|
||||
|
||||
if (value && 'detectType' in propertyDetails) {
|
||||
valueType = propertyDetails.detectType(value);
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to determine value
|
||||
if (!valueType) {
|
||||
if (!('value' in params)) {
|
||||
if (propertyDetails) {
|
||||
valueType = propertyDetails.defaultType;
|
||||
} else {
|
||||
valueType = DEFAULT_VALUE_TYPE;
|
||||
}
|
||||
} else {
|
||||
// possible to avoid this?
|
||||
valueType = params.value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
delete params.value;
|
||||
|
||||
/**
|
||||
* Note on `var result` juggling:
|
||||
*
|
||||
* I observed that building the array in pieces has adverse
|
||||
* effects on performance, so where possible we inline the creation.
|
||||
* It is a little ugly but resulted in ~2000 additional ops/sec.
|
||||
*/
|
||||
|
||||
let result;
|
||||
if (multiValue && structuredValue) {
|
||||
value = parse._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue);
|
||||
result = [ungroupedName, params, valueType, value];
|
||||
} else if (multiValue) {
|
||||
result = [ungroupedName, params, valueType];
|
||||
parse._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false);
|
||||
} else if (structuredValue) {
|
||||
value = parse._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue);
|
||||
result = [ungroupedName, params, valueType, value];
|
||||
} else {
|
||||
value = parse._parseValue(value, valueType, state.designSet, false);
|
||||
result = [ungroupedName, params, valueType, value];
|
||||
}
|
||||
// rfc6350 requires that in vCard 4.0 the first component is the VERSION
|
||||
// component with as value 4.0, note that 3.0 does not have this requirement.
|
||||
if (state.component[0] === 'vcard' && state.component[1].length === 0 &&
|
||||
!(name === 'version' && value === '4.0')) {
|
||||
state.designSet = design.getDesignSet("vcard3");
|
||||
}
|
||||
state.component[1].push(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a value from the raw value into the jCard/jCal value.
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._parseValue
|
||||
* @param {String} value Original value
|
||||
* @param {String} type Type of value
|
||||
* @param {Object} designSet The design data to use for this value
|
||||
* @return {Object} varies on type
|
||||
*/
|
||||
parse._parseValue = function(value, type, designSet, structuredValue) {
|
||||
if (type in designSet.value && 'fromICAL' in designSet.value[type]) {
|
||||
return designSet.value[type].fromICAL(value, structuredValue);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse parameters from a string to object.
|
||||
*
|
||||
* @function ICAL.parse._parseParameters
|
||||
* @private
|
||||
* @param {String} line A single unfolded line
|
||||
* @param {Number} start Position to start looking for properties
|
||||
* @param {Object} designSet The design data to use for this property
|
||||
* @return {Object} key/value pairs
|
||||
*/
|
||||
parse._parseParameters = function(line, start, designSet) {
|
||||
let lastParam = start;
|
||||
let pos = 0;
|
||||
let delim = PARAM_NAME_DELIMITER;
|
||||
let result = {};
|
||||
let name, lcname;
|
||||
let value, valuePos = -1;
|
||||
let type, multiValue, mvdelim;
|
||||
|
||||
// find the next '=' sign
|
||||
// use lastParam and pos to find name
|
||||
// check if " is used if so get value from "->"
|
||||
// then increment pos to find next ;
|
||||
|
||||
while ((pos !== false) &&
|
||||
(pos = line.indexOf(delim, pos + 1)) !== -1) {
|
||||
|
||||
name = line.slice(lastParam + 1, pos);
|
||||
if (name.length == 0) {
|
||||
throw new ParserError("Empty parameter name in '" + line + "'");
|
||||
}
|
||||
lcname = name.toLowerCase();
|
||||
mvdelim = false;
|
||||
multiValue = false;
|
||||
|
||||
if (lcname in designSet.param && designSet.param[lcname].valueType) {
|
||||
type = designSet.param[lcname].valueType;
|
||||
} else {
|
||||
type = DEFAULT_PARAM_TYPE;
|
||||
}
|
||||
|
||||
if (lcname in designSet.param) {
|
||||
multiValue = designSet.param[lcname].multiValue;
|
||||
if (designSet.param[lcname].multiValueSeparateDQuote) {
|
||||
mvdelim = parse._rfc6868Escape('"' + multiValue + '"');
|
||||
}
|
||||
}
|
||||
|
||||
let nextChar = line[pos + 1];
|
||||
if (nextChar === '"') {
|
||||
valuePos = pos + 2;
|
||||
pos = line.indexOf('"', valuePos);
|
||||
if (multiValue && pos != -1) {
|
||||
let extendedValue = true;
|
||||
while (extendedValue) {
|
||||
if (line[pos + 1] == multiValue && line[pos + 2] == '"') {
|
||||
pos = line.indexOf('"', pos + 3);
|
||||
} else {
|
||||
extendedValue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos === -1) {
|
||||
throw new ParserError(
|
||||
'invalid line (no matching double quote) "' + line + '"'
|
||||
);
|
||||
}
|
||||
value = line.slice(valuePos, pos);
|
||||
lastParam = line.indexOf(PARAM_DELIMITER, pos);
|
||||
let propValuePos = line.indexOf(VALUE_DELIMITER, pos);
|
||||
// if either no next parameter or delimeter in property value, let's stop here
|
||||
if (lastParam === -1 || (propValuePos !== -1 && lastParam > propValuePos)) {
|
||||
pos = false;
|
||||
}
|
||||
} else {
|
||||
valuePos = pos + 1;
|
||||
|
||||
// move to next ";"
|
||||
let nextPos = line.indexOf(PARAM_DELIMITER, valuePos);
|
||||
let propValuePos = line.indexOf(VALUE_DELIMITER, valuePos);
|
||||
if (propValuePos !== -1 && nextPos > propValuePos) {
|
||||
// this is a delimiter in the property value, let's stop here
|
||||
nextPos = propValuePos;
|
||||
pos = false;
|
||||
} else if (nextPos === -1) {
|
||||
// no ";"
|
||||
if (propValuePos === -1) {
|
||||
nextPos = line.length;
|
||||
} else {
|
||||
nextPos = propValuePos;
|
||||
}
|
||||
pos = false;
|
||||
} else {
|
||||
lastParam = nextPos;
|
||||
pos = nextPos;
|
||||
}
|
||||
|
||||
value = line.slice(valuePos, nextPos);
|
||||
}
|
||||
|
||||
const length_before = value.length;
|
||||
value = parse._rfc6868Escape(value);
|
||||
valuePos += length_before - value.length;
|
||||
if (multiValue) {
|
||||
let delimiter = mvdelim || multiValue;
|
||||
value = parse._parseMultiValue(value, delimiter, type, [], null, designSet);
|
||||
} else {
|
||||
value = parse._parseValue(value, type, designSet);
|
||||
}
|
||||
|
||||
if (multiValue && (lcname in result)) {
|
||||
if (Array.isArray(result[lcname])) {
|
||||
result[lcname].push(value);
|
||||
} else {
|
||||
result[lcname] = [
|
||||
result[lcname],
|
||||
value
|
||||
];
|
||||
}
|
||||
} else {
|
||||
result[lcname] = value;
|
||||
}
|
||||
}
|
||||
return [result, value, valuePos];
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal helper for rfc6868. Exposing this on ICAL.parse so that
|
||||
* hackers can disable the rfc6868 parsing if the really need to.
|
||||
*
|
||||
* @function ICAL.parse._rfc6868Escape
|
||||
* @param {String} val The value to escape
|
||||
* @return {String} The escaped value
|
||||
*/
|
||||
parse._rfc6868Escape = function(val) {
|
||||
return val.replace(/\^['n^]/g, function(x) {
|
||||
return RFC6868_REPLACE_MAP[x];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a multi value string. This function is used either for parsing
|
||||
* actual multi-value property's values, or for handling parameter values. It
|
||||
* can be used for both multi-value properties and structured value properties.
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._parseMultiValue
|
||||
* @param {String} buffer The buffer containing the full value
|
||||
* @param {String} delim The multi-value delimiter
|
||||
* @param {String} type The value type to be parsed
|
||||
* @param {Array.<?>} result The array to append results to, varies on value type
|
||||
* @param {String} innerMulti The inner delimiter to split each value with
|
||||
* @param {designSet} designSet The design data for this value
|
||||
* @return {?|Array.<?>} Either an array of results, or the first result
|
||||
*/
|
||||
parse._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) {
|
||||
let pos = 0;
|
||||
let lastPos = 0;
|
||||
let value;
|
||||
if (delim.length === 0) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// split each piece
|
||||
while ((pos = unescapedIndexOf(buffer, delim, lastPos)) !== -1) {
|
||||
value = buffer.slice(lastPos, pos);
|
||||
if (innerMulti) {
|
||||
value = parse._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
|
||||
} else {
|
||||
value = parse._parseValue(value, type, designSet, structuredValue);
|
||||
}
|
||||
result.push(value);
|
||||
lastPos = pos + delim.length;
|
||||
}
|
||||
|
||||
// on the last piece take the rest of string
|
||||
value = buffer.slice(lastPos);
|
||||
if (innerMulti) {
|
||||
value = parse._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
|
||||
} else {
|
||||
value = parse._parseValue(value, type, designSet, structuredValue);
|
||||
}
|
||||
result.push(value);
|
||||
|
||||
return result.length == 1 ? result[0] : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a complete buffer of iCalendar/vCard data line by line, correctly
|
||||
* unfolding content. Each line will be processed with the given callback
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._eachLine
|
||||
* @param {String} buffer The buffer to process
|
||||
* @param {function(?String, String)} callback The callback for each line
|
||||
*/
|
||||
parse._eachLine = function(buffer, callback) {
|
||||
let len = buffer.length;
|
||||
let lastPos = buffer.search(CHAR);
|
||||
let pos = lastPos;
|
||||
let line;
|
||||
let firstChar;
|
||||
|
||||
let newlineOffset;
|
||||
|
||||
do {
|
||||
pos = buffer.indexOf('\n', lastPos) + 1;
|
||||
|
||||
if (pos > 1 && buffer[pos - 2] === '\r') {
|
||||
newlineOffset = 2;
|
||||
} else {
|
||||
newlineOffset = 1;
|
||||
}
|
||||
|
||||
if (pos === 0) {
|
||||
pos = len;
|
||||
newlineOffset = 0;
|
||||
}
|
||||
|
||||
firstChar = buffer[lastPos];
|
||||
|
||||
if (firstChar === ' ' || firstChar === '\t') {
|
||||
// add to line
|
||||
line += buffer.slice(lastPos + 1, pos - newlineOffset);
|
||||
} else {
|
||||
if (line)
|
||||
callback(null, line);
|
||||
// push line
|
||||
line = buffer.slice(lastPos, pos - newlineOffset);
|
||||
}
|
||||
|
||||
lastPos = pos;
|
||||
} while (pos !== len);
|
||||
|
||||
// extra ending line
|
||||
line = line.trim();
|
||||
|
||||
if (line.length)
|
||||
callback(null, line);
|
||||
};
|
|
@ -0,0 +1,245 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import Time from "./time.js";
|
||||
import Duration from "./duration.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Property from "./property.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").jCalComponent} jCalComponent
|
||||
* Imports the 'occurrenceDetails' type from the "types.js" module
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class represents the "period" value type, with various calculation and manipulation methods.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Period {
|
||||
/**
|
||||
* Creates a new {@link ICAL.Period} instance from the passed string.
|
||||
*
|
||||
* @param {String} str The string to parse
|
||||
* @param {Property} prop The property this period will be on
|
||||
* @return {Period} The created period instance
|
||||
*/
|
||||
static fromString(str, prop) {
|
||||
let parts = str.split('/');
|
||||
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
'Invalid string value: "' + str + '" must contain a "/" char.'
|
||||
);
|
||||
}
|
||||
|
||||
let options = {
|
||||
start: Time.fromDateTimeString(parts[0], prop)
|
||||
};
|
||||
|
||||
let end = parts[1];
|
||||
|
||||
if (Duration.isValueString(end)) {
|
||||
options.duration = Duration.fromString(end);
|
||||
} else {
|
||||
options.end = Time.fromDateTimeString(end, prop);
|
||||
}
|
||||
|
||||
return new Period(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ICAL.Period} instance from the given data object.
|
||||
* The passed data object cannot contain both and end date and a duration.
|
||||
*
|
||||
* @param {Object} aData An object with members of the period
|
||||
* @param {Time=} aData.start The start of the period
|
||||
* @param {Time=} aData.end The end of the period
|
||||
* @param {Duration=} aData.duration The duration of the period
|
||||
* @return {Period} The period instance
|
||||
*/
|
||||
static fromData(aData) {
|
||||
return new Period(aData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new period instance from the given jCal data array. The first
|
||||
* member is always the start date string, the second member is either a
|
||||
* duration or end date string.
|
||||
*
|
||||
* @param {jCalComponent} aData The jCal data array
|
||||
* @param {Property} aProp The property this jCal data is on
|
||||
* @param {Boolean} aLenient If true, data value can be both date and date-time
|
||||
* @return {Period} The period instance
|
||||
*/
|
||||
static fromJSON(aData, aProp, aLenient) {
|
||||
function fromDateOrDateTimeString(aValue, dateProp) {
|
||||
if (aLenient) {
|
||||
return Time.fromString(aValue, dateProp);
|
||||
} else {
|
||||
return Time.fromDateTimeString(aValue, dateProp);
|
||||
}
|
||||
}
|
||||
|
||||
if (Duration.isValueString(aData[1])) {
|
||||
return Period.fromData({
|
||||
start: fromDateOrDateTimeString(aData[0], aProp),
|
||||
duration: Duration.fromString(aData[1])
|
||||
});
|
||||
} else {
|
||||
return Period.fromData({
|
||||
start: fromDateOrDateTimeString(aData[0], aProp),
|
||||
end: fromDateOrDateTimeString(aData[1], aProp)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Period instance. The passed data object cannot contain both and end date and
|
||||
* a duration.
|
||||
*
|
||||
* @param {Object} aData An object with members of the period
|
||||
* @param {Time=} aData.start The start of the period
|
||||
* @param {Time=} aData.end The end of the period
|
||||
* @param {Duration=} aData.duration The duration of the period
|
||||
*/
|
||||
constructor(aData) {
|
||||
this.wrappedJSObject = this;
|
||||
|
||||
if (aData && 'start' in aData) {
|
||||
if (aData.start && !(aData.start instanceof Time)) {
|
||||
throw new TypeError('.start must be an instance of ICAL.Time');
|
||||
}
|
||||
this.start = aData.start;
|
||||
}
|
||||
|
||||
if (aData && aData.end && aData.duration) {
|
||||
throw new Error('cannot accept both end and duration');
|
||||
}
|
||||
|
||||
if (aData && 'end' in aData) {
|
||||
if (aData.end && !(aData.end instanceof Time)) {
|
||||
throw new TypeError('.end must be an instance of ICAL.Time');
|
||||
}
|
||||
this.end = aData.end;
|
||||
}
|
||||
|
||||
if (aData && 'duration' in aData) {
|
||||
if (aData.duration && !(aData.duration instanceof Duration)) {
|
||||
throw new TypeError('.duration must be an instance of ICAL.Duration');
|
||||
}
|
||||
this.duration = aData.duration;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The start of the period
|
||||
* @type {Time}
|
||||
*/
|
||||
start = null;
|
||||
|
||||
/**
|
||||
* The end of the period
|
||||
* @type {Time}
|
||||
*/
|
||||
end = null;
|
||||
|
||||
/**
|
||||
* The duration of the period
|
||||
* @type {Duration}
|
||||
*/
|
||||
duration = null;
|
||||
|
||||
/**
|
||||
* The class identifier.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "icalperiod"
|
||||
*/
|
||||
icalclass = "icalperiod";
|
||||
|
||||
/**
|
||||
* The type name, to be used in the jCal object.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "period"
|
||||
*/
|
||||
icaltype = "period";
|
||||
|
||||
/**
|
||||
* Returns a clone of the duration object.
|
||||
*
|
||||
* @return {Period} The cloned object
|
||||
*/
|
||||
clone() {
|
||||
return Period.fromData({
|
||||
start: this.start ? this.start.clone() : null,
|
||||
end: this.end ? this.end.clone() : null,
|
||||
duration: this.duration ? this.duration.clone() : null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the duration of the period, either directly or by subtracting
|
||||
* start from end date.
|
||||
*
|
||||
* @return {Duration} The calculated duration
|
||||
*/
|
||||
getDuration() {
|
||||
if (this.duration) {
|
||||
return this.duration;
|
||||
} else {
|
||||
return this.end.subtractDate(this.start);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the end date of the period, either directly or by adding
|
||||
* duration to start date.
|
||||
*
|
||||
* @return {Time} The calculated end date
|
||||
*/
|
||||
getEnd() {
|
||||
if (this.end) {
|
||||
return this.end;
|
||||
} else {
|
||||
let end = this.start.clone();
|
||||
end.addDuration(this.duration);
|
||||
return end;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this period.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return this.start + "/" + (this.end || this.duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* The jCal representation of this period type.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return [this.start.toString(), (this.end || this.duration).toString()];
|
||||
}
|
||||
|
||||
/**
|
||||
* The iCalendar string representation of this period.
|
||||
* @return {String}
|
||||
*/
|
||||
toICALString() {
|
||||
return this.start.toICALString() + "/" +
|
||||
(this.end || this.duration).toICALString();
|
||||
}
|
||||
}
|
||||
export default Period;
|
|
@ -0,0 +1,439 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
const NAME_INDEX = 0;
|
||||
const PROP_INDEX = 1;
|
||||
const TYPE_INDEX = 2;
|
||||
const VALUE_INDEX = 3;
|
||||
|
||||
import design from "./design.js";
|
||||
import ICALStringify from "./stringify.js";
|
||||
import ICALParse from "./parse.js";
|
||||
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Component from "./component.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Duration from "./duration.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import UtcOffset from "./utc_offset.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Binary from "./binary.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Period from "./period.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Recur from "./recur.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Time from "./time.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").designSet} designSet
|
||||
* Imports the 'designSet' type from the "types.js" module
|
||||
* @typedef {import("./types.js").Geo} Geo
|
||||
* Imports the 'Geo' type from the "types.js" module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides a layer on top of the raw jCal object for manipulating a single property, with its
|
||||
* parameters and value.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Property {
|
||||
/**
|
||||
* Create an {@link ICAL.Property} by parsing the passed iCalendar string.
|
||||
*
|
||||
* @param {String} str The iCalendar string to parse
|
||||
* @param {designSet=} designSet The design data to use for this property
|
||||
* @return {Property} The created iCalendar property
|
||||
*/
|
||||
static fromString(str, designSet) {
|
||||
return new Property(ICALParse.property(str, designSet));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Property instance.
|
||||
*
|
||||
* It is important to note that mutations done in the wrapper directly mutate the jCal object used
|
||||
* to initialize.
|
||||
*
|
||||
* Can also be used to create new properties by passing the name of the property (as a String).
|
||||
*
|
||||
* @param {Array|String} jCal Raw jCal representation OR the new name of the property
|
||||
* @param {Component=} parent Parent component
|
||||
*/
|
||||
constructor(jCal, parent) {
|
||||
this._parent = parent || null;
|
||||
|
||||
if (typeof(jCal) === 'string') {
|
||||
// We are creating the property by name and need to detect the type
|
||||
this.jCal = [jCal, {}, design.defaultType];
|
||||
this.jCal[TYPE_INDEX] = this.getDefaultType();
|
||||
} else {
|
||||
this.jCal = jCal;
|
||||
}
|
||||
this._updateType();
|
||||
}
|
||||
|
||||
/**
|
||||
* The value type for this property
|
||||
* @type {String}
|
||||
*/
|
||||
get type() {
|
||||
return this.jCal[TYPE_INDEX];
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this property, in lowercase.
|
||||
* @type {String}
|
||||
*/
|
||||
get name() {
|
||||
return this.jCal[NAME_INDEX];
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent component for this property.
|
||||
* @type {Component}
|
||||
*/
|
||||
get parent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
set parent(p) {
|
||||
// Before setting the parent, check if the design set has changed. If it
|
||||
// has, we later need to update the type if it was unknown before.
|
||||
let designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet);
|
||||
|
||||
this._parent = p;
|
||||
|
||||
if (this.type == design.defaultType && designSetChanged) {
|
||||
this.jCal[TYPE_INDEX] = this.getDefaultType();
|
||||
this._updateType();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The design set for this property, e.g. icalendar vs vcard
|
||||
*
|
||||
* @type {designSet}
|
||||
* @private
|
||||
*/
|
||||
get _designSet() {
|
||||
return this.parent ? this.parent._designSet : design.defaultSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the type metadata from the current jCal type and design set.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateType() {
|
||||
let designSet = this._designSet;
|
||||
|
||||
if (this.type in designSet.value) {
|
||||
if ('decorate' in designSet.value[this.type]) {
|
||||
this.isDecorated = true;
|
||||
} else {
|
||||
this.isDecorated = false;
|
||||
}
|
||||
|
||||
if (this.name in designSet.property) {
|
||||
this.isMultiValue = ('multiValue' in designSet.property[this.name]);
|
||||
this.isStructuredValue = ('structuredValue' in designSet.property[this.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate a single value. The act of hydrating means turning the raw jCal
|
||||
* value into a potentially wrapped object, for example {@link ICAL.Time}.
|
||||
*
|
||||
* @private
|
||||
* @param {Number} index The index of the value to hydrate
|
||||
* @return {?Object} The decorated value.
|
||||
*/
|
||||
_hydrateValue(index) {
|
||||
if (this._values && this._values[index]) {
|
||||
return this._values[index];
|
||||
}
|
||||
|
||||
// for the case where there is no value.
|
||||
if (this.jCal.length <= (VALUE_INDEX + index)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.isDecorated) {
|
||||
if (!this._values) {
|
||||
this._values = [];
|
||||
}
|
||||
return (this._values[index] = this._decorate(
|
||||
this.jCal[VALUE_INDEX + index]
|
||||
));
|
||||
} else {
|
||||
return this.jCal[VALUE_INDEX + index];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate a single value, returning its wrapped object. This is used by
|
||||
* the hydrate function to actually wrap the value.
|
||||
*
|
||||
* @private
|
||||
* @param {?} value The value to decorate
|
||||
* @return {Object} The decorated value
|
||||
*/
|
||||
_decorate(value) {
|
||||
return this._designSet.value[this.type].decorate(value, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undecorate a single value, returning its raw jCal data.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} value The value to undecorate
|
||||
* @return {?} The undecorated value
|
||||
*/
|
||||
_undecorate(value) {
|
||||
return this._designSet.value[this.type].undecorate(value, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at the given index while also hydrating it. The passed
|
||||
* value can either be a decorated or undecorated value.
|
||||
*
|
||||
* @private
|
||||
* @param {?} value The value to set
|
||||
* @param {Number} index The index to set it at
|
||||
*/
|
||||
_setDecoratedValue(value, index) {
|
||||
if (!this._values) {
|
||||
this._values = [];
|
||||
}
|
||||
|
||||
if (typeof(value) === 'object' && 'icaltype' in value) {
|
||||
// decorated value
|
||||
this.jCal[VALUE_INDEX + index] = this._undecorate(value);
|
||||
this._values[index] = value;
|
||||
} else {
|
||||
// undecorated value
|
||||
this.jCal[VALUE_INDEX + index] = value;
|
||||
this._values[index] = this._decorate(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a parameter on the property.
|
||||
*
|
||||
* @param {String} name Parameter name (lowercase)
|
||||
* @return {Array|String} Parameter value
|
||||
*/
|
||||
getParameter(name) {
|
||||
if (name in this.jCal[PROP_INDEX]) {
|
||||
return this.jCal[PROP_INDEX][name];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first parameter on the property.
|
||||
*
|
||||
* @param {String} name Parameter name (lowercase)
|
||||
* @return {String} Parameter value
|
||||
*/
|
||||
getFirstParameter(name) {
|
||||
let parameters = this.getParameter(name);
|
||||
|
||||
if (Array.isArray(parameters)) {
|
||||
return parameters[0];
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a parameter on the property.
|
||||
*
|
||||
* @param {String} name The parameter name
|
||||
* @param {Array|String} value The parameter value
|
||||
*/
|
||||
setParameter(name, value) {
|
||||
let lcname = name.toLowerCase();
|
||||
if (typeof value === "string" &&
|
||||
lcname in this._designSet.param &&
|
||||
'multiValue' in this._designSet.param[lcname]) {
|
||||
value = [value];
|
||||
}
|
||||
this.jCal[PROP_INDEX][name] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a parameter
|
||||
*
|
||||
* @param {String} name The parameter name
|
||||
*/
|
||||
removeParameter(name) {
|
||||
delete this.jCal[PROP_INDEX][name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default type based on this property's name.
|
||||
*
|
||||
* @return {String} The default type for this property
|
||||
*/
|
||||
getDefaultType() {
|
||||
let name = this.jCal[NAME_INDEX];
|
||||
let designSet = this._designSet;
|
||||
|
||||
if (name in designSet.property) {
|
||||
let details = designSet.property[name];
|
||||
if ('defaultType' in details) {
|
||||
return details.defaultType;
|
||||
}
|
||||
}
|
||||
return design.defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets type of property and clears out any existing values of the current
|
||||
* type.
|
||||
*
|
||||
* @param {String} type New iCAL type (see design.*.values)
|
||||
*/
|
||||
resetType(type) {
|
||||
this.removeAllValues();
|
||||
this.jCal[TYPE_INDEX] = type;
|
||||
this._updateType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first property value.
|
||||
*
|
||||
* @return {Binary | Duration | Period |
|
||||
* Recur | Time | UtcOffset | Geo | string | null} First property value
|
||||
*/
|
||||
getFirstValue() {
|
||||
return this._hydrateValue(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all values on the property.
|
||||
*
|
||||
* NOTE: this creates an array during each call.
|
||||
*
|
||||
* @return {Array} List of values
|
||||
*/
|
||||
getValues() {
|
||||
let len = this.jCal.length - VALUE_INDEX;
|
||||
|
||||
if (len < 1) {
|
||||
// it is possible for a property to have no value.
|
||||
return [];
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let result = [];
|
||||
|
||||
for (; i < len; i++) {
|
||||
result[i] = this._hydrateValue(i);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all values from this property
|
||||
*/
|
||||
removeAllValues() {
|
||||
if (this._values) {
|
||||
this._values.length = 0;
|
||||
}
|
||||
this.jCal.length = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values of the property. Will overwrite the existing values.
|
||||
* This can only be used for multi-value properties.
|
||||
*
|
||||
* @param {Array} values An array of values
|
||||
*/
|
||||
setValues(values) {
|
||||
if (!this.isMultiValue) {
|
||||
throw new Error(
|
||||
this.name + ': does not not support mulitValue.\n' +
|
||||
'override isMultiValue'
|
||||
);
|
||||
}
|
||||
|
||||
let len = values.length;
|
||||
let i = 0;
|
||||
this.removeAllValues();
|
||||
|
||||
if (len > 0 &&
|
||||
typeof(values[0]) === 'object' &&
|
||||
'icaltype' in values[0]) {
|
||||
this.resetType(values[0].icaltype);
|
||||
}
|
||||
|
||||
if (this.isDecorated) {
|
||||
for (; i < len; i++) {
|
||||
this._setDecoratedValue(values[i], i);
|
||||
}
|
||||
} else {
|
||||
for (; i < len; i++) {
|
||||
this.jCal[VALUE_INDEX + i] = values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current value of the property. If this is a multi-value
|
||||
* property, all other values will be removed.
|
||||
*
|
||||
* @param {String|Object} value New property value.
|
||||
*/
|
||||
setValue(value) {
|
||||
this.removeAllValues();
|
||||
if (typeof(value) === 'object' && 'icaltype' in value) {
|
||||
this.resetType(value.icaltype);
|
||||
}
|
||||
|
||||
if (this.isDecorated) {
|
||||
this._setDecoratedValue(value, 0);
|
||||
} else {
|
||||
this.jCal[VALUE_INDEX] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Object representation of this component. The returned object
|
||||
* is a live jCal object and should be cloned if modified.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return this.jCal;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this component.
|
||||
* @return {String}
|
||||
*/
|
||||
toICALString() {
|
||||
return ICALStringify.property(
|
||||
this.jCal, this._designSet, true
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Property;
|
|
@ -0,0 +1,576 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import RecurIterator from "./recur_iterator.js";
|
||||
import Time from "./time.js";
|
||||
import design from "./design.js";
|
||||
import { strictParseInt, clone } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
*
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").weekDay} weekDay
|
||||
* Imports the 'weekDay' type from the "types.js" module
|
||||
* @typedef {import("./types.js").frequencyValues} frequencyValues
|
||||
* Imports the 'frequencyValues' type from the "types.js" module
|
||||
*/
|
||||
|
||||
const VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/;
|
||||
const VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/;
|
||||
const DOW_MAP = {
|
||||
SU: Time.SUNDAY,
|
||||
MO: Time.MONDAY,
|
||||
TU: Time.TUESDAY,
|
||||
WE: Time.WEDNESDAY,
|
||||
TH: Time.THURSDAY,
|
||||
FR: Time.FRIDAY,
|
||||
SA: Time.SATURDAY
|
||||
};
|
||||
|
||||
const REVERSE_DOW_MAP = Object.fromEntries(Object.entries(DOW_MAP).map(entry => entry.reverse()));
|
||||
|
||||
const ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY',
|
||||
'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'];
|
||||
|
||||
/**
|
||||
* This class represents the "recur" value type, used for example by RRULE. It provides methods to
|
||||
* calculate occurrences among others.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Recur {
|
||||
/**
|
||||
* Creates a new {@link ICAL.Recur} instance from the passed string.
|
||||
*
|
||||
* @param {String} string The string to parse
|
||||
* @return {Recur} The created recurrence instance
|
||||
*/
|
||||
static fromString(string) {
|
||||
let data = this._stringToData(string, false);
|
||||
return new Recur(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ICAL.Recur} instance using members from the passed
|
||||
* data object.
|
||||
*
|
||||
* @param {Object} aData An object with members of the recurrence
|
||||
* @param {frequencyValues=} aData.freq The frequency value
|
||||
* @param {Number=} aData.interval The INTERVAL value
|
||||
* @param {weekDay=} aData.wkst The week start value
|
||||
* @param {Time=} aData.until The end of the recurrence set
|
||||
* @param {Number=} aData.count The number of occurrences
|
||||
* @param {Array.<Number>=} aData.bysecond The seconds for the BYSECOND part
|
||||
* @param {Array.<Number>=} aData.byminute The minutes for the BYMINUTE part
|
||||
* @param {Array.<Number>=} aData.byhour The hours for the BYHOUR part
|
||||
* @param {Array.<String>=} aData.byday The BYDAY values
|
||||
* @param {Array.<Number>=} aData.bymonthday The days for the BYMONTHDAY part
|
||||
* @param {Array.<Number>=} aData.byyearday The days for the BYYEARDAY part
|
||||
* @param {Array.<Number>=} aData.byweekno The weeks for the BYWEEKNO part
|
||||
* @param {Array.<Number>=} aData.bymonth The month for the BYMONTH part
|
||||
* @param {Array.<Number>=} aData.bysetpos The positionals for the BYSETPOS part
|
||||
*/
|
||||
static fromData(aData) {
|
||||
return new Recur(aData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a recurrence string to a data object, suitable for the fromData
|
||||
* method.
|
||||
*
|
||||
* @private
|
||||
* @param {String} string The string to parse
|
||||
* @param {Boolean} fmtIcal If true, the string is considered to be an
|
||||
* iCalendar string
|
||||
* @return {Recur} The recurrence instance
|
||||
*/
|
||||
static _stringToData(string, fmtIcal) {
|
||||
let dict = Object.create(null);
|
||||
|
||||
// split is slower in FF but fast enough.
|
||||
// v8 however this is faster then manual split?
|
||||
let values = string.split(';');
|
||||
let len = values.length;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
let parts = values[i].split('=');
|
||||
let ucname = parts[0].toUpperCase();
|
||||
let lcname = parts[0].toLowerCase();
|
||||
let name = (fmtIcal ? lcname : ucname);
|
||||
let value = parts[1];
|
||||
|
||||
if (ucname in partDesign) {
|
||||
let partArr = value.split(',');
|
||||
let partSet = new Set();
|
||||
|
||||
for (let part of partArr) {
|
||||
partSet.add(partDesign[ucname](part));
|
||||
}
|
||||
partArr = [...partSet];
|
||||
|
||||
dict[name] = (partArr.length == 1 ? partArr[0] : partArr);
|
||||
} else if (ucname in optionDesign) {
|
||||
optionDesign[ucname](value, dict, fmtIcal);
|
||||
} else {
|
||||
// Don't swallow unknown values. Just set them as they are.
|
||||
dict[lcname] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ical representation of a day (SU, MO, etc..)
|
||||
* into a numeric value of that day.
|
||||
*
|
||||
* @param {String} string The iCalendar day name
|
||||
* @param {weekDay=} aWeekStart
|
||||
* The week start weekday, defaults to SUNDAY
|
||||
* @return {Number} Numeric value of given day
|
||||
*/
|
||||
static icalDayToNumericDay(string, aWeekStart) {
|
||||
//XXX: this is here so we can deal
|
||||
// with possibly invalid string values.
|
||||
let firstDow = aWeekStart || Time.SUNDAY;
|
||||
return ((DOW_MAP[string] - firstDow + 7) % 7) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a numeric day value into its ical representation (SU, MO, etc..)
|
||||
*
|
||||
* @param {Number} num Numeric value of given day
|
||||
* @param {weekDay=} aWeekStart
|
||||
* The week start weekday, defaults to SUNDAY
|
||||
* @return {String} The ICAL day value, e.g SU,MO,...
|
||||
*/
|
||||
static numericDayToIcalDay(num, aWeekStart) {
|
||||
//XXX: this is here so we can deal with possibly invalid number values.
|
||||
// Also, this allows consistent mapping between day numbers and day
|
||||
// names for external users.
|
||||
let firstDow = aWeekStart || Time.SUNDAY;
|
||||
let dow = (num + firstDow - Time.SUNDAY);
|
||||
if (dow > 7) {
|
||||
dow -= 7;
|
||||
}
|
||||
return REVERSE_DOW_MAP[dow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the Recur class.
|
||||
*
|
||||
* @param {Object} data An object with members of the recurrence
|
||||
* @param {frequencyValues=} data.freq The frequency value
|
||||
* @param {Number=} data.interval The INTERVAL value
|
||||
* @param {weekDay=} data.wkst The week start value
|
||||
* @param {Time=} data.until The end of the recurrence set
|
||||
* @param {Number=} data.count The number of occurrences
|
||||
* @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part
|
||||
* @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part
|
||||
* @param {Array.<Number>=} data.byhour The hours for the BYHOUR part
|
||||
* @param {Array.<String>=} data.byday The BYDAY values
|
||||
* @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part
|
||||
* @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part
|
||||
* @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part
|
||||
* @param {Array.<Number>=} data.bymonth The month for the BYMONTH part
|
||||
* @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part
|
||||
*/
|
||||
constructor(data) {
|
||||
this.wrappedJSObject = this;
|
||||
this.parts = {};
|
||||
|
||||
if (data && typeof(data) === 'object') {
|
||||
this.fromData(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An object holding the BY-parts of the recurrence rule
|
||||
* @memberof ICAL.Recur
|
||||
* @typedef {Object} byParts
|
||||
* @property {Array.<Number>=} BYSECOND The seconds for the BYSECOND part
|
||||
* @property {Array.<Number>=} BYMINUTE The minutes for the BYMINUTE part
|
||||
* @property {Array.<Number>=} BYHOUR The hours for the BYHOUR part
|
||||
* @property {Array.<String>=} BYDAY The BYDAY values
|
||||
* @property {Array.<Number>=} BYMONTHDAY The days for the BYMONTHDAY part
|
||||
* @property {Array.<Number>=} BYYEARDAY The days for the BYYEARDAY part
|
||||
* @property {Array.<Number>=} BYWEEKNO The weeks for the BYWEEKNO part
|
||||
* @property {Array.<Number>=} BYMONTH The month for the BYMONTH part
|
||||
* @property {Array.<Number>=} BYSETPOS The positionals for the BYSETPOS part
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object holding the BY-parts of the recurrence rule
|
||||
* @type {byParts}
|
||||
*/
|
||||
parts = null;
|
||||
|
||||
/**
|
||||
* The interval value for the recurrence rule.
|
||||
* @type {Number}
|
||||
*/
|
||||
interval = 1;
|
||||
|
||||
/**
|
||||
* The week start day
|
||||
*
|
||||
* @type {weekDay}
|
||||
* @default ICAL.Time.MONDAY
|
||||
*/
|
||||
wkst = Time.MONDAY;
|
||||
|
||||
/**
|
||||
* The end of the recurrence
|
||||
* @type {?Time}
|
||||
*/
|
||||
until = null;
|
||||
|
||||
/**
|
||||
* The maximum number of occurrences
|
||||
* @type {?Number}
|
||||
*/
|
||||
count = null;
|
||||
|
||||
/**
|
||||
* The frequency value.
|
||||
* @type {frequencyValues}
|
||||
*/
|
||||
freq = null;
|
||||
|
||||
/**
|
||||
* The class identifier.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "icalrecur"
|
||||
*/
|
||||
icalclass = "icalrecur";
|
||||
|
||||
/**
|
||||
* The type name, to be used in the jCal object.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "recur"
|
||||
*/
|
||||
icaltype = "recur";
|
||||
|
||||
/**
|
||||
* Create a new iterator for this recurrence rule. The passed start date
|
||||
* must be the start date of the event, not the start of the range to
|
||||
* search in.
|
||||
*
|
||||
* @example
|
||||
* let recur = comp.getFirstPropertyValue('rrule');
|
||||
* let dtstart = comp.getFirstPropertyValue('dtstart');
|
||||
* let iter = recur.iterator(dtstart);
|
||||
* for (let next = iter.next(); next; next = iter.next()) {
|
||||
* if (next.compare(rangeStart) < 0) {
|
||||
* continue;
|
||||
* }
|
||||
* console.log(next.toString());
|
||||
* }
|
||||
*
|
||||
* @param {Time} aStart The item's start date
|
||||
* @return {RecurIterator} The recurrence iterator
|
||||
*/
|
||||
iterator(aStart) {
|
||||
return new RecurIterator({
|
||||
rule: this,
|
||||
dtstart: aStart
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of the recurrence object.
|
||||
*
|
||||
* @return {Recur} The cloned object
|
||||
*/
|
||||
clone() {
|
||||
return new Recur(this.toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current rule is finite, i.e. has a count or until part.
|
||||
*
|
||||
* @return {Boolean} True, if the rule is finite
|
||||
*/
|
||||
isFinite() {
|
||||
return !!(this.count || this.until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current rule has a count part, and not limited by an until
|
||||
* part.
|
||||
*
|
||||
* @return {Boolean} True, if the rule is by count
|
||||
*/
|
||||
isByCount() {
|
||||
return !!(this.count && !this.until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a component (part) to the recurrence rule. This is not a component
|
||||
* in the sense of {@link ICAL.Component}, but a part of the recurrence
|
||||
* rule, i.e. BYMONTH.
|
||||
*
|
||||
* @param {String} aType The name of the component part
|
||||
* @param {Array|String} aValue The component value
|
||||
*/
|
||||
addComponent(aType, aValue) {
|
||||
let ucname = aType.toUpperCase();
|
||||
if (ucname in this.parts) {
|
||||
this.parts[ucname].push(aValue);
|
||||
} else {
|
||||
this.parts[ucname] = [aValue];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the component value for the given by-part.
|
||||
*
|
||||
* @param {String} aType The component part name
|
||||
* @param {Array} aValues The component values
|
||||
*/
|
||||
setComponent(aType, aValues) {
|
||||
this.parts[aType.toUpperCase()] = aValues.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets (a copy) of the requested component value.
|
||||
*
|
||||
* @param {String} aType The component part name
|
||||
* @return {Array} The component part value
|
||||
*/
|
||||
getComponent(aType) {
|
||||
let ucname = aType.toUpperCase();
|
||||
return (ucname in this.parts ? this.parts[ucname].slice() : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next occurrence after the given recurrence id. See the
|
||||
* guide on {@tutorial terminology} for more details.
|
||||
*
|
||||
* NOTE: Currently, this method iterates all occurrences from the start
|
||||
* date. It should not be called in a loop for performance reasons. If you
|
||||
* would like to get more than one occurrence, you can iterate the
|
||||
* occurrences manually, see the example on the
|
||||
* {@link ICAL.Recur#iterator iterator} method.
|
||||
*
|
||||
* @param {Time} aStartTime The start of the event series
|
||||
* @param {Time} aRecurrenceId The date of the last occurrence
|
||||
* @return {Time} The next occurrence after
|
||||
*/
|
||||
getNextOccurrence(aStartTime, aRecurrenceId) {
|
||||
let iter = this.iterator(aStartTime);
|
||||
let next;
|
||||
|
||||
do {
|
||||
next = iter.next();
|
||||
} while (next && next.compare(aRecurrenceId) <= 0);
|
||||
|
||||
if (next && aRecurrenceId.zone) {
|
||||
next.zone = aRecurrenceId.zone;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the current instance using members from the passed data object.
|
||||
*
|
||||
* @param {Object} data An object with members of the recurrence
|
||||
* @param {frequencyValues=} data.freq The frequency value
|
||||
* @param {Number=} data.interval The INTERVAL value
|
||||
* @param {weekDay=} data.wkst The week start value
|
||||
* @param {Time=} data.until The end of the recurrence set
|
||||
* @param {Number=} data.count The number of occurrences
|
||||
* @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part
|
||||
* @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part
|
||||
* @param {Array.<Number>=} data.byhour The hours for the BYHOUR part
|
||||
* @param {Array.<String>=} data.byday The BYDAY values
|
||||
* @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part
|
||||
* @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part
|
||||
* @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part
|
||||
* @param {Array.<Number>=} data.bymonth The month for the BYMONTH part
|
||||
* @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part
|
||||
*/
|
||||
fromData(data) {
|
||||
for (let key in data) {
|
||||
let uckey = key.toUpperCase();
|
||||
|
||||
if (uckey in partDesign) {
|
||||
if (Array.isArray(data[key])) {
|
||||
this.parts[uckey] = data[key];
|
||||
} else {
|
||||
this.parts[uckey] = [data[key]];
|
||||
}
|
||||
} else {
|
||||
this[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.interval && typeof this.interval != "number") {
|
||||
optionDesign.INTERVAL(this.interval, this);
|
||||
}
|
||||
|
||||
if (this.wkst && typeof this.wkst != "number") {
|
||||
this.wkst = Recur.icalDayToNumericDay(this.wkst);
|
||||
}
|
||||
|
||||
if (this.until && !(this.until instanceof Time)) {
|
||||
this.until = Time.fromString(this.until);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The jCal representation of this recurrence type.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
let res = Object.create(null);
|
||||
res.freq = this.freq;
|
||||
|
||||
if (this.count) {
|
||||
res.count = this.count;
|
||||
}
|
||||
|
||||
if (this.interval > 1) {
|
||||
res.interval = this.interval;
|
||||
}
|
||||
|
||||
for (let [k, kparts] of Object.entries(this.parts)) {
|
||||
if (Array.isArray(kparts) && kparts.length == 1) {
|
||||
res[k.toLowerCase()] = kparts[0];
|
||||
} else {
|
||||
res[k.toLowerCase()] = clone(kparts);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.until) {
|
||||
res.until = this.until.toString();
|
||||
}
|
||||
if ('wkst' in this && this.wkst !== Time.DEFAULT_WEEK_START) {
|
||||
res.wkst = Recur.numericDayToIcalDay(this.wkst);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this recurrence rule.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
// TODO retain order
|
||||
let str = "FREQ=" + this.freq;
|
||||
if (this.count) {
|
||||
str += ";COUNT=" + this.count;
|
||||
}
|
||||
if (this.interval > 1) {
|
||||
str += ";INTERVAL=" + this.interval;
|
||||
}
|
||||
for (let [k, v] of Object.entries(this.parts)) {
|
||||
str += ";" + k + "=" + v;
|
||||
}
|
||||
if (this.until) {
|
||||
str += ';UNTIL=' + this.until.toICALString();
|
||||
}
|
||||
if ('wkst' in this && this.wkst !== Time.DEFAULT_WEEK_START) {
|
||||
str += ';WKST=' + Recur.numericDayToIcalDay(this.wkst);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
export default Recur;
|
||||
|
||||
function parseNumericValue(type, min, max, value) {
|
||||
let result = value;
|
||||
|
||||
if (value[0] === '+') {
|
||||
result = value.slice(1);
|
||||
}
|
||||
|
||||
result = strictParseInt(result);
|
||||
|
||||
if (min !== undefined && value < min) {
|
||||
throw new Error(
|
||||
type + ': invalid value "' + value + '" must be > ' + min
|
||||
);
|
||||
}
|
||||
|
||||
if (max !== undefined && value > max) {
|
||||
throw new Error(
|
||||
type + ': invalid value "' + value + '" must be < ' + min
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const optionDesign = {
|
||||
FREQ: function(value, dict, fmtIcal) {
|
||||
// yes this is actually equal or faster then regex.
|
||||
// upside here is we can enumerate the valid values.
|
||||
if (ALLOWED_FREQ.indexOf(value) !== -1) {
|
||||
dict.freq = value;
|
||||
} else {
|
||||
throw new Error(
|
||||
'invalid frequency "' + value + '" expected: "' +
|
||||
ALLOWED_FREQ.join(', ') + '"'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
COUNT: function(value, dict, fmtIcal) {
|
||||
dict.count = strictParseInt(value);
|
||||
},
|
||||
|
||||
INTERVAL: function(value, dict, fmtIcal) {
|
||||
dict.interval = strictParseInt(value);
|
||||
if (dict.interval < 1) {
|
||||
// 0 or negative values are not allowed, some engines seem to generate
|
||||
// it though. Assume 1 instead.
|
||||
dict.interval = 1;
|
||||
}
|
||||
},
|
||||
|
||||
UNTIL: function(value, dict, fmtIcal) {
|
||||
if (value.length > 10) {
|
||||
dict.until = design.icalendar.value['date-time'].fromICAL(value);
|
||||
} else {
|
||||
dict.until = design.icalendar.value.date.fromICAL(value);
|
||||
}
|
||||
if (!fmtIcal) {
|
||||
dict.until = Time.fromString(dict.until);
|
||||
}
|
||||
},
|
||||
|
||||
WKST: function(value, dict, fmtIcal) {
|
||||
if (VALID_DAY_NAMES.test(value)) {
|
||||
dict.wkst = Recur.icalDayToNumericDay(value);
|
||||
} else {
|
||||
throw new Error('invalid WKST value "' + value + '"');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const partDesign = {
|
||||
BYSECOND: parseNumericValue.bind(undefined, 'BYSECOND', 0, 60),
|
||||
BYMINUTE: parseNumericValue.bind(undefined, 'BYMINUTE', 0, 59),
|
||||
BYHOUR: parseNumericValue.bind(undefined, 'BYHOUR', 0, 23),
|
||||
BYDAY: function(value) {
|
||||
if (VALID_BYDAY_PART.test(value)) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error('invalid BYDAY value "' + value + '"');
|
||||
}
|
||||
},
|
||||
BYMONTHDAY: parseNumericValue.bind(undefined, 'BYMONTHDAY', -31, 31),
|
||||
BYYEARDAY: parseNumericValue.bind(undefined, 'BYYEARDAY', -366, 366),
|
||||
BYWEEKNO: parseNumericValue.bind(undefined, 'BYWEEKNO', -53, 53),
|
||||
BYMONTH: parseNumericValue.bind(undefined, 'BYMONTH', 1, 12),
|
||||
BYSETPOS: parseNumericValue.bind(undefined, 'BYSETPOS', -366, 366)
|
||||
};
|
|
@ -0,0 +1,475 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import Time from "./time.js";
|
||||
import RecurIterator from "./recur_iterator.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Component from "./component.js";
|
||||
import { formatClassType, binsearchInsert } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* Primary class for expanding recurring rules. Can take multiple rrules, rdates, exdate(s) and
|
||||
* iterate (in order) over each next occurrence.
|
||||
*
|
||||
* Once initialized this class can also be serialized saved and continue iteration from the last
|
||||
* point.
|
||||
*
|
||||
* NOTE: it is intended that this class is to be used with {@link ICAL.Event} which handles recurrence
|
||||
* exceptions.
|
||||
*
|
||||
* @example
|
||||
* // assuming event is a parsed ical component
|
||||
* var event;
|
||||
*
|
||||
* var expand = new ICAL.RecurExpansion({
|
||||
* component: event,
|
||||
* dtstart: event.getFirstPropertyValue('dtstart')
|
||||
* });
|
||||
*
|
||||
* // remember there are infinite rules so it is a good idea to limit the scope of the iterations
|
||||
* // then resume later on.
|
||||
*
|
||||
* // next is always an ICAL.Time or null
|
||||
* var next;
|
||||
*
|
||||
* while (someCondition && (next = expand.next())) {
|
||||
* // do something with next
|
||||
* }
|
||||
*
|
||||
* // save instance for later
|
||||
* var json = JSON.stringify(expand);
|
||||
*
|
||||
* //...
|
||||
*
|
||||
* // NOTE: if the component's properties have changed you will need to rebuild the class and start
|
||||
* // over. This only works when the component's recurrence info is the same.
|
||||
* var expand = new ICAL.RecurExpansion(JSON.parse(json));
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class RecurExpansion {
|
||||
/**
|
||||
* Creates a new ICAL.RecurExpansion instance.
|
||||
*
|
||||
* The options object can be filled with the specified initial values. It can also contain
|
||||
* additional members, as a result of serializing a previous expansion state, as shown in the
|
||||
* example.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Recurrence expansion options
|
||||
* @param {Time} options.dtstart
|
||||
* Start time of the event
|
||||
* @param {Component=} options.component
|
||||
* Component for expansion, required if not resuming.
|
||||
*/
|
||||
constructor(options) {
|
||||
this.ruleDates = [];
|
||||
this.exDates = [];
|
||||
this.fromData(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when iteration is fully completed.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
complete = false;
|
||||
|
||||
/**
|
||||
* Array of rrule iterators.
|
||||
*
|
||||
* @type {RecurIterator[]}
|
||||
* @private
|
||||
*/
|
||||
ruleIterators = null;
|
||||
|
||||
/**
|
||||
* Array of rdate instances.
|
||||
*
|
||||
* @type {Time[]}
|
||||
* @private
|
||||
*/
|
||||
ruleDates = null;
|
||||
|
||||
/**
|
||||
* Array of exdate instances.
|
||||
*
|
||||
* @type {Time[]}
|
||||
* @private
|
||||
*/
|
||||
exDates = null;
|
||||
|
||||
/**
|
||||
* Current position in ruleDates array.
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
ruleDateInc = 0;
|
||||
|
||||
/**
|
||||
* Current position in exDates array
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
exDateInc = 0;
|
||||
|
||||
/**
|
||||
* Current negative date.
|
||||
*
|
||||
* @type {Time}
|
||||
* @private
|
||||
*/
|
||||
exDate = null;
|
||||
|
||||
/**
|
||||
* Current additional date.
|
||||
*
|
||||
* @type {Time}
|
||||
* @private
|
||||
*/
|
||||
ruleDate = null;
|
||||
|
||||
/**
|
||||
* Start date of recurring rules.
|
||||
*
|
||||
* @type {Time}
|
||||
*/
|
||||
dtstart = null;
|
||||
|
||||
/**
|
||||
* Last expanded time
|
||||
*
|
||||
* @type {Time}
|
||||
*/
|
||||
last = null;
|
||||
|
||||
/**
|
||||
* Initialize the recurrence expansion from the data object. The options
|
||||
* object may also contain additional members, see the
|
||||
* {@link ICAL.RecurExpansion constructor} for more details.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Recurrence expansion options
|
||||
* @param {Time} options.dtstart
|
||||
* Start time of the event
|
||||
* @param {Component=} options.component
|
||||
* Component for expansion, required if not resuming.
|
||||
*/
|
||||
fromData(options) {
|
||||
let start = formatClassType(options.dtstart, Time);
|
||||
|
||||
if (!start) {
|
||||
throw new Error('.dtstart (ICAL.Time) must be given');
|
||||
} else {
|
||||
this.dtstart = start;
|
||||
}
|
||||
|
||||
if (options.component) {
|
||||
this._init(options.component);
|
||||
} else {
|
||||
this.last = formatClassType(options.last, Time) || start.clone();
|
||||
|
||||
if (!options.ruleIterators) {
|
||||
throw new Error('.ruleIterators or .component must be given');
|
||||
}
|
||||
|
||||
this.ruleIterators = options.ruleIterators.map(function(item) {
|
||||
return formatClassType(item, RecurIterator);
|
||||
});
|
||||
|
||||
this.ruleDateInc = options.ruleDateInc;
|
||||
this.exDateInc = options.exDateInc;
|
||||
|
||||
if (options.ruleDates) {
|
||||
this.ruleDates = options.ruleDates.map(item => formatClassType(item, Time));
|
||||
this.ruleDate = this.ruleDates[this.ruleDateInc];
|
||||
}
|
||||
|
||||
if (options.exDates) {
|
||||
this.exDates = options.exDates.map(item => formatClassType(item, Time));
|
||||
this.exDate = this.exDates[this.exDateInc];
|
||||
}
|
||||
|
||||
if (typeof(options.complete) !== 'undefined') {
|
||||
this.complete = options.complete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next occurrence in the series.
|
||||
* @return {Time}
|
||||
*/
|
||||
next() {
|
||||
let iter;
|
||||
let next;
|
||||
let compare;
|
||||
|
||||
let maxTries = 500;
|
||||
let currentTry = 0;
|
||||
|
||||
while (true) {
|
||||
if (currentTry++ > maxTries) {
|
||||
throw new Error(
|
||||
'max tries have occurred, rule may be impossible to fulfill.'
|
||||
);
|
||||
}
|
||||
|
||||
next = this.ruleDate;
|
||||
iter = this._nextRecurrenceIter(this.last);
|
||||
|
||||
// no more matches
|
||||
// because we increment the rule day or rule
|
||||
// _after_ we choose a value this should be
|
||||
// the only spot where we need to worry about the
|
||||
// end of events.
|
||||
if (!next && !iter) {
|
||||
// there are no more iterators or rdates
|
||||
this.complete = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// no next rule day or recurrence rule is first.
|
||||
if (!next || (iter && next.compare(iter.last) > 0)) {
|
||||
// must be cloned, recur will reuse the time element.
|
||||
next = iter.last.clone();
|
||||
// move to next so we can continue
|
||||
iter.next();
|
||||
}
|
||||
|
||||
// if the ruleDate is still next increment it.
|
||||
if (this.ruleDate === next) {
|
||||
this._nextRuleDay();
|
||||
}
|
||||
|
||||
this.last = next;
|
||||
|
||||
// check the negative rules
|
||||
if (this.exDate) {
|
||||
compare = this.exDate.compare(this.last);
|
||||
|
||||
if (compare < 0) {
|
||||
this._nextExDay();
|
||||
}
|
||||
|
||||
// if the current rule is excluded skip it.
|
||||
if (compare === 0) {
|
||||
this._nextExDay();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//XXX: The spec states that after we resolve the final
|
||||
// list of dates we execute exdate this seems somewhat counter
|
||||
// intuitive to what I have seen most servers do so for now
|
||||
// I exclude based on the original date not the one that may
|
||||
// have been modified by the exception.
|
||||
return this.last;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts object into a serialize-able format. This format can be passed
|
||||
* back into the expansion to resume iteration.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
function toJSON(item) {
|
||||
return item.toJSON();
|
||||
}
|
||||
|
||||
let result = Object.create(null);
|
||||
result.ruleIterators = this.ruleIterators.map(toJSON);
|
||||
|
||||
if (this.ruleDates) {
|
||||
result.ruleDates = this.ruleDates.map(toJSON);
|
||||
}
|
||||
|
||||
if (this.exDates) {
|
||||
result.exDates = this.exDates.map(toJSON);
|
||||
}
|
||||
|
||||
result.ruleDateInc = this.ruleDateInc;
|
||||
result.exDateInc = this.exDateInc;
|
||||
result.last = this.last.toJSON();
|
||||
result.dtstart = this.dtstart.toJSON();
|
||||
result.complete = this.complete;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all dates from the properties in the given component. The
|
||||
* properties will be filtered by the property name.
|
||||
*
|
||||
* @private
|
||||
* @param {Component} component The component to search in
|
||||
* @param {String} propertyName The property name to search for
|
||||
* @return {Time[]} The extracted dates.
|
||||
*/
|
||||
_extractDates(component, propertyName) {
|
||||
let result = [];
|
||||
let props = component.getAllProperties(propertyName);
|
||||
|
||||
for (let i = 0, len = props.length; i < len; i++) {
|
||||
for (let prop of props[i].getValues()) {
|
||||
let idx = binsearchInsert(
|
||||
result,
|
||||
prop,
|
||||
(a, b) => a.compare(b)
|
||||
);
|
||||
|
||||
// ordered insert
|
||||
result.splice(idx, 0, prop);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the recurrence expansion.
|
||||
*
|
||||
* @private
|
||||
* @param {Component} component The component to initialize from.
|
||||
*/
|
||||
_init(component) {
|
||||
this.ruleIterators = [];
|
||||
|
||||
this.last = this.dtstart.clone();
|
||||
|
||||
// to provide api consistency non-recurring
|
||||
// events can also use the iterator though it will
|
||||
// only return a single time.
|
||||
if (!component.hasProperty('rdate') &&
|
||||
!component.hasProperty('rrule') &&
|
||||
!component.hasProperty('recurrence-id')) {
|
||||
this.ruleDate = this.last.clone();
|
||||
this.complete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.hasProperty('rdate')) {
|
||||
this.ruleDates = this._extractDates(component, 'rdate');
|
||||
|
||||
// special hack for cases where first rdate is prior
|
||||
// to the start date. We only check for the first rdate.
|
||||
// This is mostly for google's crazy recurring date logic
|
||||
// (contacts birthdays).
|
||||
if ((this.ruleDates[0]) &&
|
||||
(this.ruleDates[0].compare(this.dtstart) < 0)) {
|
||||
|
||||
this.ruleDateInc = 0;
|
||||
this.last = this.ruleDates[0].clone();
|
||||
} else {
|
||||
this.ruleDateInc = binsearchInsert(
|
||||
this.ruleDates,
|
||||
this.last,
|
||||
(a, b) => a.compare(b)
|
||||
);
|
||||
}
|
||||
|
||||
this.ruleDate = this.ruleDates[this.ruleDateInc];
|
||||
}
|
||||
|
||||
if (component.hasProperty('rrule')) {
|
||||
let rules = component.getAllProperties('rrule');
|
||||
let i = 0;
|
||||
let len = rules.length;
|
||||
|
||||
let rule;
|
||||
let iter;
|
||||
|
||||
for (; i < len; i++) {
|
||||
rule = rules[i].getFirstValue();
|
||||
iter = rule.iterator(this.dtstart);
|
||||
this.ruleIterators.push(iter);
|
||||
|
||||
// increment to the next occurrence so future
|
||||
// calls to next return times beyond the initial iteration.
|
||||
// XXX: I find this suspicious might be a bug?
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
|
||||
if (component.hasProperty('exdate')) {
|
||||
this.exDates = this._extractDates(component, 'exdate');
|
||||
// if we have a .last day we increment the index to beyond it.
|
||||
this.exDateInc = binsearchInsert(
|
||||
this.exDates,
|
||||
this.last,
|
||||
(a, b) => a.compare(b)
|
||||
);
|
||||
|
||||
this.exDate = this.exDates[this.exDateInc];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance to the next exdate
|
||||
* @private
|
||||
*/
|
||||
_nextExDay() {
|
||||
this.exDate = this.exDates[++this.exDateInc];
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance to the next rule date
|
||||
* @private
|
||||
*/
|
||||
_nextRuleDay() {
|
||||
this.ruleDate = this.ruleDates[++this.ruleDateInc];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return the recurrence rule with the most recent event and
|
||||
* return it.
|
||||
*
|
||||
* @private
|
||||
* @return {?RecurIterator} Found iterator.
|
||||
*/
|
||||
_nextRecurrenceIter() {
|
||||
let iters = this.ruleIterators;
|
||||
|
||||
if (iters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let len = iters.length;
|
||||
let iter;
|
||||
let iterTime;
|
||||
let iterIdx = 0;
|
||||
let chosenIter;
|
||||
|
||||
// loop through each iterator
|
||||
for (; iterIdx < len; iterIdx++) {
|
||||
iter = iters[iterIdx];
|
||||
iterTime = iter.last;
|
||||
|
||||
// if iteration is complete
|
||||
// then we must exclude it from
|
||||
// the search and remove it.
|
||||
if (iter.completed) {
|
||||
len--;
|
||||
if (iterIdx !== 0) {
|
||||
iterIdx--;
|
||||
}
|
||||
iters.splice(iterIdx, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// find the most recent possible choice
|
||||
if (!chosenIter || chosenIter.last.compare(iterTime) > 0) {
|
||||
// that iterator is saved
|
||||
chosenIter = iter;
|
||||
}
|
||||
}
|
||||
|
||||
// the chosen iterator is returned but not mutated
|
||||
// this iterator contains the most recent event.
|
||||
return chosenIter;
|
||||
}
|
||||
}
|
||||
export default RecurExpansion;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,302 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import design from "./design.js";
|
||||
import { foldline } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
*
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").designSet} designSet
|
||||
* Imports the 'designSet' type from the "types.js" module
|
||||
*/
|
||||
|
||||
const LINE_ENDING = '\r\n';
|
||||
const DEFAULT_VALUE_TYPE = 'unknown';
|
||||
const RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" };
|
||||
|
||||
/**
|
||||
* Convert a full jCal/jCard array into a iCalendar/vCard string.
|
||||
*
|
||||
* @function ICAL.stringify
|
||||
* @variation function
|
||||
* @param {Array} jCal The jCal/jCard document
|
||||
* @return {String} The stringified iCalendar/vCard document
|
||||
*/
|
||||
export default function stringify(jCal) {
|
||||
if (typeof jCal[0] == "string") {
|
||||
// This is a single component
|
||||
jCal = [jCal];
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let len = jCal.length;
|
||||
let result = '';
|
||||
|
||||
for (; i < len; i++) {
|
||||
result += stringify.component(jCal[i]) + LINE_ENDING;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an jCal component array into a ICAL string.
|
||||
* Recursive will resolve sub-components.
|
||||
*
|
||||
* Exact component/property order is not saved all
|
||||
* properties will come before subcomponents.
|
||||
*
|
||||
* @function ICAL.stringify.component
|
||||
* @param {Array} component
|
||||
* jCal/jCard fragment of a component
|
||||
* @param {designSet} designSet
|
||||
* The design data to use for this component
|
||||
* @return {String} The iCalendar/vCard string
|
||||
*/
|
||||
stringify.component = function(component, designSet) {
|
||||
let name = component[0].toUpperCase();
|
||||
let result = 'BEGIN:' + name + LINE_ENDING;
|
||||
|
||||
let props = component[1];
|
||||
let propIdx = 0;
|
||||
let propLen = props.length;
|
||||
|
||||
let designSetName = component[0];
|
||||
// rfc6350 requires that in vCard 4.0 the first component is the VERSION
|
||||
// component with as value 4.0, note that 3.0 does not have this requirement.
|
||||
if (designSetName === 'vcard' && component[1].length > 0 &&
|
||||
!(component[1][0][0] === "version" && component[1][0][3] === "4.0")) {
|
||||
designSetName = "vcard3";
|
||||
}
|
||||
designSet = designSet || design.getDesignSet(designSetName);
|
||||
|
||||
for (; propIdx < propLen; propIdx++) {
|
||||
result += stringify.property(props[propIdx], designSet) + LINE_ENDING;
|
||||
}
|
||||
|
||||
// Ignore subcomponents if none exist, e.g. in vCard.
|
||||
let comps = component[2] || [];
|
||||
let compIdx = 0;
|
||||
let compLen = comps.length;
|
||||
|
||||
for (; compIdx < compLen; compIdx++) {
|
||||
result += stringify.component(comps[compIdx], designSet) + LINE_ENDING;
|
||||
}
|
||||
|
||||
result += 'END:' + name;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a single jCal/jCard property to a iCalendar/vCard string.
|
||||
*
|
||||
* @function ICAL.stringify.property
|
||||
* @param {Array} property
|
||||
* jCal/jCard property array
|
||||
* @param {designSet} designSet
|
||||
* The design data to use for this property
|
||||
* @param {Boolean} noFold
|
||||
* If true, the line is not folded
|
||||
* @return {String} The iCalendar/vCard string
|
||||
*/
|
||||
stringify.property = function(property, designSet, noFold) {
|
||||
let name = property[0].toUpperCase();
|
||||
let jsName = property[0];
|
||||
let params = property[1];
|
||||
|
||||
if (!designSet) {
|
||||
designSet = design.defaultSet;
|
||||
}
|
||||
|
||||
let groupName = params.group;
|
||||
let line;
|
||||
if (designSet.propertyGroups && groupName) {
|
||||
line = groupName.toUpperCase() + "." + name;
|
||||
} else {
|
||||
line = name;
|
||||
}
|
||||
|
||||
for (let [paramName, value] of Object.entries(params)) {
|
||||
if (designSet.propertyGroups && paramName == 'group') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let paramDesign = designSet.param[paramName];
|
||||
let multiValue = paramDesign && paramDesign.multiValue;
|
||||
if (multiValue && Array.isArray(value)) {
|
||||
value = value.map(function(val) {
|
||||
val = stringify._rfc6868Unescape(val);
|
||||
val = stringify.paramPropertyValue(val, paramDesign.multiValueSeparateDQuote);
|
||||
return val;
|
||||
});
|
||||
value = stringify.multiValue(value, multiValue, "unknown", null, designSet);
|
||||
} else {
|
||||
value = stringify._rfc6868Unescape(value);
|
||||
value = stringify.paramPropertyValue(value);
|
||||
}
|
||||
|
||||
line += ';' + paramName.toUpperCase() + '=' + value;
|
||||
}
|
||||
|
||||
if (property.length === 3) {
|
||||
// If there are no values, we must assume a blank value
|
||||
return line + ':';
|
||||
}
|
||||
|
||||
let valueType = property[2];
|
||||
|
||||
let propDetails;
|
||||
let multiValue = false;
|
||||
let structuredValue = false;
|
||||
let isDefault = false;
|
||||
|
||||
if (jsName in designSet.property) {
|
||||
propDetails = designSet.property[jsName];
|
||||
|
||||
if ('multiValue' in propDetails) {
|
||||
multiValue = propDetails.multiValue;
|
||||
}
|
||||
|
||||
if (('structuredValue' in propDetails) && Array.isArray(property[3])) {
|
||||
structuredValue = propDetails.structuredValue;
|
||||
}
|
||||
|
||||
if ('defaultType' in propDetails) {
|
||||
if (valueType === propDetails.defaultType) {
|
||||
isDefault = true;
|
||||
}
|
||||
} else {
|
||||
if (valueType === DEFAULT_VALUE_TYPE) {
|
||||
isDefault = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (valueType === DEFAULT_VALUE_TYPE) {
|
||||
isDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
// push the VALUE property if type is not the default
|
||||
// for the current property.
|
||||
if (!isDefault) {
|
||||
// value will never contain ;/:/, so we don't escape it here.
|
||||
line += ';VALUE=' + valueType.toUpperCase();
|
||||
}
|
||||
|
||||
line += ':';
|
||||
|
||||
if (multiValue && structuredValue) {
|
||||
line += stringify.multiValue(
|
||||
property[3], structuredValue, valueType, multiValue, designSet, structuredValue
|
||||
);
|
||||
} else if (multiValue) {
|
||||
line += stringify.multiValue(
|
||||
property.slice(3), multiValue, valueType, null, designSet, false
|
||||
);
|
||||
} else if (structuredValue) {
|
||||
line += stringify.multiValue(
|
||||
property[3], structuredValue, valueType, null, designSet, structuredValue
|
||||
);
|
||||
} else {
|
||||
line += stringify.value(property[3], valueType, designSet, false);
|
||||
}
|
||||
|
||||
return noFold ? line : foldline(line);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles escaping of property values that may contain:
|
||||
*
|
||||
* COLON (:), SEMICOLON (;), or COMMA (,)
|
||||
*
|
||||
* If any of the above are present the result is wrapped
|
||||
* in double quotes.
|
||||
*
|
||||
* @function ICAL.stringify.paramPropertyValue
|
||||
* @param {String} value Raw property value
|
||||
* @param {boolean} force If value should be escaped even when unnecessary
|
||||
* @return {String} Given or escaped value when needed
|
||||
*/
|
||||
stringify.paramPropertyValue = function(value, force) {
|
||||
if (!force &&
|
||||
(value.indexOf(',') === -1) &&
|
||||
(value.indexOf(':') === -1) &&
|
||||
(value.indexOf(';') === -1)) {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return '"' + value + '"';
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an array of ical values into a single
|
||||
* string based on a type and a delimiter value (like ",").
|
||||
*
|
||||
* @function ICAL.stringify.multiValue
|
||||
* @param {Array} values List of values to convert
|
||||
* @param {String} delim Used to join the values (",", ";", ":")
|
||||
* @param {String} type Lowecase ical value type
|
||||
* (like boolean, date-time, etc..)
|
||||
* @param {?String} innerMulti If set, each value will again be processed
|
||||
* Used for structured values
|
||||
* @param {designSet} designSet
|
||||
* The design data to use for this property
|
||||
*
|
||||
* @return {String} iCalendar/vCard string for value
|
||||
*/
|
||||
stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) {
|
||||
let result = '';
|
||||
let len = values.length;
|
||||
let i = 0;
|
||||
|
||||
for (; i < len; i++) {
|
||||
if (innerMulti && Array.isArray(values[i])) {
|
||||
result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue);
|
||||
} else {
|
||||
result += stringify.value(values[i], type, designSet, structuredValue);
|
||||
}
|
||||
|
||||
if (i !== (len - 1)) {
|
||||
result += delim;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes a single ical value runs the associated "toICAL" method from the
|
||||
* design value type if available to convert the value.
|
||||
*
|
||||
* @function ICAL.stringify.value
|
||||
* @param {String|Number} value A formatted value
|
||||
* @param {String} type Lowercase iCalendar/vCard value type
|
||||
* (like boolean, date-time, etc..)
|
||||
* @return {String} iCalendar/vCard value for single value
|
||||
*/
|
||||
stringify.value = function(value, type, designSet, structuredValue) {
|
||||
if (type in designSet.value && 'toICAL' in designSet.value[type]) {
|
||||
return designSet.value[type].toICAL(value, structuredValue);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal helper for rfc6868. Exposing this on ICAL.stringify so that
|
||||
* hackers can disable the rfc6868 parsing if the really need to.
|
||||
*
|
||||
* @param {String} val The value to unescape
|
||||
* @return {String} The escaped value
|
||||
*/
|
||||
stringify._rfc6868Unescape = function(val) {
|
||||
return val.replace(/[\n^"]/g, function(x) {
|
||||
return RFC6868_REPLACE_MAP[x];
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,526 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import Time from "./time.js";
|
||||
import Component from "./component.js";
|
||||
import ICALParse from "./parse.js";
|
||||
import { clone, binsearchInsert } from "./helpers.js";
|
||||
|
||||
const OPTIONS = ["tzid", "location", "tznames", "latitude", "longitude"];
|
||||
|
||||
/**
|
||||
* Timezone representation.
|
||||
*
|
||||
* @example
|
||||
* var vcalendar;
|
||||
* var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone');
|
||||
* var tzid = timezoneComp.getFirstPropertyValue('tzid');
|
||||
*
|
||||
* var timezone = new ICAL.Timezone({
|
||||
* component: timezoneComp,
|
||||
* tzid
|
||||
* });
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Timezone {
|
||||
static _compare_change_fn(a, b) {
|
||||
if (a.year < b.year) return -1;
|
||||
else if (a.year > b.year) return 1;
|
||||
|
||||
if (a.month < b.month) return -1;
|
||||
else if (a.month > b.month) return 1;
|
||||
|
||||
if (a.day < b.day) return -1;
|
||||
else if (a.day > b.day) return 1;
|
||||
|
||||
if (a.hour < b.hour) return -1;
|
||||
else if (a.hour > b.hour) return 1;
|
||||
|
||||
if (a.minute < b.minute) return -1;
|
||||
else if (a.minute > b.minute) return 1;
|
||||
|
||||
if (a.second < b.second) return -1;
|
||||
else if (a.second > b.second) return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the date/time from one zone to the next.
|
||||
*
|
||||
* @param {Time} tt The time to convert
|
||||
* @param {Timezone} from_zone The source zone to convert from
|
||||
* @param {Timezone} to_zone The target zone to convert to
|
||||
* @return {Time} The converted date/time object
|
||||
*/
|
||||
static convert_time(tt, from_zone, to_zone) {
|
||||
if (tt.isDate ||
|
||||
from_zone.tzid == to_zone.tzid ||
|
||||
from_zone == Timezone.localTimezone ||
|
||||
to_zone == Timezone.localTimezone) {
|
||||
tt.zone = to_zone;
|
||||
return tt;
|
||||
}
|
||||
|
||||
let utcOffset = from_zone.utcOffset(tt);
|
||||
tt.adjust(0, 0, 0, - utcOffset);
|
||||
|
||||
utcOffset = to_zone.utcOffset(tt);
|
||||
tt.adjust(0, 0, 0, utcOffset);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Timezone instance from the passed data object.
|
||||
*
|
||||
* @param {Component|Object} aData options for class
|
||||
* @param {String|Component} aData.component
|
||||
* If aData is a simple object, then this member can be set to either a
|
||||
* string containing the component data, or an already parsed
|
||||
* ICAL.Component
|
||||
* @param {String} aData.tzid The timezone identifier
|
||||
* @param {String} aData.location The timezone locationw
|
||||
* @param {String} aData.tznames An alternative string representation of the
|
||||
* timezone
|
||||
* @param {Number} aData.latitude The latitude of the timezone
|
||||
* @param {Number} aData.longitude The longitude of the timezone
|
||||
*/
|
||||
static fromData(aData) {
|
||||
let tt = new Timezone();
|
||||
return tt.fromData(aData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance describing the UTC timezone
|
||||
* @type {Timezone}
|
||||
* @constant
|
||||
* @instance
|
||||
*/
|
||||
static #utcTimezone = null;
|
||||
static get utcTimezone() {
|
||||
if (!this.#utcTimezone) {
|
||||
this.#utcTimezone = Timezone.fromData({
|
||||
tzid: "UTC"
|
||||
});
|
||||
}
|
||||
return this.#utcTimezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance describing the local timezone
|
||||
* @type {Timezone}
|
||||
* @constant
|
||||
* @instance
|
||||
*/
|
||||
static #localTimezone = null;
|
||||
static get localTimezone() {
|
||||
if (!this.#localTimezone) {
|
||||
this.#localTimezone = Timezone.fromData({
|
||||
tzid: "floating"
|
||||
});
|
||||
}
|
||||
return this.#localTimezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust a timezone change object.
|
||||
* @private
|
||||
* @param {Object} change The timezone change object
|
||||
* @param {Number} days The extra amount of days
|
||||
* @param {Number} hours The extra amount of hours
|
||||
* @param {Number} minutes The extra amount of minutes
|
||||
* @param {Number} seconds The extra amount of seconds
|
||||
*/
|
||||
static adjust_change(change, days, hours, minutes, seconds) {
|
||||
return Time.prototype.adjust.call(
|
||||
change,
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
change
|
||||
);
|
||||
}
|
||||
|
||||
static _minimumExpansionYear = -1;
|
||||
static EXTRA_COVERAGE = 5;
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.Timezone instance, by passing in a tzid and component.
|
||||
*
|
||||
* @param {Component|Object} data options for class
|
||||
* @param {String|Component} data.component
|
||||
* If data is a simple object, then this member can be set to either a
|
||||
* string containing the component data, or an already parsed
|
||||
* ICAL.Component
|
||||
* @param {String} data.tzid The timezone identifier
|
||||
* @param {String} data.location The timezone locationw
|
||||
* @param {String} data.tznames An alternative string representation of the
|
||||
* timezone
|
||||
* @param {Number} data.latitude The latitude of the timezone
|
||||
* @param {Number} data.longitude The longitude of the timezone
|
||||
*/
|
||||
constructor(data) {
|
||||
this.wrappedJSObject = this;
|
||||
this.fromData(data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Timezone identifier
|
||||
* @type {String}
|
||||
*/
|
||||
tzid = "";
|
||||
|
||||
/**
|
||||
* Timezone location
|
||||
* @type {String}
|
||||
*/
|
||||
location = "";
|
||||
|
||||
/**
|
||||
* Alternative timezone name, for the string representation
|
||||
* @type {String}
|
||||
*/
|
||||
tznames = "";
|
||||
|
||||
/**
|
||||
* The primary latitude for the timezone.
|
||||
* @type {Number}
|
||||
*/
|
||||
latitude = 0.0;
|
||||
|
||||
/**
|
||||
* The primary longitude for the timezone.
|
||||
* @type {Number}
|
||||
*/
|
||||
longitude = 0.0;
|
||||
|
||||
/**
|
||||
* The vtimezone component for this timezone.
|
||||
* @type {Component}
|
||||
*/
|
||||
component = null;
|
||||
|
||||
/**
|
||||
* The year this timezone has been expanded to. All timezone transition
|
||||
* dates until this year are known and can be used for calculation
|
||||
*
|
||||
* @private
|
||||
* @type {Number}
|
||||
*/
|
||||
expandedUntilYear = 0;
|
||||
|
||||
/**
|
||||
* The class identifier.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "icaltimezone"
|
||||
*/
|
||||
icalclass = "icaltimezone";
|
||||
|
||||
/**
|
||||
* Sets up the current instance using members from the passed data object.
|
||||
*
|
||||
* @param {Component|Object} aData options for class
|
||||
* @param {String|Component} aData.component
|
||||
* If aData is a simple object, then this member can be set to either a
|
||||
* string containing the component data, or an already parsed
|
||||
* ICAL.Component
|
||||
* @param {String} aData.tzid The timezone identifier
|
||||
* @param {String} aData.location The timezone locationw
|
||||
* @param {String} aData.tznames An alternative string representation of the
|
||||
* timezone
|
||||
* @param {Number} aData.latitude The latitude of the timezone
|
||||
* @param {Number} aData.longitude The longitude of the timezone
|
||||
*/
|
||||
fromData(aData) {
|
||||
this.expandedUntilYear = 0;
|
||||
this.changes = [];
|
||||
|
||||
if (aData instanceof Component) {
|
||||
// Either a component is passed directly
|
||||
this.component = aData;
|
||||
} else {
|
||||
// Otherwise the component may be in the data object
|
||||
if (aData && "component" in aData) {
|
||||
if (typeof aData.component == "string") {
|
||||
// If a string was passed, parse it as a component
|
||||
let jCal = ICALParse(aData.component);
|
||||
this.component = new Component(jCal);
|
||||
} else if (aData.component instanceof Component) {
|
||||
// If it was a component already, then just set it
|
||||
this.component = aData.component;
|
||||
} else {
|
||||
// Otherwise just null out the component
|
||||
this.component = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy remaining passed properties
|
||||
for (let prop of OPTIONS) {
|
||||
if (aData && prop in aData) {
|
||||
this[prop] = aData[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a component but no TZID, attempt to get it from the
|
||||
// component's properties.
|
||||
if (this.component instanceof Component && !this.tzid) {
|
||||
this.tzid = this.component.getFirstPropertyValue('tzid');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the utcOffset the given time would occur in this timezone.
|
||||
*
|
||||
* @param {Time} tt The time to check for
|
||||
* @return {Number} utc offset in seconds
|
||||
*/
|
||||
utcOffset(tt) {
|
||||
if (this == Timezone.utcTimezone || this == Timezone.localTimezone) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this._ensureCoverage(tt.year);
|
||||
|
||||
if (!this.changes.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let tt_change = {
|
||||
year: tt.year,
|
||||
month: tt.month,
|
||||
day: tt.day,
|
||||
hour: tt.hour,
|
||||
minute: tt.minute,
|
||||
second: tt.second
|
||||
};
|
||||
|
||||
let change_num = this._findNearbyChange(tt_change);
|
||||
let change_num_to_use = -1;
|
||||
let step = 1;
|
||||
|
||||
// TODO: replace with bin search?
|
||||
for (;;) {
|
||||
let change = clone(this.changes[change_num], true);
|
||||
if (change.utcOffset < change.prevUtcOffset) {
|
||||
Timezone.adjust_change(change, 0, 0, 0, change.utcOffset);
|
||||
} else {
|
||||
Timezone.adjust_change(change, 0, 0, 0,
|
||||
change.prevUtcOffset);
|
||||
}
|
||||
|
||||
let cmp = Timezone._compare_change_fn(tt_change, change);
|
||||
|
||||
if (cmp >= 0) {
|
||||
change_num_to_use = change_num;
|
||||
} else {
|
||||
step = -1;
|
||||
}
|
||||
|
||||
if (step == -1 && change_num_to_use != -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
change_num += step;
|
||||
|
||||
if (change_num < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (change_num >= this.changes.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let zone_change = this.changes[change_num_to_use];
|
||||
let utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset;
|
||||
|
||||
if (utcOffset_change < 0 && change_num_to_use > 0) {
|
||||
let tmp_change = clone(zone_change, true);
|
||||
Timezone.adjust_change(tmp_change, 0, 0, 0, tmp_change.prevUtcOffset);
|
||||
|
||||
if (Timezone._compare_change_fn(tt_change, tmp_change) < 0) {
|
||||
let prev_zone_change = this.changes[change_num_to_use - 1];
|
||||
|
||||
let want_daylight = false; // TODO
|
||||
|
||||
if (zone_change.is_daylight != want_daylight &&
|
||||
prev_zone_change.is_daylight == want_daylight) {
|
||||
zone_change = prev_zone_change;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO return is_daylight?
|
||||
return zone_change.utcOffset;
|
||||
}
|
||||
|
||||
_findNearbyChange(change) {
|
||||
// find the closest match
|
||||
let idx = binsearchInsert(
|
||||
this.changes,
|
||||
change,
|
||||
Timezone._compare_change_fn
|
||||
);
|
||||
|
||||
if (idx >= this.changes.length) {
|
||||
return this.changes.length - 1;
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
_ensureCoverage(aYear) {
|
||||
if (Timezone._minimumExpansionYear == -1) {
|
||||
let today = Time.now();
|
||||
Timezone._minimumExpansionYear = today.year;
|
||||
}
|
||||
|
||||
let changesEndYear = aYear;
|
||||
if (changesEndYear < Timezone._minimumExpansionYear) {
|
||||
changesEndYear = Timezone._minimumExpansionYear;
|
||||
}
|
||||
|
||||
changesEndYear += Timezone.EXTRA_COVERAGE;
|
||||
|
||||
if (!this.changes.length || this.expandedUntilYear < aYear) {
|
||||
let subcomps = this.component.getAllSubcomponents();
|
||||
let compLen = subcomps.length;
|
||||
let compIdx = 0;
|
||||
|
||||
for (; compIdx < compLen; compIdx++) {
|
||||
this._expandComponent(
|
||||
subcomps[compIdx], changesEndYear, this.changes
|
||||
);
|
||||
}
|
||||
|
||||
this.changes.sort(Timezone._compare_change_fn);
|
||||
this.expandedUntilYear = changesEndYear;
|
||||
}
|
||||
}
|
||||
|
||||
_expandComponent(aComponent, aYear, changes) {
|
||||
if (!aComponent.hasProperty("dtstart") ||
|
||||
!aComponent.hasProperty("tzoffsetto") ||
|
||||
!aComponent.hasProperty("tzoffsetfrom")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dtstart = aComponent.getFirstProperty("dtstart").getFirstValue();
|
||||
let change;
|
||||
|
||||
function convert_tzoffset(offset) {
|
||||
return offset.factor * (offset.hours * 3600 + offset.minutes * 60);
|
||||
}
|
||||
|
||||
function init_changes() {
|
||||
let changebase = {};
|
||||
changebase.is_daylight = (aComponent.name == "daylight");
|
||||
changebase.utcOffset = convert_tzoffset(
|
||||
aComponent.getFirstProperty("tzoffsetto").getFirstValue()
|
||||
);
|
||||
|
||||
changebase.prevUtcOffset = convert_tzoffset(
|
||||
aComponent.getFirstProperty("tzoffsetfrom").getFirstValue()
|
||||
);
|
||||
|
||||
return changebase;
|
||||
}
|
||||
|
||||
if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) {
|
||||
change = init_changes();
|
||||
change.year = dtstart.year;
|
||||
change.month = dtstart.month;
|
||||
change.day = dtstart.day;
|
||||
change.hour = dtstart.hour;
|
||||
change.minute = dtstart.minute;
|
||||
change.second = dtstart.second;
|
||||
|
||||
Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset);
|
||||
changes.push(change);
|
||||
} else {
|
||||
let props = aComponent.getAllProperties("rdate");
|
||||
for (let rdate of props) {
|
||||
let time = rdate.getFirstValue();
|
||||
change = init_changes();
|
||||
|
||||
change.year = time.year;
|
||||
change.month = time.month;
|
||||
change.day = time.day;
|
||||
|
||||
if (time.isDate) {
|
||||
change.hour = dtstart.hour;
|
||||
change.minute = dtstart.minute;
|
||||
change.second = dtstart.second;
|
||||
|
||||
if (dtstart.zone != Timezone.utcTimezone) {
|
||||
Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset);
|
||||
}
|
||||
} else {
|
||||
change.hour = time.hour;
|
||||
change.minute = time.minute;
|
||||
change.second = time.second;
|
||||
|
||||
if (time.zone != Timezone.utcTimezone) {
|
||||
Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset);
|
||||
}
|
||||
}
|
||||
|
||||
changes.push(change);
|
||||
}
|
||||
|
||||
let rrule = aComponent.getFirstProperty("rrule");
|
||||
|
||||
if (rrule) {
|
||||
rrule = rrule.getFirstValue();
|
||||
change = init_changes();
|
||||
|
||||
if (rrule.until && rrule.until.zone == Timezone.utcTimezone) {
|
||||
rrule.until.adjust(0, 0, 0, change.prevUtcOffset);
|
||||
rrule.until.zone = Timezone.localTimezone;
|
||||
}
|
||||
|
||||
let iterator = rrule.iterator(dtstart);
|
||||
|
||||
let occ;
|
||||
while ((occ = iterator.next())) {
|
||||
change = init_changes();
|
||||
if (occ.year > aYear || !occ) {
|
||||
break;
|
||||
}
|
||||
|
||||
change.year = occ.year;
|
||||
change.month = occ.month;
|
||||
change.day = occ.day;
|
||||
change.hour = occ.hour;
|
||||
change.minute = occ.minute;
|
||||
change.second = occ.second;
|
||||
change.isDate = occ.isDate;
|
||||
|
||||
Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset);
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this timezone.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return (this.tznames ? this.tznames : this.tzid);
|
||||
}
|
||||
}
|
||||
export default Timezone;
|
|
@ -0,0 +1,129 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import Timezone from "./timezone.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Component from "./component.js";
|
||||
|
||||
let zones = null;
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* Singleton class to contain timezones. Right now it is all manual registry in
|
||||
* the future we may use this class to download timezone information or handle
|
||||
* loading pre-expanded timezones.
|
||||
*
|
||||
* @exports module:ICAL.TimezoneService
|
||||
* @memberof ICAL
|
||||
*/
|
||||
const TimezoneService = {
|
||||
get count() {
|
||||
if (zones === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Object.keys(zones).length;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
zones = Object.create(null);
|
||||
let utc = Timezone.utcTimezone;
|
||||
|
||||
zones.Z = utc;
|
||||
zones.UTC = utc;
|
||||
zones.GMT = utc;
|
||||
},
|
||||
_hard_reset: function() {
|
||||
zones = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if timezone id has been registered.
|
||||
*
|
||||
* @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
|
||||
* @return {Boolean} False, when not present
|
||||
*/
|
||||
has: function(tzid) {
|
||||
if (zones === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!zones[tzid];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a timezone by its tzid if present.
|
||||
*
|
||||
* @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
|
||||
* @return {Timezone | undefined} The timezone, or undefined if not found
|
||||
*/
|
||||
get: function(tzid) {
|
||||
if (zones === null) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
return zones[tzid];
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a timezone object or component.
|
||||
*
|
||||
* @param {Component|Timezone} timezone
|
||||
* The initialized zone or vtimezone.
|
||||
*
|
||||
* @param {String=} name
|
||||
* The name of the timezone. Defaults to the component's TZID if not
|
||||
* passed.
|
||||
*/
|
||||
register: function(timezone, name) {
|
||||
if (zones === null) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// This avoids a breaking change by the change of argument order
|
||||
// TODO remove in v3
|
||||
if (typeof timezone === "string" && name instanceof Timezone) {
|
||||
[timezone, name] = [name, timezone];
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
if (timezone instanceof Timezone) {
|
||||
name = timezone.tzid;
|
||||
} else {
|
||||
if (timezone.name === 'vtimezone') {
|
||||
timezone = new Timezone(timezone);
|
||||
name = timezone.tzid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new TypeError("Neither a timezone nor a name was passed");
|
||||
}
|
||||
|
||||
if (timezone instanceof Timezone) {
|
||||
zones[name] = timezone;
|
||||
} else {
|
||||
throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a timezone by its tzid from the list.
|
||||
*
|
||||
* @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
|
||||
* @return {?Timezone} The removed timezone, or null if not registered
|
||||
*/
|
||||
remove: function(tzid) {
|
||||
if (zones === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (delete zones[tzid]);
|
||||
}
|
||||
};
|
||||
|
||||
export default TimezoneService;
|
|
@ -0,0 +1,93 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch, 2024 */
|
||||
|
||||
// TODO: Once https://github.com/microsoft/TypeScript/issues/22160 and
|
||||
// https://github.com/microsoft/TypeScript/issues/46011 is fixed, update
|
||||
// @typedef(import(...)) to @import to avoid re-exporting the typedefs
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
// needed for typescript type resolution
|
||||
import Component from "./component";
|
||||
import Event from "./event";
|
||||
import Time from "./time";
|
||||
|
||||
/**
|
||||
* The weekday, 1 = SUNDAY, 7 = SATURDAY. Access via
|
||||
* ICAL.Time.MONDAY, ICAL.Time.TUESDAY, ...
|
||||
*
|
||||
* @typedef {Number} weekDay
|
||||
* @memberof ICAL.Time
|
||||
*/
|
||||
|
||||
/**
|
||||
* Possible frequency values for the FREQ part
|
||||
* (YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY)
|
||||
*
|
||||
* @typedef {String} frequencyValues
|
||||
* @memberof ICAL.Recur
|
||||
*/
|
||||
|
||||
/**
|
||||
* This object is returned by {@link ICAL.Event#getOccurrenceDetails getOccurrenceDetails}
|
||||
* @memberof ICAL.Event
|
||||
* @typedef {Object} occurrenceDetails
|
||||
* @property {Time} recurrenceId The passed in recurrence id
|
||||
* @property {Event} item The occurrence
|
||||
* @property {Time} startDate The start of the occurrence
|
||||
* @property {Time} endDate The end of the occurrence
|
||||
*/
|
||||
|
||||
/**
|
||||
* The state for parsing content lines from an iCalendar/vCard string.
|
||||
*
|
||||
* @private
|
||||
* @memberof ICAL.parse
|
||||
* @typedef {Object} parserState
|
||||
* @property {designSet} designSet The design set to use for parsing
|
||||
* @property {Component[]} stack The stack of components being processed
|
||||
* @property {Component} component The currently active component
|
||||
*/
|
||||
|
||||
/**
|
||||
* A jCal component.
|
||||
*
|
||||
* TODO: Properly typedef this when https://github.com/hegemonic/catharsis/pull/70
|
||||
* is merged. Documentation is ignored until this can be documented properly.
|
||||
*
|
||||
* @example
|
||||
* ["vevent", [...properties here...], [...components here...] ]
|
||||
*
|
||||
* @ignore
|
||||
* @typedef {Array} jCalComponent
|
||||
* @property {String} 0 The component name
|
||||
* @property {jCalProperty[]} 1 The properties of this component
|
||||
* @property {jCalComponent[]} 2 The subcomponents of this component
|
||||
*/
|
||||
|
||||
/**
|
||||
* A designSet describes value, parameter and property data. It is used by
|
||||
* ther parser and stringifier in components and properties to determine they
|
||||
* should be represented.
|
||||
*
|
||||
* @memberof ICAL.design
|
||||
* @typedef {Object} designSet
|
||||
* @property {Object} value Definitions for value types, keys are type names
|
||||
* @property {Object} param Definitions for params, keys are param names
|
||||
* @property {Object} property Definitions for properties, keys are property names
|
||||
* @property {boolean} propertyGroups If content lines may include a group name
|
||||
*/
|
||||
|
||||
/**
|
||||
* The jCal Geo type. This is a tuple representing a geographical location.
|
||||
* The first element is the Latitude and the second element is the Longitude.
|
||||
*
|
||||
* TODO: Properly typedef this when https://github.com/hegemonic/catharsis/pull/70
|
||||
*
|
||||
* @typedef {Array} Geo
|
||||
* @property {Number} 0 Latitude
|
||||
* @property {Number} 1 Longitude
|
||||
*/
|
||||
|
||||
export const _ = {};
|
|
@ -0,0 +1,187 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import { strictParseInt, trunc, pad2 } from "./helpers.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Duration from "./duration.js";
|
||||
import design from "./design.js";
|
||||
|
||||
/**
|
||||
* This class represents the "utc-offset" value type, with various calculation and manipulation
|
||||
* methods.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class UtcOffset {
|
||||
/**
|
||||
* Creates a new {@link ICAL.UtcOffset} instance from the passed string.
|
||||
*
|
||||
* @param {String} aString The string to parse
|
||||
* @return {Duration} The created utc-offset instance
|
||||
*/
|
||||
static fromString(aString) {
|
||||
// -05:00
|
||||
let options = {};
|
||||
//TODO: support seconds per rfc5545 ?
|
||||
options.factor = (aString[0] === '+') ? 1 : -1;
|
||||
options.hours = strictParseInt(aString.slice(1, 3));
|
||||
options.minutes = strictParseInt(aString.slice(4, 6));
|
||||
|
||||
return new UtcOffset(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ICAL.UtcOffset} instance from the passed seconds
|
||||
* value.
|
||||
*
|
||||
* @param {Number} aSeconds The number of seconds to convert
|
||||
*/
|
||||
static fromSeconds(aSeconds) {
|
||||
let instance = new UtcOffset();
|
||||
instance.fromSeconds(aSeconds);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.UtcOffset instance.
|
||||
*
|
||||
* @param {Object} aData An object with members of the utc offset
|
||||
* @param {Number=} aData.hours The hours for the utc offset
|
||||
* @param {Number=} aData.minutes The minutes in the utc offset
|
||||
* @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1
|
||||
*/
|
||||
constructor(aData) {
|
||||
this.fromData(aData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The hours in the utc-offset
|
||||
* @type {Number}
|
||||
*/
|
||||
hours = 0;
|
||||
|
||||
/**
|
||||
* The minutes in the utc-offset
|
||||
* @type {Number}
|
||||
*/
|
||||
minutes = 0;
|
||||
|
||||
/**
|
||||
* The sign of the utc offset, 1 for positive offset, -1 for negative
|
||||
* offsets.
|
||||
* @type {Number}
|
||||
*/
|
||||
factor = 1;
|
||||
|
||||
/**
|
||||
* The type name, to be used in the jCal object.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "utc-offset"
|
||||
*/
|
||||
icaltype = "utc-offset";
|
||||
|
||||
/**
|
||||
* Returns a clone of the utc offset object.
|
||||
*
|
||||
* @return {UtcOffset} The cloned object
|
||||
*/
|
||||
clone() {
|
||||
return UtcOffset.fromSeconds(this.toSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the current instance using members from the passed data object.
|
||||
*
|
||||
* @param {Object} aData An object with members of the utc offset
|
||||
* @param {Number=} aData.hours The hours for the utc offset
|
||||
* @param {Number=} aData.minutes The minutes in the utc offset
|
||||
* @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1
|
||||
*/
|
||||
fromData(aData) {
|
||||
if (aData) {
|
||||
for (let [key, value] of Object.entries(aData)) {
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
this._normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the current instance from the given seconds value. The seconds
|
||||
* value is truncated to the minute. Offsets are wrapped when the world
|
||||
* ends, the hour after UTC+14:00 is UTC-12:00.
|
||||
*
|
||||
* @param {Number} aSeconds The seconds to convert into an offset
|
||||
*/
|
||||
fromSeconds(aSeconds) {
|
||||
let secs = Math.abs(aSeconds);
|
||||
|
||||
this.factor = aSeconds < 0 ? -1 : 1;
|
||||
this.hours = trunc(secs / 3600);
|
||||
|
||||
secs -= (this.hours * 3600);
|
||||
this.minutes = trunc(secs / 60);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the current offset to a value in seconds
|
||||
*
|
||||
* @return {Number} The offset in seconds
|
||||
*/
|
||||
toSeconds() {
|
||||
return this.factor * (60 * this.minutes + 3600 * this.hours);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare this utc offset with another one.
|
||||
*
|
||||
* @param {UtcOffset} other The other offset to compare with
|
||||
* @return {Number} -1, 0 or 1 for less/equal/greater
|
||||
*/
|
||||
compare(other) {
|
||||
let a = this.toSeconds();
|
||||
let b = other.toSeconds();
|
||||
return (a > b) - (b > a);
|
||||
}
|
||||
|
||||
_normalize() {
|
||||
// Range: 97200 seconds (with 1 hour inbetween)
|
||||
let secs = this.toSeconds();
|
||||
let factor = this.factor;
|
||||
while (secs < -43200) { // = UTC-12:00
|
||||
secs += 97200;
|
||||
}
|
||||
while (secs > 50400) { // = UTC+14:00
|
||||
secs -= 97200;
|
||||
}
|
||||
|
||||
this.fromSeconds(secs);
|
||||
|
||||
// Avoid changing the factor when on zero seconds
|
||||
if (secs == 0) {
|
||||
this.factor = factor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The iCalendar string representation of this utc-offset.
|
||||
* @return {String}
|
||||
*/
|
||||
toICALString() {
|
||||
return design.icalendar.value['utc-offset'].toICAL(this.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this utc-offset.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return (this.factor == 1 ? "+" : "-") + pad2(this.hours) + ':' + pad2(this.minutes);
|
||||
}
|
||||
}
|
||||
export default UtcOffset;
|
|
@ -0,0 +1,183 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */
|
||||
|
||||
import UtcOffset from "./utc_offset.js";
|
||||
import Time from "./time.js";
|
||||
import Timezone from "./timezone.js";
|
||||
import design from "./design.js";
|
||||
import { pad2, strictParseInt } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* Describes a vCard time, which has slight differences to the ICAL.Time.
|
||||
* Properties can be null if not specified, for example for dates with
|
||||
* reduced accuracy or truncation.
|
||||
*
|
||||
* Note that currently not all methods are correctly re-implemented for
|
||||
* VCardTime. For example, comparison will have undefined results when some
|
||||
* members are null.
|
||||
*
|
||||
* Also, normalization is not yet implemented for this class!
|
||||
*
|
||||
* @memberof ICAL
|
||||
* @extends {ICAL.Time}
|
||||
*/
|
||||
class VCardTime extends Time {
|
||||
/**
|
||||
* Returns a new ICAL.VCardTime instance from a date and/or time string.
|
||||
*
|
||||
* @param {String} aValue The string to create from
|
||||
* @param {String} aIcalType The type for this instance, e.g. date-and-or-time
|
||||
* @return {VCardTime} The date/time instance
|
||||
*/
|
||||
static fromDateAndOrTimeString(aValue, aIcalType) {
|
||||
function part(v, s, e) {
|
||||
return v ? strictParseInt(v.slice(s, s + e)) : null;
|
||||
}
|
||||
let parts = aValue.split('T');
|
||||
let dt = parts[0], tmz = parts[1];
|
||||
let splitzone = tmz ? design.vcard.value.time._splitZone(tmz) : [];
|
||||
let zone = splitzone[0], tm = splitzone[1];
|
||||
|
||||
let dtlen = dt ? dt.length : 0;
|
||||
let tmlen = tm ? tm.length : 0;
|
||||
|
||||
let hasDashDate = dt && dt[0] == '-' && dt[1] == '-';
|
||||
let hasDashTime = tm && tm[0] == '-';
|
||||
|
||||
let o = {
|
||||
year: hasDashDate ? null : part(dt, 0, 4),
|
||||
month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null,
|
||||
day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null,
|
||||
|
||||
hour: hasDashTime ? null : part(tm, 0, 2),
|
||||
minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null,
|
||||
second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null
|
||||
};
|
||||
|
||||
if (zone == 'Z') {
|
||||
zone = Timezone.utcTimezone;
|
||||
} else if (zone && zone[3] == ':') {
|
||||
zone = UtcOffset.fromString(zone);
|
||||
} else {
|
||||
zone = null;
|
||||
}
|
||||
|
||||
return new VCardTime(o, zone, aIcalType);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new ICAL.VCardTime instance.
|
||||
*
|
||||
* @param {Object} data The data for the time instance
|
||||
* @param {Number=} data.year The year for this date
|
||||
* @param {Number=} data.month The month for this date
|
||||
* @param {Number=} data.day The day for this date
|
||||
* @param {Number=} data.hour The hour for this date
|
||||
* @param {Number=} data.minute The minute for this date
|
||||
* @param {Number=} data.second The second for this date
|
||||
* @param {Timezone|UtcOffset} zone The timezone to use
|
||||
* @param {String} icaltype The type for this date/time object
|
||||
*/
|
||||
constructor(data, zone, icaltype) {
|
||||
super(data, zone);
|
||||
this.icaltype = icaltype || "date-and-or-time";
|
||||
}
|
||||
|
||||
/**
|
||||
* The class identifier.
|
||||
* @constant
|
||||
* @type {String}
|
||||
* @default "vcardtime"
|
||||
*/
|
||||
icalclass = "vcardtime";
|
||||
|
||||
/**
|
||||
* The type name, to be used in the jCal object.
|
||||
* @type {String}
|
||||
* @default "date-and-or-time"
|
||||
*/
|
||||
icaltype = "date-and-or-time";
|
||||
|
||||
/**
|
||||
* Returns a clone of the vcard date/time object.
|
||||
*
|
||||
* @return {VCardTime} The cloned object
|
||||
*/
|
||||
clone() {
|
||||
return new VCardTime(this._time, this.zone, this.icaltype);
|
||||
}
|
||||
|
||||
_normalize() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
utcOffset() {
|
||||
if (this.zone instanceof UtcOffset) {
|
||||
return this.zone.toSeconds();
|
||||
} else {
|
||||
return Time.prototype.utcOffset.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an RFC 6350 compliant representation of this object.
|
||||
*
|
||||
* @return {String} vcard date/time string
|
||||
*/
|
||||
toICALString() {
|
||||
return design.vcard.value[this.icaltype].toICAL(this.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this date/time, in jCard form
|
||||
* (including : and - separators).
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
let y = this.year, m = this.month, d = this.day;
|
||||
let h = this.hour, mm = this.minute, s = this.second;
|
||||
|
||||
let hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null;
|
||||
let hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null;
|
||||
|
||||
let datepart = (hasYear ? pad2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) +
|
||||
(hasMonth ? pad2(m) : '') +
|
||||
(hasDay ? '-' + pad2(d) : '');
|
||||
let timepart = (hasHour ? pad2(h) : '-') + (hasHour && hasMinute ? ':' : '') +
|
||||
(hasMinute ? pad2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') +
|
||||
(hasMinute && hasSecond ? ':' : '') +
|
||||
(hasSecond ? pad2(s) : '');
|
||||
|
||||
let zone;
|
||||
if (this.zone === Timezone.utcTimezone) {
|
||||
zone = 'Z';
|
||||
} else if (this.zone instanceof UtcOffset) {
|
||||
zone = this.zone.toString();
|
||||
} else if (this.zone === Timezone.localTimezone) {
|
||||
zone = '';
|
||||
} else if (this.zone instanceof Timezone) {
|
||||
let offset = UtcOffset.fromSeconds(this.zone.utcOffset(this));
|
||||
zone = offset.toString();
|
||||
} else {
|
||||
zone = '';
|
||||
}
|
||||
|
||||
switch (this.icaltype) {
|
||||
case "time":
|
||||
return timepart + zone;
|
||||
case "date-and-or-time":
|
||||
case "date-time":
|
||||
return datepart + (timepart == '--' ? '' : 'T' + timepart + zone);
|
||||
case "date":
|
||||
return datepart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export default VCardTime;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"name": "ical.js",
|
||||
"version": "2.1.0",
|
||||
"author": "Philipp Kewisch",
|
||||
"contributors": [
|
||||
"Github Contributors (https://github.com/kewisch/ical.js/graphs/contributors)"
|
||||
],
|
||||
"description": "Javascript parser for ics (rfc5545) and vcard (rfc6350) data",
|
||||
"main": "dist/ical.js",
|
||||
"types": "dist/types/module.d.ts",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kewisch/ical.js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"calendar",
|
||||
"iCalendar",
|
||||
"jCal",
|
||||
"vCard",
|
||||
"jCard",
|
||||
"parser"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.24.3",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@octokit/core": "^6.0.1",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@stylistic/eslint-plugin": "^2.1.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"c8": "^10.1.2",
|
||||
"chai": "^5.1.0",
|
||||
"clean-jsdoc-theme": "^4.2.18",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-html": "^8.1.1",
|
||||
"globals": "^15.0.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc-tsimport-plugin": "^1.0.5",
|
||||
"karma": "^6.4.3",
|
||||
"karma-chai": "^0.1.0",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-sauce-launcher": "^4.3.6",
|
||||
"karma-spec-reporter": "^0.0.36",
|
||||
"mocha": "^10.3.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"yauzl-promise": "^4.0.0"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"engine": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run test-unit && npm run test-acceptance",
|
||||
"test-unit": "c8 mocha",
|
||||
"test-acceptance": "mocha test/acceptance/*_test.js",
|
||||
"test-performance": "mocha --reporter test/support/perfReporter.cjs test/performance/*_test.js",
|
||||
"test-browser": "karma start karma.conf.cjs",
|
||||
"test-all": "npm run test-unit && npm run test-acceptance && npm run test-performance && npm run test-browser",
|
||||
"build": "rollup -c",
|
||||
"lint": "eslint",
|
||||
"jsdoc": "rm -rf docs/api && jsdoc --configure jsdoc-prepare.json && rm -rf docs/api && jsdoc --configure jsdoc.json --verbose",
|
||||
"validator": "node tools/scriptutils.js replace-unpkg tools/validator.html docs/validator.html",
|
||||
"recurtester": "node tools/scriptutils.js replace-unpkg tools/recur-tester.html docs/recur-tester.html",
|
||||
"ghpages": "npm run jsdoc && npm run validator && npm run recurtester"
|
||||
},
|
||||
"exports": {
|
||||
"import": "./dist/ical.js",
|
||||
"require": "./dist/ical.es5.cjs",
|
||||
"types": "./dist/types/module.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist/ical.js",
|
||||
"dist/ical.min.js",
|
||||
"dist/ical.es5.cjs",
|
||||
"dist/ical.es5.min.cjs",
|
||||
"dist/types/*",
|
||||
"lib/ical/*.js"
|
||||
],
|
||||
"mocha": {
|
||||
"ui": "tdd",
|
||||
"require": "test/support/helper.js",
|
||||
"reporter": "spec"
|
||||
},
|
||||
"c8": {
|
||||
"include": "lib/ical",
|
||||
"reporter": [
|
||||
"text",
|
||||
"html",
|
||||
"lcov"
|
||||
]
|
||||
},
|
||||
"saucelabs": {
|
||||
"SL_Chrome": {
|
||||
"base": "SauceLabs",
|
||||
"browserName": "chrome"
|
||||
},
|
||||
"SL_Firefox": {
|
||||
"base": "SauceLabs",
|
||||
"browserName": "firefox"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { babel } from '@rollup/plugin-babel';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
|
||||
const LICENSE =
|
||||
`/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
* Portions Copyright (C) Philipp Kewisch */`;
|
||||
|
||||
const TERSER_OPTIONS = {
|
||||
format: {
|
||||
comments: function(node, comment) {
|
||||
if (comment.type == 'comment2') {
|
||||
return /terms of the Mozilla Public/.test(comment.value) && comment.pos === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default [{
|
||||
input: 'lib/ical/module.js',
|
||||
output: [
|
||||
{ file: 'dist/ical.js', format: 'es', exports: 'default' },
|
||||
{
|
||||
file: 'dist/ical.min.js',
|
||||
banner: LICENSE,
|
||||
format: 'es',
|
||||
exports: 'default',
|
||||
plugins: [terser(TERSER_OPTIONS)]
|
||||
}
|
||||
]
|
||||
}, {
|
||||
input: 'lib/ical/module.js',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/ical.es5.cjs',
|
||||
exports: 'default',
|
||||
name: 'ICAL',
|
||||
format: 'umd',
|
||||
banner: LICENSE,
|
||||
},
|
||||
{
|
||||
file: 'dist/ical.es5.min.cjs',
|
||||
exports: 'default',
|
||||
name: 'ICAL',
|
||||
format: 'umd',
|
||||
banner: LICENSE,
|
||||
plugins: [terser(TERSER_OPTIONS)],
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
babel({ babelHelpers: 'bundled', presets: ['@babel/preset-env'] }),
|
||||
typescript({
|
||||
include: ['lib/ical/*.js'],
|
||||
noForceEmit: true,
|
||||
compilerOptions: {
|
||||
allowJs: true,
|
||||
declaration: true,
|
||||
emitDeclarationOnly: true,
|
||||
declarationMap: true,
|
||||
declarationDir: 'dist/types',
|
||||
},
|
||||
})
|
||||
]
|
||||
}];
|
|
@ -0,0 +1,51 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@gmail.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=America/Los_Angeles:20120630T060000
|
||||
DTEND;TZID=America/Los_Angeles:20120630T070000
|
||||
DTSTAMP:20120724T212411Z
|
||||
UID:dn4vrfmfn5p05roahsopg57h48@google.com
|
||||
CREATED:20120724T212411Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120724T212411Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Really long event name thing
|
||||
TRANSP:OPAQUE
|
||||
BEGIN:VALARM
|
||||
ACTION:EMAIL
|
||||
DESCRIPTION:This is an event reminder
|
||||
SUMMARY:Alarm notification
|
||||
ATTENDEE:mailto:calmozilla1@gmail.com
|
||||
TRIGGER:-P0DT0H30M0S
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:This is an event reminder
|
||||
TRIGGER:-P0DT0H30M0S
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VCALENDAR
|
||||
END:VCALENDAR
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VCALENDAR
|
||||
COMMENT:This blank line is invalid
|
||||
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,52 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@gmail.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=America/Los_Angeles:20120801T050000
|
||||
DTEND;TZID=America/Los_Angeles:20120801T060000
|
||||
RRULE:FREQ=DAILY
|
||||
DTSTAMP:20120803T221236Z
|
||||
UID:tgh9qho17b07pk2n2ji3gluans@google.com
|
||||
CREATED:20120803T221236Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120803T221236Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Every day recurring
|
||||
TRANSP:OPAQUE
|
||||
BEGIN:VALARM
|
||||
ACTION:EMAIL
|
||||
DESCRIPTION:This is an event reminder
|
||||
SUMMARY:Alarm notification
|
||||
ATTENDEE:mailto:calmozilla1@gmail.com
|
||||
TRIGGER:-P0DT0H30M0S
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:This is an event reminder
|
||||
TRIGGER:-P0DT0H30M0S
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,52 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@gmail.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20120803
|
||||
DTEND;VALUE=DATE:20120804
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
|
||||
DTSTAMP:20120803T221306Z
|
||||
UID:4pfh824gvims850j0gar361t04@google.com
|
||||
CREATED:20120803T221306Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120803T221306Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Day Long Event
|
||||
TRANSP:TRANSPARENT
|
||||
BEGIN:VALARM
|
||||
ACTION:EMAIL
|
||||
DESCRIPTION:This is an event reminder
|
||||
SUMMARY:Alarm notification
|
||||
ATTENDEE:mailto:calmozilla1@gmail.com
|
||||
TRIGGER;VALUE=DATE-TIME:20120802T233000Z
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:This is an event reminder
|
||||
TRIGGER;VALUE=DATE-TIME:20120802T233000Z
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,39 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@example.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=America/Los_Angeles:20120630T060000
|
||||
DURATION:P1D
|
||||
DTSTAMP:20120724T212411Z
|
||||
UID:dn4vrfmfn5p05roahsopg57h48@example.com
|
||||
CREATED:20120724T212411Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120724T212411Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Really long event name thing
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,50 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@gmail.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20120904
|
||||
DTEND;VALUE=DATE:20120905
|
||||
DTSTAMP:20120905T084734Z
|
||||
UID:redgrb1l0aju5edm6h0s102eu4@google.com
|
||||
CREATED:20120905T084734Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120905T084734Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Event
|
||||
TRANSP:TRANSPARENT
|
||||
BEGIN:VALARM
|
||||
ACTION:EMAIL
|
||||
DESCRIPTION:This is an event reminder
|
||||
SUMMARY:Alarm notification
|
||||
ATTENDEE:mailto:calmozilla1@gmail.com
|
||||
TRIGGER;VALUE=DATE-TIME:20120903T233000Z
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:This is an event reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,90 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:Contacts' birthdays and events
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
X-WR-CALDESC:Your contacts' birthdays and anniversaries
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20141210
|
||||
DTEND;VALUE=DATE:20141211
|
||||
RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1
|
||||
RDATE:20131210Z
|
||||
RDATE:20121210Z
|
||||
DTSTAMP:20121207T183041Z
|
||||
UID:2014_BIRTHDAY_79d389868f96182e@google.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac
|
||||
ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct
|
||||
m6abj3dtmg@virtual
|
||||
CLASS:PUBLIC
|
||||
CREATED:20121207T183041Z
|
||||
LAST-MODIFIED:20121207T183041Z
|
||||
SEQUENCE:1
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:PErson #2's birthday
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20121210
|
||||
DTEND;VALUE=DATE:20121211
|
||||
DTSTAMP:20121207T183041Z
|
||||
UID:BIRTHDAY_79d389868f96182e@google.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac
|
||||
ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct
|
||||
m6abj3dtmg@virtual
|
||||
X-GOOGLE-CALENDAR-CONTENT-ICON:https://calendar.google.com/googlecalendar/i
|
||||
mages/cake.gif
|
||||
X-GOOGLE-CALENDAR-CONTENT-DISPLAY:chip
|
||||
RECURRENCE-ID;VALUE=DATE:20121210
|
||||
CLASS:PUBLIC
|
||||
CREATED:20121207T183041Z
|
||||
DESCRIPTION:Today is PErson #2's birthday!
|
||||
LAST-MODIFIED:20121207T183041Z
|
||||
SEQUENCE:1
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:PErson #2's birthday
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20131210
|
||||
DTEND;VALUE=DATE:20131211
|
||||
DTSTAMP:20121207T183041Z
|
||||
UID:BIRTHDAY_79d389868f96182e@google.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac
|
||||
ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct
|
||||
m6abj3dtmg@virtual
|
||||
X-GOOGLE-CALENDAR-CONTENT-ICON:https://calendar.google.com/googlecalendar/i
|
||||
mages/cake.gif
|
||||
X-GOOGLE-CALENDAR-CONTENT-DISPLAY:chip
|
||||
RECURRENCE-ID;VALUE=DATE:20131210
|
||||
CLASS:PUBLIC
|
||||
CREATED:20121207T183041Z
|
||||
DESCRIPTION:Today is PErson #2's birthday!
|
||||
LAST-MODIFIED:20121207T183041Z
|
||||
SEQUENCE:1
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:PErson #2's birthday
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20141210
|
||||
DTEND;VALUE=DATE:20141211
|
||||
DTSTAMP:20121207T183041Z
|
||||
UID:BIRTHDAY_79d389868f96182e@google.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac
|
||||
ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct
|
||||
m6abj3dtmg@virtual
|
||||
X-GOOGLE-CALENDAR-CONTENT-ICON:https://calendar.google.com/googlecalendar/i
|
||||
mages/cake.gif
|
||||
X-GOOGLE-CALENDAR-CONTENT-DISPLAY:chip
|
||||
RECURRENCE-ID;VALUE=DATE:20141210
|
||||
CLASS:PUBLIC
|
||||
CREATED:20121207T183041Z
|
||||
DESCRIPTION:Today is PErson #2's birthday!
|
||||
LAST-MODIFIED:20121207T183041Z
|
||||
SEQUENCE:1
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:PErson #2's birthday
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,39 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@gmail.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=America/Los_Angeles:20120630T060000
|
||||
DTEND;TZID=America/Los_Angeles:20120630T070000
|
||||
DTSTAMP:20120724T212411Z
|
||||
UID:dn4vrfmfn5p05roahsopg57h48@google.com
|
||||
CREATED:20120724T212411Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120724T212411Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Really long event name thing
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,45 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0800
|
||||
TZOFFSETFROM:-0700
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
|
||||
TZNAME:PST
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0700
|
||||
TZOFFSETFROM:-0800
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU
|
||||
TZNAME:PDT
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:1334F9B7-6136-444E-A58D-472564C6AA73
|
||||
RRULE:FREQ=WEEKLY;UNTIL=20120730T065959Z
|
||||
RRULE:FREQ=MONTHLY;BYDAY=SU;UNTIL=20120730T065959Z
|
||||
SUMMARY:sahaja <> frashed
|
||||
DESCRIPTION:weekly 1on1
|
||||
ATTENDEE;CN=James Lal;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS
|
||||
-ACTION;RSVP=TRUE:mailto:jlal@mozilla.com
|
||||
ORGANIZER;CN=Faramarz Rashed:mailto:frashed@mozilla.com
|
||||
DTSTART;TZID=America/Los_Angeles:20120326T110000
|
||||
DTEND;TZID=America/Los_Angeles:20120326T113000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
LAST-MODIFIED:20120326T161522Z
|
||||
DTSTAMP:20120730T165637Z
|
||||
SEQUENCE:9
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@example.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20120630
|
||||
DTSTAMP:20120724T212411Z
|
||||
UID:dn4vrfmfn5p05roahsopg57h48@example.com
|
||||
CREATED:20120724T212411Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120724T212411Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Really long event name thing
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,38 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-CALNAME:calmozilla1@example.com
|
||||
X-WR-TIMEZONE:America/Los_Angeles
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=America/Los_Angeles:20120630T060000
|
||||
DTSTAMP:20120724T212411Z
|
||||
UID:dn4vrfmfn5p05roahsopg57h48@example.com
|
||||
CREATED:20120724T212411Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20120724T212411Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Really long event name thing
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,45 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0800
|
||||
TZOFFSETFROM:-0700
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
|
||||
TZNAME:PST
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0700
|
||||
TZOFFSETFROM:-0800
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:PDT
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:44c10eaa-db0b-4223-8653-cf2b63f26326
|
||||
RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR
|
||||
SUMMARY:Calendar
|
||||
DESCRIPTION:desc
|
||||
ATTENDEE;CN=XXX;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRU
|
||||
E:mailto:foo@bar.com
|
||||
ATTENDEE;CN=XXXX;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TR
|
||||
UE:mailto:x@bar.com
|
||||
ORGANIZER;CN=foobar:mailto:x@bar.com
|
||||
DTSTART;TZID=America/Los_Angeles:20120911T103000
|
||||
DTEND;TZID=America/Los_Angeles:20120911T110000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
LAST-MODIFIED:20120911T184851Z
|
||||
DTSTAMP:20120911T184851Z
|
||||
SEQUENCE:1
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,98 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0800
|
||||
TZOFFSETFROM:-0700
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
|
||||
TZNAME:PST
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0700
|
||||
TZOFFSETFROM:-0800
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU
|
||||
TZNAME:PDT
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VTIMEZONE
|
||||
X-INVALID-TIMEZONE:TRUE
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
|
||||
DESCRIPTION:IAM FOO
|
||||
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU
|
||||
SUMMARY:Crazy Event Thingy!
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Sahaja
|
||||
Lal;X-NUM-GUESTS=0:mailto:calmozilla1@gmail.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ja
|
||||
mes@lightsofapollo.com;X-NUM-GUESTS=0:mailto:james@lightsofapollo.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ia
|
||||
m.revelation@gmail.com;X-NUM-GUESTS=0:mailto:iam.revelation@gmail.com
|
||||
LOCATION:PLACE
|
||||
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
|
||||
DTSTART;TZID=America/Los_Angeles:20121002T100000
|
||||
DTEND;TZID=America/Los_Angeles:20121002T103000
|
||||
STATUS:CONFIRMED
|
||||
COLOR:red
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
LAST-MODIFIED:20120912T171506Z
|
||||
DTSTAMP:20120912T171506Z
|
||||
SEQUENCE:0
|
||||
RDATE;TZID=America/Los_Angeles:20121105T100000
|
||||
RDATE;TZID=America/Los_Angeles:20121110T100000,20121130T100000
|
||||
EXDATE;TZID=America/Los_Angeles:20130402T100000
|
||||
EXDATE;TZID=America/Los_Angeles:20121204T100000
|
||||
EXDATE;TZID=America/Los_Angeles:20130205T100000
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
|
||||
SUMMARY:Crazy Event Thingy!
|
||||
DESCRIPTION:I HAZ CHANGED!
|
||||
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
|
||||
DTSTART;TZID=America/Los_Angeles:20121002T150000
|
||||
DTEND;TZID=America/Los_Angeles:20121002T153000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
RECURRENCE-ID;TZID=America/Los_Angeles:20121002T100000
|
||||
LAST-MODIFIED:20120912T171540Z
|
||||
DTSTAMP:20120912T171540Z
|
||||
SEQUENCE:1
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
|
||||
SUMMARY:Crazy Event Thingy!
|
||||
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
|
||||
DTSTART;TZID=America/Los_Angeles:20121106T200000
|
||||
DTEND;TZID=America/Los_Angeles:20121106T203000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
RECURRENCE-ID:20121105T180000Z
|
||||
LAST-MODIFIED:20120912T171820Z
|
||||
DTSTAMP:20120912T171820Z
|
||||
SEQUENCE:1
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:X-UNKNOWN
|
||||
END:X-UNKNOWN
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,50 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0800
|
||||
TZOFFSETFROM:-0700
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU
|
||||
TZNAME:PST
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19710101T020000
|
||||
TZOFFSETTO:-0700
|
||||
TZOFFSETFROM:-0800
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU
|
||||
TZNAME:PDT
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868
|
||||
DESCRIPTION:IAM FOO
|
||||
RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU;UNTIL=20121231T100000
|
||||
SUMMARY:Crazy Event Thingy!
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Sahaja
|
||||
Lal;X-NUM-GUESTS=0:mailto:calmozilla1@gmail.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ja
|
||||
mes@lightsofapollo.com;X-NUM-GUESTS=0:mailto:james@lightsofapollo.com
|
||||
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ia
|
||||
m.revelation@gmail.com;X-NUM-GUESTS=0:mailto:iam.revelation@gmail.com
|
||||
LOCATION:PLACE
|
||||
ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com
|
||||
DTSTART;TZID=America/Los_Angeles:20121002T100000
|
||||
DTEND;TZID=America/Los_Angeles:20121002T103000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
LAST-MODIFIED:20120912T171506Z
|
||||
DTSTAMP:20120912T171506Z
|
||||
SEQUENCE:0
|
||||
RDATE;TZID=America/Los_Angeles:20121110T100000
|
||||
RDATE;TZID=America/Los_Angeles:20121105T100000
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,18 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Nowhere/Middle
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T000000
|
||||
TZOFFSETFROM:-0741
|
||||
TZOFFSETTO:-0741
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20230306T080000Z
|
||||
DTSTART;TZID=Nowhere/Middle:20230306T134200
|
||||
DTEND;TZID=Nowhere/Middle:20230306T144200
|
||||
SUMMARY:A test event
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,14 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//tzurl.org//NONSGML Olson 2012h//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Atikokan
|
||||
X-LIC-LOCATION:America/Atikokan
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0500
|
||||
TZNAME:EST
|
||||
DTSTART:19700101T000000
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,41 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//custom/thing
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Denver
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0600
|
||||
TZNAME:MDT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0600
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:MST
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0600
|
||||
TZNAME:MDT
|
||||
DTSTART:19180331T020000
|
||||
RDATE:20030406T020000
|
||||
RDATE:20040404T020000
|
||||
RDATE:20050403T020000
|
||||
RDATE:20060402T020000
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0600
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:MST
|
||||
DTSTART:19181027T020000
|
||||
RDATE:20031026T020000
|
||||
RDATE:20041031T020000
|
||||
RDATE:20051030T020000
|
||||
RDATE:20061029T020000
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,22 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//tzurl.org//NONSGML Olson 2012h//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Los_Angeles
|
||||
X-LIC-LOCATION:America/Los_Angeles
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0800
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:PDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0800
|
||||
TZNAME:PST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,22 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//tzurl.org//NONSGML Olson 2012h//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/New_York
|
||||
X-LIC-LOCATION:America/New_York
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
TZNAME:EDT
|
||||
DTSTART:19700308T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
TZNAME:EST
|
||||
DTSTART:19701101T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,26 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//ical.js//NONSGML Makebelieve//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Makebelieve/RDATE_as_date
|
||||
X-LIC-LOCATION:Makebelieve/RDATE_as_date
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
TZNAME:RDATE_as_date_standard
|
||||
DTSTART:19700101T020000
|
||||
RDATE:19700101T020000
|
||||
RDATE;VALUE=DATE:19800101
|
||||
RDATE:19900101T070000Z
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
TZNAME:RDATE_as_date_daylight
|
||||
DTSTART:19750101T020000
|
||||
RDATE:19750101T020000
|
||||
RDATE;VALUE=DATE:19850101
|
||||
RDATE:19950101T070000Z
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,26 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//ical.js//NONSGML Makebelieve//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Makebelieve/RDATE_as_date_utc
|
||||
X-LIC-LOCATION:Makebelieve/RDATE_as_date_utc
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
TZNAME:RDATE_as_date_utc_standard
|
||||
DTSTART:19700101T020000Z
|
||||
RDATE:19700101T020000
|
||||
RDATE;VALUE=DATE:19800101
|
||||
RDATE:19900101T070000Z
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
TZNAME:RDATE_as_date_utc_daylight
|
||||
DTSTART:19750101T020000Z
|
||||
RDATE:19750101T020000
|
||||
RDATE;VALUE=DATE:19850101
|
||||
RDATE:19950101T070000Z
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,22 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//ical.js//NONSGML Makebelieve//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Makebelieve/RRULE_UNTIL
|
||||
X-LIC-LOCATION:Makebelieve/RRULE_UNTIL
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
TZNAME:RRULE_UNTIL_standard
|
||||
DTSTART:19700101T020000Z
|
||||
RRULE:FREQ=YEARLY;INTERVAL=5;UNTIL=19800101T020000Z
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
TZNAME:RDATE_UNTIL_daylight
|
||||
DTSTART:19750101T020000
|
||||
RRULE:FREQ=YEARLY;INTERVAL=5;UNTIL=19850101T020000Z
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,27 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Etc/GMT
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19710101T000000
|
||||
TZOFFSETTO:-0000
|
||||
TZOFFSETFROM:-0000
|
||||
TZNAME:GMT
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:d118e997-3683-4552-8fe8-57c641f1f179
|
||||
SUMMARY:And another
|
||||
ORGANIZER;CN=Sahaja Lal:mailto:calmozilla1@yahoo.com
|
||||
DTSTART;TZID=Etc/GMT:20120821T210000
|
||||
DTEND;TZID=Etc/GMT:20120821T213000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
|
||||
TRANSP:OPAQUE
|
||||
X-MICROSOFT-DISALLOW-COUNTER:TRUE
|
||||
DTSTAMP:20120817T032509Z
|
||||
SEQUENCE:0
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,12 @@
|
|||
suite('ics - blank description', function() {
|
||||
let icsData;
|
||||
|
||||
suiteSetup(async function() {
|
||||
icsData = await testSupport.loadSample('blank_description.ics');
|
||||
});
|
||||
|
||||
test('summary', function() {
|
||||
// just verify it can parse blank lines
|
||||
ICAL.parse(icsData);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
suite('ics - blank description', function() {
|
||||
let icsData;
|
||||
|
||||
suiteSetup(async function() {
|
||||
icsData = await testSupport.loadSample('daily_recur.ics');
|
||||
});
|
||||
|
||||
test('summary', function() {
|
||||
// just verify it can parse blank lines
|
||||
let result = ICAL.parse(icsData);
|
||||
let component = new ICAL.Component(result);
|
||||
let vevent = component.getFirstSubcomponent(
|
||||
'vevent'
|
||||
);
|
||||
|
||||
let recur = vevent.getFirstPropertyValue(
|
||||
'rrule'
|
||||
);
|
||||
|
||||
let start = vevent.getFirstPropertyValue(
|
||||
'dtstart'
|
||||
);
|
||||
|
||||
let iter = recur.iterator(start);
|
||||
let limit = 10;
|
||||
while (limit) {
|
||||
iter.next();
|
||||
limit--;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
suite('ics test', function() {
|
||||
let icsData;
|
||||
|
||||
suiteSetup(async function() {
|
||||
icsData = await testSupport.loadSample('forced_types.ics');
|
||||
});
|
||||
|
||||
test('force type', function() {
|
||||
// just verify it can parse forced types
|
||||
let result = ICAL.parse(icsData);
|
||||
let component = new ICAL.Component(result);
|
||||
let vevent = component.getFirstSubcomponent(
|
||||
'vevent'
|
||||
);
|
||||
|
||||
let start = vevent.getFirstPropertyValue('dtstart');
|
||||
|
||||
assert.isTrue(start.isDate, 'is date type');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
suite('google birthday events', function() {
|
||||
let icsData;
|
||||
|
||||
suiteSetup(async function() {
|
||||
icsData = await testSupport.loadSample('google_birthday.ics');
|
||||
});
|
||||
|
||||
test('expanding malformatted recurring event', function(done) {
|
||||
// just verify it can parse forced types
|
||||
let parser = new ICAL.ComponentParser();
|
||||
let primary;
|
||||
let exceptions = [];
|
||||
|
||||
let expectedDates = [
|
||||
new Date(2012, 11, 10),
|
||||
new Date(2013, 11, 10),
|
||||
new Date(2014, 11, 10)
|
||||
];
|
||||
|
||||
parser.onevent = function(event) {
|
||||
if (event.isRecurrenceException()) {
|
||||
exceptions.push(event);
|
||||
} else {
|
||||
primary = event;
|
||||
}
|
||||
};
|
||||
|
||||
parser.oncomplete = function() {
|
||||
exceptions.forEach(function(item) {
|
||||
primary.relateException(item);
|
||||
});
|
||||
|
||||
let iter = primary.iterator();
|
||||
let next;
|
||||
let dates = [];
|
||||
while ((next = iter.next())) {
|
||||
dates.push(next.toJSDate());
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
dates,
|
||||
expectedDates
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
parser.process(icsData);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
suite('ics - negative zero', function() {
|
||||
let icsData;
|
||||
|
||||
suiteSetup(async function() {
|
||||
icsData = await testSupport.loadSample('utc_negative_zero.ics');
|
||||
});
|
||||
|
||||
test('summary', function() {
|
||||
let result = ICAL.parse(icsData);
|
||||
let component = new ICAL.Component(result);
|
||||
let vtimezone = component.getFirstSubcomponent(
|
||||
'vtimezone'
|
||||
);
|
||||
|
||||
let standard = vtimezone.getFirstSubcomponent(
|
||||
'standard'
|
||||
);
|
||||
|
||||
let props = standard.getAllProperties();
|
||||
let offset = props[1].getFirstValue();
|
||||
|
||||
assert.equal(
|
||||
offset.factor,
|
||||
-1,
|
||||
'offset'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
suite('ICAL.Binary', function() {
|
||||
let subject;
|
||||
|
||||
setup(function() {
|
||||
subject = new ICAL.Binary();
|
||||
});
|
||||
|
||||
test('setEncodedValue', function() {
|
||||
subject.setEncodedValue('bananas');
|
||||
assert.equal(subject.decodeValue(), 'bananas');
|
||||
assert.equal(subject.value, 'YmFuYW5hcw==');
|
||||
|
||||
subject.setEncodedValue('apples');
|
||||
assert.equal(subject.decodeValue(), 'apples');
|
||||
assert.equal(subject.value, 'YXBwbGVz');
|
||||
});
|
||||
|
||||
test('null values', function() {
|
||||
subject.setEncodedValue(null);
|
||||
assert.equal(subject.decodeValue(), null);
|
||||
assert.equal(subject.value, null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
suite('component_parser', function() {
|
||||
let subject;
|
||||
let icsData;
|
||||
|
||||
suiteSetup(async function() {
|
||||
icsData = await testSupport.loadSample('recur_instances.ics');
|
||||
});
|
||||
|
||||
suite('#process', function() {
|
||||
let events = [];
|
||||
let exceptions = [];
|
||||
let timezones = [];
|
||||
|
||||
function eventEquals(a, b, msg) {
|
||||
if (!a)
|
||||
throw new Error('actual is falsy');
|
||||
|
||||
if (!b)
|
||||
throw new Error('expected is falsy');
|
||||
|
||||
if (a instanceof ICAL.Event) {
|
||||
a = a.component;
|
||||
}
|
||||
|
||||
if (b instanceof ICAL.Event) {
|
||||
b = b.component;
|
||||
}
|
||||
|
||||
assert.deepEqual(a.toJSON(), b.toJSON(), msg);
|
||||
}
|
||||
|
||||
function setupProcess(options) {
|
||||
setup(function(done) {
|
||||
events.length = 0;
|
||||
timezones.length = 0;
|
||||
|
||||
subject = new ICAL.ComponentParser(options);
|
||||
|
||||
subject.onrecurrenceexception = function(item) {
|
||||
exceptions.push(item);
|
||||
};
|
||||
|
||||
subject.onevent = function(event) {
|
||||
events.push(event);
|
||||
};
|
||||
|
||||
subject.ontimezone = function(tz) {
|
||||
timezones.push(tz);
|
||||
};
|
||||
|
||||
subject.oncomplete = function() {
|
||||
done();
|
||||
};
|
||||
|
||||
subject.process(ICAL.parse(icsData));
|
||||
});
|
||||
}
|
||||
|
||||
suite('without events', function() {
|
||||
setupProcess({ parseEvent: false });
|
||||
|
||||
test('parse result', function() {
|
||||
assert.lengthOf(events, 0);
|
||||
assert.lengthOf(timezones, 1);
|
||||
|
||||
let tz = timezones[0];
|
||||
assert.instanceOf(tz, ICAL.Timezone);
|
||||
assert.equal(tz.tzid, 'America/Los_Angeles');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('with events', function() {
|
||||
setupProcess();
|
||||
|
||||
test('parse result', function() {
|
||||
let component = new ICAL.Component(ICAL.parse(icsData));
|
||||
let list = component.getAllSubcomponents('vevent');
|
||||
|
||||
let expectedEvents = [];
|
||||
|
||||
list.forEach(function(item) {
|
||||
expectedEvents.push(new ICAL.Event(item));
|
||||
});
|
||||
|
||||
assert.instanceOf(expectedEvents[0], ICAL.Event);
|
||||
|
||||
eventEquals(events[0], expectedEvents[0]);
|
||||
eventEquals(events[1], expectedEvents[1]);
|
||||
eventEquals(events[2], expectedEvents[2]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('without parsing timezones', function() {
|
||||
setupProcess({ parseTimezone: false });
|
||||
|
||||
test('parse result', function() {
|
||||
assert.lengthOf(timezones, 0);
|
||||
assert.lengthOf(events, 3);
|
||||
});
|
||||
});
|
||||
|
||||
suite('alternate input', function() {
|
||||
test('parsing component from string', function(done) {
|
||||
subject = new ICAL.ComponentParser();
|
||||
subject.oncomplete = function() {
|
||||
assert.lengthOf(events, 3);
|
||||
done();
|
||||
};
|
||||
subject.process(icsData);
|
||||
});
|
||||
test('parsing component from component', function(done) {
|
||||
subject = new ICAL.ComponentParser();
|
||||
subject.oncomplete = function() {
|
||||
assert.lengthOf(events, 3);
|
||||
done();
|
||||
};
|
||||
let comp = new ICAL.Component(ICAL.parse(icsData));
|
||||
subject.process(comp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,627 @@
|
|||
suite('Component', function() {
|
||||
let subject;
|
||||
let fixtures;
|
||||
|
||||
setup(function() {
|
||||
fixtures = {
|
||||
components: [
|
||||
'vevent',
|
||||
[
|
||||
['description', {}, 'text', 'xfoo'],
|
||||
['description', {}, 'text', 'xfoo2'],
|
||||
['xfoo', {}, 'text', 'xfoo3']
|
||||
],
|
||||
[
|
||||
['valarm', [], []],
|
||||
['vtodo', [], []],
|
||||
['valarm', [['description', {}, 'text', 'foo']], []]
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
subject = new ICAL.Component(fixtures.components);
|
||||
});
|
||||
|
||||
suite("initialization", function() {
|
||||
test('initialize component', function() {
|
||||
let raw = ['description', {}, 'text', 'value'];
|
||||
subject = new ICAL.Component(raw);
|
||||
|
||||
assert.equal(subject.jCal, raw, 'has jCal');
|
||||
assert.equal(subject.name, 'description');
|
||||
});
|
||||
|
||||
test('new component without jCal', function() {
|
||||
let newComp = new ICAL.Component('vevent');
|
||||
|
||||
assert.equal(newComp.jCal[0], 'vevent');
|
||||
|
||||
assert.lengthOf(newComp.getAllSubcomponents(), 0);
|
||||
assert.lengthOf(newComp.getAllProperties(), 0);
|
||||
});
|
||||
|
||||
test("#fromString", function() {
|
||||
let comp = ICAL.Component.fromString("BEGIN:VCALENDAR\nX-CALPROP:value\nEND:VCALENDAR");
|
||||
assert.equal(comp.name, "vcalendar");
|
||||
let prop = comp.getFirstProperty();
|
||||
assert.equal(prop.name, "x-calprop");
|
||||
assert.equal(prop.getFirstValue(), "value");
|
||||
});
|
||||
});
|
||||
|
||||
suite('parenting', function() {
|
||||
// Today we hear a tale about Tom, Marge, Bernhard and Claire.
|
||||
let tom, bernhard, claire, marge, relationship;
|
||||
let house, otherhouse;
|
||||
setup(function() {
|
||||
tom = new ICAL.Component("tom");
|
||||
bernhard = new ICAL.Component("bernhard");
|
||||
claire = new ICAL.Component("claire");
|
||||
marge = new ICAL.Component("marge");
|
||||
relationship = new ICAL.Component("vrelationship");
|
||||
house = new ICAL.Property("house");
|
||||
otherhouse = new ICAL.Property("otherhouse");
|
||||
});
|
||||
|
||||
test('basic', function() {
|
||||
// Tom and Bernhard are best friends. They are happy and single.
|
||||
assert.isNull(tom.parent);
|
||||
assert.isNull(bernhard.parent);
|
||||
|
||||
// One day, they get to know Marge, who is also single.
|
||||
assert.isNull(marge.parent);
|
||||
|
||||
// Tom and Bernhard play rock paper scissors on who gets a first shot at
|
||||
// Marge and Tom wins. After a few nice dates they get together.
|
||||
relationship.addSubcomponent(tom);
|
||||
relationship.addSubcomponent(marge);
|
||||
|
||||
// Both are happy as can be and tell everyone about their love. Nothing
|
||||
// goes above their relationship!
|
||||
assert.isNull(relationship.parent);
|
||||
assert.equal(tom.parent, relationship);
|
||||
assert.equal(marge.parent, relationship);
|
||||
|
||||
// Over the years, there are a few ups and downs.
|
||||
relationship.removeSubcomponent(tom);
|
||||
assert.isNull(relationship.parent);
|
||||
assert.isNull(tom.parent);
|
||||
assert.equal(marge.parent, relationship);
|
||||
relationship.removeAllSubcomponents();
|
||||
assert.isNull(marge.parent);
|
||||
|
||||
// But in the end they stay together.
|
||||
relationship.addSubcomponent(tom);
|
||||
relationship.addSubcomponent(marge);
|
||||
});
|
||||
|
||||
test('multiple children', function() {
|
||||
// After some happy years Tom and Marge get married. Tom is going to be father
|
||||
// of his beautiful daughter Claire.
|
||||
tom.addSubcomponent(claire);
|
||||
|
||||
// He has no doubt he is the father
|
||||
assert.equal(claire.parent, tom);
|
||||
|
||||
// One day, Tom catches his wife in bed with his best friend Bernhard.
|
||||
// Tom is very unhappy and requests a paternity test. It turns out that
|
||||
// Claire is actually Bernhard's daughter.
|
||||
bernhard.addSubcomponent(claire);
|
||||
|
||||
// Bernhard is happy to hear about his daughter, while Tom goes about to
|
||||
// tell everyone he knows. Claire is devastated and would have rather
|
||||
// found out about this.
|
||||
assert.isFalse(tom.removeSubcomponent(claire));
|
||||
|
||||
// Marge knew it all along. What a sad day. Claire is not Tom's daughter,
|
||||
// but instead Bernhard's. Tom has no children, and Bernhard is the happy
|
||||
// father of his daughter claire.
|
||||
assert.equal(claire.parent, bernhard);
|
||||
assert.isNull(tom.getFirstSubcomponent());
|
||||
assert.equal(bernhard.getFirstSubcomponent(), claire);
|
||||
|
||||
// Feeling depressed, Tom tries to find happyness with a pet, but all he
|
||||
// got was scratches and sadness. That didn't go so well.
|
||||
assert.throws(function() {
|
||||
tom.addProperty("bird");
|
||||
}, 'must be instance of ICAL.Property');
|
||||
});
|
||||
|
||||
test('properties', function() {
|
||||
// Marge lives on a property near the Hamptons, she thinks it belongs to
|
||||
// her.
|
||||
marge.addProperty(house);
|
||||
assert.equal(house.parent, marge);
|
||||
|
||||
// It seems that Tom didn't always trust Marge, he had fooled her. The
|
||||
// house belongs to him.
|
||||
tom.addProperty(house);
|
||||
assert.equal(house.parent, tom);
|
||||
assert.isNull(marge.getFirstProperty());
|
||||
|
||||
// Bernhard being an aggressive character, tries to throw Tom out of his
|
||||
// own house. A long visit in the hospital lets neighbors believe noone
|
||||
// lives there anymore.
|
||||
tom.removeProperty(house);
|
||||
assert.isNull(house.parent);
|
||||
|
||||
// Marge spends a few nights there, but also lives in her other house.
|
||||
marge.addProperty(house);
|
||||
marge.addProperty(otherhouse);
|
||||
assert.equal(house.parent, marge);
|
||||
assert.equal(otherhouse.parent, marge);
|
||||
|
||||
// Tom is back from the hospital and very mad. He throws marge out of his
|
||||
// house. Unfortunately marge can no longer pay the rent for her other
|
||||
// house either.
|
||||
marge.removeAllProperties();
|
||||
assert.isNull(house.parent);
|
||||
assert.isNull(otherhouse.parent);
|
||||
|
||||
// What a mess. What do we learn from this testsuite? Infidelity is not a
|
||||
// good idea. Always be faithful!
|
||||
});
|
||||
});
|
||||
|
||||
suite('#getFirstSubcomponent', function() {
|
||||
let jCal;
|
||||
setup(function() {
|
||||
jCal = fixtures.components;
|
||||
subject = new ICAL.Component(jCal);
|
||||
});
|
||||
|
||||
test('without name', function() {
|
||||
let component = subject.getFirstSubcomponent();
|
||||
assert.equal(component.parent, subject);
|
||||
assert.equal(component.name, 'valarm');
|
||||
|
||||
// first sub component
|
||||
let expected = jCal[2][0];
|
||||
|
||||
assert.equal(component.jCal, expected);
|
||||
});
|
||||
|
||||
test('with name (when not first)', function() {
|
||||
let component = subject.getFirstSubcomponent(
|
||||
'vtodo'
|
||||
);
|
||||
|
||||
assert.equal(component.parent, subject);
|
||||
|
||||
assert.equal(component.name, 'vtodo');
|
||||
assert.equal(
|
||||
component.jCal,
|
||||
jCal[2][1]
|
||||
);
|
||||
});
|
||||
|
||||
test('with name (when there are two)', function() {
|
||||
let component = subject.getFirstSubcomponent(
|
||||
'valarm'
|
||||
);
|
||||
assert.equal(component.name, 'valarm');
|
||||
assert.equal(
|
||||
component.jCal,
|
||||
jCal[2][0]
|
||||
);
|
||||
});
|
||||
|
||||
test('equality between calls', function() {
|
||||
assert.equal(
|
||||
subject.getFirstSubcomponent(),
|
||||
subject.getFirstSubcomponent()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('#getAllSubcomponents', function() {
|
||||
test('with components', function() {
|
||||
// 2 is the component array
|
||||
let comps = fixtures.components[2];
|
||||
|
||||
subject = new ICAL.Component(
|
||||
fixtures.components
|
||||
);
|
||||
|
||||
let result = subject.getAllSubcomponents();
|
||||
assert.lengthOf(result, comps.length);
|
||||
|
||||
for (let i = 0; i < comps.length; i++) {
|
||||
assert.instanceOf(result[i], ICAL.Component);
|
||||
assert.equal(result[i].jCal, comps[i]);
|
||||
}
|
||||
});
|
||||
|
||||
test('with name', function() {
|
||||
subject = new ICAL.Component(fixtures.components);
|
||||
|
||||
let result = subject.getAllSubcomponents('valarm');
|
||||
assert.lengthOf(result, 2);
|
||||
|
||||
result.forEach(function(item) {
|
||||
assert.equal(item.name, 'valarm');
|
||||
});
|
||||
});
|
||||
|
||||
test('without components', function() {
|
||||
subject = new ICAL.Component(['foo', [], []]);
|
||||
assert.equal(subject.name, 'foo');
|
||||
assert.lengthOf(subject.getAllSubcomponents(), 0);
|
||||
});
|
||||
|
||||
test('with name from end', function() {
|
||||
// We need our own subject for this test
|
||||
let oursubject = new ICAL.Component(fixtures.components);
|
||||
|
||||
// Get one from the end first
|
||||
let comps = fixtures.components[2];
|
||||
oursubject.getAllSubcomponents(comps[comps.length - 1][0]);
|
||||
|
||||
// Now get them all, they MUST be hydrated
|
||||
let results = oursubject.getAllSubcomponents();
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
assert.isDefined(results[i]);
|
||||
assert.equal(results[i].jCal, subject.jCal[2][i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('#addSubcomponent', function() {
|
||||
let newComp = new ICAL.Component('xnew');
|
||||
|
||||
subject.addSubcomponent(newComp);
|
||||
let all = subject.getAllSubcomponents();
|
||||
|
||||
assert.equal(
|
||||
all[all.length - 1],
|
||||
newComp,
|
||||
'can reference component'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
all.length,
|
||||
subject.jCal[2].length,
|
||||
'has same number of items'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.jCal[2][all.length - 1],
|
||||
newComp.jCal,
|
||||
'adds jCal'
|
||||
);
|
||||
});
|
||||
|
||||
suite('#removeSubcomponent', function() {
|
||||
test('by name', function() {
|
||||
subject.removeSubcomponent('vtodo');
|
||||
|
||||
let all = subject.getAllSubcomponents();
|
||||
|
||||
all.forEach(function(item) {
|
||||
assert.equal(item.name, 'valarm');
|
||||
});
|
||||
});
|
||||
|
||||
test('by component', function() {
|
||||
let first = subject.getFirstSubcomponent();
|
||||
|
||||
subject.removeSubcomponent(first);
|
||||
|
||||
assert.notEqual(
|
||||
subject.getFirstSubcomponent(),
|
||||
first
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.getFirstSubcomponent().name,
|
||||
'vtodo'
|
||||
);
|
||||
});
|
||||
|
||||
test('remove non hydrated subcomponent should not shift hydrated property', function() {
|
||||
let component = new ICAL.Component([
|
||||
'vevent',
|
||||
[],
|
||||
[
|
||||
['a', [], []],
|
||||
['b', [], []],
|
||||
['c', [], []]
|
||||
]
|
||||
]);
|
||||
component.getFirstSubcomponent('b');
|
||||
component.removeSubcomponent('a');
|
||||
let cValue = component.getFirstSubcomponent('c').name;
|
||||
assert.equal(cValue, 'c');
|
||||
});
|
||||
});
|
||||
|
||||
suite('#removeAllSubcomponents', function() {
|
||||
test('with name', function() {
|
||||
subject.removeAllSubcomponents('valarm');
|
||||
assert.lengthOf(subject.jCal[2], 1);
|
||||
assert.equal(subject.jCal[2][0][0], 'vtodo');
|
||||
assert.lengthOf(subject.getAllSubcomponents(), 1);
|
||||
});
|
||||
|
||||
test('all', function() {
|
||||
subject.removeAllSubcomponents();
|
||||
assert.lengthOf(subject.jCal[2], 0);
|
||||
assert.lengthOf(subject.getAllSubcomponents(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('#hasProperty', function() {
|
||||
subject = new ICAL.Component(
|
||||
fixtures.components
|
||||
);
|
||||
|
||||
assert.ok(subject.hasProperty('description'));
|
||||
assert.ok(!subject.hasProperty('iknowitsnothere'));
|
||||
});
|
||||
|
||||
suite('#getFirstProperty', function() {
|
||||
setup(function() {
|
||||
subject = new ICAL.Component(fixtures.components);
|
||||
});
|
||||
|
||||
test('name missing', function() {
|
||||
assert.ok(!subject.getFirstProperty('x-foo'));
|
||||
});
|
||||
|
||||
test('name has multiple', function() {
|
||||
let first = subject.getFirstProperty('description');
|
||||
assert.equal(first, subject.getFirstProperty());
|
||||
|
||||
assert.equal(
|
||||
first.getFirstValue(),
|
||||
'xfoo'
|
||||
);
|
||||
});
|
||||
|
||||
test('without name', function() {
|
||||
let first = subject.getFirstProperty();
|
||||
assert.equal(
|
||||
first.jCal,
|
||||
fixtures.components[1][0]
|
||||
);
|
||||
});
|
||||
|
||||
test('without name empty', function() {
|
||||
subject = new ICAL.Component(['foo', [], []]);
|
||||
assert.ok(!subject.getFirstProperty());
|
||||
});
|
||||
});
|
||||
|
||||
test('#getFirstPropertyValue', function() {
|
||||
subject = new ICAL.Component(fixtures.components);
|
||||
assert.equal(
|
||||
subject.getFirstPropertyValue(),
|
||||
'xfoo'
|
||||
);
|
||||
});
|
||||
|
||||
suite('#getAllProperties', function() {
|
||||
setup(function() {
|
||||
subject = new ICAL.Component(fixtures.components);
|
||||
});
|
||||
|
||||
test('with name', function() {
|
||||
let results = subject.getAllProperties('description');
|
||||
assert.lengthOf(results, 2);
|
||||
|
||||
results.forEach(function(item, i) {
|
||||
assert.equal(
|
||||
item.jCal,
|
||||
subject.jCal[1][i]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('with name empty', function() {
|
||||
let results = subject.getAllProperties('wtfmissing');
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
|
||||
test('without name', function() {
|
||||
let results = subject.getAllProperties();
|
||||
results.forEach(function(item, i) {
|
||||
assert.equal(item.jCal, subject.jCal[1][i]);
|
||||
});
|
||||
});
|
||||
|
||||
test('with name from end', function() {
|
||||
// We need our own subject for this test
|
||||
let oursubject = new ICAL.Component(fixtures.components);
|
||||
|
||||
// Get one from the end first
|
||||
let props = fixtures.components[1];
|
||||
oursubject.getAllProperties(props[props.length - 1][0]);
|
||||
|
||||
// Now get them all, they MUST be hydrated
|
||||
let results = oursubject.getAllProperties();
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
assert.isDefined(results[i]);
|
||||
assert.equal(results[i].jCal, subject.jCal[1][i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('#addProperty', function() {
|
||||
let prop = new ICAL.Property('description');
|
||||
|
||||
subject.addProperty(prop);
|
||||
assert.equal(subject.jCal[1][3], prop.jCal);
|
||||
|
||||
let all = subject.getAllProperties();
|
||||
let lastProp = all[all.length - 1];
|
||||
|
||||
assert.equal(lastProp, prop);
|
||||
assert.equal(lastProp.parent, subject);
|
||||
});
|
||||
|
||||
test('#addPropertyWithValue', function() {
|
||||
subject = new ICAL.Component('vevent');
|
||||
|
||||
subject.addPropertyWithValue('description', 'value');
|
||||
|
||||
let all = subject.getAllProperties();
|
||||
|
||||
assert.equal(all[0].name, 'description');
|
||||
assert.equal(all[0].getFirstValue(), 'value');
|
||||
});
|
||||
|
||||
test('#updatePropertyWithValue', function() {
|
||||
subject = new ICAL.Component('vevent');
|
||||
subject.addPropertyWithValue('description', 'foo');
|
||||
assert.lengthOf(subject.getAllProperties(), 1);
|
||||
|
||||
subject.updatePropertyWithValue('description', 'xxx');
|
||||
|
||||
assert.equal(subject.getFirstPropertyValue('description'), 'xxx');
|
||||
subject.updatePropertyWithValue('x-foo', 'bar');
|
||||
|
||||
let list = subject.getAllProperties();
|
||||
assert.sameDeepMembers(list.map(prop => [prop.name, prop.getValues()]), [["x-foo", ["bar"]], ["description", ["xxx"]]]);
|
||||
assert.equal(subject.getFirstPropertyValue('x-foo'), 'bar');
|
||||
});
|
||||
|
||||
suite('#removeProperty', function() {
|
||||
setup(function() {
|
||||
subject = new ICAL.Component(
|
||||
fixtures.components
|
||||
);
|
||||
});
|
||||
|
||||
test('try to remove non-existent', function() {
|
||||
let result = subject.removeProperty('wtfbbq');
|
||||
assert.isFalse(result);
|
||||
});
|
||||
|
||||
test('remove by property', function() {
|
||||
let first = subject.getFirstProperty('description');
|
||||
|
||||
let result = subject.removeProperty(first);
|
||||
assert.isTrue(result, 'removes property');
|
||||
|
||||
assert.notEqual(
|
||||
subject.getFirstProperty('description'),
|
||||
first
|
||||
);
|
||||
|
||||
assert.lengthOf(subject.jCal[1], 2);
|
||||
});
|
||||
|
||||
test('remove by name', function() {
|
||||
// there are two descriptions
|
||||
let list = subject.getAllProperties();
|
||||
let first = subject.getFirstProperty('description');
|
||||
|
||||
let result = subject.removeProperty('description');
|
||||
assert.isTrue(result);
|
||||
|
||||
assert.notEqual(
|
||||
subject.getFirstProperty('description'),
|
||||
first
|
||||
);
|
||||
|
||||
assert.lengthOf(list, 2);
|
||||
});
|
||||
|
||||
test('remove non hydrated property should not shift hydrated property', function() {
|
||||
let component = new ICAL.Component([
|
||||
'vevent',
|
||||
[
|
||||
['a', {}, 'text', 'a'],
|
||||
['b', {}, 'text', 'b'],
|
||||
['c', {}, 'text', 'c']
|
||||
],
|
||||
]);
|
||||
component.getFirstPropertyValue('b');
|
||||
component.removeProperty('a');
|
||||
let cValue = component.getFirstPropertyValue('c');
|
||||
assert.equal(cValue, 'c');
|
||||
});
|
||||
});
|
||||
|
||||
suite('#removeAllProperties', function() {
|
||||
test('no name when empty', function() {
|
||||
subject = new ICAL.Component(
|
||||
fixtures.components
|
||||
);
|
||||
|
||||
assert.lengthOf(subject.jCal[1], 3);
|
||||
|
||||
subject.removeAllProperties();
|
||||
|
||||
assert.lengthOf(subject.jCal[1], 0);
|
||||
assert.ok(!subject.getFirstProperty());
|
||||
});
|
||||
|
||||
test('no name when not empty', function() {
|
||||
subject = new ICAL.Component(['vevent', [], []]);
|
||||
subject.removeAllProperties();
|
||||
subject.removeAllProperties('xfoo');
|
||||
});
|
||||
|
||||
test('with name', function() {
|
||||
subject = new ICAL.Component(
|
||||
fixtures.components
|
||||
);
|
||||
|
||||
subject.removeAllProperties('description');
|
||||
assert.lengthOf(subject.jCal[1], 1);
|
||||
|
||||
let first = subject.getFirstProperty();
|
||||
|
||||
assert.equal(first.name, 'xfoo');
|
||||
assert.equal(subject.jCal[1][0][0], 'xfoo');
|
||||
});
|
||||
});
|
||||
|
||||
test('#toJSON', function() {
|
||||
let json = JSON.stringify(subject);
|
||||
let fromJSON = new ICAL.Component(JSON.parse(json));
|
||||
|
||||
assert.deepEqual(
|
||||
fromJSON.jCal,
|
||||
subject.jCal
|
||||
);
|
||||
});
|
||||
|
||||
test('#toString', function() {
|
||||
let ical = subject.toString();
|
||||
let parsed = ICAL.parse(ical);
|
||||
let fromICAL = new ICAL.Component(parsed);
|
||||
|
||||
assert.deepEqual(subject.jCal, fromICAL.jCal);
|
||||
});
|
||||
|
||||
test('#getTimeZoneByID', async function() {
|
||||
let icsData = await testSupport.loadSample('timezone_from_file.ics');
|
||||
let vcalendar = new ICAL.Component(ICAL.parse(icsData));
|
||||
|
||||
let zone = vcalendar.getTimeZoneByID("Nowhere/Middle");
|
||||
assert.equal(zone.tzid, "Nowhere/Middle");
|
||||
|
||||
// Zone remains in cache
|
||||
vcalendar.removeSubcomponent("vtimezone");
|
||||
zone = vcalendar.getTimeZoneByID("Nowhere/Middle");
|
||||
assert.equal(zone.tzid, "Nowhere/Middle");
|
||||
|
||||
// Lookup from child component
|
||||
zone = vcalendar.getFirstSubcomponent("vevent").getTimeZoneByID("Nowhere/Middle");
|
||||
assert.equal(zone.tzid, "Nowhere/Middle");
|
||||
|
||||
// Non vcalendar root component
|
||||
let vother = new ICAL.Component(["x-other", [], [["vtimezone", [], []]]]);
|
||||
zone = vother.getFirstSubcomponent().getTimeZoneByID("Nowhere/Middle");
|
||||
assert.isNull(zone);
|
||||
|
||||
|
||||
// Missing timezone definition
|
||||
vcalendar = new ICAL.Component(ICAL.parse(icsData));
|
||||
vcalendar.removeSubcomponent("vtimezone");
|
||||
zone = vcalendar.getTimeZoneByID("Nowhere/Middle");
|
||||
assert.isNull(zone);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,967 @@
|
|||
suite('design', function() {
|
||||
|
||||
let timezone;
|
||||
suiteSetup(async function() {
|
||||
let data = await testSupport.loadSample('timezones/America/New_York.ics');
|
||||
let parsed = ICAL.parse(data);
|
||||
let vcalendar = new ICAL.Component(parsed);
|
||||
let vtimezone = vcalendar.getFirstSubcomponent('vtimezone');
|
||||
|
||||
timezone = new ICAL.Timezone(vtimezone);
|
||||
ICAL.TimezoneService.register('test', timezone);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
ICAL.TimezoneService.reset();
|
||||
});
|
||||
|
||||
let subject;
|
||||
setup(function() {
|
||||
subject = ICAL.design.defaultSet;
|
||||
});
|
||||
|
||||
suite('types', function() {
|
||||
|
||||
suite('binary', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.binary;
|
||||
});
|
||||
|
||||
test('#(un)decorate', function() {
|
||||
let expectedDecode = 'The quick brown fox jumps over the lazy dog.';
|
||||
let undecorated = 'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcy' +
|
||||
'BvdmVyIHRoZSBsYXp5IGRvZy4=';
|
||||
|
||||
let decorated = subject.decorate(undecorated);
|
||||
let decoded = decorated.decodeValue();
|
||||
|
||||
assert.equal(decoded, expectedDecode);
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(decorated),
|
||||
undecorated
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('date', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.date;
|
||||
});
|
||||
|
||||
test('#fromICAL', function() {
|
||||
let value = subject.fromICAL(
|
||||
'20121010'
|
||||
);
|
||||
|
||||
assert.equal(value, '2012-10-10');
|
||||
});
|
||||
|
||||
test('#toICAL', function() {
|
||||
let value = subject.toICAL(
|
||||
'2012-10-10'
|
||||
);
|
||||
|
||||
assert.equal(value, '20121010');
|
||||
});
|
||||
|
||||
test('#to/fromICAL (lenient)', function() {
|
||||
let value = '20120901T130000';
|
||||
let expected = '2012-09-01T13:00:00';
|
||||
|
||||
ICAL.design.strict = false;
|
||||
assert.equal(
|
||||
subject.fromICAL(value),
|
||||
expected
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.toICAL(expected),
|
||||
value
|
||||
);
|
||||
ICAL.design.strict = true;
|
||||
});
|
||||
|
||||
test('#toICAL invalid', function() {
|
||||
let value = subject.toICAL(
|
||||
'wheeeeeeeeeeeeee'
|
||||
);
|
||||
|
||||
assert.equal(value, 'wheeeeeeeeeeeeee');
|
||||
});
|
||||
|
||||
|
||||
test('#fromICAL somewhat invalid', function() {
|
||||
// Strict mode is not completely strict, it takes a lot of shortcuts in the name of
|
||||
// performance. The functions in ICAL.design don't actually throw errors, given there is no
|
||||
// error collector. With a working error collector we should make lenient mode the default
|
||||
// and have strict mode be more pedantic.
|
||||
let value = subject.fromICAL('20131210Z');
|
||||
assert.equal(value, '2013-12-10');
|
||||
});
|
||||
|
||||
test('#(un)decorate (lenient)', function() {
|
||||
let value = '2012-10-10T11:12:13';
|
||||
let prop = new ICAL.Property(['date', { tzid: 'test' }]);
|
||||
|
||||
ICAL.design.strict = false;
|
||||
|
||||
let time = subject.decorate(
|
||||
value,
|
||||
prop
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
time,
|
||||
{ year: 2012, month: 10, day: 10, hour: 11, minute: 12, second: 13, isDate: false }
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(time),
|
||||
value
|
||||
);
|
||||
ICAL.design.strict = true;
|
||||
|
||||
});
|
||||
|
||||
test('#(un)decorate (custom timezone)', function() {
|
||||
let value = '2012-10-10';
|
||||
let prop = new ICAL.Property(['date', { tzid: 'test' }]);
|
||||
|
||||
let time = subject.decorate(
|
||||
value,
|
||||
prop
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
time,
|
||||
{ year: 2012, month: 10, day: 10, isDate: true }
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(time),
|
||||
value
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('date-time', function() {
|
||||
setup(function() {
|
||||
subject = subject.value['date-time'];
|
||||
});
|
||||
|
||||
test('#(from|to)ICAL', function() {
|
||||
let value = '20120901T130000';
|
||||
let expected = '2012-09-01T13:00:00';
|
||||
|
||||
assert.equal(
|
||||
subject.fromICAL(value),
|
||||
expected
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.toICAL(expected),
|
||||
value
|
||||
);
|
||||
});
|
||||
test('#toICAL invalid', function() {
|
||||
let value = subject.toICAL(
|
||||
'wheeeeeeeeeeeeee'
|
||||
);
|
||||
|
||||
assert.equal(value, 'wheeeeeeeeeeeeee');
|
||||
});
|
||||
|
||||
test('#from/toICAL (lenient)', function() {
|
||||
let value = '20190102';
|
||||
let expected = '2019-01-02';
|
||||
|
||||
ICAL.design.strict = false;
|
||||
assert.equal(
|
||||
subject.fromICAL(value),
|
||||
expected
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.toICAL(expected),
|
||||
value
|
||||
);
|
||||
ICAL.design.strict = true;
|
||||
});
|
||||
test('#(un)decorate (lenient)', function() {
|
||||
ICAL.design.strict = false;
|
||||
let undecorated = '2012-09-01';
|
||||
let prop = new ICAL.Property(['date-time', {}]);
|
||||
|
||||
let decorated = subject.decorate(undecorated, prop);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated,
|
||||
{
|
||||
year: 2012,
|
||||
month: 9,
|
||||
day: 1,
|
||||
isDate: true
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(decorated),
|
||||
undecorated
|
||||
);
|
||||
ICAL.design.strict = true;
|
||||
});
|
||||
|
||||
test('#(un)decorate (utc)', function() {
|
||||
let undecorated = '2012-09-01T13:05:11Z';
|
||||
let prop = new ICAL.Property(['date-time', {}]);
|
||||
|
||||
let decorated = subject.decorate(undecorated, prop);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated,
|
||||
{
|
||||
year: 2012,
|
||||
month: 9,
|
||||
day: 1,
|
||||
hour: 13,
|
||||
minute: 5,
|
||||
second: 11,
|
||||
isDate: false,
|
||||
zone: ICAL.Timezone.utcTimezone
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(decorated),
|
||||
undecorated
|
||||
);
|
||||
});
|
||||
|
||||
test('#(un)decorate (custom timezone)', function() {
|
||||
let prop = new ICAL.Property(
|
||||
['date-time', { tzid: 'test' }]
|
||||
);
|
||||
assert.equal(prop.getParameter('tzid'), 'test');
|
||||
|
||||
ICAL.TimezoneService.register(
|
||||
'America/Los_Angeles',
|
||||
ICAL.Timezone.utcTimezone
|
||||
);
|
||||
|
||||
let undecorated = '2012-09-01T13:05:11';
|
||||
let decorated = subject.decorate(undecorated, prop);
|
||||
assert.equal(decorated.zone, timezone);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated,
|
||||
{
|
||||
year: 2012,
|
||||
month: 9,
|
||||
day: 1,
|
||||
hour: 13,
|
||||
minute: 5,
|
||||
second: 11,
|
||||
isDate: false
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(decorated),
|
||||
undecorated
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('time', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.time;
|
||||
});
|
||||
|
||||
test('#fromICAL', function() {
|
||||
let value = subject.fromICAL(
|
||||
'232050'
|
||||
);
|
||||
|
||||
assert.equal(value, '23:20:50');
|
||||
});
|
||||
test('#fromICAL invalid', function() {
|
||||
let value = subject.fromICAL(
|
||||
'whoop'
|
||||
);
|
||||
|
||||
assert.equal(value, 'whoop');
|
||||
});
|
||||
|
||||
test('#toICAL', function() {
|
||||
let value = subject.toICAL(
|
||||
'23:20:50'
|
||||
);
|
||||
|
||||
assert.equal(value, '232050');
|
||||
});
|
||||
test('#toICAL invalid', function() {
|
||||
let value = subject.toICAL(
|
||||
'whoop'
|
||||
);
|
||||
|
||||
assert.equal(value, 'whoop');
|
||||
});
|
||||
});
|
||||
|
||||
suite('vcard date/time types', function() {
|
||||
function testRoundtrip(jcal, ical, props, only) {
|
||||
function testForType(type, valuePrefix, valueSuffix, zone) {
|
||||
let valueType = ICAL.design.vcard.value[type];
|
||||
let prefix = valuePrefix || '';
|
||||
let suffix = valueSuffix || '';
|
||||
let jcalvalue = prefix + jcal + suffix;
|
||||
let icalvalue = prefix + ical + suffix.replace(':', '');
|
||||
let zoneName = zone || valueSuffix || "floating";
|
||||
|
||||
test(type + ' ' + zoneName + ' fromICAL/toICAL', function() {
|
||||
assert.equal(valueType.fromICAL(icalvalue), jcalvalue);
|
||||
assert.equal(valueType.toICAL(jcalvalue), icalvalue);
|
||||
});
|
||||
|
||||
test(type + ' ' + zoneName + ' decorated/undecorated', function() {
|
||||
let prop = new ICAL.Property(['anniversary', {}, type]);
|
||||
let decorated = valueType.decorate(jcalvalue, prop);
|
||||
let undecorated = valueType.undecorate(decorated);
|
||||
|
||||
assert.hasProperties(decorated._time, props);
|
||||
assert.equal(zoneName, decorated.zone.toString());
|
||||
assert.equal(undecorated, jcalvalue);
|
||||
assert.equal(decorated.toICALString(), icalvalue);
|
||||
});
|
||||
}
|
||||
(only ? suite.only : suite)(jcal, function() {
|
||||
|
||||
if (props.year || props.month || props.day) {
|
||||
testForType('date-and-or-time');
|
||||
if (!props.hour && !props.minute && !props.second) {
|
||||
testForType('date');
|
||||
} else {
|
||||
testForType('date-time');
|
||||
}
|
||||
} else if (props.hour || props.minute || props.second) {
|
||||
if (!props.year && !props.month && !props.day) {
|
||||
testForType('date-and-or-time', 'T');
|
||||
testForType('date-and-or-time', 'T', 'Z', 'UTC');
|
||||
testForType('date-and-or-time', 'T', '-08:00');
|
||||
testForType('date-and-or-time', 'T', '+08:00');
|
||||
testForType('time');
|
||||
testForType('time', null, 'Z', 'UTC');
|
||||
testForType('time', null, '-08:00');
|
||||
testForType('time', null, '+08:00');
|
||||
} else {
|
||||
testForType('date-and-or-time', null);
|
||||
testForType('date-and-or-time', null, 'Z', 'UTC');
|
||||
testForType('date-and-or-time', null, '-08:00');
|
||||
testForType('date-and-or-time', null, '+08:00');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
testRoundtrip.only = function(jcal, ical, props) {
|
||||
testRoundtrip(jcal, ical, props, true);
|
||||
};
|
||||
|
||||
// dates
|
||||
testRoundtrip('1985-04-12', '19850412', {
|
||||
year: 1985,
|
||||
month: 4,
|
||||
day: 12,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('1985-04', '1985-04', {
|
||||
year: 1985,
|
||||
month: 4,
|
||||
day: null,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('1985', '1985', {
|
||||
year: 1985,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('--04-12', '--0412', {
|
||||
year: null,
|
||||
month: 4,
|
||||
day: 12,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('--04', '--04', {
|
||||
year: null,
|
||||
month: 4,
|
||||
day: null,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('---12', '---12', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: 12,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
|
||||
// times
|
||||
testRoundtrip('23:20:50', '232050', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: 50,
|
||||
});
|
||||
testRoundtrip('23:20', '2320', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: null,
|
||||
});
|
||||
testRoundtrip('23', '23', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: 23,
|
||||
minute: null,
|
||||
second: null,
|
||||
});
|
||||
testRoundtrip('-20:50', '-2050', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: null,
|
||||
minute: 20,
|
||||
second: 50,
|
||||
});
|
||||
testRoundtrip('-20', '-20', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: null,
|
||||
minute: 20,
|
||||
second: null,
|
||||
});
|
||||
testRoundtrip('--50', '--50', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: null,
|
||||
hour: null,
|
||||
minute: null,
|
||||
second: 50,
|
||||
});
|
||||
|
||||
// date-times
|
||||
testRoundtrip('1985-04-12T23:20:50', '19850412T232050', {
|
||||
year: 1985,
|
||||
month: 4,
|
||||
day: 12,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: 50
|
||||
});
|
||||
testRoundtrip('1985-04-12T23:20', '19850412T2320', {
|
||||
year: 1985,
|
||||
month: 4,
|
||||
day: 12,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('1985-04-12T23', '19850412T23', {
|
||||
year: 1985,
|
||||
month: 4,
|
||||
day: 12,
|
||||
hour: 23,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('--04-12T23:20', '--0412T2320', {
|
||||
year: null,
|
||||
month: 4,
|
||||
day: 12,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('--04T23:20', '--04T2320', {
|
||||
year: null,
|
||||
month: 4,
|
||||
day: null,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('---12T23:20', '---12T2320', {
|
||||
year: null,
|
||||
month: null,
|
||||
day: 12,
|
||||
hour: 23,
|
||||
minute: 20,
|
||||
second: null
|
||||
});
|
||||
testRoundtrip('--04T23', '--04T23', {
|
||||
year: null,
|
||||
month: 4,
|
||||
day: null,
|
||||
hour: 23,
|
||||
minute: null,
|
||||
second: null
|
||||
});
|
||||
});
|
||||
|
||||
suite('duration', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.duration;
|
||||
});
|
||||
|
||||
test('#(un)decorate', function() {
|
||||
let undecorated = 'P15DT5H5M20S';
|
||||
let decorated = subject.decorate(undecorated);
|
||||
assert.equal(subject.undecorate(decorated), undecorated);
|
||||
});
|
||||
});
|
||||
|
||||
suite('float', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.float;
|
||||
});
|
||||
|
||||
test('#(from|to)ICAL', function() {
|
||||
let original = '1.5';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.equal(fromICAL, 1.5);
|
||||
assert.equal(subject.toICAL(fromICAL), original);
|
||||
});
|
||||
});
|
||||
|
||||
suite('integer', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.integer;
|
||||
});
|
||||
|
||||
test('#(from|to)ICAL', function() {
|
||||
let original = '105';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.equal(fromICAL, 105);
|
||||
assert.equal(subject.toICAL(fromICAL), original);
|
||||
});
|
||||
});
|
||||
|
||||
suite('period', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.period;
|
||||
});
|
||||
test('#(to|from)ICAL date/date (lenient)', function() {
|
||||
let original = '19970101/19970102';
|
||||
ICAL.design.strict = false;
|
||||
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.deepEqual(
|
||||
fromICAL,
|
||||
['1997-01-01', '1997-01-02']
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.toICAL(fromICAL),
|
||||
original
|
||||
);
|
||||
|
||||
ICAL.design.strict = true;
|
||||
});
|
||||
|
||||
test('#(to|from)ICAL date/date', function() {
|
||||
let original = '19970101T180000Z/19970102T070000Z';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.deepEqual(
|
||||
fromICAL,
|
||||
['1997-01-01T18:00:00Z', '1997-01-02T07:00:00Z']
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
subject.toICAL(fromICAL),
|
||||
original
|
||||
);
|
||||
});
|
||||
|
||||
test('#(un)decorate (date-time/duration)', function() {
|
||||
let prop = new ICAL.Property(['date', { tzid: 'test' }]);
|
||||
|
||||
let undecorated = ['1997-01-01T18:00:00', 'PT5H30M'];
|
||||
let decorated = subject.decorate(
|
||||
undecorated,
|
||||
prop
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.start,
|
||||
{
|
||||
year: 1997,
|
||||
day: 1,
|
||||
month: 1,
|
||||
hour: 18
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(decorated.start.zone, timezone);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.duration,
|
||||
{
|
||||
hours: 5,
|
||||
minutes: 30
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(subject.undecorate(decorated), undecorated);
|
||||
});
|
||||
|
||||
test('#(un)decorate (date-time/date-time)', function() {
|
||||
let prop = new ICAL.Property(['date', { tzid: 'test' }]);
|
||||
|
||||
let undecorated = ['1997-01-01T18:00:00', '1998-01-01T17:00:00'];
|
||||
let decorated = subject.decorate(
|
||||
undecorated,
|
||||
prop
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.start,
|
||||
{
|
||||
year: 1997,
|
||||
day: 1,
|
||||
month: 1,
|
||||
hour: 18
|
||||
}
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.end,
|
||||
{
|
||||
year: 1998,
|
||||
day: 1,
|
||||
month: 1,
|
||||
hour: 17
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
assert.equal(decorated.start.zone, timezone);
|
||||
assert.equal(decorated.end.zone, timezone);
|
||||
|
||||
assert.deepEqual(subject.undecorate(decorated), undecorated);
|
||||
});
|
||||
|
||||
test('#(un)decorate (lenient, date/date)', function() {
|
||||
ICAL.design.strict = false;
|
||||
|
||||
let prop = new ICAL.Property(['date', { tzid: 'test' }]);
|
||||
|
||||
let undecorated = ['1997-01-01', '1998-01-01'];
|
||||
let decorated = subject.decorate(
|
||||
undecorated,
|
||||
prop
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.start,
|
||||
{
|
||||
year: 1997,
|
||||
day: 1,
|
||||
month: 1,
|
||||
isDate: true
|
||||
}
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.end,
|
||||
{
|
||||
year: 1998,
|
||||
day: 1,
|
||||
month: 1,
|
||||
isDate: true
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(subject.undecorate(decorated), undecorated);
|
||||
|
||||
ICAL.design.strict = true;
|
||||
});
|
||||
|
||||
test('#(un)decorate (date-time/duration)', function() {
|
||||
let prop = new ICAL.Property(['date', { tzid: 'test' }]);
|
||||
|
||||
let undecorated = ['1997-01-01T18:00:00', 'PT5H30M'];
|
||||
let decorated = subject.decorate(
|
||||
undecorated,
|
||||
prop
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.start,
|
||||
{
|
||||
year: 1997,
|
||||
day: 1,
|
||||
month: 1,
|
||||
hour: 18
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(decorated.start.zone, timezone);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.duration,
|
||||
{
|
||||
hours: 5,
|
||||
minutes: 30
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(subject.undecorate(decorated), undecorated);
|
||||
});
|
||||
});
|
||||
|
||||
suite('recur', function() {
|
||||
setup(function() {
|
||||
subject = subject.value.recur;
|
||||
});
|
||||
|
||||
test('#(to|from)ICAL', function() {
|
||||
let original = 'FREQ=MONTHLY;UNTIL=20121112T131415;COUNT=1';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.deepEqual(fromICAL, {
|
||||
freq: 'MONTHLY',
|
||||
until: '2012-11-12T13:14:15',
|
||||
count: 1
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
subject.toICAL(fromICAL),
|
||||
original
|
||||
);
|
||||
});
|
||||
|
||||
test('#(un)decorate', function() {
|
||||
let undecorated = { freq: "MONTHLY", byday: ["MO", "TU", "WE", "TH", "FR"], until: "2012-10-12" };
|
||||
let decorated = subject.decorate(undecorated);
|
||||
|
||||
assert.instanceOf(decorated, ICAL.Recur);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated,
|
||||
{
|
||||
freq: 'MONTHLY',
|
||||
parts: {
|
||||
BYDAY: ['MO', 'TU', 'WE', 'TH', 'FR']
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.hasProperties(
|
||||
decorated.until,
|
||||
{
|
||||
year: 2012,
|
||||
month: 10,
|
||||
day: 12
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(subject.undecorate(decorated), undecorated);
|
||||
});
|
||||
});
|
||||
|
||||
suite('utc-offset', function() {
|
||||
setup(function() {
|
||||
subject = subject.value['utc-offset'];
|
||||
});
|
||||
|
||||
test('#(to|from)ICAL without seconds', function() {
|
||||
let original = '-0500';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.equal(fromICAL, '-05:00');
|
||||
assert.equal(
|
||||
subject.toICAL(fromICAL),
|
||||
original
|
||||
);
|
||||
});
|
||||
|
||||
test('#(to|from)ICAL with seconds', function() {
|
||||
let original = '+054515';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.equal(fromICAL, '+05:45:15');
|
||||
assert.equal(
|
||||
subject.toICAL(fromICAL),
|
||||
original
|
||||
);
|
||||
});
|
||||
|
||||
test('#(un)decorate', function() {
|
||||
let undecorated = '-05:00';
|
||||
let decorated = subject.decorate(undecorated);
|
||||
|
||||
assert.equal(decorated.hours, 5, 'hours');
|
||||
assert.equal(decorated.factor, -1, 'factor');
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(decorated),
|
||||
undecorated
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('utc-offset (vcard3)', function() {
|
||||
setup(function() {
|
||||
subject = ICAL.design.vcard3.value['utc-offset'];
|
||||
});
|
||||
|
||||
test('#(to|from)ICAL', function() {
|
||||
let original = '-05:00';
|
||||
let fromICAL = subject.fromICAL(original);
|
||||
|
||||
assert.equal(fromICAL, '-05:00');
|
||||
assert.equal(
|
||||
subject.toICAL(fromICAL),
|
||||
original
|
||||
);
|
||||
});
|
||||
|
||||
test('#(un)decorate', function() {
|
||||
let undecorated = '-05:00';
|
||||
let decorated = subject.decorate(undecorated);
|
||||
|
||||
assert.equal(decorated.hours, 5, 'hours');
|
||||
assert.equal(decorated.factor, -1, 'factor');
|
||||
|
||||
assert.equal(
|
||||
subject.undecorate(decorated),
|
||||
undecorated
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite("unknown and default values", function() {
|
||||
test("unknown x-prop", function() {
|
||||
let prop = new ICAL.Property("x-wr-calname");
|
||||
assert.equal(prop.type, "unknown");
|
||||
|
||||
prop = ICAL.Property.fromString("X-WR-CALNAME:value");
|
||||
assert.equal(prop.type, "unknown");
|
||||
});
|
||||
|
||||
test("unknown iana prop", function() {
|
||||
let prop = new ICAL.Property("standardized");
|
||||
assert.equal(prop.type, "unknown");
|
||||
|
||||
prop = ICAL.Property.fromString("STANDARDIZED:value");
|
||||
assert.equal(prop.type, "unknown");
|
||||
});
|
||||
|
||||
test("known text type", function() {
|
||||
let prop = new ICAL.Property("description");
|
||||
assert.equal(prop.type, "text");
|
||||
|
||||
prop = ICAL.Property.fromString("DESCRIPTION:value");
|
||||
assert.equal(prop.type, "text");
|
||||
});
|
||||
|
||||
test("encoded text value roundtrip", function() {
|
||||
let prop = new ICAL.Property("description");
|
||||
prop.setValue("hello, world");
|
||||
let propVal = prop.toICALString();
|
||||
assert.equal(propVal, "DESCRIPTION:hello\\, world");
|
||||
|
||||
prop = ICAL.Property.fromString(propVal);
|
||||
assert.equal(prop.getFirstValue(), "hello, world");
|
||||
});
|
||||
|
||||
test("encoded unknown value roundtrip", function() {
|
||||
let prop = new ICAL.Property("x-wr-calname");
|
||||
prop.setValue("hello, world");
|
||||
let propVal = prop.toICALString();
|
||||
assert.equal(propVal, "X-WR-CALNAME:hello, world");
|
||||
|
||||
prop = ICAL.Property.fromString(propVal);
|
||||
assert.equal(prop.getFirstValue(), "hello, world");
|
||||
});
|
||||
|
||||
test("encoded unknown value from string", function() {
|
||||
let prop = ICAL.Property.fromString("X-WR-CALNAME:hello\\, world");
|
||||
assert.equal(prop.getFirstValue(), "hello\\, world");
|
||||
});
|
||||
|
||||
suite("registration", function() {
|
||||
test("newly registered property", function() {
|
||||
let prop = new ICAL.Property("nonstandard");
|
||||
assert.equal(prop.type, "unknown");
|
||||
|
||||
ICAL.design.defaultSet.property.nonstandard = {
|
||||
defaultType: "date-time"
|
||||
};
|
||||
|
||||
prop = new ICAL.Property("nonstandard");
|
||||
assert.equal(prop.type, "date-time");
|
||||
});
|
||||
|
||||
test("unknown value type", function() {
|
||||
let prop = ICAL.Property.fromString("X-PROP;VALUE=FUZZY:WARM");
|
||||
assert.equal(prop.name, "x-prop");
|
||||
assert.equal(prop.type, "fuzzy");
|
||||
assert.equal(prop.getFirstValue(), "WARM");
|
||||
prop.setValue("FREEZING");
|
||||
assert.equal(prop.getFirstValue(), "FREEZING");
|
||||
});
|
||||
|
||||
test("newly registered value type", function() {
|
||||
ICAL.design.defaultSet.value.fuzzy = {
|
||||
fromICAL: function(aValue) {
|
||||
return aValue.toLowerCase();
|
||||
},
|
||||
toICAL: function(aValue) {
|
||||
return aValue.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
let prop = ICAL.Property.fromString("X-PROP;VALUE=FUZZY:WARM");
|
||||
assert.equal(prop.name, "x-prop");
|
||||
assert.equal(prop.getFirstValue(), "warm");
|
||||
assert.match(prop.toICALString(), /WARM/);
|
||||
});
|
||||
|
||||
test("newly registered parameter", function() {
|
||||
let prop = ICAL.Property.fromString("X-PROP;VALS=a,b,c:def");
|
||||
let param = prop.getParameter("vals");
|
||||
assert.equal(param, "a,b,c");
|
||||
|
||||
ICAL.design.defaultSet.param.vals = { multiValue: "," };
|
||||
|
||||
prop = ICAL.Property.fromString("X-PROP;VALS=a,b,c:def");
|
||||
param = prop.getParameter("vals");
|
||||
assert.deepEqual(param, ["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,177 @@
|
|||
suite('ical/duration', function() {
|
||||
test('#clone', function() {
|
||||
let subject = ICAL.Duration.fromData({
|
||||
weeks: 1,
|
||||
days: 2,
|
||||
hours: 3,
|
||||
minutes: 4,
|
||||
seconds: 5,
|
||||
isNegative: true
|
||||
});
|
||||
|
||||
let expected = {
|
||||
weeks: 1,
|
||||
days: 2,
|
||||
hours: 3,
|
||||
minutes: 4,
|
||||
seconds: 5,
|
||||
isNegative: true
|
||||
};
|
||||
|
||||
let expected2 = {
|
||||
weeks: 6,
|
||||
days: 7,
|
||||
hours: 8,
|
||||
minutes: 9,
|
||||
seconds: 10,
|
||||
isNegative: true
|
||||
};
|
||||
|
||||
let subject2 = subject.clone();
|
||||
assert.hasProperties(subject, expected, 'base object unchanged');
|
||||
assert.hasProperties(subject2, expected, 'cloned object unchanged');
|
||||
|
||||
for (let k in expected2) {
|
||||
subject2[k] = expected2[k];
|
||||
}
|
||||
|
||||
assert.hasProperties(subject, expected, 'base object unchanged');
|
||||
assert.hasProperties(subject2, expected2, 'cloned object changed');
|
||||
});
|
||||
|
||||
test('#reset', function() {
|
||||
let expected = {
|
||||
weeks: 1,
|
||||
days: 2,
|
||||
hours: 3,
|
||||
minutes: 4,
|
||||
seconds: 5,
|
||||
isNegative: true
|
||||
};
|
||||
let subject = new ICAL.Duration(expected);
|
||||
assert.hasProperties(subject, expected);
|
||||
|
||||
subject.reset();
|
||||
|
||||
assert.hasProperties(subject, {
|
||||
weeks: 0,
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isNegative: false
|
||||
});
|
||||
|
||||
assert.equal(subject.toString(), "PT0S");
|
||||
});
|
||||
|
||||
suite('#normalize', function() {
|
||||
function verify(name, str, data) {
|
||||
test(name, function() {
|
||||
let subject = new ICAL.Duration();
|
||||
for (let k in data) {
|
||||
subject[k] = data[k];
|
||||
}
|
||||
subject.normalize();
|
||||
assert.equal(subject.toString(), str);
|
||||
assert.equal(subject.toICALString(), str);
|
||||
});
|
||||
}
|
||||
|
||||
verify('weeks and day => days', 'P50D', {
|
||||
weeks: 7,
|
||||
days: 1
|
||||
});
|
||||
verify('days => week', 'P2W', {
|
||||
days: 14
|
||||
});
|
||||
verify('days and weeks => week', 'P4W', {
|
||||
weeks: 2,
|
||||
days: 14
|
||||
});
|
||||
verify('seconds => everything', 'P1DT1H1M1S', {
|
||||
seconds: 86400 + 3600 + 60 + 1
|
||||
});
|
||||
});
|
||||
|
||||
suite("#compare", function() {
|
||||
function verify(str, a, b, cmp) {
|
||||
test(str, function() {
|
||||
let dur_a = ICAL.Duration.fromString(a);
|
||||
let dur_b = ICAL.Duration.fromString(b);
|
||||
assert.equal(dur_a.compare(dur_b), cmp);
|
||||
});
|
||||
}
|
||||
|
||||
verify('a>b', 'PT3H', 'PT1S', 1);
|
||||
verify('a<b', 'PT2M', 'P1W', -1);
|
||||
verify('a=b', 'P1W', 'P7D', 0);
|
||||
verify('negative/positive', 'P2H', '-P2H', 1);
|
||||
});
|
||||
|
||||
suite('#fromString', function() {
|
||||
let base = {
|
||||
weeks: 0,
|
||||
days: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isNegative: false
|
||||
};
|
||||
|
||||
function verify(string, data, verifystring) {
|
||||
let expected = {};
|
||||
let key;
|
||||
|
||||
for (key in base) {
|
||||
expected[key] = base[key];
|
||||
}
|
||||
|
||||
for (key in data) {
|
||||
expected[key] = data[key];
|
||||
}
|
||||
|
||||
test('parse: "' + string + '"', function() {
|
||||
let subject = ICAL.Duration.fromString(string);
|
||||
assert.hasProperties(subject, expected);
|
||||
assert.equal(subject.toString(), verifystring || string);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyFail(string, errorParam) {
|
||||
test('expected failure: ' + string, function() {
|
||||
assert.throws(function() {
|
||||
ICAL.Duration.fromString(string);
|
||||
}, errorParam);
|
||||
});
|
||||
}
|
||||
|
||||
verify('P7W', {
|
||||
weeks: 7
|
||||
});
|
||||
|
||||
verify('PT1H0M0S', {
|
||||
hours: 1
|
||||
}, "PT1H");
|
||||
|
||||
verify('PT15M', {
|
||||
minutes: 15
|
||||
});
|
||||
|
||||
verify('P15DT5H0M20S', {
|
||||
days: 15,
|
||||
hours: 5,
|
||||
seconds: 20
|
||||
}, "P15DT5H20S");
|
||||
|
||||
verify('-P0DT0H30M0S', {
|
||||
isNegative: true,
|
||||
weeks: 0,
|
||||
days: 0,
|
||||
minutes: 30,
|
||||
seconds: 0
|
||||
}, "-PT30M");
|
||||
|
||||
verifyFail('PT1WH', /Missing number before "H"/);
|
||||
verifyFail('PT1WsomeH', /Invalid number "some" before "H"/);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,67 @@
|
|||
import assert from "assert";
|
||||
|
||||
|
||||
/**
|
||||
* The tests in this suite are known to fail, due to a bug in the library. If the tests here start
|
||||
* failing in the sense of mocha, then the test is passing and you have either:
|
||||
*
|
||||
* 1) Fixed the bug (yay! remove the test)
|
||||
* 2) Triggered some unknown underlying issue (boo! investigate)
|
||||
*
|
||||
* When adding something here, make sure to link the issue.
|
||||
*/
|
||||
suite('Known failures', function() {
|
||||
function testKnownFailure(message, testFn, only) {
|
||||
let runner = only ? test.only : test;
|
||||
runner(message, function(done) {
|
||||
try {
|
||||
testFn(done);
|
||||
done(new Error("Expected test fo fail"));
|
||||
} catch (e) {
|
||||
if (e instanceof assert.AssertionError) {
|
||||
this.skip();
|
||||
} else {
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
testKnownFailure.only = function(message, testFn) {
|
||||
return testKnownFailure(message, testFn, true);
|
||||
};
|
||||
|
||||
// Escaped parameters are not correctly parsed
|
||||
// Please see https://github.com/kewisch/ical.js/issues/669
|
||||
testKnownFailure('Parameter escaping', function() {
|
||||
let subject = ICAL.Property.fromString(`ATTENDEE;CN="Z\\;":mailto:z@example.org`);
|
||||
assert.equal(subject.getParameter("cn"), "Z\\;");
|
||||
assert.equal(subject.getFirstValue(), "mailto:z@example.org");
|
||||
});
|
||||
|
||||
// Quoted multi-value parameters leak into the value
|
||||
// Please see https://github.com/kewisch/ical.js/issues/634
|
||||
testKnownFailure('with quoted multi-value parameter', function() {
|
||||
let attendee = ICAL.Property.fromString(
|
||||
'ATTENDEE;MEMBER=' +
|
||||
'"mailto:mygroup@localhost",' +
|
||||
'"mailto:mygroup2@localhost",' +
|
||||
'"mailto:mygroup3@localhost":' +
|
||||
'mailto:user2@localhost'
|
||||
);
|
||||
|
||||
let expected = [
|
||||
'attendee',
|
||||
{
|
||||
member: [
|
||||
'mailto:mygroup@localhost',
|
||||
'mailto:mygroup2@localhost',
|
||||
'mailto:mygroup3@localhost'
|
||||
]
|
||||
},
|
||||
'cal-address',
|
||||
'mailto:user2@localhost'
|
||||
];
|
||||
|
||||
assert.deepEqual(attendee.toJSON(), expected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,160 @@
|
|||
suite('ICAL.helpers', function() {
|
||||
|
||||
suite('#clone', function() {
|
||||
let subject = ICAL.helpers.clone;
|
||||
test('some primatives', function() {
|
||||
assert.equal(subject(null, false), null);
|
||||
assert.equal(subject(123, false), 123);
|
||||
assert.equal(subject(null, true), null);
|
||||
assert.equal(subject(123, true), 123);
|
||||
});
|
||||
|
||||
test('a date', function() {
|
||||
let date = new Date(2015, 1, 1);
|
||||
let time = date.getTime();
|
||||
let copy = subject(date, false);
|
||||
|
||||
copy.setYear(2016);
|
||||
assert.notEqual(time, copy.getTime());
|
||||
});
|
||||
|
||||
test('clonable', function() {
|
||||
let obj = { clone: function() { return "test"; } };
|
||||
assert.equal(subject(obj, false), "test");
|
||||
});
|
||||
|
||||
test('shallow array', function() {
|
||||
let obj = { v: 2 };
|
||||
let arr = [obj, 2, 3];
|
||||
|
||||
let result = subject(arr, false);
|
||||
assert.deepEqual(result, [{ v: 2 }, 2, 3]);
|
||||
obj.v = 3;
|
||||
assert.deepEqual(result, [{ v: 3 }, 2, 3]);
|
||||
});
|
||||
|
||||
test('deep array', function() {
|
||||
let obj = { v: 2 };
|
||||
let arr = [obj, 2, 3];
|
||||
|
||||
let result = subject(arr, true);
|
||||
assert.deepEqual(result, [{ v: 2 }, 2, 3]);
|
||||
obj.v = 3;
|
||||
assert.deepEqual(result, [{ v: 2 }, 2, 3]);
|
||||
});
|
||||
|
||||
test('shallow object', function() {
|
||||
let deepobj = { v: 2 };
|
||||
let obj = { a: deepobj, b: 2 };
|
||||
|
||||
let result = subject(obj, false);
|
||||
assert.deepEqual(result, { a: { v: 2 }, b: 2 });
|
||||
deepobj.v = 3;
|
||||
assert.deepEqual(result, { a: { v: 3 }, b: 2 });
|
||||
});
|
||||
|
||||
test('deep object', function() {
|
||||
let deepobj = { v: 2 };
|
||||
let obj = { a: deepobj, b: 2 };
|
||||
|
||||
let result = subject(obj, true);
|
||||
assert.deepEqual(result, { a: { v: 2 }, b: 2 });
|
||||
deepobj.v = 3;
|
||||
assert.deepEqual(result, { a: { v: 2 }, b: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
suite('#pad2', function() {
|
||||
let subject = ICAL.helpers.pad2;
|
||||
|
||||
test('with string', function() {
|
||||
assert.equal(subject(""), "00");
|
||||
assert.equal(subject("1"), "01");
|
||||
assert.equal(subject("12"), "12");
|
||||
assert.equal(subject("123"), "123");
|
||||
});
|
||||
|
||||
test('with number', function() {
|
||||
assert.equal(subject(0), "00");
|
||||
assert.equal(subject(1), "01");
|
||||
assert.equal(subject(12), "12");
|
||||
assert.equal(subject(123), "123");
|
||||
});
|
||||
|
||||
test('with boolean', function() {
|
||||
assert.equal(subject(true), "true");
|
||||
});
|
||||
});
|
||||
|
||||
suite('#foldline', function() {
|
||||
let subject = ICAL.helpers.foldline;
|
||||
|
||||
test('empty values', function() {
|
||||
assert.strictEqual(subject(null), "");
|
||||
assert.strictEqual(subject(""), "");
|
||||
});
|
||||
|
||||
// Most other cases are covered by other tests
|
||||
});
|
||||
|
||||
suite('#updateTimezones', function() {
|
||||
let subject = ICAL.helpers.updateTimezones;
|
||||
let cal;
|
||||
|
||||
suiteSetup(async function() {
|
||||
let data = await testSupport.loadSample('minimal.ics');
|
||||
cal = new ICAL.Component(ICAL.parse(data));
|
||||
|
||||
data = await testSupport.loadSample('timezones/America/Atikokan.ics');
|
||||
ICAL.TimezoneService.register(
|
||||
(new ICAL.Component(ICAL.parse(data))).getFirstSubcomponent("vtimezone")
|
||||
);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
ICAL.TimezoneService.reset();
|
||||
});
|
||||
|
||||
test('timezones already correct', function() {
|
||||
let vtimezones;
|
||||
vtimezones = cal.getAllSubcomponents("vtimezone");
|
||||
assert.strictEqual(vtimezones.length, 1);
|
||||
assert.strictEqual(
|
||||
vtimezones[0].getFirstProperty("tzid").getFirstValue(),
|
||||
"America/Los_Angeles"
|
||||
);
|
||||
});
|
||||
|
||||
test('remove extra timezones', function() {
|
||||
let vtimezones;
|
||||
cal.addSubcomponent(
|
||||
ICAL.TimezoneService.get("America/Atikokan").component
|
||||
);
|
||||
vtimezones = cal.getAllSubcomponents("vtimezone");
|
||||
assert.strictEqual(vtimezones.length, 2);
|
||||
|
||||
vtimezones = subject(cal).getAllSubcomponents("vtimezone");
|
||||
assert.strictEqual(vtimezones.length, 1);
|
||||
assert.strictEqual(
|
||||
vtimezones[0].getFirstProperty("tzid").getFirstValue(),
|
||||
"America/Los_Angeles"
|
||||
);
|
||||
});
|
||||
|
||||
test('add missing timezones', function() {
|
||||
let vtimezones;
|
||||
cal.getFirstSubcomponent("vevent")
|
||||
.getFirstProperty("dtend").setParameter("tzid", "America/Atikokan");
|
||||
vtimezones = cal.getAllSubcomponents("vtimezone");
|
||||
assert(vtimezones.length, 1);
|
||||
|
||||
vtimezones = subject(cal).getAllSubcomponents("vtimezone");
|
||||
assert.strictEqual(vtimezones.length, 2);
|
||||
});
|
||||
|
||||
test('return non-vcalendar components unchanged', function() {
|
||||
let vevent = cal.getFirstSubcomponent("vevent");
|
||||
assert.deepEqual(subject(vevent), vevent);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,317 @@
|
|||
suite('parserv2', function() {
|
||||
|
||||
let subject;
|
||||
setup(function() {
|
||||
subject = ICAL.parse;
|
||||
});
|
||||
|
||||
/**
|
||||
* Full parser tests fetch two resources
|
||||
* (one to parse, one is expected
|
||||
*/
|
||||
suite('full parser tests', function() {
|
||||
let root = 'test/parser/';
|
||||
let list = [
|
||||
// icalendar tests
|
||||
'rfc.ics',
|
||||
'single_empty_vcalendar.ics',
|
||||
'property_params.ics',
|
||||
'newline_junk.ics',
|
||||
'unfold_properties.ics',
|
||||
'quoted_params.ics',
|
||||
'multivalue.ics',
|
||||
'values.ics',
|
||||
'recur.ics',
|
||||
'base64.ics',
|
||||
'dates.ics',
|
||||
'time.ics',
|
||||
'boolean.ics',
|
||||
'float.ics',
|
||||
'integer.ics',
|
||||
'period.ics',
|
||||
'utc_offset.ics',
|
||||
'component.ics',
|
||||
'tzid_with_gmt.ics',
|
||||
'multiple_root_components.ics',
|
||||
'grouped.ics',
|
||||
|
||||
// vcard tests
|
||||
'vcard.vcf',
|
||||
'vcard_author.vcf',
|
||||
'vcard3.vcf',
|
||||
'vcard_grouped.vcf',
|
||||
'escape_semicolon.vcf'
|
||||
];
|
||||
|
||||
list.forEach(function(path) {
|
||||
suite(path.replace('_', ' '), function() {
|
||||
let input;
|
||||
let expected;
|
||||
|
||||
// fetch ical
|
||||
setup(async function() {
|
||||
input = await testSupport.load(root + path);
|
||||
});
|
||||
|
||||
// fetch json
|
||||
setup(async function() {
|
||||
let data = await testSupport.load(root + path.replace(/vcf|ics$/, 'json'));
|
||||
try {
|
||||
expected = JSON.parse(data.trim());
|
||||
} catch {
|
||||
throw new Error('expect json is invalid: \n\n' + data);
|
||||
}
|
||||
});
|
||||
|
||||
function jsonEqual(jsonActual, jsonExpected) {
|
||||
assert.deepEqual(
|
||||
jsonActual,
|
||||
jsonExpected,
|
||||
'hint use: ' +
|
||||
'http://tlrobinson.net/projects/javascript-fun/jsondiff/\n\n' +
|
||||
'\nexpected:\n\n' +
|
||||
JSON.stringify(jsonActual, null, 2) +
|
||||
'\n\n to equal:\n\n ' +
|
||||
JSON.stringify(jsonExpected, null, 2) + '\n\n'
|
||||
);
|
||||
}
|
||||
|
||||
test('round-trip', function() {
|
||||
let parsed = subject(input);
|
||||
let ical = ICAL.stringify(parsed);
|
||||
|
||||
// NOTE: this is not an absolute test that serialization
|
||||
// works as our parser should be error tolerant and
|
||||
// it is remotely possible that we consistently produce
|
||||
// ICAL that only we can parse.
|
||||
jsonEqual(
|
||||
subject(ical),
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('compare', function() {
|
||||
let actual = subject(input);
|
||||
jsonEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('invalid ical', function() {
|
||||
|
||||
test('invalid property', function() {
|
||||
let ical = 'BEGIN:VCALENDAR\n';
|
||||
// no param or value token
|
||||
ical += 'DTSTART\n';
|
||||
ical += 'DESCRIPTION:1\n';
|
||||
ical += 'END:VCALENDAR';
|
||||
|
||||
assert.throws(function() {
|
||||
subject(ical);
|
||||
}, /invalid line/);
|
||||
});
|
||||
|
||||
test('invalid quoted params', function() {
|
||||
let ical = 'BEGIN:VCALENDAR\n';
|
||||
ical += 'X-FOO;BAR="quoted\n';
|
||||
// an invalid newline inside quoted parameter
|
||||
ical += 'params";FOO=baz:realvalue\n';
|
||||
ical += 'END:VCALENDAR';
|
||||
|
||||
assert.throws(function() {
|
||||
subject(ical);
|
||||
}, /invalid line/);
|
||||
});
|
||||
|
||||
test('missing value with param delimiter', function() {
|
||||
let ical = 'BEGIN:VCALENDAR\n' +
|
||||
'X-FOO;\n';
|
||||
assert.throws(function() {
|
||||
subject(ical);
|
||||
}, "Invalid parameters in");
|
||||
});
|
||||
|
||||
test('missing param name ', function() {
|
||||
let ical = 'BEGIN:VCALENDAR\n' +
|
||||
'X-FOO;=\n';
|
||||
assert.throws(function() {
|
||||
subject(ical);
|
||||
}, "Empty parameter name in");
|
||||
});
|
||||
|
||||
test('missing param value', function() {
|
||||
let ical = 'BEGIN:VCALENDAR\n' +
|
||||
'X-FOO;BAR=\n';
|
||||
assert.throws(function() {
|
||||
subject(ical);
|
||||
}, "Missing parameter value in");
|
||||
});
|
||||
|
||||
test('missing component end', function() {
|
||||
let ical = 'BEGIN:VCALENDAR\n';
|
||||
ical += 'BEGIN:VEVENT\n';
|
||||
ical += 'BEGIN:VALARM\n';
|
||||
ical += 'DESCRIPTION: foo\n';
|
||||
ical += 'END:VALARM';
|
||||
// ended calendar before event
|
||||
ical += 'END:VCALENDAR';
|
||||
|
||||
assert.throws(function() {
|
||||
subject(ical);
|
||||
}, /invalid/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('#_parseParameters', function() {
|
||||
test('with processed text', function() {
|
||||
let input = ';FOO=x\\na';
|
||||
let expected = {
|
||||
'foo': 'x\na'
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
subject._parseParameters(input, 0, ICAL.design.defaultSet)[0],
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('with multiple vCard TYPE parameters', function() {
|
||||
let input = ';TYPE=work;TYPE=voice';
|
||||
let expected = {
|
||||
'type': ['work', 'voice']
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
subject._parseParameters(input, 0, ICAL.design.components.vcard)[0],
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('with multiple iCalendar MEMBER parameters', function() {
|
||||
let input = ';MEMBER="urn:one","urn:two";MEMBER="urn:three"';
|
||||
let expected = {
|
||||
'member': ['urn:one', 'urn:two', 'urn:three']
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
subject._parseParameters(input, 0, ICAL.design.components.vevent)[0],
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('with comma in singleValue parameter', function() {
|
||||
let input = ';LABEL="A, B"';
|
||||
let expected = {
|
||||
'label': 'A, B'
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
subject._parseParameters(input, 0, ICAL.design.components.vcard)[0],
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('with comma in singleValue parameter after multiValue parameter', function() {
|
||||
// TYPE allows multiple values, whereas LABEL doesn't.
|
||||
let input = ';TYPE=home;LABEL="A, B"';
|
||||
let expected = {
|
||||
'type': 'home',
|
||||
'label': 'A, B'
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
subject._parseParameters(input, 0, ICAL.design.components.vcard)[0],
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('with quoted value', function() {
|
||||
let input = ';FMTTYPE="text/html":Here is HTML with signs like =;';
|
||||
let expected = {
|
||||
'fmttype': 'text/html'
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
subject._parseParameters(input, 0, ICAL.design.components.vevent)[0],
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('#_parseMultiValue', function() {
|
||||
let values = 'woot\\, category,foo,bar,baz';
|
||||
let result = [];
|
||||
assert.deepEqual(
|
||||
subject._parseMultiValue(values, ',', 'text', result, null, ICAL.design.defaultSet),
|
||||
['woot, category', 'foo', 'bar', 'baz']
|
||||
);
|
||||
});
|
||||
|
||||
suite('#_parseValue', function() {
|
||||
test('text', function() {
|
||||
let value = 'start \\n next';
|
||||
let expected = 'start \n next';
|
||||
|
||||
assert.equal(
|
||||
subject._parseValue(value, 'text', ICAL.design.defaultSet),
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('#_eachLine', function() {
|
||||
|
||||
function unfold(input) {
|
||||
let result = [];
|
||||
|
||||
subject._eachLine(input, function(err, line) {
|
||||
result.push(line);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
test('unfold single with \\r\\n', function() {
|
||||
let input = 'foo\r\n bar';
|
||||
let expected = ['foobar'];
|
||||
|
||||
assert.deepEqual(unfold(input), expected);
|
||||
});
|
||||
|
||||
test('with \\n', function() {
|
||||
let input = 'foo\nbar\n baz';
|
||||
let expected = [
|
||||
'foo',
|
||||
'bar baz'
|
||||
];
|
||||
|
||||
assert.deepEqual(unfold(input), expected);
|
||||
});
|
||||
});
|
||||
|
||||
suite('embedded timezones', function() {
|
||||
let icsDataEmbeddedTimezones;
|
||||
suiteSetup(async function() {
|
||||
icsDataEmbeddedTimezones = await testSupport.loadSample('timezone_from_file.ics');
|
||||
});
|
||||
|
||||
test('used in event date', function() {
|
||||
const parsed = ICAL.parse(icsDataEmbeddedTimezones);
|
||||
const component = new ICAL.Component(parsed);
|
||||
|
||||
const event = new ICAL.Event(component.getFirstSubcomponent('vevent'));
|
||||
const startDate = event.startDate.toJSDate();
|
||||
const endDate = event.endDate.toJSDate();
|
||||
|
||||
assert.equal(startDate.getUTCDate(), 6);
|
||||
assert.equal(startDate.getUTCHours(), 21);
|
||||
assert.equal(startDate.getUTCMinutes(), 23);
|
||||
|
||||
assert.equal(endDate.getUTCDate(), 6);
|
||||
assert.equal(endDate.getUTCHours(), 22);
|
||||
assert.equal(endDate.getUTCMinutes(), 23);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
ATTACH;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY;X-BASE
|
||||
64-PARAM=UGFyYW1ldGVyCg=:WW91IHJlYWxseSBzcGVudCB0aGUgdGltZS
|
||||
B0byBiYXNlNjQgZGVjb2RlIHRoaXM/Cg=
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
"vcalendar",
|
||||
[
|
||||
[
|
||||
"attach",
|
||||
{
|
||||
"fmttype": "text/plain",
|
||||
"encoding": "BASE64",
|
||||
"x-base64-param": "UGFyYW1ldGVyCg="
|
||||
},
|
||||
"binary",
|
||||
"WW91IHJlYWxseSBzcGVudCB0aGUgdGltZSB0byBiYXNlNjQgZGVjb2RlIHRoaXM/Cg="
|
||||
]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
X-TRUE;VALUE=BOOLEAN:TRUE
|
||||
X-FALSE;VALUE=BOOLEAN:FALSE
|
||||
X-MAYBE;VALUE=BOOLEAN:MAYBE
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,23 @@
|
|||
["vcalendar",
|
||||
[
|
||||
[
|
||||
"x-true",
|
||||
{},
|
||||
"boolean",
|
||||
true
|
||||
],
|
||||
[
|
||||
"x-false",
|
||||
{},
|
||||
"boolean",
|
||||
false
|
||||
],
|
||||
[
|
||||
"x-maybe",
|
||||
{},
|
||||
"boolean",
|
||||
false
|
||||
]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:foo \\n
|
||||
bar
|
||||
BEGIN:VALARM
|
||||
SUMMARY:escaped\, comma and\; semicolon\nnewline
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:another
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,21 @@
|
|||
["vcalendar",
|
||||
[],
|
||||
[
|
||||
[
|
||||
"vevent",
|
||||
[["summary", {}, "text", "foo \\nbar"]],
|
||||
[
|
||||
[
|
||||
"valarm",
|
||||
[["summary", {}, "text", "escaped, comma and; semicolon\nnewline"]],
|
||||
[]
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"vevent",
|
||||
[["summary", {}, "text", "another"]],
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN:VCALENDAR
|
||||
DTSTART:20120901T130000Z
|
||||
DTEND;VALUE=DATE:20120901
|
||||
RDATE:20131210
|
||||
X-MYDATE;VALUE=DATE:20120901
|
||||
X-MYDATETIME;VALUE=DATE-TIME:20120901T130000
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,35 @@
|
|||
["vcalendar",
|
||||
[
|
||||
[
|
||||
"dtstart",
|
||||
{},
|
||||
"date-time",
|
||||
"2012-09-01T13:00:00Z"
|
||||
],
|
||||
[
|
||||
"dtend",
|
||||
{},
|
||||
"date",
|
||||
"2012-09-01"
|
||||
],
|
||||
[
|
||||
"rdate",
|
||||
{},
|
||||
"date",
|
||||
"2013-12-10"
|
||||
],
|
||||
[
|
||||
"x-mydate",
|
||||
{},
|
||||
"date",
|
||||
"2012-09-01"
|
||||
],
|
||||
[
|
||||
"x-mydatetime",
|
||||
{},
|
||||
"date-time",
|
||||
"2012-09-01T13:00:00"
|
||||
]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
"vcard",
|
||||
[
|
||||
[ "version", {}, "text", "3.0" ],
|
||||
[ "prodid", {}, "text", "-//Sabre//Sabre VObject 4.1.6//EN" ],
|
||||
[ "uid", {}, "text", "ad612c16-fe12-4ec5-abf6-49998ee5ab88" ],
|
||||
[ "fn", {}, "text", "First Last NextCloud" ],
|
||||
[ "adr", { "type": "HOME" }, "text", [ "PO", "Street 2", "Street", "City", "AL", "zip", "Germany" ] ],
|
||||
[ "adr", { "type": "WORK" }, "text", [ "PO W", "Street 2 W", "Street W", "City Work", "AL work", "zip Work", "Germany work" ] ],
|
||||
[ "email", { "type": "HOME" }, "text", "home@gmx.de" ],
|
||||
[ "email", { "type": "WORK" }, "text", "work@gmx.de" ],
|
||||
[ "tel", { "type": [ "HOME", "VOICE" ] }, "phone-number", "11111111" ],
|
||||
[ "tel", { "type": "CELL" }, "phone-number", "205333" ],
|
||||
[ "tel", { "type": ["WORK", "FAX" ] }, "phone-number", "205246;;,;" ],
|
||||
[ "tel", { "type": ["WORK", "VOICE" ] }, "phone-number", "222222" ],
|
||||
[ "org", {}, "text", "Firma" ],
|
||||
[ "bday", {}, "date-time", "2019-02-10T00:00:33" ],
|
||||
[ "nickname", {}, "text", "Hugo" ],
|
||||
[ "x-abdate", {"group": "item1"}, "date-and-or-time", "20190220T000035" ],
|
||||
[ "x-ablabel", {"group": "item1"}, "unknown", "_$!<Anniversary>!$_" ],
|
||||
[ "x-anniversary", {}, "date-and-or-time", "20190220T000035" ],
|
||||
[ "categories", {}, "text", "Test-Kontakte" ],
|
||||
[ "rev", {}, "date-time", "2019-10-08T17:05:14Z" ]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Sabre//Sabre VObject 4.1.6//EN
|
||||
UID:ad612c16-fe12-4ec5-abf6-49998ee5ab88
|
||||
FN:First Last NextCloud
|
||||
ADR;TYPE=HOME:PO;Street 2;Street;City;AL;zip;Germany
|
||||
ADR;TYPE=WORK:PO W;Street 2 W;Street W;City Work;AL work;zip Work;Germany w
|
||||
ork
|
||||
EMAIL;TYPE=HOME:home@gmx.de
|
||||
EMAIL;TYPE=WORK:work@gmx.de
|
||||
TEL;TYPE="HOME,VOICE":11111111
|
||||
TEL;TYPE=CELL:205333
|
||||
TEL;TYPE="WORK,FAX":205246\;\;\,\;
|
||||
TEL;TYPE="WORK,VOICE":222222
|
||||
ORG:Firma
|
||||
BDAY:20190210T000033
|
||||
NICKNAME:Hugo
|
||||
ITEM1.X-ABDATE;VALUE=DATE-AND-OR-TIME:20190220T000035
|
||||
ITEM1.X-ABLABEL:_$!<Anniversary>!$_
|
||||
X-ANNIVERSARY;VALUE=DATE-AND-OR-TIME:20190220T000035
|
||||
CATEGORIES:Test-Kontakte
|
||||
REV:20191008T170514Z
|
||||
END:VCARD
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
X-FLOAT;VALUE=FLOAT:10.35
|
||||
X-INVALID-FLOAT;VALUE=FLOAT:my foo!
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
["vcalendar",
|
||||
[
|
||||
[
|
||||
"x-float",
|
||||
{},
|
||||
"float",
|
||||
10.35
|
||||
],
|
||||
[
|
||||
"x-invalid-float",
|
||||
{},
|
||||
"float",
|
||||
0.0
|
||||
]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VCALENDAR
|
||||
GROUP1.X-TEST;VALUE=TEXT:test!
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
"vcalendar",
|
||||
[
|
||||
[
|
||||
"group1.x-test",
|
||||
{},
|
||||
"text",
|
||||
"test!"
|
||||
]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
X-INTEGER;VALUE=INTEGER:105
|
||||
X-INVALID;VALUE=INTEGER:foobar
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
["vcalendar",
|
||||
[
|
||||
[
|
||||
"x-integer",
|
||||
{},
|
||||
"integer",
|
||||
105
|
||||
],
|
||||
[
|
||||
"x-invalid",
|
||||
{},
|
||||
"integer",
|
||||
0
|
||||
]
|
||||
],
|
||||
[]
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
BEGIN:VCALENDAR
|
||||
DTSTAMP:20140401T010101Z
|
||||
END:VCALENDAR
|
||||
BEGIN:VCALENDAR
|
||||
DTSTAMP:20140401T020202Z
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
["vcalendar",
|
||||
[ ["dtstamp", {}, "date-time", "2014-04-01T01:01:01Z" ] ],
|
||||
[]
|
||||
],
|
||||
["vcalendar",
|
||||
[ ["dtstamp", {}, "date-time", "2014-04-01T02:02:02Z" ] ],
|
||||
[]
|
||||
]
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN:VCALENDAR
|
||||
CATEGORIES:foo,blue\, fish,woot
|
||||
GEO:10.10;10.05
|
||||
REQUEST-STATUS:3.1;Invalid property value;DTSTART:96-Apr-01
|
||||
RDATE;VALUE=DATE:20121001,20121002,20121003
|
||||
EXDATE:20120901T130000,20120905T130000
|
||||
END:VCALENDAR
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue