Розбираємо RSS за допомогою jQuery


194

Я хочу використовувати jQuery для розбору RSS-каналів. Чи можна це зробити з базовою бібліотекою jQuery поза коробкою чи мені потрібно буде використовувати плагін?


1
Я б пішов на github.com/sdepold/jquery-rss - це найкращий варіант зараз, доступні кілька варіантів! :)
Комрат

Для тих, хто приїхав сюди з google, мені довелося створити подібну річ для перегляду мініатюрних відхилень. Хороший і простий і легко розширюваний: adamjamesnaylor.com/2012/11/05 / ... . Зауважте, він використовує зчитувач каналів google, але лише для перетворення його в JSON.
Адам Нейлор

Відповіді:


208

УВАГА

API Google Feed офіційно застарілий і більше не працює !


Немає потреби у цілому плагіні. Це поверне ваш RSS як об'єкт JSON до функції зворотного виклику:

function parseRSS(url, callback) {
  $.ajax({
    url: document.location.protocol + '//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=10&callback=?&q=' + encodeURIComponent(url),
    dataType: 'json',
    success: function(data) {
      callback(data.responseData.feed);
    }
  });
}

4
Спробував jFeed, але це не спрацювало. Це працює чудово і не потребує додаткової бібліотеки.
diggersworld

15
Будьте в курсі ... використовуючи api google, канали кешуються, щоб ви не отримали останні та найкращі канали.
c0deNinja

3
де це кешування? Як я можу видалити кеш?
Jeg Bagus

39
Це не чудова відповідь. Це залежить від того, щоб третя сторона компанії [Google] підтримувала свою послугу. Він не відповідає на початкове запитання ("Розбір RSS з jQuery") і натомість рекламує Google. Що робити, якщо Google скасує або модифікує їх API ajax? Ваш веб-сайт працює.
Чарльз Гудвін

11
@CharlesGoodwin Google щойно видалив цей API! developers.google.com/feed/?hl=uk
GôTô

185

Використовуйте jFeed - плагін jQuery RSS / Atom. Згідно з документами, це так просто, як:

jQuery.getFeed({
   url: 'rss.xml',
   success: function(feed) {
      alert(feed.title);
   }
});

2
будь-які приклади використання реального світу? тобто розбір і показ, а не оповіщення. або це так просто, як $ ("# results"). append (feed.title)
Andy Brudtkuhl

3
ПРИМІТКА. У завантаженні є всілякі чудові приклади
Енді Брудткуль,

12
Аніруда, можливо, ти можеш спробувати 7-блискавку? Це безкоштовно, з відкритим кодом та відкриває різні типи файлів, включаючи tar / gzip.
Натан Струц

100
Зверніть увагу, що остання версія цього плагіна доступна на Github .
Алан Х.

3
jFeed, здається, більше не підтримується (остання зміна ноти - 2 роки, і багато запитів на відкрите тягання, здається, ігноруються), і не працює з останніми випусками jQuery.
Тило

159

Для тих, хто з нас пізніше приходить до дискусії, починаючи з 1,5 jQuery має вбудовані можливості аналізу XML, що дозволяє зробити це досить легко без плагінів або сторонніх служб. Він має функцію parseXml, а також автоматично розбере xml при використанні функції $ .get. Наприклад:

$.get(rssurl, function(data) {
    var $xml = $(data);
    $xml.find("item").each(function() {
        var $this = $(this),
            item = {
                title: $this.find("title").text(),
                link: $this.find("link").text(),
                description: $this.find("description").text(),
                pubDate: $this.find("pubDate").text(),
                author: $this.find("author").text()
        }
        //Do something with item here...
    });
});

10
Помилка XmlHttpRequest: Origin не дозволено Access-Control-Allow-Origin
jackocnr

12
@jackocnr, так, це недолік цього методу. Ви не можете робити запити між доменами, якщо у вас немає доступу для встановлення заголовка Access-Control-Allow-Origin на вихідному сервері. Якщо сервер підтримує jsonp, то це найкраща ставка. В іншому випадку ви можете використовувати проксі-скрипт у вашому домені, щоб отримати XML і потім викликати цей сценарій замість зовнішнього сервера.
Девід Хаммонд

