初始提交
CodeQL / Analyze (push) Has been cancelled Details

This commit is contained in:
Your Name 2025-09-19 11:54:49 +08:00
commit e344a3b581
166 changed files with 36202 additions and 0 deletions

4
.codeclimate.yml Normal file
View File

@ -0,0 +1,4 @@
languages:
JavaScript: true
exclude_paths:
- "build/*"

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_size = 2
indent_style = space

7
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10

147
.github/workflows/ci.yml vendored Normal file
View File

@ -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

47
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -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

25
.github/workflows/needinfo-remove.yml vendored Normal file
View File

@ -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 }}

21
.github/workflows/needinfo-stale.yml vendored Normal file
View File

@ -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.

53
.github/workflows/publish.yaml vendored Normal file
View File

@ -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 }}

12
.gitignore vendored Normal file
View File

@ -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/

55
CHANGELOG.md Normal file
View File

@ -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

15
CODE_OF_CONDUCT.md Normal file
View File

@ -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.
-->

46
CONTRIBUTING.md Normal file
View File

@ -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/

373
LICENSE Normal file
View File

@ -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.

120
README.md Normal file
View File

@ -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.
![Build Status](https://github.com/kewisch/ical.js/workflows/Checkin/badge.svg) [![Coverage Status](https://coveralls.io/repos/kewisch/ical.js/badge.svg)](https://coveralls.io/r/kewisch/ical.js) [![npm version](https://badge.fury.io/js/ical.js.svg)](http://badge.fury.io/js/ical.js) [![CDNJS](https://img.shields.io/cdnjs/v/ical.js.svg)](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.

7
docs/index.html Normal file
View File

@ -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>

389
eslint.config.js Normal file
View File

@ -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
}
}
];

10
jsdoc-prepare.json Normal file
View File

@ -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/"
}
}

20
jsdoc.json Normal file
View File

@ -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
}
}

48
karma.conf.cjs Normal file
View File

@ -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
}
});
}
};

173
lib/ical/binary.js Normal file
View File

@ -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;

621
lib/ical/component.js Normal file
View File

@ -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;

View File

@ -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;

1026
lib/ical/design.js Normal file

File diff suppressed because it is too large Load Diff

354
lib/ical/duration.js Normal file
View File

@ -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;
}

569
lib/ical/event.js Normal file
View File

@ -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;
}

317
lib/ical/helpers.js Normal file
View File

@ -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;
}

73
lib/ical/module.js Normal file
View File

@ -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
};

547
lib/ical/parse.js Normal file
View File

@ -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);
};

245
lib/ical/period.js Normal file
View File

@ -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;

439
lib/ical/property.js Normal file
View File

@ -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;

576
lib/ical/recur.js Normal file
View File

@ -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)
};

475
lib/ical/recur_expansion.js Normal file
View File

@ -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;

1441
lib/ical/recur_iterator.js Normal file

File diff suppressed because it is too large Load Diff

302
lib/ical/stringify.js Normal file
View File

@ -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];
});
};

1334
lib/ical/time.js Normal file

File diff suppressed because it is too large Load Diff

526
lib/ical/timezone.js Normal file
View File

@ -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;

View File

@ -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;

93
lib/ical/types.js Normal file
View File

@ -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 _ = {};

187
lib/ical/utc_offset.js Normal file
View File

@ -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;

183
lib/ical/vcard_time.js Normal file
View File

@ -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;

11026
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

104
package.json Normal file
View File

@ -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"
}
}
}

67
rollup.config.js Normal file
View File

@ -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',
},
})
]
}];

View File

@ -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

View File

@ -0,0 +1,4 @@
BEGIN:VCALENDAR
END:VCALENDAR

View File

@ -0,0 +1,4 @@
BEGIN:VCALENDAR
COMMENT:This blank line is invalid
END:VCALENDAR

52
samples/daily_recur.ics Normal file
View File

@ -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

View File

@ -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

View File

@ -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

50
samples/forced_types.ics Normal file
View File

@ -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

View File

@ -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

39
samples/minimal.ics Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

45
samples/parserv2.ics Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
});
});

View File

@ -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--;
}
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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'
);
});
});

23
test/binary_test.js Normal file
View File

@ -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);
});
});

View File

@ -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);
});
});
});
});

627
test/component_test.js Normal file
View File

@ -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);
});
});

967
test/design_test.js Normal file
View File

@ -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"]);
});
});
});
});
});

177
test/duration_test.js Normal file
View File

@ -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"/);
});
});

