├── .dockerignore ├── tests └── Mf2 │ ├── bootstrap.php │ ├── ParseHtmlIdTest.php │ ├── PlainTextTest.php │ ├── ParseValueClassTitleTest.php │ ├── ParsePTest.php │ ├── MicroformatsWikiExamplesTest.php │ ├── MicroformatsTestSuiteTest.php │ ├── RelTest.php │ ├── URLTest.php │ ├── ParseUTest.php │ ├── CombinedMicroformatsTest.php │ ├── ParseImpliedTest.php │ ├── snarfed.org.html │ ├── ParseLanguageTest.php │ ├── ParseDTTest.php │ └── fberriman.com.html ├── .gitignore ├── phpcs.xml ├── .editorconfig ├── phpunit.xml ├── php56.Dockerfile ├── bin ├── parse-mf2 └── fetch-mf2 ├── composer.json ├── .github └── workflows │ └── main.yml └── LICENSE.md /.dockerignore: -------------------------------------------------------------------------------- 1 | *.Dockerfile 2 | vendor/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /tests/Mf2/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | PHP-MF2 Standards 4 | ./Mf2/Parser.php 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | charset = utf-8 8 | 9 | [*.{json,yml}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/Mf2 6 | 7 | 8 | 9 | 10 | microformats/tests/mf1 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /php56.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:5.6-cli 2 | 3 | COPY --from=composer:2.2.12 /usr/bin/composer /usr/bin/composer 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | zip \ 7 | && rm -rf /var/cache/apt/ 8 | 9 | RUN pecl install xdebug-2.5.5 \ 10 | && docker-php-ext-enable xdebug 11 | 12 | WORKDIR /usr/share/php-mf2 13 | COPY . . 14 | 15 | RUN composer install --prefer-dist --no-cache --no-interaction 16 | 17 | CMD ["composer", "--no-interaction", "run", "check-and-test-all"] 18 | -------------------------------------------------------------------------------- /bin/parse-mf2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 |

Recent Articles

Lorem Ipsum
23 |
Max Mustermann
24 |
empty id should not be parsed
25 |
id=0 should work and not be treated false-y
26 | '; 27 | $result = Mf2\parse($test); 28 | $this->assertArrayHasKey('id', $result['items'][0]); 29 | $this->assertEquals('recentArticles', $result['items'][0]['id']); 30 | $this->assertArrayHasKey('id', $result['items'][0]['children'][0]); 31 | $this->assertEquals('article', $result['items'][0]['children'][0]['id']); 32 | $this->assertArrayHasKey('id', $result['items'][0]['properties']['author'][0]); 33 | $this->assertEquals('theAuthor', $result['items'][0]['properties']['author'][0]['id']); 34 | $this->assertArrayNotHasKey('id', $result['items'][0]['children'][1]); 35 | $this->assertArrayHasKey('id', $result['items'][0]['children'][2]); 36 | $this->assertEquals('0', $result['items'][0]['children'][2]['id']); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mf2/mf2", 3 | "type": "library", 4 | "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", 5 | "keywords": ["microformats", "microformats 2", "parser", "semantic", "html"], 6 | "authors" : [ 7 | { 8 | "name": "Barnaby Walters", 9 | "homepage": "http://waterpigs.co.uk" 10 | } 11 | ], 12 | "bin": ["bin/fetch-mf2", "bin/parse-mf2"], 13 | "require": { 14 | "php": ">=5.6.0" 15 | }, 16 | "config": { 17 | "platform": { 18 | }, 19 | "allow-plugins": { 20 | "dealerdirect/phpcodesniffer-composer-installer": true 21 | } 22 | }, 23 | "require-dev": { 24 | "mf2/tests": "dev-master#e9e2b905821ba0a5b59dab1a8eaf40634ce9cd49", 25 | "squizlabs/php_codesniffer": "^3.10.2", 26 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 27 | "phpcompatibility/php-compatibility": "^9.3", 28 | "yoast/phpunit-polyfills": "^1.0" 29 | }, 30 | "scripts": { 31 | "cs-check": "phpcs", 32 | "tests": "XDEBUG_MODE=coverage ./vendor/bin/phpunit tests --coverage-text --whitelist Mf2", 33 | "test-mf1": "./vendor/bin/phpunit --group microformats/tests/mf1", 34 | "check-and-test": [ 35 | "@cs-check", 36 | "@tests" 37 | ], 38 | "check-and-test-all": [ 39 | "@check-and-test", 40 | "@test-mf1" 41 | ] 42 | }, 43 | "autoload": { 44 | "files": ["Mf2/Parser.php"] 45 | }, 46 | "license": "CC0-1.0", 47 | "suggest": { 48 | "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", 49 | "masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support." 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * 0' 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | phpcs: 13 | name: 'PHPCS' 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up PHP environment 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: 'latest' 24 | coverage: none 25 | tools: cs2pr 26 | 27 | - name: Install Composer dependencies & cache dependencies 28 | uses: "ramsey/composer-install@v3" 29 | with: 30 | composer-options: "--prefer-dist" 31 | env: 32 | COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} 33 | 34 | - name: Run Code Sniffer 35 | id: phpcs 36 | run: ./vendor/bin/phpcs -ps --report-full --report-checkstyle=./phpcs-report.xml 37 | 38 | - name: Show PHPCS results in PR 39 | if: ${{ always() && steps.phpcs.outcome == 'failure' }} 40 | run: cs2pr ./phpcs-report.xml 41 | 42 | build: 43 | 44 | strategy: 45 | matrix: 46 | php: ['5.6', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 47 | 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Set up PHP environment 52 | uses: shivammathur/setup-php@v2 53 | with: 54 | php-version: '${{ matrix.php }}' 55 | ini-values: zend.assertions=1, error_reporting=-1, display_errors=On, log_errors_max_len=0 56 | coverage: 'xdebug' 57 | - uses: actions/checkout@v4 58 | 59 | - name: Install Composer dependencies & cache dependencies 60 | uses: "ramsey/composer-install@v3" 61 | with: 62 | composer-options: "--prefer-dist" 63 | custom-cache-key: "{{ runner.os }}-composer-${{ matrix.php }}" 64 | env: 65 | COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} 66 | 67 | - name: Validate composer.json and composer.lock 68 | #run: composer validate --strict # Currently we’re installing mf2/tests from a commit ref. 69 | run: composer validate 70 | 71 | - name: Run Test Suite 72 | run: XDEBUG_MODE=coverage ./vendor/bin/phpunit tests --coverage-text 73 | 74 | #- name: Run Static Analysis 75 | # run: ./vendor/bin/psalm 76 | -------------------------------------------------------------------------------- /tests/Mf2/PlainTextTest.php: -------------------------------------------------------------------------------- 1 | parse(); 13 | $entryProperties = $output['items'][0]['properties']; 14 | $this->assertEquals($pName, $entryProperties['name'][0]); 15 | $this->assertEquals($eValue, $entryProperties['content'][0]['value']); 16 | $this->assertEquals($eHtml, $entryProperties['content'][0]['html']); 17 | } 18 | 19 | public function aaronpkExpectations() { 20 | return array( 21 | 1 => array( 22 | "
\n

Hello World

\n
", 23 | "Hello World", 24 | "Hello World", 25 | "

Hello World

" 26 | ), 27 | 2 => array( 28 | "
\n

Hello
World

\n
", 29 | "Hello\nWorld", 30 | "Hello\nWorld", 31 | "

Hello
World

" 32 | ), 33 | 3 => array( 34 | "
\n

Hello
\nWorld

\n
", 35 | "Hello\nWorld", 36 | "Hello\nWorld", 37 | "

Hello
\nWorld

" 38 | ), 39 | 4 => array( 40 | "
\n
\n

Hello World

\n
\n
", 41 | "Hello World", 42 | "Hello World", 43 | "

Hello World

" 44 | ), 45 | 5 => array( 46 | "
\n
Hello\nWorld
\n
", 47 | "Hello World", 48 | "Hello World", 49 | "Hello\nWorld" 50 | ), 51 | 6 => array( 52 | "
\n

Hello

World

\n
", 53 | "Hello\nWorld", 54 | "Hello\nWorld", 55 | "

Hello

World

" 56 | ), 57 | 7 => array( 58 | "
\n
Hello
\n World
\n
", 59 | "Hello\nWorld", 60 | "Hello\nWorld", 61 | "Hello
\n World", 62 | ), 63 | 8 => array( 64 | "
\n

Hello
World
\n
", 65 | "Hello\nWorld", 66 | "Hello\nWorld", 67 | "
Hello
World
" 68 | ), 69 | 9 => array( 70 | "
\n
\n

One

\n

Two

\n

Three

\n
\n
", 71 | "One\nTwo\nThree", 72 | "One\nTwo\nThree", 73 | "

One

\n

Two

\n

Three

" 74 | ) 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Mf2/ParseValueClassTitleTest.php: -------------------------------------------------------------------------------- 1 |

Name (this should not be included)

'; 20 | $parser = new Parser($input); 21 | $output = $parser->parse(); 22 | 23 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 24 | $this->assertEquals('Name', $output['items'][0]['properties']['name'][0]); 25 | } 26 | 27 | public function testValueClassTitleHandlesMultipleValueClass() { 28 | $input = '

Name (this should not be included) Endname

'; 29 | $parser = new Parser($input); 30 | $output = $parser->parse(); 31 | 32 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 33 | $this->assertEquals('NameEndname', $output['items'][0]['properties']['name'][0]); 34 | } 35 | 36 | public function testValueClassTitleHandlesSingleValueTitle() { 37 | $input = '

Wrong Name (this should not be included)

'; 38 | $parser = new Parser($input); 39 | $output = $parser->parse(); 40 | 41 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 42 | $this->assertEquals('Real Name', $output['items'][0]['properties']['name'][0]); 43 | } 44 | 45 | public function testValueClassTitleHandlesMultipleValueTitle() { 46 | $input = '

Wrong Name (this should not be included)

'; 47 | $parser = new Parser($input); 48 | $output = $parser->parse(); 49 | 50 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 51 | $this->assertEquals('Real Name', $output['items'][0]['properties']['name'][0]); 52 | } 53 | 54 | /** 55 | * @see https://github.com/indieweb/php-mf2/issues/25 56 | */ 57 | public function testValueClassDatetimeWorksWithUrlProperties() { 58 | $input = << 60 | 63 | 64 | on 65 | 66 | 67 | EOT; 68 | 69 | $parser = new Parser($input); 70 | $output = $parser->parse(); 71 | 72 | $this->assertArrayHasKey('published', $output['items'][0]['properties']); 73 | $this->assertEquals('2013-06-27 10:17', $output['items'][0]['properties']['published'][0]); 74 | } 75 | 76 | /** 77 | * @see https://github.com/indieweb/php-mf2/issues/27 78 | */ 79 | public function testParsesValueTitleDatetimes() { 80 | $input = << 82 |

test