Це дійсно єдина відповідь, яка не покладається на зовнішні плагіни чи послуги?
Blazemonger

Чому $this.find("link").text()завжди повертається порожній рядок ''?
Jeff Tian

@JeffTian, ​​важко сказати, не бачачи своїх xml. Найбільш очевидною причиною буде те, що елемент <link> відсутній або порожній.
Девід Хаммонд

16

jFeed не працює в IE.

Використовуйте zRSSFeed . Він працював за 5 хвилин


2
Доступний на zazar.net/developers/zrssfeed Про те, щоб спробувати сам, щоб побачити, як це відбувається, виглядає багатообіцяючим.
thewinchester

3
BTW, zRssFeed використовує внутрішньо API RSS Feed Google . Тож якщо ви хочете зробити макет HTML самостійно, то простіше просто подивитися на це.
Сіантік

п’ять хвилин або менше :)
Констанца

супер круто ... єдине, якби вони надавали об'єкти каналу, а не весь html у функції зворотного виклику, ніж це було б чудово ...
shahil

2
FYI, хто хоче скористатися цим плагіном. Розробник розмістив наступне. "ЦИЙ ПЛУГІН ВІДМОВАЄТЬСЯ Через те, що API служби каналів Google видалено зі служби, на який плагін відповів, він більше не буде доступний та не підтримується." Джерело: zazar.net/developers/jquery/zrssfeed
phanf

16

Оновлення (15 жовтня 2019 р.)

Я витягнув основну логіку з jquery-rss до нової бібліотеки під назвою RSS Vanilla, яка використовує API отримання та може працювати без додаткових залежностей:

const RSS = require('vanilla-rss');
const rss = new RSS(
    document.querySelector("#your-div"),
    "http://www.recruiter.com/feed/career.xml",
    { 
      // options go here
    }
);
rss.render().then(() => {
  console.log('Everything is loaded and rendered');
});

Оригінал

посада:

Ви також можете використовувати jquery-rss , який має гарні шаблони та дуже простий у використанні:

$("#your-div").rss("http://www.recruiter.com/feed/career.xml", {
    limit: 3,
    layoutTemplate: '<ul class="inline">{entries}</ul>',
    entryTemplate: '<li><a href="{url}">[{author}@{date}] {title}</a><br/>{shortBodyPlain}</li>'
})

врожайність (станом на 18 вересня 2013 р.):

<div id="your-div">
    <ul class="inline">
    <entries></entries>
    </ul>
    <ul class="inline">
        <li><a href="http://www.recruiter.com/i/when-to-go-over-a-recruiter%e2%80%99s-head/">[@Tue, 10 Sep 2013 22:23:51 -0700] When to Go Over a Recruiter's Head</a><br>Job seekers tend to have a certain "fear" of recruiters and hiring managers, and I mean fear in the reverence and respect ...</li>
        <li><a href="http://www.recruiter.com/i/the-perfect-job/">[@Tue, 10 Sep 2013 14:52:40 -0700] The Perfect Job</a><br>Having long ago dealt with the "perfect resume" namely God's, in a previous article of mine, it makes sense to consider the ...</li>
        <li><a href="http://www.recruiter.com/i/unemployment-benefits-applications-remain-near-5-year-low-decline-again/">[@Mon, 09 Sep 2013 12:49:17 -0700] Unemployment Benefits Applications Remain Near 5-Year Low, Decline Again</a><br>As reported by the U.S. Department of Labor, the number of workers seeking unemployment benefits continued to sit near ...</li>
    </ul>
</div>

Дивіться http://jsfiddle.net/sdepold/ozq2dn9e/1/ для робочого прикладу.


Майте на увазі, що jquery-rss використовує API Google Feed, який кешуватиме канал, що може призвести до проблеми. Ви можете обдурити його, додавши параметр небажаної URL - адреса , хоча: stackoverflow.com/questions/13401936 / ...
kukabuka

наведіть приклад для форматування дати без використанняmoment.js
Purvesh Desai

Перевірте такий фрагмент gist.github.com/sdepold/d1e5e0e7a66fc77930fe Це створить щось подібне: "<деякий вміст>, [@ 2015-11-18]"
sdepold