1026
test/event_test.js Normal file

File diff suppressed because it is too large Load Diff

67
test/failure_test.js Normal file
View File

@ -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);
});
});

160
test/helper_test.js Normal file
View File

@ -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);
});
});
});

317
test/parse_test.js Normal file
View File

@ -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);
});
});
});

5
test/parser/base64.ics Normal file
View File

@ -0,0 +1,5 @@
BEGIN:VCALENDAR
ATTACH;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY;X-BASE
64-PARAM=UGFyYW1ldGVyCg=:WW91IHJlYWxseSBzcGVudCB0aGUgdGltZS
B0byBiYXNlNjQgZGVjb2RlIHRoaXM/Cg=
END:VCALENDAR

16
test/parser/base64.json Normal file
View File

@ -0,0 +1,16 @@
[
"vcalendar",
[
[
"attach",
{
"fmttype": "text/plain",
"encoding": "BASE64",
"x-base64-param": "UGFyYW1ldGVyCg="
},
"binary",
"WW91IHJlYWxseSBzcGVudCB0aGUgdGltZSB0byBiYXNlNjQgZGVjb2RlIHRoaXM/Cg="
]
],
[]
]

5
test/parser/boolean.ics Normal file
View File

@ -0,0 +1,5 @@
BEGIN:VCALENDAR
X-TRUE;VALUE=BOOLEAN:TRUE
X-FALSE;VALUE=BOOLEAN:FALSE
X-MAYBE;VALUE=BOOLEAN:MAYBE
END:VCALENDAR

23
test/parser/boolean.json Normal file
View File

@ -0,0 +1,23 @@
["vcalendar",
[
[
"x-true",
{},
"boolean",
true
],
[
"x-false",
{},
"boolean",
false
],
[
"x-maybe",
{},
"boolean",
false
]
],
[]
]

12
test/parser/component.ics Normal file
View File

@ -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

View File

@ -0,0 +1,21 @@
["vcalendar",
[],
[
[
"vevent",
[["summary", {}, "text", "foo \\nbar"]],
[
[
"valarm",
[["summary", {}, "text", "escaped, comma and; semicolon\nnewline"]],
[]
]
]
],
[
"vevent",
[["summary", {}, "text", "another"]],
[]
]
]
]

7
test/parser/dates.ics Normal file
View File

@ -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

35
test/parser/dates.json Normal file
View File

@ -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"
]
],
[]
]

View File

@ -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" ]
],
[]
]

View File

@ -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

5
test/parser/float.ics Normal file
View File

@ -0,0 +1,5 @@
BEGIN:VCALENDAR
X-FLOAT;VALUE=FLOAT:10.35
X-INVALID-FLOAT;VALUE=FLOAT:my foo!
END:VCALENDAR

17
test/parser/float.json Normal file
View File

@ -0,0 +1,17 @@
["vcalendar",
[
[
"x-float",
{},
"float",
10.35
],
[
"x-invalid-float",
{},
"float",
0.0
]
],
[]
]

3
test/parser/grouped.ics Normal file
View File

@ -0,0 +1,3 @@
BEGIN:VCALENDAR
GROUP1.X-TEST;VALUE=TEXT:test!
END:VCALENDAR

12
test/parser/grouped.json Normal file
View File

@ -0,0 +1,12 @@
[
"vcalendar",
[
[
"group1.x-test",
{},
"text",
"test!"
]
],
[]
]

5
test/parser/integer.ics Normal file
View File

@ -0,0 +1,5 @@
BEGIN:VCALENDAR
X-INTEGER;VALUE=INTEGER:105
X-INVALID;VALUE=INTEGER:foobar
END:VCALENDAR

17
test/parser/integer.json Normal file
View File

@ -0,0 +1,17 @@
["vcalendar",
[
[
"x-integer",
{},
"integer",
105
],
[
"x-invalid",
{},
"integer",
0
]
],
[]
]

View File

@ -0,0 +1,6 @@
BEGIN:VCALENDAR
DTSTAMP:20140401T010101Z
END:VCALENDAR
BEGIN:VCALENDAR
DTSTAMP:20140401T020202Z
END:VCALENDAR

View File

@ -0,0 +1,10 @@
[
["vcalendar",
[ ["dtstamp", {}, "date-time", "2014-04-01T01:01:01Z" ] ],
[]
],
["vcalendar",
[ ["dtstamp", {}, "date-time", "2014-04-01T02:02:02Z" ] ],
[]
]
]

View File

@ -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