83 | 16.02.2012 84 | 85 | EOT; 86 | 87 | $parser = new Parser($input); 88 | $output = $parser->parse(); 89 | 90 | $this->assertEquals('2012-02-16T16:14:47+00:00', $output['items'][0]['properties']['published'][0]); 91 | } 92 | 93 | /** @see https://github.com/indieweb/php-mf2/issues/34 */ 94 | public function testIgnoresValueClassNestedFurtherThanChild() { 95 | $test = '
12345678'; 96 | $result = Mf2\parse($test); 97 | $this->assertEquals('1234', $result['items'][0]['properties']['tel'][0]); 98 | $this->assertEquals('5678', $result['items'][0]['children'][0]['properties']['tel'][0]); 99 | } 100 | 101 | /** @see https://github.com/indieweb/php-mf2/issues/38 */ 102 | public function testValueClassDtMatchesSingleDigitTimeComponent() { 103 | $test = '
,
'; 104 | $result = Mf2\parse($test); 105 | $this->assertEquals('2013-02-01 6:01', $result['items'][0]['properties']['published'][0]); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Mf2/ParsePTest.php: -------------------------------------------------------------------------------- 1 |

Example User

'; 24 | $parser = new Parser($input); 25 | $output = $parser->parse(); 26 | 27 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 28 | $this->assertEquals('Example User', $output['items'][0]['properties']['name'][0]); 29 | } 30 | 31 | /** 32 | * @group parseP 33 | */ 34 | public function testParsePHandlesImg() { 35 | $input = '
Example User
'; 36 | $parser = new Parser($input); 37 | $output = $parser->parse(); 38 | 39 | 40 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 41 | $this->assertEquals('Example User', $output['items'][0]['properties']['name'][0]); 42 | } 43 | 44 | /** 45 | * @group parseP 46 | */ 47 | public function testParsePHandlesAbbr() { 48 | $input = '
@example
'; 49 | $parser = new Parser($input); 50 | $output = $parser->parse(); 51 | 52 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 53 | $this->assertEquals('Example User', $output['items'][0]['properties']['name'][0]); 54 | } 55 | 56 | /** 57 | * @group parseP 58 | */ 59 | public function testParsePHandlesData() { 60 | $input = '
'; 61 | $parser = new Parser($input); 62 | $output = $parser->parse(); 63 | 64 | 65 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 66 | $this->assertEquals('Example User', $output['items'][0]['properties']['name'][0]); 67 | } 68 | 69 | /** 70 | * @group parseP 71 | */ 72 | public function testParsePHandlesDataWithBlankValueAttribute() { 73 | $input = '
Example User
'; 74 | $parser = new Parser($input); 75 | $output = $parser->parse(); 76 | 77 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 78 | $this->assertEquals('', $output['items'][0]['properties']['name'][0]); 79 | } 80 | 81 | /** 82 | * @group parseP 83 | */ 84 | public function testParsePReturnsEmptyStringForBrHr() { 85 | $input = '


'; 86 | $parser = new Parser($input); 87 | $output = $parser->parse(); 88 | 89 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 90 | $this->assertEquals('', $output['items'][0]['properties']['name'][0]); 91 | $this->assertEquals('', $output['items'][0]['properties']['name'][0]); 92 | } 93 | 94 | public function testParsesInputValue() { 95 | $input = ''; 96 | $result = Mf2\parse($input); 97 | $this->assertEquals('https://example.com', $result['items'][0]['properties']['url'][0]); 98 | } 99 | 100 | /** 101 | * @see https://github.com/indieweb/php-mf2/issues/53 102 | * @see https://microformats.org/wiki/microformats2-parsing#parsing_an_e-_property 103 | */ 104 | public function testConvertsNestedImgElementToAltOrSrc() { 105 | $input = << 107 |

The day I saw a five legged elephant

108 |

Blah blah

109 | 110 | EOT; 111 | $result = Mf2\parse($input, 'http://waterpigs.co.uk/articles/five-legged-elephant'); 112 | $this->assertEquals('The day I saw a five legged elephant', $result['items'][0]['properties']['name'][0]); 113 | $this->assertEquals('Blah blah http://waterpigs.co.uk/photos/five-legged-elephant.jpg', $result['items'][0]['properties']['summary'][0]); 114 | } 115 | 116 | /** 117 | * @see https://github.com/indieweb/php-mf2/issues/69 118 | */ 119 | public function testBrWhitespaceIssue69() { 120 | $input = '

Street Name 9
12345 NY, USA

'; 121 | $result = Mf2\parse($input); 122 | 123 | $this->assertEquals('Street Name 9' . "\n" . '12345 NY, USA', $result['items'][0]['properties']['adr'][0]); 124 | $this->assertEquals('Street Name 9', $result['items'][0]['properties']['street-address'][0]); 125 | $this->assertEquals('12345 NY, USA', $result['items'][0]['properties']['locality'][0]); 126 | // p-name is no longer implied for this test due to other p-* 127 | // see https://github.com/microformats/microformats2-parsing/issues/6 128 | $this->assertArrayNotHasKey('name', $result['items'][0]['properties']); 129 | } 130 | 131 | /** 132 | * @see https://github.com/indieweb/php-mf2/issues/89 133 | */ 134 | public function testEmptyAlt() { 135 | $input = ''; 136 | $result = Mf2\parse($input); 137 | 138 | $this->assertEquals('mention.tech', $result['items'][0]['properties']['org'][0]); 139 | $this->assertEquals('mention.tech', $result['items'][0]['properties']['name'][0]); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /tests/Mf2/MicroformatsWikiExamplesTest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class MicroformatsWikiExamplesTest extends TestCase { 23 | protected function set_up() { 24 | date_default_timezone_set('Europe/London'); 25 | } 26 | 27 | public function testHandlesEmptyStringsCorrectly() { 28 | $input = ''; 29 | $expected = '{ 30 | "rels": {}, 31 | "rel-urls": {}, 32 | "items": [] 33 | }'; 34 | 35 | $parser = new Parser($input, '', true); 36 | $output = $parser->parse(); 37 | 38 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 39 | } 40 | 41 | public function testHandlesNullCorrectly() { 42 | $input = Null; 43 | $expected = '{ 44 | "rels": {}, 45 | "rel-urls": {}, 46 | "items": [] 47 | }'; 48 | 49 | $parser = new Parser($input, '', true); 50 | $parser->jsonMode = true; 51 | $output = $parser->parse(); 52 | 53 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 54 | } 55 | 56 | /** 57 | * From https://microformats.org/wiki/microformats-2 58 | */ 59 | public function testSimplePersonReference() { 60 | $input = 'Frances Berriman'; 61 | $expected = '{ 62 | "rels": {}, 63 | "rel-urls": {}, 64 | "items": [{ 65 | "type": ["h-card"], 66 | "properties": { 67 | "name": ["Frances Berriman"] 68 | } 69 | }] 70 | }'; 71 | $parser = new Parser($input, '', true); 72 | $output = $parser->parse(); 73 | 74 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 75 | } 76 | 77 | /** 78 | * From https://microformats.org/wiki/microformats-2 79 | */ 80 | public function testSimpleHyperlinkedPersonReference() { 81 | $input = 'Ben Ward'; 82 | $expected = '{ 83 | "rels": {}, 84 | "rel-urls": {}, 85 | "items": [{ 86 | "type": ["h-card"], 87 | "properties": { 88 | "name": ["Ben Ward"], 89 | "url": ["http://benward.me"] 90 | } 91 | }] 92 | }'; 93 | $parser = new Parser($input, '', true); 94 | $output = $parser->parse(); 95 | 96 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 97 | } 98 | 99 | /** 100 | * From https://microformats.org/wiki/microformats-2-implied-properties 101 | */ 102 | public function testSimplePersonImage() { 103 | $input = 'Chris Messina'; 104 | // Added root items key 105 | $expected = '{ 106 | "rels": {}, 107 | "rel-urls": {}, 108 | "items": [{ 109 | "type": ["h-card"], 110 | "properties": { 111 | "name": ["Chris Messina"], 112 | "photo": [{ 113 | "value": "http://example.com/pic.jpg", 114 | "alt": "Chris Messina" 115 | }] 116 | } 117 | }]}'; 118 | $parser = new Parser($input, '', true); 119 | $output = $parser->parse(); 120 | 121 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 122 | } 123 | 124 | /** 125 | * From https://microformats.org/wiki/microformats-2-implied-properties 126 | */ 127 | public function testHyperlinkedImageNameAndPhotoProperties() { 128 | $input = ' 129 | Rohit Khare 131 | '; 132 | // Added root items key 133 | $expected = '{ 134 | "rels": {}, 135 | "rel-urls": {}, 136 | "items": [{ 137 | "type": ["h-card"], 138 | "properties": { 139 | "name": ["Rohit Khare"], 140 | "url": ["http://rohit.khare.org/"], 141 | "photo": [{ 142 | "value": "https://s3.amazonaws.com/twitter_production/profile_images/53307499/180px-Rohit-sq_bigger.jpg", 143 | "alt": "Rohit Khare" 144 | }] 145 | } 146 | }]}'; 147 | $parser = new Parser($input, '', true); 148 | $output = $parser->parse(); 149 | 150 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 151 | } 152 | 153 | /** 154 | * From https://microformats.org/wiki/microformats-2 155 | */ 156 | public function testMoreDetailedPerson() { 157 | $input = '
158 | photo of Mitchell 160 | Mitchell Baker 163 | (@MitchellBaker) 166 | Mozilla Foundation 167 |

168 | Mitchell is responsible for setting the direction and scope of the Mozilla Foundation and its activities. 169 |

170 | Strategy 171 | Leadership 172 |
'; 173 | 174 | $expected = '{ 175 | "rels": {}, 176 | "rel-urls": {}, 177 | "items": [{ 178 | "type": ["h-card"], 179 | "properties": { 180 | "photo": [ 181 | { 182 | "value": "https://webfwd.org/content/about-experts/300.mitchellbaker/mentor_mbaker.jpg", 183 | "alt": "photo of Mitchell" 184 | } 185 | ], 186 | "name": ["Mitchell Baker"], 187 | "url": [ 188 | "http://blog.lizardwrangler.com/", 189 | "https://twitter.com/MitchellBaker" 190 | ], 191 | "org": ["Mozilla Foundation"], 192 | "note": ["Mitchell is responsible for setting the direction and scope of the Mozilla Foundation and its activities."], 193 | "category": [ 194 | "Strategy", 195 | "Leadership" 196 | ] 197 | } 198 | }] 199 | }'; 200 | $parser = new Parser($input, '', true); 201 | $output = $parser->parse(); 202 | 203 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Legal Code 2 | 3 | ## CC0 1.0 Universal 4 | 5 | http://creativecommons.org/publicdomain/zero/1.0 6 | 7 | Official translations of this legal tool are available> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. 8 | 9 | ### _Statement of Purpose_ 10 | 11 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 12 | 13 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 14 | 15 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 16 | 17 | **1. Copyright and Related Rights.** A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 18 | 19 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 20 | 2. moral rights retained by the original author(s) and/or performer(s); 21 | 3. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 22 | 4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 23 | 5. rights protecting the extraction, dissemination, use and reuse of data in a Work; 24 | 6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 25 | 7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 26 | 27 | **2. Waiver.** To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 28 | 29 | **3. Public License Fallback.** Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 30 | 31 | **4. Limitations and Disclaimers.** 32 | 33 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 34 | 2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 35 | 3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 36 | 4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 37 | -------------------------------------------------------------------------------- /tests/Mf2/MicroformatsTestSuiteTest.php: -------------------------------------------------------------------------------- 1 | tagName) and in_array(strtolower($el->tagName), $excludeTags)) { 14 | return ''; 15 | } 16 | 17 | $this->_resolveChildUrls($el); 18 | 19 | $clonedEl = $el->cloneNode(true); 20 | 21 | foreach ($this->xpath->query('.//img', $clonedEl) as $imgEl) { 22 | if ($imgEl->hasAttribute('alt')) { 23 | $replacement = $imgEl->getAttribute('alt'); 24 | } else if ($imgEl->hasAttribute('src')) { 25 | $replacement = ' ' . $imgEl->getAttribute('src') . ' '; 26 | } else { 27 | $replacement = ''; // Bye bye IMG element. 28 | } 29 | $newNode = $this->doc->createTextNode($replacement); 30 | $imgEl->parentNode->replaceChild($newNode, $imgEl); 31 | } 32 | 33 | foreach ($excludeTags as $tagName) { 34 | foreach ($this->xpath->query(".//{$tagName}", $clonedEl) as $elToRemove) { 35 | $elToRemove->parentNode->removeChild($elToRemove); 36 | } 37 | } 38 | 39 | return \Mf2\unicodeTrim($clonedEl->textContent); 40 | } 41 | 42 | // Hack. Old textContent requires "resolveChildUrls", but that method is private. 43 | private $__resolveChildUrls = null; 44 | private function _resolveChildUrls(\DOMElement $element) { 45 | if (null === $this->__resolveChildUrls) { 46 | $reflectUpon = new \ReflectionClass($this); 47 | $this->__resolveChildUrls = $reflectUpon->getMethod('resolveChildUrls'); 48 | $this->__resolveChildUrls->setAccessible(true); 49 | } 50 | return $this->__resolveChildUrls->invoke($this, $element); 51 | } 52 | } 53 | 54 | class MicroformatsTestSuiteTest extends TestCase 55 | { 56 | /** 57 | * @dataProvider mf1TestsProvider 58 | * @group microformats/tests/mf1 59 | */ 60 | public function testMf1FromTestSuite($input, $expectedOutput) 61 | { 62 | $parser = new TestSuiteParser($input, 'http://example.com/'); 63 | $this->assertEquals( 64 | $this->makeComparible(json_decode($expectedOutput, true)), 65 | $this->makeComparible(json_decode(json_encode($parser->parse()), true)) 66 | ); 67 | } 68 | 69 | /** 70 | * @dataProvider mf2TestsProvider 71 | * @group microformats/tests/mf2 72 | */ 73 | public function testMf2FromTestSuite($input, $expectedOutput, $test) 74 | { 75 | if ($test === 'h-event/time') { 76 | $this->markTestIncomplete('This test does not match because we implement a proposed spec: https://github.com/microformats/microformats2-parsing/issues/4#issuecomment-373457720.'); 77 | } 78 | 79 | $parser = new TestSuiteParser($input, 'http://example.com/'); 80 | $this->assertEquals( 81 | $this->makeComparible(json_decode($expectedOutput, true)), 82 | $this->makeComparible(json_decode(json_encode($parser->parse()), true)) 83 | ); 84 | } 85 | 86 | /** 87 | * @dataProvider mixedTestsProvider 88 | * @group microformats/tests/mixed 89 | */ 90 | public function testMixedFromTestSuite($input, $expectedOutput) 91 | { 92 | $parser = new TestSuiteParser($input, 'http://example.com/'); 93 | $this->assertEquals( 94 | $this->makeComparible(json_decode($expectedOutput, true)), 95 | $this->makeComparible(json_decode(json_encode($parser->parse()), true)) 96 | ); 97 | } 98 | 99 | /** 100 | * To make arrays coming from JSON more easily comparible by PHPUnit: 101 | * * We sort arrays by key, normalising them, because JSON objects are unordered. 102 | * * We json_encode strings, and cut the starting and ending ", so PHPUnit better 103 | * shows whitespace characters like tabs and newlines. 104 | **/ 105 | public function makeComparible($array) 106 | { 107 | ksort($array); 108 | foreach ($array as $key => $value) { 109 | if (gettype($value) === 'array') { 110 | $array[$key] = $this->makeComparible($value); 111 | } else if (gettype($value) === 'string') { 112 | $array[$key] = substr(json_encode($value), 1, -1); 113 | } 114 | } 115 | return $array; 116 | } 117 | 118 | /** 119 | * Data provider lists all tests from a specific directory in mf2/tests. 120 | **/ 121 | public function htmlAndJsonProvider($subFolder = '') 122 | { 123 | // Get the actual wanted subfolder. 124 | $subFolder = '/mf2/tests/tests' . $subFolder; 125 | // Ripped out of the test-suite.php code: 126 | $finder = new \RegexIterator( 127 | new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator( 128 | dirname(__FILE__) . '/../../vendor/' . $subFolder, 129 | \RecursiveDirectoryIterator::SKIP_DOTS 130 | )), 131 | '/^.+\.html$/i', 132 | \RecursiveRegexIterator::GET_MATCH 133 | ); 134 | // Build the array of separate tests: 135 | $tests = array(); 136 | foreach ($finder as $key => $value) { 137 | $dir = realpath(pathinfo($key, PATHINFO_DIRNAME)); 138 | $testname = substr($dir, strpos($dir, $subFolder) + strlen($subFolder) + 1) . '/' . pathinfo($key, PATHINFO_FILENAME); 139 | $test = pathinfo($key, PATHINFO_BASENAME); 140 | $result = pathinfo($key, PATHINFO_FILENAME) . '.json'; 141 | if (is_file($dir . '/' . $result)) { 142 | $tests[$testname] = array( 143 | 'input' => file_get_contents($dir . '/' . $test), 144 | 'expectedOutput' => file_get_contents($dir . '/' . $result), 145 | 'name' => $testname 146 | ); 147 | } 148 | } 149 | return $tests; 150 | } 151 | 152 | /** 153 | * Following three functions are the actual dataProviders used by the test methods. 154 | */ 155 | public function mf1TestsProvider() 156 | { 157 | return $this->htmlAndJsonProvider('/microformats-v1'); 158 | } 159 | 160 | public function mf2TestsProvider() 161 | { 162 | return $this->htmlAndJsonProvider('/microformats-v2'); 163 | } 164 | 165 | public function mixedTestsProvider() 166 | { 167 | return $this->htmlAndJsonProvider('/microformats-mixed'); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/Mf2/RelTest.php: -------------------------------------------------------------------------------- 1 | '; 15 | $parser = new Parser($input); 16 | $output = $parser->parse(); 17 | 18 | $this->assertArrayHasKey('webmention', $output['rels']); 19 | $this->assertEquals('https://example.com/webmention', $output['rels']['webmention'][0]); 20 | } 21 | 22 | public function testRelValueOnATag() { 23 | $input = 'webmention me'; 24 | $parser = new Parser($input); 25 | $output = $parser->parse(); 26 | 27 | $this->assertArrayHasKey('webmention', $output['rels']); 28 | $this->assertEquals('https://example.com/webmention', $output['rels']['webmention'][0]); 29 | } 30 | 31 | public function testRelValueOnAreaTag() { 32 | $input = ''; 33 | $parser = new Parser($input); 34 | $output = $parser->parse(); 35 | 36 | $this->assertArrayHasKey('webmention', $output['rels']); 37 | $this->assertEquals('https://example.com/webmention', $output['rels']['webmention'][0]); 38 | } 39 | 40 | public function testRelValueOrder() { 41 | $input = ' 42 | webmention me 43 | '; 44 | $parser = new Parser($input); 45 | $output = $parser->parse(); 46 | 47 | $this->assertArrayHasKey('webmention', $output['rels']); 48 | $this->assertEquals('https://example.com/area', $output['rels']['webmention'][0]); 49 | $this->assertEquals('https://example.com/a', $output['rels']['webmention'][1]); 50 | $this->assertEquals('https://example.com/link', $output['rels']['webmention'][2]); 51 | } 52 | 53 | public function testRelValueOrder2() { 54 | $input = ' 55 | 56 | webmention me'; 57 | $parser = new Parser($input); 58 | $output = $parser->parse(); 59 | 60 | $this->assertArrayHasKey('webmention', $output['rels']); 61 | $this->assertEquals('https://example.com/area', $output['rels']['webmention'][0]); 62 | $this->assertEquals('https://example.com/link', $output['rels']['webmention'][1]); 63 | $this->assertEquals('https://example.com/a', $output['rels']['webmention'][2]); 64 | } 65 | 66 | public function testRelValueOrder3() { 67 | $input = ' 68 | 69 | 70 | 71 | 72 | webmention me 73 | 74 | 75 | '; 76 | $parser = new Parser($input); 77 | $output = $parser->parse(); 78 | 79 | $this->assertArrayHasKey('webmention', $output['rels']); 80 | $this->assertEquals('https://example.com/link', $output['rels']['webmention'][0]); 81 | $this->assertEquals('https://example.com/a', $output['rels']['webmention'][1]); 82 | $this->assertEquals('https://example.com/area', $output['rels']['webmention'][2]); 83 | } 84 | 85 | public function testRelValueOnBTag() { 86 | $input = 'this makes no sense'; 87 | $parser = new Parser($input); 88 | $output = $parser->parse(); 89 | 90 | $this->assertArrayNotHasKey('webmention', $output['rels']); 91 | } 92 | 93 | public function testEnableAlternatesFlagTrue() { 94 | $input = ' 95 | 96 | post 1 97 | post 2 98 | French mobile homepage'; 102 | $parser = new Parser($input); 103 | $parser->enableAlternates = true; 104 | $output = $parser->parse(); 105 | 106 | $this->assertArrayHasKey('alternates', $output); 107 | } 108 | 109 | public function testEnableAlternatesFlagFalse() { 110 | $input = ' 111 | 112 | post 1 113 | post 2 114 | French mobile homepage'; 118 | $parser = new Parser($input); 119 | $parser->enableAlternates = false; 120 | $output = $parser->parse(); 121 | 122 | $this->assertArrayNotHasKey('alternates', $output); 123 | } 124 | 125 | /** 126 | * @see https://github.com/indieweb/php-mf2/issues/112 127 | * @see https://microformats.org/wiki/microformats2-parsing#rel_parse_examples 128 | */ 129 | public function testRelURLs() { 130 | $input = ' 131 | 132 | post 1 133 | post 2 134 | French mobile homepage 138 | '; 139 | $parser = new Parser($input); 140 | $output = $parser->parse(); 141 | 142 | $this->assertArrayHasKey('rels', $output); 143 | $this->assertCount(4, $output['rels']); 144 | $this->assertArrayHasKey('author', $output['rels']); 145 | $this->assertArrayHasKey('in-reply-to', $output['rels']); 146 | $this->assertArrayHasKey('alternate', $output['rels']); 147 | $this->assertArrayHasKey('home', $output['rels']); 148 | 149 | $this->assertArrayHasKey('rel-urls', $output); 150 | $this->assertCount(6, $output['rel-urls']); 151 | $this->assertArrayHasKey('https://example.com/a', $output['rel-urls']); 152 | $this->assertArrayHasKey('https://example.com/b', $output['rel-urls']); 153 | $this->assertArrayHasKey('https://example.com/1', $output['rel-urls']); 154 | $this->assertArrayHasKey('https://example.com/2', $output['rel-urls']); 155 | $this->assertArrayHasKey('https://example.com/fr', $output['rel-urls']); 156 | $this->assertArrayHasKey('https://example.com/articles.atom', $output['rel-urls']); 157 | 158 | $this->assertArrayHasKey('rels', $output['rel-urls']['https://example.com/a']); 159 | $this->assertArrayHasKey('text', $output['rel-urls']['https://example.com/a']); 160 | $this->assertArrayHasKey('rels', $output['rel-urls']['https://example.com/b']); 161 | $this->assertArrayHasKey('text', $output['rel-urls']['https://example.com/b']); 162 | $this->assertArrayHasKey('rels', $output['rel-urls']['https://example.com/1']); 163 | $this->assertArrayHasKey('text', $output['rel-urls']['https://example.com/1']); 164 | $this->assertArrayHasKey('rels', $output['rel-urls']['https://example.com/2']); 165 | $this->assertArrayHasKey('text', $output['rel-urls']['https://example.com/2']); 166 | $this->assertArrayHasKey('rels', $output['rel-urls']['https://example.com/fr']); 167 | $this->assertArrayHasKey('text', $output['rel-urls']['https://example.com/fr']); 168 | $this->assertArrayHasKey('media', $output['rel-urls']['https://example.com/fr']); 169 | $this->assertArrayHasKey('hreflang', $output['rel-urls']['https://example.com/fr']); 170 | $this->assertArrayHasKey('title', $output['rel-urls']['https://example.com/articles.atom']); 171 | $this->assertArrayHasKey('type', $output['rel-urls']['https://example.com/articles.atom']); 172 | $this->assertArrayHasKey('rels', $output['rel-urls']['https://example.com/articles.atom']); 173 | } 174 | 175 | /** 176 | * @see https://github.com/microformats/microformats2-parsing/issues/29 177 | * @see https://github.com/microformats/microformats2-parsing/issues/30 178 | */ 179 | public function testRelURLsRelsUniqueAndSorted() { 180 | $input = ' 181 | '; 182 | $parser = new Parser($input); 183 | $output = $parser->parse(); 184 | $this->assertEquals($output['rel-urls']['#']['rels'], array('archived', 'bookmark', 'me')); 185 | } 186 | 187 | public function testRelURLsInfoMergesCorrectly() { 188 | $input = 'This nodeValue 189 | Not this nodeValue'; 190 | $parser = new Parser($input); 191 | $output = $parser->parse(); 192 | $this->assertEquals($output['rel-urls']['#']['hreflang'], 'en'); 193 | $this->assertArrayNotHasKey('media', $output['rel-urls']['#']); 194 | $this->assertArrayNotHasKey('title', $output['rel-urls']['#']); 195 | $this->assertArrayNotHasKey('type', $output['rel-urls']['#']); 196 | $this->assertEquals($output['rel-urls']['#']['text'], 'This nodeValue'); 197 | } 198 | 199 | public function testRelURLsNoDuplicates() { 200 | $input = ' 201 | 202 | '; 203 | $parser = new Parser($input); 204 | $output = $parser->parse(); 205 | $this->assertEquals($output['rels']['a'], array('#a', '#b')); 206 | } 207 | 208 | public function testRelURLsFalsyTextVSEmpty() { 209 | $input = '0 210 | '; 211 | $parser = new Parser($input); 212 | $output = $parser->parse(); 213 | $this->assertArrayHasKey('text', $output['rel-urls']['#a']); 214 | $this->assertEquals($output['rel-urls']['#a']['text'], '0'); 215 | $this->assertArrayNotHasKey('text', $output['rel-urls']['#b']); 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /tests/Mf2/URLTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('one/two', $input); 21 | 22 | $input = './one/two'; 23 | mf2\removeLeadingDotSlash($input); 24 | $this->assertEquals('one/two', $input); 25 | } 26 | 27 | public function testRemoveLeadingSlashDot() { 28 | $input = '/./one/two'; 29 | mf2\removeLeadingSlashDot($input); 30 | $this->assertEquals('/one/two', $input); 31 | 32 | $input = '/.'; 33 | mf2\removeLeadingSlashDot($input); 34 | $this->assertEquals('/', $input); 35 | 36 | $input = '/./../'; 37 | mf2\removeLeadingSlashDot($input); 38 | $this->assertEquals('/../', $input); 39 | 40 | $input = '/./../../g'; 41 | mf2\removeLeadingSlashDot($input); 42 | $this->assertEquals('/../../g', $input); 43 | } 44 | 45 | public function testRemoveOneDirLevel() { 46 | $input = '/../../g'; 47 | $output = '/a/b/c'; 48 | mf2\removeOneDirLevel($input, $output); 49 | $this->assertEquals('/../g', $input); 50 | $this->assertEquals('/a/b', $output); 51 | 52 | $input = '/..'; 53 | $output = '/a/b/c'; 54 | mf2\removeOneDirLevel($input, $output); 55 | $this->assertEquals('/', $input); 56 | $this->assertEquals('/a/b', $output); 57 | } 58 | 59 | public function testRemoveLoneDotDot() { 60 | $input = '.'; 61 | mf2\removeLoneDotDot($input); 62 | $this->assertEquals('', $input); 63 | 64 | $input = '..'; 65 | mf2\removeLoneDotDot($input); 66 | $this->assertEquals('', $input); 67 | } 68 | 69 | public function testMoveOneSegmentFromInput() { 70 | $input = '/a/b/c/./../../g'; 71 | $output = ''; 72 | mf2\moveOneSegmentFromInput($input, $output); 73 | $this->assertEquals('/b/c/./../../g', $input); 74 | $this->assertEquals('/a', $output); 75 | 76 | $input = '/b/c/./../../g'; 77 | $output = '/a'; 78 | mf2\moveOneSegmentFromInput($input, $output); 79 | $this->assertEquals('/c/./../../g', $input); 80 | $this->assertEquals('/a/b', $output); 81 | 82 | $input = '/c/./../../g'; 83 | $output = '/a/b'; 84 | mf2\moveOneSegmentFromInput($input, $output); 85 | $this->assertEquals('/./../../g', $input); 86 | $this->assertEquals('/a/b/c', $output); 87 | 88 | $input = '/g'; 89 | $output = '/a'; 90 | mf2\moveOneSegmentFromInput($input, $output); 91 | $this->assertEquals('', $input); 92 | $this->assertEquals('/a/g', $output); 93 | } 94 | 95 | /** 96 | * @dataProvider removeDotSegmentsData 97 | */ 98 | public function testRemoveDotSegments($assert, $path, $expected) { 99 | $actual = mf2\removeDotSegments($path); 100 | $this->assertEquals($expected, $actual, $assert); 101 | } 102 | 103 | public function removeDotSegmentsData() { 104 | return array( 105 | array('Should remove .. and .', 106 | '/a/b/c/./../../g', '/a/g'), 107 | array('Should remove ../..', 108 | '/a/b/c/d/../../../g', '/a/g'), 109 | array('Should not add leading slash', 110 | 'a/b/c', 'a/b/c'), 111 | 112 | ); 113 | } 114 | 115 | public function testNoPathOnBase() { 116 | $actual = mf2\resolveUrl('https://example.com', ''); 117 | $this->assertEquals('https://example.com/', $actual); 118 | 119 | $actual = mf2\resolveUrl('https://example.com', '#'); 120 | $this->assertEquals('https://example.com/#', $actual); 121 | 122 | $actual = mf2\resolveUrl('https://example.com', '#thing'); 123 | $this->assertEquals('https://example.com/#thing', $actual); 124 | } 125 | 126 | public function testMisc() { 127 | $expected = 'http://a/b/c/g'; 128 | $actual = mf2\resolveUrl('http://a/b/c/d;p?q', './g'); 129 | $this->assertEquals($expected, $actual); 130 | 131 | $expected = 'http://a/b/c/g/'; 132 | $actual = mf2\resolveUrl('http://a/b/c/d;p?q', './g/'); 133 | $this->assertEquals($expected, $actual); 134 | 135 | $expected = 'http://a/b/'; 136 | $actual = mf2\resolveUrl('http://a/b/c/d;p?q', '..'); 137 | $this->assertEquals($expected, $actual); 138 | } 139 | 140 | /** as per https://github.com/indieweb/php-mf2/issues/35 */ 141 | public function testResolvesProtocolRelativeUrlsCorrectly() { 142 | $expected = 'https://cdn.example.com/thing/asset.css'; 143 | $actual = Mf2\resolveUrl('https://example.com', '//cdn.example.com/thing/asset.css'); 144 | $this->assertEquals($expected, $actual); 145 | 146 | $expected = 'https://cdn.example.com/thing/asset.css'; 147 | $actual = Mf2\resolveUrl('https://example.com', '//cdn.example.com/thing/asset.css'); 148 | $this->assertEquals($expected, $actual); 149 | } 150 | 151 | /** 152 | * @dataProvider dataProvider 153 | */ 154 | public function testReturnsUrlIfAbsolute($assert, $base, $url, $expected) { 155 | $actual = mf2\resolveUrl($base, $url); 156 | 157 | $this->assertEquals($expected, $actual, $assert); 158 | } 159 | 160 | public function dataProvider() { 161 | // seriously, please update to PHP 5.4 so I can use nice array syntax ;) 162 | // fail message, base, url, expected 163 | $cases = array( 164 | array('Should return absolute URL unchanged', 165 | 'https://example.com', 'https://example.com', 'https://example.com'), 166 | 167 | array('Should return root given blank path', 168 | 'https://example.com', '', 'https://example.com/'), 169 | 170 | array('Should return input unchanged given full URL and blank path', 171 | 'https://example.com/something', '', 'https://example.com/something'), 172 | 173 | array('Should handle blank base URL', 174 | '', 'https://example.com', 'https://example.com'), 175 | 176 | array('Should resolve fragment ID', 177 | 'https://example.com', '#thing', 'https://example.com/#thing'), 178 | 179 | array('Should resolve blank fragment ID', 180 | 'https://example.com', '#', 'https://example.com/#'), 181 | 182 | array('Should resolve same level URL', 183 | 'https://example.com', 'thing', 'https://example.com/thing'), 184 | 185 | array('Should resolve directory level URL', 186 | 'https://example.com', './thing', 'https://example.com/thing'), 187 | 188 | array('Should resolve parent level URL at root level', 189 | 'https://example.com', '../thing', 'https://example.com/thing'), 190 | 191 | array('Should resolve nested URL', 192 | 'https://example.com/something', 'another', 'https://example.com/another'), 193 | 194 | array('Should ignore query strings in base url', 195 | 'https://example.com/index.php?url=https://example.org', '/thing', 'https://example.com/thing'), 196 | 197 | array('Should resolve query strings', 198 | 'https://example.com/thing', '?stuff=yes', 'https://example.com/thing?stuff=yes'), 199 | 200 | array('Should resolve dir level query strings', 201 | 'https://example.com', './?thing=yes', 'https://example.com/?thing=yes'), 202 | 203 | array('Should resolve up one level from root domain', 204 | 'https://example.com', 'path/to/the/../file', 'https://example.com/path/to/file'), 205 | 206 | array('Should resolve up one level from base with path', 207 | 'https://example.com/path/the', 'to/the/../file', 'https://example.com/path/to/file'), 208 | 209 | // Tests from webignition library 210 | 211 | array('relative add host from base', 212 | 'http://www.example.com', 'server.php', 'http://www.example.com/server.php'), 213 | 214 | array('relative add scheme host pass from base', 215 | 'http://:pass@www.example.com', 'server.php', 'http://:pass@www.example.com/server.php'), 216 | 217 | array('relative add scheme host user pass from base', 218 | 'http://user:pass@www.example.com', 'server.php', 'http://user:pass@www.example.com/server.php'), 219 | 220 | array('relative base has file path', 221 | 'https://example.com/index.html', 'example.html', 'https://example.com/example.html'), 222 | 223 | array('input has absolute path', 224 | 'http://www.example.com/pathOne/pathTwo/pathThree', '/server.php?param1=value1', 'http://www.example.com/server.php?param1=value1'), 225 | 226 | array('test absolute url with path', 227 | 'http://www.example.com/', 'http://www.example.com/pathOne', 'http://www.example.com/pathOne'), 228 | 229 | array('testRelativePathIsTransformedIntoCorrectAbsoluteUrl', 230 | 'http://www.example.com/pathOne/pathTwo/pathThree', 'server.php?param1=value1', 'http://www.example.com/pathOne/pathTwo/server.php?param1=value1'), 231 | 232 | array('testAbsolutePathHasDotDotDirecoryAndSourceHasFileName', 233 | 'http://www.example.com/pathOne/index.php', '../jquery.js', 'http://www.example.com/jquery.js'), 234 | 235 | array('testAbsolutePathHasDotDotDirecoryAndSourceHasDirectoryWithTrailingSlash', 236 | 'http://www.example.com/pathOne/', '../jquery.js', 'http://www.example.com/jquery.js'), 237 | 238 | array('testAbsolutePathHasDotDotDirecoryAndSourceHasDirectoryWithoutTrailingSlash', 239 | 'http://www.example.com/pathOne', '../jquery.js', 'http://www.example.com/jquery.js'), 240 | 241 | array('testAbsolutePathHasDotDirecoryAndSourceHasFilename', 242 | 'http://www.example.com/pathOne/index.php', './jquery.js', 'http://www.example.com/pathOne/jquery.js'), 243 | 244 | array('testAbsolutePathHasDotDirecoryAndSourceHasDirectoryWithTrailingSlash', 245 | 'http://www.example.com/pathOne/', './jquery.js', 'http://www.example.com/pathOne/jquery.js'), 246 | 247 | array('testAbsolutePathHasDotDirecoryAndSourceHasDirectoryWithoutTrailingSlash', 248 | 'http://www.example.com/pathOne', './jquery.js', 'http://www.example.com/jquery.js'), 249 | 250 | array('testAbsolutePathIncludesPortNumber', 251 | 'https://example.com:8080/index.html', '/photo.jpg', 'https://example.com:8080/photo.jpg') 252 | 253 | ); 254 | 255 | // PHP 5.4 and before returns a different result, but either are acceptable 256 | if(PHP_MAJOR_VERSION <= 5 && PHP_MINOR_VERSION <= 4) { 257 | $cases[] = array('relative add scheme host user from base', 258 | 'http://user:@www.example.com', 'server.php', 'http://user@www.example.com/server.php'); 259 | } else { 260 | $cases[] = array('relative add scheme host user from base', 261 | 'http://user:@www.example.com', 'server.php', 'http://user:@www.example.com/server.php'); 262 | } 263 | 264 | // Test cases from RFC 265 | // http://tools.ietf.org/html/rfc3986#section-5.4 266 | 267 | $rfcTests = array( 268 | array("g:h", "g:h"), 269 | array("g", "http://a/b/c/g"), 270 | array("./g", "http://a/b/c/g"), 271 | array("g/", "http://a/b/c/g/"), 272 | array("/g", "http://a/g"), 273 | array("//g", "http://g"), 274 | array("?y", "http://a/b/c/d;p?y"), 275 | array("g?y", "http://a/b/c/g?y"), 276 | array("#s", "http://a/b/c/d;p?q#s"), 277 | array("g#s", "http://a/b/c/g#s"), 278 | array("g?y#s", "http://a/b/c/g?y#s"), 279 | array(";x", "http://a/b/c/;x"), 280 | array("g;x", "http://a/b/c/g;x"), 281 | array("g;x?y#s", "http://a/b/c/g;x?y#s"), 282 | array("", "http://a/b/c/d;p?q"), 283 | array(".", "http://a/b/c/"), 284 | array("./", "http://a/b/c/"), 285 | array("..", "http://a/b/"), 286 | array("../", "http://a/b/"), 287 | array("../g", "http://a/b/g"), 288 | array("../..", "http://a/"), 289 | array("../../", "http://a/"), 290 | array("../../g", "http://a/g") 291 | ); 292 | 293 | foreach($rfcTests as $i=>$test) { 294 | $cases[] = array( 295 | 'test rfc ' . $i, 'http://a/b/c/d;p?q', $test[0], $test[1] 296 | ); 297 | } 298 | 299 | return $cases; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /tests/Mf2/ParseUTest.php: -------------------------------------------------------------------------------- 1 | Awesome example website'; 22 | $parser = new Parser($input); 23 | $output = $parser->parse(); 24 | 25 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 26 | $this->assertEquals('https://example.com', $output['items'][0]['properties']['url'][0]); 27 | } 28 | 29 | /** 30 | * @group parseU 31 | */ 32 | public function testParseUHandlesEmptyHrefAttribute() { 33 | $input = ''; 34 | $parser = new Parser($input, "https://example.com/"); 35 | $output = $parser->parse(); 36 | 37 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 38 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 39 | } 40 | 41 | /** 42 | * @group parseU 43 | */ 44 | public function testParseUHandlesMissingHrefAttribute() { 45 | $input = ''; 46 | $parser = new Parser($input, "https://example.com/"); 47 | $output = $parser->parse(); 48 | 49 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 50 | $this->assertEquals('https://example.com/Awesome example website', $output['items'][0]['properties']['url'][0]); 51 | } 52 | 53 | /** 54 | * @group parseU 55 | */ 56 | public function testParseUHandlesImg() { 57 | $input = '
'; 58 | $parser = new Parser($input); 59 | $output = $parser->parse(); 60 | 61 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 62 | $this->assertEquals('https://example.com/someimage.png', $output['items'][0]['properties']['photo'][0]); 63 | } 64 | 65 | /** 66 | * @group parseU 67 | */ 68 | public function testParseUHandlesImgwithAlt() { 69 | $input = '
Test Alt
'; 70 | $parser = new Parser($input); 71 | $output = $parser->parse(); 72 | 73 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 74 | $result = array( 75 | 'value' => 'https://example.com/someimage.png', 76 | 'alt' => 'Test Alt' 77 | ); 78 | $this->assertEquals( $result, $output['items'][0]['properties']['photo'][0]); 79 | } 80 | 81 | /** 82 | * @group parseU 83 | */ 84 | public function testParseUHandlesImgwithoutAlt() { 85 | $input = '
'; 86 | $parser = new Parser($input); 87 | $output = $parser->parse(); 88 | 89 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 90 | $this->assertEquals( 'https://example.com/someimage.png', $output['items'][0]['properties']['photo'][0]); 91 | } 92 | 93 | /** 94 | * @group parseU 95 | */ 96 | public function testParseUHandlesArea() { 97 | $input = '
'; 98 | $parser = new Parser($input); 99 | $output = $parser->parse(); 100 | 101 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 102 | $this->assertEquals('https://example.com/someimage.png', $output['items'][0]['properties']['photo'][0]); 103 | } 104 | 105 | /** 106 | * @group parseU 107 | */ 108 | public function testParseUHandlesObject() { 109 | $input = '
'; 110 | $parser = new Parser($input); 111 | $output = $parser->parse(); 112 | 113 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 114 | $this->assertEquals('https://example.com/someimage.png', $output['items'][0]['properties']['photo'][0]); 115 | } 116 | 117 | /** 118 | * @group parseU 119 | */ 120 | public function testParseUHandlesAbbr() { 121 | $input = '
'; 122 | $parser = new Parser($input); 123 | $output = $parser->parse(); 124 | 125 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 126 | $this->assertEquals('https://example.com/someimage.png', $output['items'][0]['properties']['photo'][0]); 127 | } 128 | 129 | /** 130 | * @group parseU 131 | */ 132 | public function testParseUHandlesAbbrNoTitle() { 133 | $input = '
no title attribute
'; 134 | $parser = new Parser($input); 135 | $output = $parser->parse(); 136 | 137 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 138 | $this->assertEquals('no title attribute', $output['items'][0]['properties']['photo'][0]); 139 | } 140 | 141 | /** 142 | * @group parseU 143 | */ 144 | public function testParseUHandlesData() { 145 | $input = '
'; 146 | $parser = new Parser($input); 147 | $output = $parser->parse(); 148 | 149 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 150 | $this->assertEquals('https://example.com/someimage.png', $output['items'][0]['properties']['photo'][0]); 151 | } 152 | 153 | /** 154 | * @group baseUrl 155 | */ 156 | public function testResolvesRelativeUrlsFromDocumentUrl() { 157 | $input = '
'; 158 | $parser = new Parser($input, 'https://example.com/things/more/more.html'); 159 | $output = $parser->parse(); 160 | 161 | $this->assertEquals('https://example.com/things/image.png', $output['items'][0]['properties']['photo'][0]); 162 | } 163 | 164 | /** 165 | * @group baseUrl 166 | */ 167 | public function testResolvesRelativeUrlsFromBaseUrl() { 168 | $input = '
'; 169 | $parser = new Parser($input, 'https://example.com/things/more.html'); 170 | $output = $parser->parse(); 171 | 172 | $this->assertEquals('https://example.com/things/more/image.png', $output['items'][0]['properties']['photo'][0]); 173 | } 174 | 175 | /** 176 | * @group baseUrl 177 | */ 178 | public function testResolvesRelativeUrlsInImpliedMicroformats() { 179 | $input = ''; 180 | $parser = new Parser($input, 'https://example.com/things/more.html'); 181 | $output = $parser->parse(); 182 | 183 | $this->assertEquals('https://example.com/things/image.png', $output['items'][0]['properties']['photo'][0]); 184 | } 185 | 186 | /** 187 | * @group baseUrl 188 | */ 189 | public function testResolvesRelativeBaseRelativeUrlsInImpliedMicroformats() { 190 | $input = ''; 191 | $parser = new Parser($input, 'https://example.com/'); 192 | $output = $parser->parse(); 193 | 194 | $this->assertEquals('https://example.com/things/image.png', $output['items'][0]['properties']['photo'][0]); 195 | } 196 | 197 | /** @see https://github.com/indieweb/php-mf2/issues/33 */ 198 | public function testParsesHrefBeforeValueClass() { 199 | $input = 'WRONG'; 200 | $result = Mf2\parse($input); 201 | $this->assertEquals('https://example.com/right', $result['items'][0]['properties']['url'][0]); 202 | } 203 | 204 | /** 205 | * @group parseU 206 | */ 207 | public function testParseUHandlesAudio() { 208 | $input = '
'; 209 | $parser = new Parser($input); 210 | $output = $parser->parse(); 211 | 212 | $this->assertArrayHasKey('audio', $output['items'][0]['properties']); 213 | $this->assertEquals('https://example.com/audio.mp3', $output['items'][0]['properties']['audio'][0]); 214 | } 215 | 216 | /** 217 | * @group parseU 218 | */ 219 | public function testParseUHandlesVideo() { 220 | $input = '
'; 221 | $parser = new Parser($input); 222 | $output = $parser->parse(); 223 | 224 | $this->assertArrayHasKey('video', $output['items'][0]['properties']); 225 | $this->assertEquals('https://example.com/video.mp4', $output['items'][0]['properties']['video'][0]); 226 | } 227 | 228 | /** 229 | * @group parseU 230 | */ 231 | public function testParseUHandlesVideoNoSrc() { 232 | $input = '
'; 233 | $parser = new Parser($input); 234 | $output = $parser->parse(); 235 | 236 | $this->assertArrayHasKey('video', $output['items'][0]['properties']); 237 | $this->assertEquals('no video support', $output['items'][0]['properties']['video'][0]); 238 | } 239 | 240 | /** 241 | * @group parseU 242 | */ 243 | public function testParseUHandlesSource() { 244 | $input = '
'; 245 | $parser = new Parser($input); 246 | $output = $parser->parse(); 247 | 248 | $this->assertArrayHasKey('video', $output['items'][0]['properties']); 249 | $this->assertEquals('https://example.com/video.mp4', $output['items'][0]['properties']['video'][0]); 250 | $this->assertEquals('https://example.com/video.ogg', $output['items'][0]['properties']['video'][1]); 251 | } 252 | 253 | /** 254 | * @group parseU 255 | */ 256 | public function testParseUHandlesVideoPoster() { 257 | $input = '
'; 258 | $parser = new Parser($input); 259 | $output = $parser->parse(); 260 | 261 | $this->assertArrayHasKey('video', $output['items'][0]['properties']); 262 | $this->assertEquals('https://example.com/video.mp4', $output['items'][0]['properties']['video'][0]); 263 | $this->assertEquals('https://example.com/posterimage.jpg', $output['items'][0]['properties']['photo'][0]); 264 | } 265 | 266 | /** 267 | * @group parseU 268 | */ 269 | public function testParseUWithSpaces() { 270 | $input = ''; 271 | $parser = new Parser($input); 272 | $output = $parser->parse(); 273 | 274 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 275 | $this->assertEquals('https://example.com', $output['items'][0]['properties']['url'][0]); 276 | } 277 | 278 | /** 279 | * @see https://github.com/indieweb/php-mf2/issues/130 280 | */ 281 | public function testImpliedUWithEmptyHref() { 282 | $input = 'Jane Doe 283 | Jane Doe 284 | 285 |
Jane Doe

286 | '; 287 | $parser = new Parser($input, 'https://example.com'); 288 | $output = $parser->parse(); 289 | 290 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 291 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 292 | 293 | $this->assertArrayHasKey('url', $output['items'][1]['properties']); 294 | $this->assertEquals('https://example.com/', $output['items'][1]['properties']['url'][0]); 295 | 296 | $this->assertArrayHasKey('url', $output['items'][2]['properties']); 297 | $this->assertEquals('https://example.com/', $output['items'][2]['properties']['url'][0]); 298 | 299 | $this->assertArrayHasKey('url', $output['items'][3]['properties']); 300 | $this->assertEquals('https://example.com/', $output['items'][3]['properties']['url'][0]); 301 | 302 | $this->assertArrayHasKey('url', $output['items'][4]['children'][0]['properties']); 303 | $this->assertEquals('https://example.com/', $output['items'][4]['children'][0]['properties']['url'][0]); 304 | } 305 | 306 | public function testValueFromLinkTag() { 307 | $input = <<< END 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | END; 316 | 317 | $parser = new Parser($input, 'https://example.com'); 318 | $output = $parser->parse(); 319 | 320 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 321 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 322 | 323 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 324 | $this->assertEquals('Example.com homepage', $output['items'][0]['properties']['name'][0]); 325 | } 326 | 327 | public function testResolveFromDataElement() { 328 | $parser = new Parser('
', 'https://example.com/index.html'); 329 | $output = $parser->parse(); 330 | 331 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 332 | $this->assertEquals('https://example.com/relative.html', $output['items'][0]['properties']['url'][0]); 333 | } 334 | 335 | /** 336 | * @see https://github.com/microformats/php-mf2/issues/182 337 | */ 338 | public function testResolveFromIframeElement() { 339 | $input = '
340 |

Title

341 | 344 |
'; 345 | $parser = new Parser($input, 'https://example.com'); 346 | $output = $parser->parse(); 347 | 348 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 349 | $this->assertEquals('https://example.com/index.html', $output['items'][0]['properties']['url'][0]); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /tests/Mf2/CombinedMicroformatsTest.php: -------------------------------------------------------------------------------- 1 | 32 | 33 | IndieWebCamp 2012 34 | 35 | from 36 | to at 37 | 38 | 39 | Powell’s, 1005 W Burnside St., Portland, OR 40 | 41 | '; 42 | $expected = '{ 43 | "rels": {}, 44 | "rel-urls": {}, 45 | "items": [{ 46 | "type": ["h-event"], 47 | "properties": { 48 | "name": ["IndieWebCamp 2012"], 49 | "url": ["https://indieweb.org/2012"], 50 | "start": ["2012-06-30"], 51 | "end": ["2012-07-01"], 52 | "location": [{ 53 | "value": "Powell’s", 54 | "type": ["h-card"], 55 | "properties": { 56 | "name": ["Powell’s"], 57 | "org": ["Powell’s"], 58 | "url": ["https://www.powells.com/"], 59 | "street-address": ["1005 W Burnside St."], 60 | "locality": ["Portland"], 61 | "region": ["Oregon"] 62 | } 63 | }] 64 | } 65 | }] 66 | }'; 67 | 68 | $parser = new Parser($input, '', true); 69 | $output = $parser->parse(); 70 | 71 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 72 | } 73 | 74 | /** 75 | * From https://microformats.org/wiki/microformats2#combining_microformats 76 | */ 77 | public function testHCardOrgPOrg() { 78 | $input = '
79 | Mitchell Baker 82 | (Mozilla Foundation) 83 |
'; 84 | $expected = '{ 85 | "rels": {}, 86 | "rel-urls": {}, 87 | "items": [{ 88 | "type": ["h-card"], 89 | "properties": { 90 | "name": ["Mitchell Baker"], 91 | "url": ["http://blog.lizardwrangler.com/"], 92 | "org": ["Mozilla Foundation"] 93 | } 94 | }] 95 | }'; 96 | 97 | $parser = new Parser($input, '', true); 98 | $output = $parser->parse(); 99 | 100 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 101 | } 102 | 103 | /** 104 | * From https://microformats.org/wiki/microformats2#combining_microformats 105 | */ 106 | public function testHCardOrgHCard() { 107 | $input = '
108 | Mitchell Baker 111 | (Mozilla Foundation) 114 |
'; 115 | $expected = '{ 116 | "rels": {}, 117 | "rel-urls": {}, 118 | "items": [{ 119 | "type": ["h-card"], 120 | "properties": { 121 | "name": ["Mitchell Baker"], 122 | "url": ["http://blog.lizardwrangler.com/"], 123 | "org": [{ 124 | "value": "Mozilla Foundation", 125 | "type": ["h-card"], 126 | "properties": { 127 | "name": ["Mozilla Foundation"], 128 | "url": ["http://mozilla.org/"] 129 | } 130 | }] 131 | } 132 | }] 133 | }'; 134 | 135 | $parser = new Parser($input, '', true); 136 | $output = $parser->parse(); 137 | 138 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 139 | } 140 | 141 | /** 142 | * From https://microformats.org/wiki/microformats2#combining_microformats 143 | */ 144 | public function testHCardPOrgHCardHOrg() { 145 | $input = '
146 | Mitchell Baker 149 | (Mozilla Foundation) 152 |
'; 153 | $expected = '{ 154 | "rels": {}, 155 | "rel-urls": {}, 156 | "items": [{ 157 | "type": ["h-card"], 158 | "properties": { 159 | "name": ["Mitchell Baker"], 160 | "url": ["http://blog.lizardwrangler.com/"], 161 | "org": [{ 162 | "value": "Mozilla Foundation", 163 | "type": ["h-card", "h-org"], 164 | "properties": { 165 | "name": ["Mozilla Foundation"], 166 | "url": ["http://mozilla.org/"] 167 | } 168 | }] 169 | } 170 | }] 171 | }'; 172 | 173 | $parser = new Parser($input, '', true); 174 | $output = $parser->parse(); 175 | 176 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 177 | } 178 | 179 | /** 180 | * From https://microformats.org/wiki/microformats2#combining_microformats 181 | */ 182 | public function testHCardChildHCard() { 183 | $input = ''; 190 | $expected = '{ 191 | "rels": {}, 192 | "rel-urls": {}, 193 | "items": [{ 194 | "type": ["h-card"], 195 | "properties": { 196 | "name": ["Mitchell Baker"], 197 | "url": ["http://blog.lizardwrangler.com/"] 198 | }, 199 | "children": [{ 200 | "type": ["h-card","h-org"], 201 | "properties": { 202 | "name": ["Mozilla Foundation"], 203 | "url": ["http://mozilla.org/"] 204 | } 205 | }] 206 | }] 207 | }'; 208 | 209 | $parser = new Parser($input, '', true); 210 | $output = $parser->parse(); 211 | 212 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 213 | } 214 | 215 | /** 216 | * Regression test for https://github.com/indieweb/php-mf2/issues/42 217 | * 218 | * This was occurring because mfPropertyNamesFromClass was only ever returning the first property name 219 | * rather than all of them. 220 | */ 221 | public function testNestedMicroformatUnderMultipleProperties() { 222 | $input = '
'; 223 | $mf = Mf2\parse($input); 224 | 225 | $this->assertCount(1, $mf['items'][0]['properties']['like-of']); 226 | $this->assertCount(1, $mf['items'][0]['properties']['in-reply-to']); 227 | } 228 | 229 | /** 230 | * Test microformats nested under e-* property classnames retain html: key in structure 231 | * 232 | * @see https://github.com/indieweb/php-mf2/issues/64 233 | */ 234 | public function testMicroformatsNestedUnderEPropertyClassnamesRetainHtmlKey() { 235 | $input = '

Hello

'; 236 | $mf = Mf2\parse($input); 237 | 238 | $this->assertArrayHasKey('value', $mf['items'][0]['properties']['content'][0]); 239 | $this->assertEquals($mf['items'][0]['properties']['content'][0]['value'], 'Hello'); 240 | $this->assertArrayHasKey('html', $mf['items'][0]['properties']['content'][0]); 241 | $this->assertEquals($mf['items'][0]['properties']['content'][0]['html'], '

Hello

'); 242 | } 243 | 244 | /** 245 | * Test microformats nested under u-* property classnames derive value: key from parsing as u-* 246 | */ 247 | public function testMicroformatsNestedUnderUPropertyClassnamesDeriveValueCorrectly() { 248 | $input = '
This should not be the value
'; 249 | $mf = Mf2\parse($input); 250 | $this->assertEquals($mf['items'][0]['properties']['url'][0]['value'], 'This should be the value', json_encode($mf['items'][0]['properties']['url'][0])); 251 | } 252 | 253 | public function testMicroformatsNestedUnderUPropertyClassnamesDeriveValueFromURL() { 254 | $input = '
255 |

Name

256 |

Hello World

257 | 264 |
'; 265 | $expected = '{ 266 | "items": [{ 267 | "type": ["h-entry"], 268 | "properties": { 269 | "name": ["Name"], 270 | "content": [{ 271 | "html": "Hello World", 272 | "value": "Hello World" 273 | }], 274 | "comment": [{ 275 | "type": ["h-cite"], 276 | "properties": { 277 | "author": [{ 278 | "type": ["h-card"], 279 | "properties": { 280 | "name": ["Jane Bloggs"], 281 | "url": ["https:\/\/jane.example.com\/"] 282 | }, 283 | "value": "https:\/\/jane.example.com\/" 284 | }], 285 | "content": ["lol"], 286 | "name": ["lol"], 287 | "url": ["https:\/\/example.com\/post1234"], 288 | "published": ["2015-07-12 12:03"] 289 | }, 290 | "value": "https:\/\/example.com\/post1234" 291 | }] 292 | } 293 | }], 294 | "rels":[], 295 | "rel-urls": [] 296 | }'; 297 | $mf = Mf2\parse($input); 298 | 299 | $this->assertJsonStringEqualsJsonString(json_encode($mf), $expected); 300 | $this->assertEquals($mf['items'][0]['properties']['comment'][0]['value'], 'https://example.com/post1234'); 301 | $this->assertEquals($mf['items'][0]['properties']['comment'][0]['properties']['author'][0]['value'], 'https://jane.example.com/'); 302 | } 303 | 304 | public function testMicroformatsNestedUnderPPropertyClassnamesDeriveValueFromFirstPName() { 305 | $input = '
This post was written by Zoe.
'; 306 | $mf = Mf2\parse($input); 307 | 308 | $this->assertEquals($mf['items'][0]['properties']['author'][0]['value'], 'Zoe'); 309 | } 310 | 311 | 312 | /** 313 | * @see https://github.com/indieweb/php-mf2/issues/98 314 | * @see https://github.com/microformats/tests/issues/58 315 | */ 316 | public function testNoValueForNestedMicroformatWithoutProperty() { 317 | $input = ''; 318 | $parser = new Parser($input); 319 | $output = $parser->parse(); 320 | 321 | $this->assertArrayHasKey('children', $output['items'][0]); 322 | $this->assertArrayNotHasKey('value', $output['items'][0]['children'][0]); 323 | } 324 | 325 | 326 | /** 327 | * With the backcompat changes I worked on in this PR, I ran into a case where 328 | * nested mf1 without properties were not added to the 'children' property properly. 329 | * I fixed that but then wanted to ensure it worked beyond 1-level deep. This example 330 | * is contrived, but lets me test to confirm 'children' is set correctly. - Gregor Morrill 331 | */ 332 | public function testNestedMf1() { 333 | $input = '
Jane Doe and
John Doe
'; 334 | $parser = new Parser($input); 335 | $output = $parser->parse(); 336 | 337 | $this->assertEmpty($output['items'][0]['properties']); 338 | $this->assertArrayHasKey('children', $output['items'][0]); 339 | $this->assertEquals('h-card', $output['items'][0]['children'][0]['type'][0]); 340 | $this->assertEquals('Jane Doe', $output['items'][0]['children'][0]['properties']['name'][0]); 341 | $child_mf = $output['items'][0]['children'][0]; 342 | $this->assertArrayHasKey('children', $child_mf); 343 | $this->assertEquals('h-card', $child_mf['children'][0]['type'][0]); 344 | $this->assertEquals('John Doe', $child_mf['children'][0]['properties']['name'][0]); 345 | } 346 | 347 | public function testNoUrlFromRelOnMf2() { 348 | $input = <<< END 349 |
350 |

Title of Post

351 |

This is the post

352 |
353 | END; 354 | $parser = new Parser($input); 355 | $output = $parser->parse(); 356 | 357 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 358 | $this->assertArrayHasKey('content', $output['items'][0]['properties']); 359 | $this->assertArrayNotHasKey('url', $output['items'][0]['properties']); 360 | } 361 | 362 | /** 363 | * Simplified h-entry with `p-location h-adr` from https://aaronparecki.com/2018/03/14/3/ 364 | * Whitespace cleaned up for easier test assertion 365 | * @see https://github.com/indieweb/php-mf2/issues/151 366 | */ 367 | public function testNestedValuePProperty() { 368 | $input = <<< END 369 |
370 | 371 | Portland, Oregon 44°F 372 | 373 | 374 | 375 |
376 | END; 377 | $parser = new Parser($input); 378 | $output = $parser->parse(); 379 | 380 | $this->assertArrayHasKey('value', $output['items'][0]['properties']['location'][0]); 381 | $this->assertEquals("Portland, Oregon • 44°F", $output['items'][0]['properties']['location'][0]['value']); 382 | } 383 | 384 | /** 385 | * @see https://github.com/indieweb/php-mf2/issues/151 386 | */ 387 | public function testNestedValueDTProperty() { 388 | $input = <<< END 389 |
390 |
1997-12-12
391 |
392 | END; 393 | $parser = new Parser($input); 394 | $output = $parser->parse(); 395 | 396 | $this->assertArrayHasKey('value', $output['items'][0]['properties']['acme'][0]); 397 | $this->assertEquals('1997-12-12', $output['items'][0]['properties']['acme'][0]['value']); 398 | } 399 | 400 | /** 401 | * rel=tag should not be upgraded within microformats2 402 | * @see https://github.com/indieweb/php-mf2/issues/157 403 | */ 404 | public function testMf2DoesNotParseRelTag() { 405 | $input = '
406 | 407 |
408 | 409 |
410 | 411 |
412 | '; 413 | $parser = new Parser($input); 414 | $output = $parser->parse(); 415 | 416 | $this->assertArrayNotHasKey('category', $output['items'][0]['properties']); 417 | $this->assertArrayNotHasKey('category', $output['items'][1]['properties']); 418 | } 419 | 420 | /** 421 | * JSON-mode should return an empty stdClass when a microformat has no properties. 422 | * @see https://github.com/indieweb/php-mf2/issues/171 423 | */ 424 | public function testEmptyPropertiesObjectInJSONMode() { 425 | $input = '
'; 426 | // Try in JSON-mode: expect the raw PHP to be an stdClass instance that serializes to an empty object. 427 | $parser = new Parser($input, null, true); 428 | $output = $parser->parse(); 429 | $this->assertInstanceOf('\stdClass', $output['items'][0]['properties']); 430 | $this->assertSame('{}', json_encode($output['items'][0]['properties'])); 431 | // Repeat in non-JSON-mode: expect the raw PHP to be an array. Check that its serialization is not what we need for mf2 JSON. 432 | $parser = new Parser($input, null, false); 433 | $output = $parser->parse(); 434 | $this->assertIsArray($output['items'][0]['properties']); 435 | $this->assertSame('[]', json_encode($output['items'][0]['properties'])); 436 | } 437 | 438 | } 439 | 440 | -------------------------------------------------------------------------------- /tests/Mf2/ParseImpliedTest.php: -------------------------------------------------------------------------------- 1 | The Name'; 22 | $parser = new Parser($input); 23 | $output = $parser->parse(); 24 | 25 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 26 | $this->assertEquals('The Name', $output['items'][0]['properties']['name'][0]); 27 | } 28 | 29 | public function testParsesImpliedPNameFromImgAlt() { 30 | $input = 'The Name'; 31 | $parser = new Parser($input); 32 | $output = $parser->parse(); 33 | 34 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 35 | $this->assertEquals('The Name', $output['items'][0]['properties']['name'][0]); 36 | } 37 | 38 | public function testParsesImpliedPNameFromNestedImgAlt() { 39 | $input = '
The Name
'; 40 | $parser = new Parser($input); 41 | $output = $parser->parse(); 42 | 43 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 44 | $this->assertEquals('The Name', $output['items'][0]['properties']['name'][0]); 45 | } 46 | 47 | public function testParsesImpliedPNameFromDoublyNestedImgAlt() { 48 | $input = '
The Name
'; 49 | $parser = new Parser($input); 50 | $output = $parser->parse(); 51 | 52 | $this->assertArrayHasKey('name', $output['items'][0]['properties']); 53 | $this->assertEquals('The Name', $output['items'][0]['properties']['name'][0]); 54 | } 55 | 56 | public function testParsesImpliedUPhotoFromImgSrcWithoutAlt() { 57 | $input = ''; 58 | $parser = new Parser($input); 59 | $output = $parser->parse(); 60 | 61 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 62 | $this->assertEquals('https://example.com/img.png', $output['items'][0]['properties']['photo'][0]); 63 | } 64 | 65 | public function testParsesImpliedUPhotoFromImgSrcWithEmptyAlt() { 66 | $input = ''; 67 | $parser = new Parser($input); 68 | $output = $parser->parse(); 69 | 70 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 71 | $this->assertEquals('https://example.com/img.png', $output['items'][0]['properties']['photo'][0]['value']); 72 | $this->assertEquals('', $output['items'][0]['properties']['photo'][0]['alt']); 73 | } 74 | 75 | public function testParsesImpliedUPhotoFromImgSrcWithAlt() { 76 | $input = 'Example'; 77 | $parser = new Parser($input); 78 | $output = $parser->parse(); 79 | 80 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 81 | $this->assertEquals('https://example.com/img.png', $output['items'][0]['properties']['photo'][0]['value']); 82 | $this->assertEquals('Example', $output['items'][0]['properties']['photo'][0]['alt']); 83 | } 84 | 85 | public function testParsesImpliedUPhotoFromNestedImgSrc() { 86 | $input = '
'; 87 | $parser = new Parser($input); 88 | $output = $parser->parse(); 89 | 90 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 91 | $return = [ 92 | 'value' => 'https://example.com/img.png', 93 | 'alt'=> '' 94 | ]; 95 | $this->assertEquals( $return, $output['items'][0]['properties']['photo'][0]); 96 | } 97 | 98 | public function testParsesImpliedUPhotoFromDoublyNestedImgSrc() { 99 | $input = '
'; 100 | $parser = new Parser($input); 101 | $output = $parser->parse(); 102 | 103 | $this->assertArrayHasKey('photo', $output['items'][0]['properties']); 104 | $result = [ 105 | 'alt' => '', 106 | 'value' => 'https://example.com/img.png' 107 | ]; 108 | $this->assertEquals($result, $output['items'][0]['properties']['photo'][0]); 109 | } 110 | 111 | /* 112 | * see testImpliedPhotoFromNestedObject() and testImpliedPhotoFromNestedObject() 113 | public function testIgnoresImgIfNotOnlyChild() { 114 | $input = '

Moar text

'; 115 | $parser = new Parser($input); 116 | $output = $parser->parse(); 117 | 118 | $this->assertArrayNotHasKey('photo', $output['items'][0]['properties']); 119 | } 120 | 121 | public function testIgnoresDoublyNestedImgIfNotOnlyDoublyNestedChild() { 122 | $input = '

Moar text

'; 123 | $parser = new Parser($input); 124 | $output = $parser->parse(); 125 | 126 | $this->assertArrayNotHasKey('photo', $output['items'][0]['properties']); 127 | } 128 | */ 129 | 130 | public function testParsesImpliedUUrlFromAHref() { 131 | $input = 'Some Name'; 132 | $parser = new Parser($input); 133 | $output = $parser->parse(); 134 | 135 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 136 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 137 | } 138 | 139 | 140 | public function testParsesImpliedUUrlFromNestedAHref() { 141 | $input = 'Some Name'; 142 | $parser = new Parser($input); 143 | $output = $parser->parse(); 144 | 145 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 146 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 147 | } 148 | 149 | public function testParsesImpliedUUrlWithExplicitName() { 150 | $input = 'Some Name'; 151 | $parser = new Parser($input); 152 | $output = $parser->parse(); 153 | 154 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 155 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 156 | } 157 | 158 | public function testParsesImpliedNameWithExplicitURL() { 159 | $input = 'Some Name'; 160 | $parser = new Parser($input); 161 | $output = $parser->parse(); 162 | 163 | $this->assertArrayHasKey('url', $output['items'][0]['properties']); 164 | $this->assertEquals('https://example.com/', $output['items'][0]['properties']['url'][0]); 165 | $this->assertEquals('Some Name', $output['items'][0]['properties']['name'][0]); 166 | } 167 | 168 | public function testMultipleImpliedHCards() { 169 | $input = 'Frances Berriman 170 | 171 | Ben Ward 172 | 173 | Sally Ride 175 | 176 | 177 | Tantek Çelik 178 | '; 179 | $expected = '{ 180 | "rels": {}, 181 | "rel-urls": {}, 182 | "items": [{ 183 | "type": ["h-card"], 184 | "properties": { 185 | "name": ["Frances Berriman"] 186 | } 187 | }, 188 | { 189 | "type": ["h-card"], 190 | "properties": { 191 | "name": ["Ben Ward"], 192 | "url": ["http://benward.me"] 193 | } 194 | }, 195 | { 196 | "type": ["h-card"], 197 | "properties": { 198 | "name": ["Sally Ride"], 199 | "photo": [{ 200 | "value": "http://upload.wikimedia.org/wikipedia/commons/a/a4/Ride-s.jpg", 201 | "alt": "Sally Ride" 202 | }] 203 | } 204 | }, 205 | { 206 | "type": ["h-card"], 207 | "properties": { 208 | "name": ["Tantek Çelik"], 209 | "url": ["http://tantek.com"], 210 | "photo": [{ 211 | "value": "http://ttk.me/logo.jpg", 212 | "alt": "Tantek Çelik" 213 | }] 214 | } 215 | }] 216 | }'; 217 | 218 | $parser = new Parser($input, '', true); 219 | $output = $parser->parse(); 220 | 221 | $this->assertJsonStringEqualsJsonString(json_encode($output), $expected); 222 | } 223 | 224 | /** as per https://github.com/indieweb/php-mf2/issues/37 */ 225 | public function testParsesImpliedNameConsistentWithPName() { 226 | $inner = "Name \nand more"; 227 | $test = ' ' . $inner .' ' . $inner . ' '; 228 | $result = Mf2\parse($test); 229 | $this->assertEquals('Name and more', $result['items'][0]['properties']['name'][0]); 230 | $this->assertEquals('Name and more', $result['items'][1]['properties']['name'][0]); 231 | } 232 | 233 | 234 | /** @see https://github.com/indieweb/php-mf2/issues/6 */ 235 | public function testParsesImpliedNameFromAbbrTitle() { 236 | $input = 'BJW'; 237 | $result = Mf2\parse($input); 238 | $this->assertEquals('Barnaby Walters', $result['items'][0]['properties']['name'][0]); 239 | } 240 | 241 | public function testImpliedPhotoFromObject() { 242 | $input = 'John Doe'; 243 | $result = Mf2\parse($input); 244 | 245 | $this->assertArrayHasKey('photo', $result['items'][0]['properties']); 246 | $this->assertEquals('http://example/photo1.jpg', $result['items'][0]['properties']['photo'][0]); 247 | } 248 | 249 | /** 250 | * Correcting previous test testIgnoresImgIfNotOnlyChild() 251 | * This *should* return the photo since h-x>img[src]:only-of-type:not[.h-*] 252 | * @see https://indieweb.org/User:Tantek.com 253 | */ 254 | public function testImpliedPhotoFromNestedImg() { 255 | $input = 'Tantek Çelik'; 256 | $result = Mf2\parse($input); 257 | 258 | $this->assertArrayHasKey('photo', $result['items'][0]['properties']); 259 | $this->assertEquals('https://pbs.twimg.com/profile_images/423350922408767488/nlA_m2WH.jpeg', $result['items'][0]['properties']['photo'][0]); 260 | } 261 | 262 | public function testIgnoredPhotoIfMultipleImg() { 263 | $input = '
'; 264 | $result = Mf2\parse($input); 265 | 266 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 267 | } 268 | 269 | /** 270 | * Correcting previous test testIgnoresDoublyNestedImgIfNotOnlyDoublyNestedChild() 271 | * This *should* return the photo since .h-x>object[data]:only-of-type:not[.h-*] 272 | */ 273 | public function testImpliedPhotoFromNestedObject() { 274 | $input = '
John Doe

Moar text

'; 275 | $result = Mf2\parse($input); 276 | 277 | $this->assertArrayHasKey('photo', $result['items'][0]['properties']); 278 | $this->assertEquals('http://example/photo3.jpg', $result['items'][0]['properties']['photo'][0]); 279 | } 280 | 281 | public function testIgnoredPhotoIfMultipleObject() { 282 | $input = '
John Doe
'; 283 | $result = Mf2\parse($input); 284 | 285 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 286 | } 287 | 288 | public function testIgnoredPhotoIfNestedImgH() { 289 | $input = '
'; 290 | $result = Mf2\parse($input); 291 | 292 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 293 | } 294 | 295 | public function testIgnoredPhotoIfNestedImgHasHClass() { 296 | $input = '
John Doe
'; 297 | $result = Mf2\parse($input); 298 | 299 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 300 | } 301 | 302 | public function testIgnoredPhotoIfNestedObjectHasHClass() { 303 | $input = '
John Doe
'; 304 | $result = Mf2\parse($input); 305 | 306 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 307 | } 308 | 309 | /** 310 | * @see https://github.com/indieweb/php-mf2/issues/176 311 | */ 312 | public function testIgnoredPhotoImgInNestedH() { 313 | $input = '
'; 314 | $result = Mf2\parse($input); 315 | 316 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 317 | } 318 | 319 | /** 320 | * @see https://github.com/indieweb/php-mf2/issues/176 321 | */ 322 | public function testIgnoredPhotoObjectInNestedH() { 323 | $input = '
John Doe
'; 324 | $result = Mf2\parse($input); 325 | 326 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 327 | } 328 | 329 | /** 330 | * @see https://github.com/indieweb/php-mf2/issues/190 331 | */ 332 | public function testIgnoredMultiChildrenWithNestedPhotoImg() { 333 | $input = ''; 334 | $result = Mf2\parse($input); 335 | 336 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 337 | } 338 | 339 | /** 340 | * @see https://github.com/indieweb/php-mf2/issues/190 341 | */ 342 | public function testIgnoredMultiChildrenWithNestedPhotoObject() { 343 | $input = ''; 344 | $result = Mf2\parse($input); 345 | 346 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 347 | } 348 | 349 | /** 350 | * Imply properties only on explicit h-x class name root microformat element (no backcompat roots) 351 | * @see https://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties 352 | */ 353 | public function testBackcompatNoImpliedName() { 354 | $input = '

blah blah blah

'; 355 | $result = Mf2\parse($input); 356 | 357 | $this->assertArrayNotHasKey('name', $result['items'][0]['properties']); 358 | $this->assertArrayHasKey('content', $result['items'][0]['properties']); 359 | } 360 | 361 | 362 | /** 363 | * Imply properties only on explicit h-x class name root microformat element (no backcompat roots) 364 | * @see https://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties 365 | */ 366 | public function testBackcompatNoImpliedPhoto() { 367 | $input = '
photo
'; 368 | $result = Mf2\parse($input); 369 | 370 | $this->assertEmpty($result['items'][0]['properties']); 371 | } 372 | 373 | 374 | /** 375 | * Imply properties only on explicit h-x class name root microformat element (no backcompat roots) 376 | * @see https://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties 377 | */ 378 | public function testBackcompatNoImpliedUrl() { 379 | $input = '
Title

blah blah blah

'; 380 | $result = Mf2\parse($input); 381 | 382 | $this->assertArrayNotHasKey('url', $result['items'][0]['properties']); 383 | $this->assertArrayHasKey('name', $result['items'][0]['properties']); 384 | $this->assertArrayHasKey('content', $result['items'][0]['properties']); 385 | } 386 | 387 | 388 | /** 389 | * Don't imply u-url if there are other u-* 390 | * @see https://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties 391 | * @see https://github.com/microformats/php-mf2/issues/183 392 | */ 393 | public function testNoImpliedUrl() { 394 | $input = '

Title

blah blah blah

'; 395 | $result = Mf2\parse($input); 396 | 397 | $this->assertArrayNotHasKey('url', $result['items'][0]['properties']); 398 | } 399 | 400 | /** 401 | * Do not use img src in implied p-name 402 | * @see https://github.com/microformats/php-mf2/issues/180 403 | */ 404 | public function testNoImgSrcImpliedName() { 405 | $input = '

My Name

'; 406 | $result = Mf2\parse($input); 407 | 408 | $this->assertArrayHasKey('name', $result['items'][0]['properties']); 409 | $this->assertEquals('My Name', $result['items'][0]['properties']['name'][0]); 410 | } 411 | 412 | /** 413 | * @see https://github.com/microformats/php-mf2/issues/198 414 | */ 415 | public function testNoImpliedPhotoWhenExplicitUProperty() { 416 | $input = '
Organization Name
'; 417 | $result = Mf2\parse($input); 418 | 419 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 420 | } 421 | 422 | /** 423 | * @see https://github.com/microformats/php-mf2/issues/198 424 | */ 425 | public function testNoImpliedPhotoWhenNestedMicroformat() { 426 | $input = '
Alice Organization Name
'; 427 | $result = Mf2\parse($input); 428 | 429 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']); 430 | $this->assertArrayHasKey('author', $result['items'][0]['properties']); 431 | $this->assertArrayNotHasKey('photo', $result['items'][0]['properties']['author'][0]['properties']); 432 | } 433 | } 434 | 435 | -------------------------------------------------------------------------------- /tests/Mf2/snarfed.org.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oauth-dropins | snarfed.org 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 |
140 | 141 | 142 |
143 | 144 |
149 | 150 |
151 | 165 |
166 | 167 | 181 | 182 |
183 |
184 |
185 | 186 | 187 | 188 |
189 |
190 | 191 |
192 | Uncategorized