Я просто хотів зазначити, що jquery-rss НЕ використовує API Google Feed, а заміну, що випадає під назвою Feedr ( github.com/sdepold/feedrapp ) і відповідно працює нормально, незважаючи на те, що оригінальний API відключений.
sdepold

15

Використання JFeed

function getFeed(sender, uri) {
    jQuery.getFeed({
        url: 'proxy.php?url=' + uri,
        success: function(feed) {
            jQuery(sender).append('<h2>'
            + '<a href="'
            + feed.link
            + '">'
            + feed.title
            + '</a>'
            + '</h2>');

            var html = '';

            for(var i = 0; i < feed.items.length && i < 5; i++) {

                var item = feed.items[i];

                html += '<h3>'
                + '<a href="'
                + item.link
                + '">'
                + item.title
                + '</a>'
                + '</h3>';

                html += '<div class="updated">'
                + item.updated
                + '</div>';

                html += '<div>'
                + item.description
                + '</div>';
            }

            jQuery(sender).append(html);
        }    
    });
}

<div id="getanewbrowser">
  <script type="text/javascript">
    getFeed($("#getanewbrowser"), 'http://feeds.feedburner.com/getanewbrowser')
  </script>
</div>

9

Використовуйте API подачі Google AJAX, якщо ваші дані RSS не є приватними. Це швидко, звичайно.

https://developers.google.com/feed/


1
Хороша ідея, але вона не працює, якщо ви знаходитесь у брандмауері, який вимагає аутентифікації проксі, використовуючи діалогове вікно.
thewinchester

подача google застаріла і більше не підтримується.
vikas etagi

8

ОНОВЛЕННЯ [ 25.04.2016 ] Тепер краще написана та повністю підтримувана версія з більшою кількістю можливостей та можливостей, розміщених на GitHub.jQRSS

Я бачив Обраних відповідь на Nathan Strutz , однак, сторінка посилання JQuery плагін ще вниз і домашня сторінка для цього сайту , схоже, не навантаження. Я спробував декілька інших рішень і виявив, що більшість з них є не тільки застарілими, але ЛЕГКИМИ ! Таким чином, я кинув капелюх і зробив власний плагін, і з мертвими посиланнями це здається чудовим місцем для подання відповіді. Якщо ви шукаєте цю відповідь у 2012 році (незабаром до 2013 року), ви можете помітити розчарування мертвими посиланнями та старими порадами тут, як і я. Нижче посилання на мій сучасний приклад плагіна, а також код на плагін! Просто скопіюйте код у файл JS та зв’яжіть його у своєму заголовку, як і будь-який інший плагін. Використання ВИКЛЮЧНО EZ!

jsFiddle

Код плагіна
2/9/2015 - здійснив давно прострочене оновлення, щоб перевірити його, consoleперш ніж надсилати на нього команди! Має допомогти у вирішенні старих проблем з IE

(function($) {
    if (!$.jQRSS) { 
        $.extend({  
            jQRSS: function(rss, options, func) {
                if (arguments.length <= 0) return false;

                var str, obj, fun;
                for (i=0;i<arguments.length;i++) {
                    switch(typeof arguments[i]) {
                        case "string":
                            str = arguments[i];
                            break;
                        case "object":
                            obj = arguments[i];
                            break;
                        case "function":
                            fun = arguments[i];
                            break;
                    }
                }

                if (str == null || str == "") {
                    if (!obj['rss']) return false;
                    if (obj.rss == null || obj.rss == "") return false;
                }

                var o = $.extend(true, {}, $.jQRSS.defaults);

                if (typeof obj == "object") {
                    if ($.jQRSS.methods.getObjLength(obj) > 0) {
                        o = $.extend(true, o, obj);
                    }
                }

                if (str != "" && !o.rss) o.rss = str;
                o.rss = escape(o.rss);

                var gURL = $.jQRSS.props.gURL 
                    + $.jQRSS.props.type 
                    + "?v=" + $.jQRSS.props.ver
                    + "&q=" + o.rss
                    + "&callback=" + $.jQRSS.props.callback;

                var ajaxData = {
                        num: o.count,
                        output: o.output,
                    };

                if (o.historical) ajaxData.scoring = $.jQRSS.props.scoring;
                if (o.userip != null) ajaxData.scoring = o.userip;

                $.ajax({
                    url: gURL,
                    beforeSend: function (jqXHR, settings) { if (window['console']) { console.log(new Array(30).join('-'), "REQUESTING RSS XML", new Array(30).join('-')); console.log({ ajaxData: ajaxData, ajaxRequest: settings.url, jqXHR: jqXHR, settings: settings, options: o }); console.log(new Array(80).join('-')); } },
                    dataType: o.output != "xml" ? "json" : "xml",
                    data: ajaxData,
                    type: "GET",
                    xhrFields: { withCredentials: true },
                    error: function (jqXHR, textStatus, errorThrown) { return new Array("ERROR", { jqXHR: jqXHR, textStatus: textStatus, errorThrown: errorThrown } ); },
                    success: function (data, textStatus, jqXHR) {  
                        var f = data['responseData'] ? data.responseData['feed'] ? data.responseData.feed : null : null,
                            e = data['responseData'] ? data.responseData['feed'] ? data.responseData.feed['entries'] ? data.responseData.feed.entries : null : null : null
                        if (window['console']) {
                            console.log(new Array(30).join('-'), "SUCCESS", new Array(30).join('-'));
                            console.log({ data: data, textStatus: textStatus, jqXHR: jqXHR, feed: f, entries: e });
                            console.log(new Array(70).join('-'));
                        }

                        if (fun) {
                            return fun.call(this, data['responseData'] ? data.responseData['feed'] ? data.responseData.feed : data.responseData : null);
                        }
                        else {
                            return { data: data, textStatus: textStatus, jqXHR: jqXHR, feed: f, entries: e };
                        }
                    }
                });
            }
        });
        $.jQRSS.props = {
            callback: "?",
            gURL: "http://ajax.googleapis.com/ajax/services/feed/",
            scoring: "h",
            type: "load",
            ver: "1.0"
        };
        $.jQRSS.methods = {
            getObjLength: function(obj) {
                if (typeof obj != "object") return -1;
                var objLength = 0;
                $.each(obj, function(k, v) { objLength++; })
                return objLength;
            }
        };
        $.jQRSS.defaults = {
            count: "10", // max 100, -1 defaults 100
            historical: false,
            output: "json", // json, json_xml, xml
            rss: null,  //  url OR search term like "Official Google Blog"
            userip: null
        };
    }
})(jQuery);

ВИКОРИСТАННЯ

//  Param ORDER does not matter, however, you must have a link and a callback function
//  link can be passed as "rss" in options
//  $.jQRSS(linkORsearchString, callbackFunction, { options })

$.jQRSS('someUrl.xml', function(feed) { /* do work */ })

$.jQRSS(function(feed) { /* do work */ }, 'someUrl.xml', { count: 20 })

$.jQRSS('someUrl.xml', function(feed) { /* do work */ }, { count: 20 })

$.jQRSS({ count: 20, rss: 'someLink.xml' }, function(feed) { /* do work */ })

$ .jQRSS ('Шукати слова тут замість посилання', функція (канал) {/ * виконувати роботу * /}) // TODO: Виправлення потреб

Параметри

{
    count: // default is 10; max is 100. Setting to -1 defaults to 100
    historical: // default is false; a value of true instructs the system to return any additional historical entries that it might have in its cache. 
    output: // default is "json"; "json_xml" retuns json object with xmlString / "xml" returns the XML as String
    rss: // simply an alternate place to put news feed link or search terms
    userip: // as this uses Google API, I'll simply insert there comment on this:
        /*  Reference: https://developers.google.com/feed/v1/jsondevguide
            This argument supplies the IP address of the end-user on 
            whose behalf the request is being made. Google is less 
            likely to mistake requests for abuse when they include 
            userip. In choosing to utilize this parameter, please be 
            sure that you're in compliance with any local laws, 
            including any laws relating to disclosure of personal 
            information being sent.
        */
}

5
(function(url, callback) {
    jQuery.ajax({
        url: document.location.protocol + '//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=10&callback=?&q=' + encodeURIComponent(url),
        dataType: 'json',
        success: function(data) {
            callback(data.responseData.feed);
        }
    });
})('http://news.hitb.org/rss.xml', function(feed){ // Change to desired URL
    var entries = feed.entries, feedList = '';
    for (var i = 0; i < entries.length; i++) {
        feedList +='<li><a href="' + entries[i].link + '">' + entries[i].title + '</a></li>';
    }
    jQuery('.feed > ul').append(feedList);
});