oauth-dropins

193 | 194 | 199 | 200 |
201 |

202 | 203 |

204 | 205 |

Need to use an OAuth-protected API in a Python webapp? 206 | oauth-dropins is for you! It’s a 207 | collection of drop-in OAuth client flows for many popular sites. It supports 208 | Facebook, Twitter, Google+, Instagram, Dropbox, Blogger, Tumblr, and 209 | WordPress.com, with more on the way. It also currently requires 210 | Google App Engine, but should support other 211 | platforms in the future.

212 | 213 |

Try the demo!

214 | 215 |

You can use oauth-dropins in your project with just a bit of code. For example, 216 | to use it for Facebook, just add these two lines to your WSGI application:

217 | 218 |
219 | from oauth_dropins import facebook
220 | 
221 | application = webapp2.WSGIApplication([
222 |   ...
223 |   ('/facebook/start_oauth', facebook.StartHandler.to('/facebook/oauth_callback')),
224 |   ('/facebook/oauth_callback', facebook.CallbackHandler.to('/next')),
225 |   ]
226 | 
227 | 228 |

Then map those URLs in your 229 | app.yaml 230 | and put your Facebook app‘s key and 231 | secret in facebook_app_id and facebook_app_secret files in your app’s root 232 | directory, and you’re good to go!

233 | 234 |

See the GitHub repo for more 235 | details. Happy hacking!

236 |
237 | 238 | Standard 239 |
240 |
241 | 242 | 243 |
244 |
245 | 246 |

247 | One thought on “oauth-dropins

248 | 249 | 250 |
    251 |
  1. 252 | 268 | 269 |
  2. 270 |
271 | 272 | 273 | 274 | 275 |
276 |

Leave a Reply

277 |
278 |

Your email address will not be published.

279 | 280 |

281 |

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

282 | 283 | 284 | 285 |

286 |

287 |
288 | 289 |
290 |
291 | 292 |
293 |
294 | 295 | 296 |
297 | 298 | 304 |
305 | 306 |
307 |
308 |
309 |
310 |
311 |
312 | 313 | 314 | 320 | 321 | 322 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | -------------------------------------------------------------------------------- /tests/Mf2/ParseLanguageTest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function testHtmlLangOnly() 22 | { 23 | $input = '
This test is in English.
'; 24 | $parser = new Parser($input); 25 | $parser->lang = true; 26 | $result = $parser->parse(); 27 | 28 | $this->assertArrayHasKey('lang', $result['items'][0]); 29 | $this->assertEquals('en', $result['items'][0]['lang']); 30 | } # end method testHtmlLangOnly() 31 | 32 | /** 33 | * Test with only h-entry lang 34 | */ 35 | public function testHEntryLangOnly() 36 | { 37 | $input = '
This test is in English.
'; 38 | $parser = new Parser($input); 39 | $parser->lang = true; 40 | $result = $parser->parse(); 41 | 42 | $this->assertArrayHasKey('lang', $result['items'][0]); 43 | $this->assertEquals('en', $result['items'][0]['lang']); 44 | } # end method testHEntryLangOnly() 45 | 46 | /** 47 | * Test with different and h-entry lang 48 | */ 49 | public function testHtmlAndHEntryLang() 50 | { 51 | $input = '
Esta prueba está en español.
'; 52 | $parser = new Parser($input); 53 | $parser->lang = true; 54 | $result = $parser->parse(); 55 | 56 | $this->assertArrayHasKey('lang', $result['items'][0]); 57 | $this->assertEquals('es', $result['items'][0]['lang']); 58 | } # end method testHtmlAndHEntryLang() 59 | 60 | /** 61 | * Test HTML fragment with only h-entry lang 62 | */ 63 | public function testFragmentHEntryLangOnly() 64 | { 65 | $input = '
This test is in English.
'; 66 | $parser = new Parser($input); 67 | $parser->lang = true; 68 | $result = $parser->parse(); 69 | 70 | $this->assertArrayHasKey('lang', $result['items'][0]); 71 | $this->assertEquals('en', $result['items'][0]['lang']); 72 | } # end method testFragmentHEntryLangOnly() 73 | 74 | /** 75 | * Test HTML fragment with no lang 76 | */ 77 | public function testFragmentHEntryNoLang() 78 | { 79 | $input = '
This test is in English.
'; 80 | $parser = new Parser($input); 81 | $parser->lang = true; 82 | $result = $parser->parse(); 83 | 84 | $this->assertArrayNotHasKey('lang', $result['items'][0]); 85 | } # end method testFragmentHEntryNoLang() 86 | 87 | /** 88 | * Test HTML fragment with no lang, loaded with loadXML() 89 | */ 90 | public function testFragmentHEntryNoLangXML() 91 | { 92 | $input = new \DOMDocument(); 93 | $input->loadXML('
This test is in English.
'); 94 | $parser = new Parser($input); 95 | $result = $parser->parse(); 96 | 97 | $this->assertArrayNotHasKey('lang', $result['items'][0]); 98 | } # end method testFragmentHEntryNoLangXML() 99 | 100 | /** 101 | * Test with different , h-entry lang, and h-entry without lang, 102 | * which should inherit from the 103 | */ 104 | public function testMultiLanguageInheritance() 105 | { 106 | $input = '
This test is in English.
Esta prueba está en español.
'; 107 | $parser = new Parser($input); 108 | $parser->lang = true; 109 | $result = $parser->parse(); 110 | 111 | $this->assertArrayHasKey('lang', $result['items'][0]); 112 | $this->assertArrayHasKey('lang', $result['items'][1]); 113 | $this->assertEquals('en', $result['items'][0]['lang']); 114 | $this->assertEquals('es', $result['items'][1]['lang']); 115 | } # end method testMultiLanguageInheritance() 116 | 117 | /** 118 | * Test feed with .h-feed lang which contains multiple h-entries of different languages 119 | * (or none specified), which should inherit from the .h-feed lang. 120 | */ 121 | public function testMultiLanguageFeed() 122 | { 123 | $input = '

Test Feed

This test is in English.
Esta prueba está en español.
Ce test est en français.
'; 124 | $parser = new Parser($input); 125 | $parser->lang = true; 126 | $result = $parser->parse(); 127 | 128 | $this->assertArrayHasKey('lang', $result['items'][0]); 129 | $this->assertEquals('en', $result['items'][0]['lang']); 130 | 131 | $this->assertArrayHasKey('lang', $result['items'][0]['children'][0]); 132 | $this->assertEquals('en', $result['items'][0]['children'][0]['lang']); 133 | 134 | $this->assertArrayHasKey('lang', $result['items'][0]['children'][1]); 135 | $this->assertEquals('es', $result['items'][0]['children'][1]['lang']); 136 | 137 | $this->assertArrayHasKey('lang', $result['items'][0]['children'][2]); 138 | $this->assertEquals('fr', $result['items'][0]['children'][2]['lang']); 139 | } # end method testMultiLanguageFeed() 140 | 141 | /** 142 | * Test with language specified in http-equiv Content-Language 143 | */ 144 | public function testMetaContentLanguage() 145 | { 146 | $input = '
Esta prueba está en español.
'; 147 | $parser = new Parser($input); 148 | $parser->lang = true; 149 | $result = $parser->parse(); 150 | 151 | $this->assertArrayHasKey('lang', $result['items'][0]); 152 | $this->assertEquals('es', $result['items'][0]['lang']); 153 | } # end method testMetaContentLanguage() 154 | 155 | /** 156 | * Test with real-world post on voxpelli.com 157 | * @see http://voxpelli.com/2015/09/oberoende-sociala-webben-2015/ 158 | */ 159 | public function testVoxpelliCom() 160 | { 161 | $input = <<< END 162 | 163 | 164 | 165 | 166 | Den oberoende sociala webben 2015 – Pelle Wessman 167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 191 | 192 | 193 |
194 |
195 |

Pelle Wessman

196 |
Things about me and the world around us
197 |
198 |
199 |
200 |

Den oberoende sociala webben 2015

201 |
202 |
203 |

Sociala medier. Synonymt med Facebook, Twitter, Instagram. Vad händer när en av dem försvinner? Är det game over då? Jaiku i repris, fast denna gång utan livlina?

204 |

Framtiden för sociala medier kan låta dyster. Om de inte läggs ner helt så kan de ju i alla fall ha moraliska frågetecken kring konst, aktivism eller annat. Men i det fördolda har många länge ställt frågan: Om webben är öppen, varför måste det sociala vara stängt? Och nu börjar det röra sig där.

205 |

Att ställa en fråga har alltid varit bra. Att fixa en lösning har ofta varit bättre. Att fixa lösningar har dock inte lyckats lika bra som alla frågor som ställts. Många fina lösningar har tänkts ut, men de flesta har ignorerat den verklighet och det sammanhang vi lever i och framförallt den nätverkseffekt som ger de sociala medierna sitt största värde idag. Men – turligt nog rättar sig inte alla i det ledet och nu börjar det bli spännande.

206 |

IndieWebCamp startade 2010 av Aaron Parecki och Tantek Çelik för att de ville se ett annat fokus än det tanke-centrerade, små-akademiska som varit innan. Ett fokus på att skapa snarare än att prata. Ett fokus på att lösa ens egna problem. Och ett fokus på att äga sin egna data i praktiken, inte bara i teorin. Sedan dess har många anslutit sig och flertalet verktyg och tjänster växt fram – IndieWebben har blivit en rörelse.

207 |

Idag är i princip alla komponenter redo för att kunna återföra det som en gång i tiden kallades för mikrobloggar tillbaka till sina bloggrötter. Och det utan att bryta med de stora sociala medierna så som många tidigare lösningar krävt. Det finns de som redan gör så idag, bl.a. grundarna av IndieWebCamp, och även min blogg är väl nästan redo att sälla sig dit.

208 |

Så – idag kan man alltså i princip både vara sin egen och vara en del i de stora sociala plattformarna – och de skarpa hörnen som ännu finns blir mer och mer rundade för var dag som går.

209 |

För att vara mer konkret så har det ur IndieWeb-rörelsen kommit möjligheter att:

210 |
    211 |
  • Ta emot replies, likes m.m. på sina inlägg direkt från andra bloggar via en teknik som kallas WebMentions. Bl.a. jag själv tillhandahåller en Disqus-liknande tjänst som gör det lätt att ta emot sådana reaktioner i realtid på vilken sida som helst – något flera har valt att nyttja.
  • 212 |
  • Posta poster från ens egna sajt till existerande sociala medier via vad som kallas POSSE (Publish (on your) Own Site, Syndicate Elsewhere) och få tillbaka reaktioner därifrån som WebMentions genom tjänster såsom Brid.gy – så att de som ännu hänger kvar i klassiska sociala medier kan fortsatt vara lika delaktiga som de är idag.
  • 213 |
  • Svara på inlägg direkt från någon annans sajt genom Webactions och Indie-Config (se mitt tidigare inlägg). Sådana knappar finns t.ex. på denna posten, men i nuläget har jag inte själv kopplat in reaktioner från Twitter så de kommentarer som postas via Twitter syns tyvärr inte här ännu.
  • 214 |
  • Posta nytt innehåll via ett standardiserat API, Micropub. Det möjliggör t.ex. för mobila klienter (har spelat in en screencast som visar en prototyp på ett flöde som man kan få till redan idag) liksom för automatisk import från existerande medier såsom från Instagram via OwnYourGram. Mitt nuvarande IndieWeb-projekt som i princip är klart exponerar till och med ett sådant API för statiska Jekyll-sajter på GitHub Pages eftersom jag själv kör en sådan. Kommer blogga mer om det längre fram.
  • 215 |
  • En inloggningsmekanism, IndieAuth, som gör att man kan logga in på alla olika tjänster med sin egna sajt och ge tillgång till olika saker där – likt ”Sign in with Twitter” och liknande – fast som allt annat inom IndieWebben helt distribuerat.
  • 216 |
  • Olika läsare för oberoende innehåll om man vill följa dem och interagera med dem oberoende av existerande medier. Vissa av dessa kan också användas för att interagera med poster och publicera nya vilket i princip gör dem till fullgoda sociala medie-alternativ, med enda skillnaden att alla poster och aktiviteter postas på individuella och helt separata bloggar. Hittills efterliknar tyvärr de flesta sådana läsarna RSS-läsare snarare än sociala medier, men det är mer av en designutmaning än en teknisk utmaning – grunden är densamma oavsett.
  • 217 |
218 |

Så I stort sett täcker IndieWebben idag åtminstone allt det en plattform som Twitter behöver kunna – och i stort så kan det göras lika smidigt där som det kan på Twitter.

219 |

IndieWeb-rörelsen tog dessutom för någon månad sedan och demonstrerade att de kunde genomföra det så kallade SWAT0-testet som är tänkt att vara ett implementationsoberoende test för att visa att ens tjänster distribuerat klarar av att hantera ett måttligt vanligt sociala medier scenario. Tre aktörer med tre olika implementationer deltog i testet och många fler är på gång att kunna demonstrera detsamma, däribland min egna WebMentions-tjänst som nästan har alla bitar på plats.

220 |

Så – vad är slutsatsen med allt detta Indie-hit och Indie-dit? Är det inte ändå till syvende och sist bara massa tekniknördar som flippar loss med sina älskade tekniska termer och som saknar varje liten verklighetsförankring man skulle kunna önska? Nej. Denna gången är det annorlunda. Denna gången är fokus på verkstad. Denna gången bevisas att något funkar innan det dokumenteras som en standard. Denna gången löser något ett verkligt behov före att det lyfts fram som frälsaren för alla de andra. Ingen IndieWeb-standard har kommit till utan att också ha haft en fungerade använd implementation i verkligheten från dag ett.

221 |

Eftersom fokuset varit på verkstad så har indiewebben byggts för att kunna samexistera med dagens sociala webb och i takt med att de stora sociala mediernas roll därmed så sakteliga kan minska i takt med att oberoende lösningar börjar användas så kan ett gradvis överflyttande ske i takt med att allt fler upptäcker fördelarna – ett hyfsat realistiskt scenario alltså till skillnad mot många tidigare drömmar.

222 |

Allt är såklart inte guld och gröna skogar ännu. Det finns fortfarande svårigheter kvar och alla varken kan, bör eller kommer att byta över idag eller imorgon. Men det viktiga är: Det finns en lovande start som det är lätt att börja nosa på och i takt med att allt fler nosar så fås allt mer erfarenhet och med mer erfarenhet byggs än bättre upplevelser och plötsligt finns där en klar väg framåt (en väg som dessutom öppnar för många nya spännande innovationer inom området).

223 |

Så när Twitter kanske så småningom försvinner så kanske den inte blir ett nytt Jaiku. För då kanske vi flyttat tillbaka hem. Vi kanske då har vår bas på den egna bloggen igen. Och då kan ingen någonsin stänga ner oss igen. Då är vi våra egna och ingen annans. Oberoende på riktigt. Så som webben är ment att vara.

224 |
225 | 231 | 234 | See mentions of this post
235 |
Have you written a response to this? Let me know the URL: 236 |
237 |
238 |
239 | 240 | 241 | END; 242 | $parser = new Parser($input); 243 | $parser->lang = true; 244 | $result = $parser->parse(); 245 | 246 | $this->assertArrayHasKey('lang', $result['items'][0]); 247 | $this->assertEquals('sv', $result['items'][0]['lang']); 248 | } # end method testVoxpelliCom() 249 | 250 | 251 | /** 252 | * @see https://github.com/indieweb/php-mf2/issues/96#issuecomment-304457341 253 | */ 254 | public function testNoLangInParsedProperties() { 255 | $input = '
256 |

En svensk titel

257 |
With an english summary
258 |
Och svensk huvudtext
259 |
'; 260 | $parser = new Parser($input); 261 | $parser->lang = true; 262 | $result = $parser->parse(); 263 | 264 | $this->assertArrayNotHasKey('lang', $result['items'][0]['properties']); 265 | $this->assertArrayHasKey('lang', $result['items'][0]); 266 | } 267 | 268 | } 269 | -------------------------------------------------------------------------------- /tests/Mf2/ParseDTTest.php: -------------------------------------------------------------------------------- 1 | 2012-08-05T14:50
'; 24 | $parser = new Parser($input); 25 | $output = $parser->parse(); 26 | 27 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 28 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 29 | } 30 | 31 | /** 32 | * @group parseDT 33 | */ 34 | public function testParseDTHandlesDataValueAttr() { 35 | $input = '
'; 36 | $parser = new Parser($input); 37 | $output = $parser->parse(); 38 | 39 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 40 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 41 | } 42 | 43 | /** 44 | * @group parseDT 45 | */ 46 | public function testParseDTHandlesDataInnerHTML() { 47 | $input = '
2012-08-05T14:50
'; 48 | $parser = new Parser($input); 49 | $output = $parser->parse(); 50 | 51 | 52 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 53 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 54 | } 55 | 56 | /** 57 | * @group parseDT 58 | */ 59 | public function testParseDTHandlesAbbrValueAttr() { 60 | $input = '
'; 61 | $parser = new Parser($input); 62 | $output = $parser->parse(); 63 | 64 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 65 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 66 | } 67 | 68 | /** 69 | * @group parseDT 70 | */ 71 | public function testParseDTHandlesAbbrInnerHTML() { 72 | $input = '
2012-08-05T14:50
'; 73 | $parser = new Parser($input); 74 | $output = $parser->parse(); 75 | 76 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 77 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 78 | } 79 | 80 | /** 81 | * @group parseDT 82 | */ 83 | public function testParseDTHandlesTimeDatetimeAttr() { 84 | $input = '
'; 85 | $parser = new Parser($input); 86 | $output = $parser->parse(); 87 | 88 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 89 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 90 | } 91 | 92 | /** 93 | * @group parseDT 94 | */ 95 | public function testParseDTHandlesTimeDatetimeAttrWithZ() { 96 | $input = '
'; 97 | $parser = new Parser($input); 98 | $output = $parser->parse(); 99 | 100 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 101 | $this->assertEquals('2012-08-05T14:50:00Z', $output['items'][0]['properties']['start'][0]); 102 | } 103 | 104 | /** 105 | * @group parseDT 106 | */ 107 | public function testParseDTHandlesTimeDatetimeAttrWithTZOffset() { 108 | $input = '
'; 109 | $parser = new Parser($input); 110 | $output = $parser->parse(); 111 | 112 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 113 | $this->assertEquals('2012-08-05T14:50:00-0700', $output['items'][0]['properties']['start'][0]); 114 | } 115 | 116 | /** 117 | * @group parseDT 118 | */ 119 | public function testParseDTHandlesTimeDatetimeAttrWithTZOffset2() { 120 | $input = '
'; 121 | $parser = new Parser($input); 122 | $output = $parser->parse(); 123 | 124 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 125 | $this->assertEquals('2012-08-05T14:50:00-07:00', $output['items'][0]['properties']['start'][0]); 126 | } 127 | 128 | /** 129 | * @group parseDT 130 | */ 131 | public function testParseDTHandlesTimeInnerHTML() { 132 | $input = '
'; 133 | $parser = new Parser($input); 134 | $output = $parser->parse(); 135 | 136 | 137 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 138 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 139 | } 140 | 141 | /** 142 | * @group parseDT 143 | */ 144 | public function testParseDTHandlesInsDelDatetime() { 145 | $input = '
'; 146 | $parser = new Parser($input); 147 | $output = $parser->parse(); 148 | 149 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 150 | $this->assertArrayHasKey('end', $output['items'][0]['properties']); 151 | $this->assertEquals('2012-08-05T14:50', $output['items'][0]['properties']['start'][0]); 152 | $this->assertEquals('2012-08-05T18:00', $output['items'][0]['properties']['end'][0]); 153 | } 154 | 155 | /** 156 | * @group parseDT 157 | * @group valueClass 158 | */ 159 | public function testYYYY_MM_DD__HH_MM() { 160 | $input = '
2012-10-07 at 21:18
'; 161 | $parser = new Parser($input); 162 | $output = $parser->parse(); 163 | 164 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 165 | $this->assertEquals('2012-10-07 21:18', $output['items'][0]['properties']['start'][0]); 166 | } 167 | 168 | /** 169 | * @group parseDT 170 | * @group valueClass 171 | */ 172 | public function testAbbrYYYY_MM_DD__HH_MM() { 173 | $input = '
some day at 21:18
'; 174 | $parser = new Parser($input); 175 | $output = $parser->parse(); 176 | 177 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 178 | $this->assertEquals('2012-10-07 21:18', $output['items'][0]['properties']['start'][0]); 179 | } 180 | 181 | /** 182 | * @group parseDT 183 | * @group valueClass 184 | */ 185 | public function testYYYY_MM_DD__HHpm() { 186 | $input = '
2012-10-07 at 9pm
'; 187 | $parser = new Parser($input); 188 | $output = $parser->parse(); 189 | 190 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 191 | $this->assertEquals('2012-10-07 21:00', $output['items'][0]['properties']['start'][0]); 192 | } 193 | 194 | /** 195 | * @group parseDT 196 | * @group valueClass 197 | */ 198 | public function testYYYY_MM_DD__HH_MMpm() { 199 | $input = '
2012-10-07 at 9:00pm
'; 200 | $parser = new Parser($input); 201 | $output = $parser->parse(); 202 | 203 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 204 | $this->assertEquals('2012-10-07 21:00', $output['items'][0]['properties']['start'][0]); 205 | } 206 | 207 | /** 208 | * @group parseDT 209 | * @group valueClass 210 | */ 211 | public function testYYYY_MM_DD__HH_MM_SSpm() { 212 | $input = '
2012-10-07 at 9:00:00pm
'; 213 | $parser = new Parser($input); 214 | $output = $parser->parse(); 215 | 216 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 217 | $this->assertEquals('2012-10-07 21:00:00', $output['items'][0]['properties']['start'][0]); 218 | } 219 | 220 | /** 221 | * This test name refers to the value-class used within the dt-end. 222 | * @group parseDT 223 | * @group valueClass 224 | */ 225 | public function testImpliedDTEndWithValueClass() { 226 | $input = '
2014-06-04 at 18:30 19:30
'; 227 | $parser = new Parser($input); 228 | $output = $parser->parse(); 229 | 230 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 231 | $this->assertArrayHasKey('end', $output['items'][0]['properties']); 232 | $this->assertEquals('2014-06-04 18:30', $output['items'][0]['properties']['start'][0]); 233 | $this->assertEquals('2014-06-04 19:30', $output['items'][0]['properties']['end'][0]); 234 | } 235 | 236 | /** 237 | * This test name refers to the lack of value-class within the dt-end. 238 | * @group parseDT 239 | * @group valueClass 240 | */ 241 | public function testImpliedDTEndWithoutValueClass() { 242 | $input = '
2014-06-05 at 18:31 19:31
'; 243 | 244 | $parser = new Parser($input); 245 | $output = $parser->parse(); 246 | 247 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 248 | $this->assertArrayHasKey('end', $output['items'][0]['properties']); 249 | $this->assertEquals('2014-06-05 18:31', $output['items'][0]['properties']['start'][0]); 250 | $this->assertEquals('2014-06-05 19:31', $output['items'][0]['properties']['end'][0]); 251 | } 252 | 253 | /** 254 | * @see https://github.com/indieweb/php-mf2/pull/46 255 | * @group parseDT 256 | * @group valueClass 257 | */ 258 | public function testImpliedDTEndUsingNonValueClassDTStart() { 259 | $input = '
until 19:31
'; 260 | 261 | $parser = new Parser($input); 262 | $output = $parser->parse(); 263 | 264 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 265 | $this->assertArrayHasKey('end', $output['items'][0]['properties']); 266 | $this->assertEquals('2014-06-05T18:31', $output['items'][0]['properties']['start'][0]); 267 | $this->assertEquals('2014-06-05 19:31', $output['items'][0]['properties']['end'][0]); 268 | } 269 | 270 | /** 271 | * @group parseDT 272 | * @group valueClass 273 | */ 274 | public function testDTStartOnly() { 275 | $input = '
2014-06-06 at 18:32
'; 276 | $parser = new Parser($input); 277 | $output = $parser->parse(); 278 | 279 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 280 | $this->assertEquals('2014-06-06 18:32', $output['items'][0]['properties']['start'][0]); 281 | $this->assertArrayNotHasKey('end', $output['items'][0]['properties']); 282 | } 283 | 284 | /** 285 | * @group parseDT 286 | * @group valueClass 287 | */ 288 | public function testDTStartDateOnly() { 289 | $input = '
2014-06-07
'; 290 | $parser = new Parser($input); 291 | $output = $parser->parse(); 292 | 293 | $this->assertArrayHasKey('start', $output['items'][0]['properties']); 294 | $this->assertEquals('2014-06-07', $output['items'][0]['properties']['start'][0]); 295 | } 296 | 297 | /** 298 | * TZ offsets normalized only for VCP. 299 | * This behavior is implied from "However the colons ":" separating the hours and minutes of any timezone offset are optional and discouraged in order to make it less likely that a timezone offset will be confused for a time." 300 | * @see https://microformats.org/wiki/index.php?title=value-class-pattern&oldid=66473##However+the+colons 301 | */ 302 | public function testNormalizeTZOffsetVCP() { 303 | $input = '
304 | , from 305 | 306 |
'; 307 | $parser = new Parser($input); 308 | $output = $parser->parse(); 309 | 310 | $this->assertEquals('2017-05-27 20:57-0700', $output['items'][0]['properties']['start'][0]); 311 | } 312 | 313 | 314 | /** 315 | * TZ offsets *not* normalized for non-VCP dates 316 | */ 317 | public function testNoNormalizeTZOffset() { 318 | $input = '
'; 319 | $parser = new Parser($input); 320 | $output = $parser->parse(); 321 | 322 | $this->assertEquals('2018-03-13 15:30-07:00', $output['items'][0]['properties']['start'][0]); 323 | } 324 | 325 | 326 | /** 327 | * @see https://github.com/indieweb/php-mf2/issues/115 328 | */ 329 | public function testDoNotAddT() { 330 | $input = '
331 | 332 | , from 333 | 334 | 335 |
'; 336 | $parser = new Parser($input); 337 | $output = $parser->parse(); 338 | 339 | $this->assertEquals('2009-06-26 19:00:00-0800', $output['items'][0]['properties']['start'][0]); 340 | } 341 | 342 | /** 343 | * @see https://github.com/indieweb/php-mf2/issues/115 344 | */ 345 | public function testPreserrveTIfAuthored() { 346 | $input = '
'; 347 | $parser = new Parser($input); 348 | $output = $parser->parse(); 349 | 350 | $this->assertEquals('2009-06-26T19:01-08:00', $output['items'][0]['properties']['start'][0]); 351 | } 352 | 353 | /** 354 | * @see https://github.com/indieweb/php-mf2/issues/126 355 | */ 356 | public function testDtVCPTimezone() { 357 | $input = '
358 | HomebrewWebsiteClub Berlin will be next on 359 | 360 | 2017-05-31, from 361 | 19:00 (UTC+02:00) 362 | to 21:00.
'; 363 | $parser = new Parser($input); 364 | $output = $parser->parse(); 365 | 366 | $this->assertEquals('2017-05-31 19:00+0200', $output['items'][0]['properties']['start'][0]); 367 | $this->assertEquals('2017-05-31 21:00+0200', $output['items'][0]['properties']['end'][0]); 368 | } 369 | 370 | /** 371 | * @see https://github.com/indieweb/php-mf2/issues/126 372 | */ 373 | public function testDtVCPTimezoneShort() { 374 | $input = '
375 | HomebrewWebsiteClub Berlin will be next on 376 | 377 | 2017-05-31, from 378 | 19:00 (UTC+2) 379 | to 21:00.
'; 380 | $parser = new Parser($input); 381 | $output = $parser->parse(); 382 | 383 | $this->assertEquals('2017-05-31 19:00+0200', $output['items'][0]['properties']['start'][0]); 384 | $this->assertEquals('2017-05-31 21:00+0200', $output['items'][0]['properties']['end'][0]); 385 | } 386 | 387 | /** 388 | * @see https://github.com/indieweb/php-mf2/issues/126 389 | */ 390 | public function testDtVCPTimezoneNoLeadingZero() { 391 | $input = '
392 | 393 | 2017-06-17 394 | 22:00-700 395 | 396 | 397 | 2017-06-17 398 | 23:00-700 399 | 400 |
'; 401 | $parser = new Parser($input); 402 | $output = $parser->parse(); 403 | 404 | $this->assertEquals('2017-06-17 22:00-0700', $output['items'][0]['properties']['start'][0]); 405 | $this->assertEquals('2017-06-17 23:00-0700', $output['items'][0]['properties']['end'][0]); 406 | } 407 | 408 | /** 409 | * @see https://github.com/microformats/microformats2-parsing/issues/4 410 | */ 411 | public function testImplyTimezoneFromStart() { 412 | $input = '
to
'; 413 | $parser = new Parser($input); 414 | $output = $parser->parse(); 415 | 416 | $this->assertEquals('2014-09-11 13:30-0700', $output['items'][0]['properties']['start'][0]); 417 | $this->assertEquals('2014-09-11 15:30-0700', $output['items'][0]['properties']['end'][0]); 418 | } 419 | 420 | /** 421 | * @see https://github.com/microformats/microformats2-parsing/issues/4 422 | */ 423 | public function testImplyTimezoneFromEnd() { 424 | $input = '
to
'; 425 | $parser = new Parser($input); 426 | $output = $parser->parse(); 427 | 428 | $this->assertEquals('2014-09-11 13:30-0700', $output['items'][0]['properties']['start'][0]); 429 | $this->assertEquals('2014-09-11 15:30-0700', $output['items'][0]['properties']['end'][0]); 430 | } 431 | 432 | /** 433 | * 434 | */ 435 | public function testAMPMWithPeriods() { 436 | $input = '
437 | 438 | 2017-06-11 439 | 10:00P.M. 440 | 441 | 442 | 2017-06-12 443 | 02:00a.m. 444 | 445 |
'; 446 | $parser = new Parser($input); 447 | $output = $parser->parse(); 448 | 449 | $this->assertEquals('2017-06-11 22:00', $output['items'][0]['properties']['start'][0]); 450 | $this->assertEquals('2017-06-12 02:00', $output['items'][0]['properties']['end'][0]); 451 | } 452 | 453 | /** 454 | * 455 | */ 456 | public function testAMPMWithoutPeriods() { 457 | $input = '
458 | 459 | 2017-06-17 460 | 10:30pm 461 | 462 | 463 | 2017-06-18 464 | 02:30AM 465 | 466 |
'; 467 | $parser = new Parser($input); 468 | $output = $parser->parse(); 469 | 470 | $this->assertEquals('2017-06-17 22:30', $output['items'][0]['properties']['start'][0]); 471 | $this->assertEquals('2017-06-18 02:30', $output['items'][0]['properties']['end'][0]); 472 | } 473 | 474 | /** 475 | * 476 | */ 477 | public function testDtVCPTimeAndTimezone() { 478 | $input = '
479 | 480 | 2017-06-17 481 | 13:30-07:00 482 | 483 | 484 | 2017-06-17 485 | 15:30-0700 486 | 487 |
'; 488 | $parser = new Parser($input); 489 | $output = $parser->parse(); 490 | 491 | $this->assertEquals('2017-06-17 13:30-0700', $output['items'][0]['properties']['start'][0]); 492 | $this->assertEquals('2017-06-17 15:30-0700', $output['items'][0]['properties']['end'][0]); 493 | } 494 | 495 | /** 496 | * @see https://github.com/indieweb/php-mf2/issues/147 497 | */ 498 | public function testDtVCPMultipleDatesAndTimezones() { 499 | $input = '
500 |

Multiple date and time values

501 | 502 |

When: 503 | 504 | 2014-06-01 505 | 3014-06-01 506 | 12:30 507 | (UTC-06:00) 508 | 23:00 509 | (UTC+01:00) 510 | – 511 | 19:30 512 |

513 | 514 |
'; 515 | $parser = new Parser($input); 516 | $output = $parser->parse(); 517 | 518 | $this->assertEquals('2014-06-01 12:30-0600', $output['items'][0]['properties']['start'][0]); 519 | $this->assertEquals('2014-06-01 19:30-0600', $output['items'][0]['properties']['end'][0]); 520 | } 521 | 522 | /** 523 | * @see https://github.com/indieweb/php-mf2/issues/149 524 | */ 525 | public function testDtWithoutYear() { 526 | $input = '
'; 527 | $parser = new Parser($input); 528 | $output = $parser->parse(); 529 | 530 | $this->assertEquals('--12-28', $output['items'][0]['properties']['bday'][0]); 531 | } 532 | 533 | /** 534 | * @see https://github.com/indieweb/php-mf2/issues/167 535 | * @see https://github.com/microformats/mf2py/blob/master/test/examples/datetimes.html 536 | */ 537 | public function testNormalizeOrdinalDate() { 538 | $input = '
539 |

Ordinal date

540 |

When: 541 | 542 | 2016-062 543 | 12:30AM 544 | (UTC-06:00) 545 |

546 |
'; 547 | $parser = new Parser($input); 548 | $output = $parser->parse(); 549 | 550 | $this->assertEquals('2016-03-02 12:30-0600', $output['items'][0]['properties']['start'][0]); 551 | } 552 | } 553 | 554 | -------------------------------------------------------------------------------- /tests/Mf2/fberriman.com.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | fberriman | a blog for frances 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 | 47 |
48 | 68 | 69 |
70 | 71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 |

79 | April recap – TXJS & Front-Trends 80 |

81 | 82 | 84 |
85 | 86 |
87 |

April was pretty decent. I got to attend two very good conferences and I got to speak at them.

88 |

TXJS, Austin, USA

89 |

Austin! One of my favourite cities (mostly because I love tacos). Was very pleased to be asked to return to this conference after I spoke there last year. The day was remarkable, if only because it’s one of the first conferences in a very long time where I actually watched all of the talks (although Rebecca, being on before me, may have only had half of my attention). Really a very well curated day, and I felt very lucky to be in the line-up.

90 |

Alex was not overly prescriptive in what I should talk about, but suggested he liked the content of last year and would like a little more on that. So, I decided to pick an aspect about that that I felt was important to us at GDS and fundamental to the success of our Design Principles.

91 |

For me, it’s been our honesty and simple language. The words that we’ve used to talk about user needs, technical aspects of the site and the ethos have been plain and no-nonsense. I think this is hugely down to the strength of a team that has the confidence to cut through bullshit and say what it really means – Russell and Sarah are particularly brilliant at this, and have had huge parts to play in getting this cult of simple down in writing.

92 |

The tech scene is sort of rife with nonsense words. Buzzwords and clichés and the new name for the next big thing, which is actually the new name for the same old sensible thing – but with better marketing and a twitter hashtag. Ugh. I want a lot less of that in our world.

93 |

So, I picked on a few of these and showed a few examples from how we’re dealing with them at GDS. I believe the video for that talk is out now, but the slides are here.

94 |

Front-Trends, Warsaw, Poland

95 |

I attended this conference last year – definitely a favourite for its surprisingly sunny weather and for being one of the most friendly events I had been to in 2012. So, I was really glad to get to come back and share our Design Principles with the crowd.

96 |

It was very similar to the talk I gave at TXJS last year, except we’ve done a whole lot more at GDS since June of last year – we released v1.0 of gov.uk, and a bunch of other stuff like the performance platform, Inside Government (and the 24 departments) and foreign travel advice, to name a few. I showcased some of these things, and then went through the design principles with the lovely, receptive, Polish audience and it seemed to go over rather well. The slides for this version of the talk are here.

97 |

Three days are a lot for a conference, but it was really high quality through-out and the breadth of subjects was really great. I wouldn’t recommend putting the party on the second night again, however – that last morning was something of a challenge. :)