<div class="feed">
        <h4>Hacker News</h4>
        <ul></ul>
</div>

5

Я погоджуюся з @Andrew , використовуючи Google - це надійний спосіб багаторазового використання цього з величезною вигодою від того, що ви повернете JSON замість XML. Додатковою перевагою використання Google як проксі-сервера є те, що сервіси, які можуть блокувати ваш прямий доступ до своїх даних, навряд чи зупинять Google. Ось приклад з використанням лижного звіту та даних про умови катання. У цьому є всі поширені програми в реальному світі: 1) Сторінка RSS / XML 2) JSONP 3) Очищення рядків і рядків для масиву, коли ви не можете отримати дані саме так, як вам потрібно 4) під час завантаження додайте елементи до DOM. Сподіваюся, це допомагає деяким людям!

<!-- Load RSS Through Google as JSON using jQuery -->
<script type="text/javascript">

    function displaySkiReport (feedResponse) {

    // Get ski report content strings
    var itemString = feedResponse.entries[0].content;
    var publishedDate = feedResponse.entries[0].publishedDate;

    // Clean up strings manually as needed
    itemString = itemString.replace("Primary: N/A", "Early Season Conditions"); 
    publishedDate = publishedDate.substring(0,17);

    // Parse ski report data from string
    var itemsArray = itemString.split("/");


    //Build Unordered List
    var html = '<h2>' + feedResponse.entries[0].title + '</h2>';
    html += '<ul>';

    html += '<li>Skiing Status: ' + itemsArray[0] + '</li>';
    // Last 48 Hours
    html += '<li>' + itemsArray[1] + '</li>';
    // Snow condition
    html += '<li>' + itemsArray[2] + '</li>';
    // Base depth
    html += '<li>' + itemsArray[3] + '</li>';

    html += '<li>Ski Report Date: ' + publishedDate + '</li>';

    html += '</ul>';

    $('body').append(html);    

    }


    function parseRSS(url, callback) {
      $.ajax({
    url: document.location.protocol + '//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=10&callback=?&q=' + encodeURIComponent(url),
    dataType: 'json',
    success: function(data) {
      callback(data.responseData.feed);
    }
      });
    }

    $(document).ready(function() {              

        // Ski report
        parseRSS("http://www.onthesnow.com/michigan/boyne-highlands/snow.rss", displaySkiReport);

    });

</script>

2
Це не спрацює через проблеми між домену. Вам потрібен JSONP.
gotofritz

1
Це працює для мене. Ви тестували це? Досить впевнений, що Google повертає jsonp, використовуючи параметр зворотного виклику не ajax dataType.
Ділан Валаде

Не впевнений, для чого був потік. Цей код все ще працює через три роки. Вставте все це в консоль, і ви побачите поточні (XML) умови лиж, додані до нижнього колонтитулу цієї сторінки.
Ділан Валаде

4

jFeed дещо застарілий, працюючи лише зі старими версіями jQuery. Минуло два роки з моменту її оновлення.

zRSSFeed, можливо, трохи менш гнучкий, але він простий у використанні, і він працює з поточною версією jQuery (наразі 1.4). http://www.zazar.net/developers/zrssfeed/

Ось короткий приклад із документів zRSSFeed:

<div id="test"><div>

<script type="text/javascript">
$(document).ready(function () {
  $('#test').rssfeed('http://feeds.reuters.com/reuters/oddlyEnoughNews', {
    limit: 5
  });
});
</script>

Можливо, зауважте, що це працює лише з немісцевими каналами, оскільки він використовує API подачі Google (Google повинен мати можливість завантажувати XML каналу).
CmdrTallen


2

Раджу використовувати FeedEk . Після офіційного застарівання API Google Feed більшість плагінів не працює. Але FeedEk все ще працює. Він дуже простий у використанні та має багато варіантів налаштування.

$('#divRss').FeedEk({
   FeedUrl:'http://jquery-plugins.net/rss'
});

З опціями