98 |
99 | 100 | 105 |
106 | 107 |
108 |
109 | 110 |

111 | Jawbone Up Review 112 |

113 | 114 | 116 |
117 | 118 |
119 |

I’ve had a fair few people ask about the Jawbone Up I’ve been wearing since November (the second version, not the recalled first one – although, as you’ll read, perhaps this one should have been too). Here’s how I’ve found it.

120 |

The good

121 |

The reason I waited on the Up, over say the Nike Fuelband, was because I wanted a wrist-wearable tracker plus sleep data. The FitBit One has a wearable night-time band, but it looks rather large and cumbersome and I didn’t want a clothes-clip tracker in the day time (where do dress wearers clip them?).

122 |

Jawbone Up

123 |

The Up band’s size is really good and it’s comfy and it doesn’t look ridiculous.

124 |

I like the sleep tracking, although I feel like it’s not terribly accurate – if I wake and don’t move around much, it doesn’t record it as a waking period – but it’s accurate enough to collect the information I’m interested in.

125 |

I have been a bad sleeper for a long time, but having actual data about the length of time I’ve been asleep and awake has helped reduce my anxiety about a bad night’s sleep (it always feels like a lifetime when you’re awake in the middle of the night and don’t want to be – but turns out it isn’t), which in turn has helped improve how well I go to sleep generally, I think.

126 |

I also like the smart-alarm – before I’d put off looking at the clock to see the time, but the gentle nudge that, yes, it is about time I got up is really useful, and again, anxiety reducing.

127 |

The steps tracking seems fine. I’ve never bothered to calibrate it, since I don’t do much exercise except walking – but it seems to match the distances I do regularly around the city. It’s fun – I’m not competing, so it’s mostly just interesting. I hear from others that it basically can’t cope with running or cycling, though.

128 |

The bad

129 |

It broke. Twice. The first time, it broke after about 6 weeks – the vibration feature (needed for the smart-alarm and idle alert) just stopped working for no apparent reason. At the time, the Up band wasn’t out in the UK, so Jawbone were not willing to replace it (ugh) but when I said I’d be in New York for a week, they agreed to courier me a replacement to the Google office there while I was in town – which I think was really just a nice act on the part of one very good customer service rep I’d met on the support forums. Had I not been on the forum or nagged on twitter, I suspect I’d have been left out of pocket.

130 |

Unfortunately, the second band stopped working a couple of months later. The smart-alarm feature became temperamental and often wouldn’t go off at all, and the button on the end of the band had become dislodged and no longer clicked. This time, the band was out in the UK, and they sent me another one immediately.

131 |

I’ve been wearing the third one for about a week and I honestly expect it’ll break soon, too, sadly. Edd, who originally picked up my first band for me while he was in New York, had his first and second bands break too (the second after only 2 weeks) – so the statistical data I have available to me is not very favourable and a quick look through the forums will find most people in similar situations.