$('#divRss').FeedEk({
  FeedUrl:'http://jquery-plugins.net/rss',
  MaxCount : 5,
  ShowDesc : true,
  ShowPubDate:true,
  DescCharacterLimit:100,
  TitleLinkTarget:'_blank',
  DateFormat: 'MM/DD/YYYY',
  DateFormatLang:'en'
});

Я не впевнений, чи можна насправді визначити альтернативну кінцеву точку api, але якби ви могли, можливо, буде можливість замінити api каналу google на feedrapp: github.com/sdepold/feedrapp (що також є основою jquery -rss нині)
sdepold

Це не робить розбору. Він використовує Yahooapis для розбору, а потім просто відображає вміст.
Девід Л.

1
<script type="text/javascript" src="./js/jquery/jquery.js"></script>
<script type="text/javascript" src="./js/jFeed/build/dist/jquery.jfeed.pack.js"></script>
<script type="text/javascript">
    function loadFeed(){
        $.getFeed({
            url: 'url=http://sports.espn.go.com/espn/rss/news/',
            success: function(feed) {

                //Title
                $('#result').append('<h2><a href="' + feed.link + '">' + feed.title + '</a>' + '</h2>');

                //Unordered List
                var html = '<ul>';

                $(feed.items).each(function(){
                    var $item = $(this);

                    //trace( $item.attr("link") );
                    html += '<li>' +
                        '<h3><a href ="' + $item.attr("link") + '" target="_new">' +
                        $item.attr("title") + '</a></h3> ' +
                        '<p>' + $item.attr("description") + '</p>' +
                        // '<p>' + $item.attr("c:date") + '</p>' +
                        '</li>';
                });

                html += '</ul>';

                $('#result').append(html);
            }
        });
    }
</script>

Непогана відповідь, але, на жаль, ви не зробили найкращої роботи, вставляючи код. ;-)
До

0

Використовуйте google ajax api , кешований Google, та будь-який потрібний формат виводу.

Зразок коду; http://code.google.com/apis/ajax/playground/#load_feed

<script src="http://www.google.com/jsapi?key=AIzaSyA5m1Nc8ws2BbmPRwKu5gFradvD_hgq6G0" type="text/javascript"></script>
<script type="text/javascript">
/*
*  How to load a feed via the Feeds API.
*/

google.load("feeds", "1");

// Our callback function, for when a feed is loaded.
function feedLoaded(result) {
  if (!result.error) {
    // Grab the container we will put the results into
    var container = document.getElementById("content");
    container.innerHTML = '';

    // Loop through the feeds, putting the titles onto the page.
    // Check out the result object for a list of properties returned in each entry.
    // http://code.google.com/apis/ajaxfeeds/documentation/reference.html#JSON
    for (var i = 0; i < result.feed.entries.length; i++) {
      var entry = result.feed.entries[i];
      var div = document.createElement("div");
      div.appendChild(document.createTextNode(entry.title));
      container.appendChild(div);
    }
  }
}

function OnLoad() {
  // Create a feed instance that will grab Digg's feed.
  var feed = new google.feeds.Feed("http://www.digg.com/rss/index.xml");

  // Calling load sends the request off.  It requires a callback function.
  feed.load(feedLoaded);
}

google.setOnLoadCallback(OnLoad);
</script>

це чудовий варіант, оскільки він не покладається на jquery!
Піт Гарднер



-1

jQuery канали - це приємний варіант, він має вбудовану систему шаблонів і використовує API Google Feed, тому він має підтримку між доменами.



-2

jFeed простий та має приклад для тестування. Але якщо ви аналізуєте канал з іншого сервера, вам потрібно дозволити перехресний поділ ресурсів (CORS) на сервері каналу. Вам також потрібно перевірити підтримку браузера .

Я завантажив зразок але все ще не отримав підтримку IE в будь-якій версії, коли я змінив URL-адресу в прикладі на щось на зразок example.com/feed.rss через протокол http. CORS слід підтримувати для IE 8 і вище, але приклад jFeed не подає канал.

Ваша найкраща ставка - використовувати API Google:
https://developers.google.com/feed/v1/devguide

Дивіться:
https://github.com/jfhovinne/jFeed
http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
http://en.wikipedia.org/wiki/Same_origin_policy
http://caniuse.com/cors

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.