132 |

The other stuff

133 |

They just released third-party app integration, but sadly on iOS devices only (I use a Nexus 4 day to day, so syncing with an iOS device is an extra annoyance if I want to use those features). I expect that’ll help make the data the band is recording more interesting.

134 |

Otherwise, these are things I wish it had:

135 |
    136 |
  • A visible metre or something on the band. I have to sync it with my phone to find out how I’m doing. It doesn’t even tell me the time. I feel like it’s not providing me with much in return for the space it’s taking up on my wrist.
  • 137 |
  • There’s no web view – the only way to share the data is through facebook (meh) or if your friend is also an Up user (which is basically no one). I’d like to be able to let my husband see my sleep data – then he’ll know that I’m just grumpy because I’m tired. He can sneak a look at it on my phone, I guess, but it would just be nice to have a public view somewhere on the web.
  • 138 |
  • The food and mood logging is boring and pointless. It may be that the new app integration gives this value, but it was onerous and I gave up after a week. The insights offered to you only ever related to steps and sleep, so no matter how much food and mood you logged, it was for your own entertainment only. These features appear to be rather tacked-on.
  • 139 |
  • Some people complain about the lack of wireless sync as a deal-breaker (you sync it via the mic jack). This personally doesn’t greatly bother me (longer battery life is a reasonable trade), but given that I have to take it off my arm to find out anything about it, as mentioned above, then I think it would have been preferential in this case to sync wirelessly.
  • 140 |
141 |

But, these are all minor gripes – I’d recommend but for the fact that they clearly have not managed to make a band that doesn’t expire every 2 months.

142 |

I’m mostly just hoping this band will hold out long enough for the delivery of the Fitbit Flex I just pre-ordered.

143 |

Update: My 3rd band has the same smart alarm fault. Sigh.

144 |
145 | 146 | 151 |
152 | 153 |
154 |
155 | 156 |

157 | Back the Pastry Box Book 158 |

159 | 160 | 162 |
163 | 164 |
165 |

As I mentioned, I wrote for the Pastry Box Project for all of 2012.

166 |

Now, it’s hopefully going to be printed in dead tree form with the royalties going to the Red Cross. That’s kind of nice, as are many of the fancier offerings at the higher tiers (hand press? illustrations? all sorts!).

167 |

So, if you’re a fan of paper and of the folks that wrote last year, the details are all available here.

168 |

It’s being crowd sourced, so it’ll only be as successful as your interest allows. That’s how the internet works now, or something.

169 |
170 | 171 | 176 |
177 | 178 | 187 | 188 | 189 |
190 |
191 | 192 | 214 | 215 |
216 | 281 |
282 | 283 | 284 | 285 | 286 | 287 | --------------------------------------------------------------------------------